如何用C++做遊戲(2)
上一節講了一些基本的Lua應用,或許你會說,還是很簡單麼。呵呵,恩,是的,本來Lua就是為了讓大家使用的方便快捷而設計的。如果設計的過為複雜,就不會有人使用了。
下面,我要強調一下,Lua的棧的一些概念,因為這個確實很重要,你會經常用到。熟練使用Lua,最重要的就是要時刻知道什麼時候棧裡面的資料是什麼順序,都是什麼。如果你能熟練知道這些,實際你已經是Lua運用的高手了。
說 真的,第一次我接觸棧的時候,沒有把它想的很複雜,倒是看了網上很多的關於Lua的文章讓我對棧的理解雲裡霧裡,什麼元表,什麼User,什麼區域性變數, 什麼全域性變數位移。說的那叫一個暈。本人腦子笨,理解不了這麼多,也不知道為什麼很多人喜歡把Lua棧弄的七上八下,程式碼晦澀難懂。後來實在受不了了,去 Lua網站下載了Lua的文件,寫的很清晰。Lua的棧實際上幾句話足以。
當你初始化一個棧的時候,它的棧底是1,而棧頂相對位置是-1,說形 象一些,你可以把棧想象成一個環,有一個指標標記當前位置,如果-1,就是當前棧頂,如果是-2就是當前棧頂前面一個引數的位置。以此類推。
當然,你也可 以正序去取,這裡要注意,對於Lua的很多API,下標是從1開始的。這個和C++有些不同。而且,在棧的下標中,正數表示絕對棧底的下標,負數表示相對 棧頂的相對地址,這個一定要有清晰的概念,否則很容易看暈了。 讓我們看一些例子,加深理解。
lua_pushnumber(m_pState, 11);
lua_pushnumber(m_pState, 12);
int nIn = lua_gettop(m_pState); <–這裡加了一行, lua_gettop()這個API是告訴你目前棧裡元素的個數。
如果僅僅是Push兩個引數,那麼nIn的數值是2 ,對。沒錯。那麼咱們看看棧裡面是怎麼放的。我再加兩行程式碼。
lua_pushnumber(m_pState, 11);
lua_pushnumber(m_pState, 12);
int nIn = lua_gettop(m_pState)
int nData1 = lua_tonumber(m_pState, 1); <–讀取棧底第一個絕對座標中的元素
int nData2 = lua_tonumber(m_pState, 2); <–讀取棧底第二個絕對座標中的元素
printf(“[Test]nData1 = %d, nData2 = %d./n”);
如果是你,憑直覺,告訴我答案是什麼?
現在公佈答案,看看是不是和你想的一樣。
[Test]nData1 = 11, nData2 = 12
那麼,如果我把程式碼換成
lua_pushnumber(m_pState, 11);
lua_pushnumber(m_pState, 12);
int nIn = lua_gettop(m_pState)
int nData1 = lua_tonumber(m_pState, -1); <–讀取棧頂第一個相對座標中的元素
int nData2 = lua_tonumber(m_pState, -2); <–讀取棧頂第二個相對座標中的元素
printf(“[Test]nData1 = %d, nData2 = %d./n”);
請你告訴我輸出是什麼? 答案是
[Test]nData1 = 12, nData2 = 11
呵呵,挺簡單的吧,對了,其實就這麼簡單。網上其它的高階運用,其實大部分都是對棧的位置進行調整。只要你抓住主要概念,看懂還是不難的。什麼元表,什麼變數,其實都一樣,抓住核心,時刻知道棧裡面的樣子,就沒有問題。
好了,回到我上一節的那個程式碼。
bool CLuaFn::CallFileFn(const char* pFunctionName, int nParam1, int nParam2)
{
int nRet = 0;
if(NULL == m_pState)
{
printf(“[CLuaFn::CallFileFn]m_pState is NULL./n”);
return false;
}
lua_getglobal(m_pState, pFunctionName);
lua_pushnumber(m_pState, nParam1);
lua_pushnumber(m_pState, nParam2);
int nIn = lua_gettop(m_pState); <–在這裡加一行。
nRet = lua_pcall(m_pState, 2, 1, 0);
if (nRet != 0)
{
printf(“[CLuaFn::CallFileFn]call function(%s) error(%d)./n”, pFunctionName, nRet);
return false;
}
if (lua_isnumber(m_pState, -1) == 1)
{
int nSum = lua_tonumber(m_pState, -1);
printf(“[CLuaFn::CallFileFn]Sum = %d./n”, nSum);
}
int nOut = lua_gettop(m_pState); <–在這裡加一行。
return true;
}
nIn的答案是多少?或許你會說是2吧,呵呵,實際是3。或許你會問,為什麼會多一個?其實我第一次看到這個數字,也很詫異。但是確實是3。因為你 呼叫的函式名稱佔據了一個堆疊的位置。其實,在獲取nIn那一刻,堆疊的樣子是這樣的(函式介面地址,引數1,引數2),函式名稱也是一個變數入棧的。
而 nOut輸出是1,lua_pcall()函式在呼叫成功之後,會自動的清空棧,然後把結果放入棧中。在獲取nOut的一刻,棧內是這幅摸樣(輸出引數 1)。
這裡就要再遷出一個更重要的概念了,Lua不是C++,對於C++程式設計師而言,一個函式會自動建立棧,當函式執行完畢後會自動清理 棧,Lua可不會給你這麼做,對於Lua而言,它沒有函式這個概念,一個棧對應一個lua_State指標,也就是說,你必須手動去清理你不用的棧,否則 會造成垃圾資料佔據你的記憶體。
不信?那麼咱們來驗證一下,就拿昨天的程式碼吧,你用for迴圈呼叫100萬次。看看nOut的輸出結果。。我相信,程式執行不到100萬次就會崩潰,而你的記憶體也會變的碩大無比。而nOut的輸出也會是這樣的 1,2,3,4,5,6。。。。。 原因就是,Lua不會清除你以前棧內的資料,每呼叫一次都會給你生成一個新的棧元素插入其中。
那麼怎麼解決呢?呵呵,其實,如果不考慮多執行緒的話,在你的函式最後退出前加一句話,就可以輕鬆解決這個問題。(Lua棧操作是非執行緒安全的!)
lua_settop(m_pState, -2);
這句話的意思是什麼?lua_settop()是設定棧頂的位置,我這麼寫,意思就是,棧頂指標目前在當前位置的-2的元素上。這樣,我就實現了對棧的清除。仔細想一下,是不是這個道理呢?
bool CLuaFn::CallFileFn(const char* pFunctionName, int nParam1, int nParam2)
{
int nRet = 0;
if(NULL == m_pState)
{
printf(“[CLuaFn::CallFileFn]m_pState is NULL./n”);
return false;
}
lua_getglobal(m_pState, pFunctionName);
lua_pushnumber(m_pState, nParam1);
lua_pushnumber(m_pState, nParam2);
int nIn = lua_gettop(m_pState); <–在這裡加一行。
nRet = lua_pcall(m_pState, 2, 1, 0);
if (nRet != 0)
{
printf(“[CLuaFn::CallFileFn]call function(%s) error(%d)./n”, pFunctionName, nRet);
return false;
}
if (lua_isnumber(m_pState, -1) == 1)
{
int nSum = lua_tonumber(m_pState, -1);
printf(“[CLuaFn::CallFileFn]Sum = %d./n”, nSum);
}
int nOut = lua_gettop(m_pState); <–在這裡加一行。
lua_settop(m_pState, -2); <–清除不用的棧。
return true;
}
好了,再讓我們執行100萬次,看看你的程式記憶體,看看你的程式還崩潰不? 如果你想列印 nOut的話,輸出會變成1,1,1,1,1。。。。
最後說一句,lua_tonumber()或lua_tostring()還有以後我們要用到的lua_touserdata()一定要將資料完全取出後儲存到你的別的變數中去,否則會因為清棧操作,導致你的程式異常,切記!
呵呵,說了這麼多,主要是讓大家如何寫一個嚴謹的Lua程式,不要執行沒兩下就崩潰了。好了,基礎棧的知識先說到這裡,以後還有一些技巧的運用,到時候會給大家展示。
下面說一下,Lua的工具。(為什麼要說這個呢?呵呵,因為我們下一步要用到其中的一個幫助我們的開發。)
呵呵,其實,Lua裡面有很多簡化開發的工具,你可以去http://www.sourceforge.net/去找一下。它們能夠幫助你簡化C++物件與Lua物件互轉之間的程式碼。
這裡說幾個有名的,當然可能不全。
(lua tinker)如果你的系統在windows下,而且不考慮移植,那麼我強烈推薦你去下載一個叫做lua tinker的小工具,整個工具非常簡單,一個.h和一個.cpp。直接就可以引用到你的工程中,連獨立編譯都不用,這是一個韓國人寫的Lua與 C++介面轉換的類,十分方便,程式碼簡潔(居家旅行,必備良藥)。
它是基於模板的,所以你可以很輕鬆的把你的C物件繫結到Lua中。程式碼較長,呵呵, 有興趣的朋友可以給我留言索要lua tinker的例子。就不貼在這裡了。不過我個人不推薦這個東西,因為它在Linux下是編譯不過去的。它使用了一種g不支援的模板寫法,雖然有人在 嘗試把它修改到Linux下編譯,但據我所知,修改後效果較好的似乎還沒有。不過如果你只是在 windows下,那就沒什麼可猶豫的,強烈推薦,你會喜歡它的。
(Luabinder)相信用過Boost庫的朋友,或許對這個傢伙很熟悉。它是一個很強大的Linux下Lua擴充套件包,幫你封裝了很多Lua的復 雜操作,主要解決了繫結C++物件和Lua物件互動的關係,非常強大,不過嘛,對於freeeyes而言,還是不推薦,因為freeeyes很懶,不想為 了一個Lua還要去編譯一個龐大的boost庫,當然,見仁見智,如果你的程式本身就已經載入了boost,那麼就應該毫不猶豫的選擇它。
(lua++)呵呵,這是我最喜歡,也是我一直用到現在的庫,比較前兩個而言,lua++的封裝性沒有那麼好,很多東西還是需要一點程式碼的,不過之 所以我喜歡,是因為它是用C寫的,可以在windows下和linux下輕鬆轉換
還記得我昨天說過如何編譯Lua麼,現在請你再做一遍,不同的是,請把lua的程式包中的src/lib中的所有h和cpp,還有 include下的那個.h拷貝到你上次建立的lua工程中。然後全部新增到你的靜態連結庫工程中去,重新編譯。會生成一個新的lua.lib,這個 lua就自動包含了lua的功能。最後記得把tolua++.h放在你的Include資料夾下。 行了,我們把上次CLuaFn類稍微改一下。
extern “C”
{
#include “lua.h”
#include “lualib.h”
#include “lauxlib.h”
#include “tolua++” //這裡加一行
};
class CLuaFn
{
public:
CLuaFn(void);
~CLuaFn(void);
void Init(); //初始化Lua物件指標引數
void Close(); //關閉Lua物件指標
bool LoadLuaFile(const char* pFileName); //載入指定的Lua檔案
bool CallFileFn(const char* pFunctionName, int nParam1, int nParam2); //執行指定Lua檔案中的函式
private:
lua_State* m_pState; //這個是Lua的State物件指標,你可以一個lua檔案對應一個。
};
行了,這樣我們就能用Lua++下的功能了。
大家看到了 bool CallFileFn(const char* pFunctionName, int nParam1, int nParam2);這個函式的運用。演示了真麼呼叫Lua函式。
下面,我改一下,這個函式。為什麼?還是因為freeeyes很懶,我可不想每有一個函式,我都要寫一個C++函式去呼叫,太累!我要寫一個通用的!支援任意函式呼叫的介面!
於是我建立了兩個類。支援任意引數的輸入和輸出,並打包送給lua去執行,說幹就幹。
#ifndef _PARAMDATA_H
#define _PARAMDATA_H
#include <vector>
#define MAX_PARAM_200 200
using namespace std;
struct _ParamData
{
public:
void* m_pParam;
char m_szType[MAX_PARAM_200];
int m_TypeLen;
public:
_ParamData()
{
m_pParam = NULL;
m_szType[0] = ‘/0′;
m_TypeLen = 0;
};
_ParamData(void* pParam, const char* szType, int nTypeLen)
{
SetParam(pParam, szType, nTypeLen);
}
~_ParamData() {};
void SetParam(void* pParam, const char* szType, int nTypeLen)
{
m_pParam = pParam;
sprintf(m_szType, “%s”, szType);
m_TypeLen = nTypeLen;
};
bool SetData(void* pParam, int nLen)
{
if(m_TypeLen < nLen)
{
return false;
}
if(nLen > 0)
{
memcpy(m_pParam, pParam, nLen);
}
else
{
memcpy(m_pParam, pParam, m_TypeLen);
}
return true;
}
void* GetParam()
{
return m_pParam;
}
const char* GetType()
{
return m_szType;
}
bool CompareType(const char* pType)
{
if(0 == strcmp(m_szType, pType))
{
return true;
}
else
{
return false;
}
}
};
class CParamGroup
{
public:
CParamGroup() {};
~CParamGroup()
{
Close();
};
void Init()
{
m_vecParamData.clear();
};
void Close()
{
for(int i = 0; i < (int)m_vecParamData.size(); i++)
{
_ParamData* pParamData = m_vecParamData;
delete pParamData;
pParamData = NULL;
}
m_vecParamData.clear();
};
void Push(_ParamData* pParam)
{
if(pParam != NULL)
{
m_vecParamData.push_back(pParam);
}
};
_ParamData* GetParam(int nIndex)
{
if(nIndex < (int)m_vecParamData.size())
{
return m_vecParamData[nIndex];
}
else
{
return NULL;
}
};
int GetCount()
{
return (int)m_vecParamData.size();
}
private:
typedef vector<_ParamData*> vecParamData;
vecParamData m_vecParamData;
};
#endif
#endif
我建立了兩個類,把Lua要用到的型別,資料都封裝起來了。這樣,我只需要這麼改寫這個函式。 bool CallFileFn(const char* pFunctionName, CParamGroup& ParamIn, CParamGroup& ParamOut); 它就能按照不同的引數自動給我呼叫,嘿嘿,懶到家吧!
其實這兩個類很簡單,_ParamData是引數類,把你要用到的引數放入到這個物件中去,標明型別的大小,型別名稱,記憶體塊。而CParamGroup負責將很多很多的_ParamData打包在一起,放在vector裡面。
好了,讓我們看看CallFileFn函式裡面我怎麼改的。
bool CLuaFn::CallFileFn(const char* pFunctionName, CParamGroup& ParamIn, CParamGroup& ParamOut)
{
int nRet = 0;
int i = 0;
if(NULL == m_pState)
{
printf(“[CLuaFn::CallFileFn]m_pState is NULL./n”);
return false;
}
lua_getglobal(m_pState, pFunctionName);
//載入輸入引數
for(i = 0; i < ParamIn.GetCount(); i++)
{
PushLuaData(m_pState, ParamIn.GetParam(i));
}
nRet = lua_pcall(m_pState, ParamIn.GetCount(), ParamOut.GetCount(), 0);
if (nRet != 0)
{
printf(“[CLuaFn::CallFileFn]call function(%s) error(%s)./n”, pFunctionName, lua_tostring(m_pState, -1));
return false;
}
//獲得輸出引數
int nPos = 0;
for(i = ParamOut.GetCount() – 1; i >= 0; i–)
{
nPos–;
PopLuaData(m_pState, ParamOut.GetParam(i), nPos);
}
int nCount = lua_gettop(m_pState);
lua_settop(m_pState, -1-ParamOut.GetCount());
return true;
}
別的沒變,加了兩個迴圈,因為考慮lua是可以支援多結果返回的,所以我也做了一個迴圈接受引數。
lua_settop(m_pState, -1-ParamOut.GetCount());這句話是不是有些意思,恩,是的,我這裡做了一個小技巧,因為我不知道返回引數有幾個,所以我會根據返回引數的個數重新設定棧頂。這樣做可以返回任意數量的棧而且清除乾淨。
或許細心的你已經發現,裡面多了兩個函式。恩,是的。來看看這兩個函式在幹什麼。
bool CLuaFn::PushLuaData(lua_State* pState, _ParamData* pParam)
{
if(pParam == NULL)
{
return false;
}
if(pParam->CompareType(“string”))
{
lua_pushstring(m_pState, (char* )pParam->GetParam());
return true;
}
if(pParam->CompareType(“int”))
{
int* nData = (int* )pParam->GetParam();
lua_pushnumber(m_pState, *nData);
return true;
}
else
{
void* pVoid = pParam->GetParam();
tolua_pushusertype(m_pState, pVoid, pParam->GetType());
return true;
}
}
引數入棧操作,呵呵,或許你會問tolua_pushusertype(m_pState, pVoid, pParam->GetType());這句話,你可能有些看不懂,沒關係,我會在下一講詳細的解釋Lua++的一些API的用法。
現在大概和你說 一下,這句話的意思就是,把一個C++物件傳輸給Lua函式。 再看看,下面一個。
bool CLuaFn:: PopLuaData(lua_State* pState, _ParamData* pParam, int nIndex)
{
if(pParam == NULL)
{
return false;
}
if(pParam->CompareType(“string”))
{
if (lua_isstring(m_pState, nIndex) == 1)
{
const char* pData = (const char*)lua_tostring(m_pState, nIndex);
pParam->SetData((void* )pData, (int)strlen(pData));
}
return true;
}
if(pParam->CompareType(“int”))
{
if (lua_isnumber(m_pState, nIndex) == 1)
{
int nData = (int)lua_tonumber(m_pState, nIndex);
pParam->SetData(&nData, sizeof(int));
}
return true;
}
else
{
pParam->SetData(tolua_tousertype(m_pState, nIndex, NULL), -1);
return true;
}
}
彈出一個引數並賦值。pParam->SetData(tolua_tousertype(m_pState, nIndex, NULL), -1);這句話同樣,我在下一講中詳細介紹。
好了,我們又進了一步,我們可以用這個函式繫結任意一個Lua函式格式。而程式碼不用多寫,懶蛋的目的達到了。
這一講主要是介紹了一些基本知識,或許有點多餘,但是我覺得是必要的,在下一講中,我講開始詳細介紹如何繫結一個C++物件給Lua,並讓Lua對其修改。然後返回結果。
小夥伴們,還請持續關注更新,更多幹貨和資料請直接聯絡我,也可以加群710520381,邀請碼:柳貓,歡迎大家共同討論