手把手教你架構3D引擎高階篇系列八
本篇部落格是給讀者介紹引擎底層如何與Lua進行結合,方便開發者直接使用指令碼程式設計,給讀者介紹的是最基本的C++與Lua的互動,引擎的封裝會在下篇部落格中具體講解。
為什麼選擇Lua,通常開發者會使用Json,XML,txt等等。相比Lua有哪些優點呢?
a、除了Lua庫,在沒有使用其他庫可以使用。
b、可以在檔案中使用不同的公式,例如:some_variable = math.sqrt(2)* 2
c、它非常輕巧,速度快
d、它是在MIT許可下換句話說程式碼是開源的,因此我們可以以任何想要的方式使用它
e、它是用C語言編寫的,幾乎可以編譯任何C編譯器
f、可以使用表對資料進行分類,易於編輯和閱讀
既然這麼多優點,我們就採用Lua作為指令碼使用,Lua與引擎的結合還是非常重要的。我們下面先從基礎的講起,慢慢給讀者深入。先看看Lua語言編寫的指令碼:
player = { pos = { X = 20, Y = 30, }, filename = "res/images/player.png", HP = 20,-- you can also have comments}
我們要使用Lua指令碼中的內容,需要執行下面的語句:
LuaScript script("player.lua");
std::string filename = script.get("player.filename");
int posX = script.get("player.pos.X");
如何使用Lua與C++繫結,讀者可以參考網址:
接下來我們分析一下上面的Lua指令碼,Player表是全域性的,因此需要通過lua_getglobal方法獲取它, 現在Player表將位於堆疊頂部,使用lua_getfield函式獲取pos表,然後使用變數x,如下圖所示:
對應的程式碼下載地址如下所示:
https://github.com/EliasD/unnamed_lua_binder
使用上面的程式碼時,不要忘記把Lua的庫加進工程裡面。我們可以使用Lua做很多事情,如下所示:
-- somefile.luasome_array = { 1, 2, 3, 4, 5, 6} std::vector v = script.getIntVector("some_array")
如何清理Lua堆疊?我們可以使用lua_gettop函式返回陣列中元素的數量,從而弄清楚我們必須彈出多少項。
void clean()
{
int n = lua_gettop(L);
lua_pop(L, n);
}
下面實現的介面:
std::vector LuaScript::getIntVector(const std::string& name)
{
std::vector<int> v;
lua_getglobal(L, name.c_str());
if(lua_isnil(L, -1))
{
return std::vector();
}
lua_pushnil(L);
while(lua_next(L, -2))
{
v.push_back((int)lua_tonumber(L, -1));
lua_pop(L, 1);
}
clean();
return v;
}
它們是如何工作的?首先,我們獲取全域性表並檢查是否找到它。 如果它是nil(尚未定義,或者…,nil),我們只返回一個空向量。
然後我們將nil值推到Lua堆疊的頂部, 這是因為lua_next的作用,它從堆疊中彈出鍵值,然後將鍵值對推送到堆疊。 如果陣列中沒有更多元素,我們清理堆疊並返回結果向量。
還可以建立一個函式來獲取字串或浮點陣列, 這需要更改的是向量型別和一些強制轉換(請不要忘記將lua_tonumber更改為lua_tostring)
使用Lua指令碼程式設計,因為它是指令碼,對效能要求比較高的程式碼建議不要使用Lua指令碼,直接使用C或者C++程式設計。
通過案例的方式給讀者介紹,比如下面程式碼:
function sum(x, y)
return x + y
end
對應的C++程式碼如下所示:
int sum(int x, int y)
{
lua_State* L = luaL_newstate();
if (luaL_loadfile(L, "sum.lua") || lua_pcall(L, 0, 0, 0))
{
std::cout<<"Error: failed to load sum.lua"<<std::endl;
return 0;
}
lua_getglobal(L, "sum");
lua_pushnumber(L, x);
lua_pushnumber(L, y);
std::cout<<"loaded"<<std::endl;
lua_pcall(L, 2, 1, 0);
int result = (int)lua_tonumber(L, -1);
lua_pop(L, 1);
return result;
}
接下來給讀者分析一下,首先,我們建立新的Lua狀態並載入檔案。注意:這只是一個示例,我們應該將狀態與載入的檔案保持在某個位置,以防止每次使用函式時重新載入,因為這樣做效率不高。
然後我們在Lua堆疊的頂部得到名為sum的全域性函式。 使用lua_pushnumber函式然後我們推送2個變數,現在我們的堆疊看起來像這樣:
第一個是lua_State,第二個是你要呼叫的函式中的引數個數, 第三是你希望返回的功能。 第四是錯誤程式碼(應該在Lua參考手冊中閱讀)
在我們呼叫一個函式之後,它會從它的引數中彈出堆疊。 堆疊中剩下的唯一東西是值sum函式返回,所以現在我們可以用lua_tonumber獲取它的值並彈出它。
說了這麼多,現在給讀者介紹如何使用它們?
假設我們在遊戲中實現NPC, 當玩家靠近NPC時,NPC會做不同的事情。
我們經常會安排玩家與NPC的一些對話,比如說“讓我幫助你”,而另一個NPC只是說“你好”並且什麼都不做。我們的互動程式碼可能如下所示:
if(isPressed(ACTIVATION_BUTTON))
{
Character* character = find_nearby_character(player);
if(character)
{
character->interact(player);
}
}
如何實現這個互動方法?我們很容易想到使用列舉,如下所示:
enum CharacterType { Player, Talker, Healer };
CharacterType type;
函式如下所示:
void Character::interact(Character* secondCharacter)
{
switch(type)
{
case Character::Player:
break;
case Character::Talker:
say("Hello");
break;
case Character::Healer:
say("Let me help you");
heal(secondCharacter);
break;
}
}
通過程式碼我們可以看出,這麼設計非常不利於擴充套件,我們還可以想到使用另一種解決方案是使互動虛擬功能和使用繼承, 但是我們會去實現每種型別的NPC,這種方案也是不可取的。
或者有讀者可以使用更好的策略模式,用C ++編碼的,必須重新編譯程式碼,也是不可取的。
最終的解決方案就想到了Lua的編寫,在使用Lua之前,我們首先要建立一個Character類,如下所示:
class Character
{
public:
Character(const char* name, int hp);
void say(const char* text);
void heal(Character* character);
const char* getName() { return name; }
int getHealth() { return health; }
void setHealth(int hp) { health = hp; }
// will be implemented later
void interact(Character* character);
private:
const char* name;
int health;
};
Character::Character(const char* name, int hp)
{ this->name = name; health = hp;}
void Character::say(const char* text)
{ std::cout << name << ":" << text << std::endl;}
void Character::heal(Character* character)
{ character->setHealth(100);}
我們遇到了一個問題, 如果Lua現在沒有關於這種型別,我們如何將Character *作為引數傳遞? 我們如何在Lua中註冊非靜態成員函式並呼叫它們?Lua包裝器可以參考網址:http://lua-users.org/wiki/BindingCodeToLua
決定使用LuaWrapper,它沒有額外的依賴關係而且不需要構建, 只需將一個頭檔案複製到專案中即可開始使用。
使用LuaWrapper,函式的編寫如下所示:
int Character_getName(lua_State* L)
{
Character* character = luaW_check<Character>(L, 1); lua_pushstring(L, character->getName());
return 1;
}
int Character_getHealth(lua_State* L)
{
Character* character = luaW_check<Character>(L, 1); lua_pushnumber(L, character->getHealth());
return 1;
}
int Character_setHealth(lua_State* L)
{
Character* character = luaW_check<Character>(L, 1); int hp = luaL_checknumber(L, 2);
character->setHealth(hp);
return 0;
}
從現在開始,我們將使用checknumber而不是tonumber。 它基本相同,但如果出現問題,它會丟擲錯誤資訊。
LuaWrapper提供了相同的方法,可以用它來獲取C ++物件,還可以建立物件並呼叫它們的方法,如下所示:
player = Character.new(“Hero”, 100)
player:getHealth()
使用luaW_check(L,1)可以獲得玩家物件在C ++中使用它,餘下的程式碼如下所示:
static luaL_Reg Character_table[] = { { NULL, NULL }};
static luaL_Reg Character_metatable[] = {
{ "getName", Character_getName },
{ "getHealth", Character_getHealth },
{ "setHealth", Character_setHealth },
{ NULL, NULL }
};
static int luaopen_Character(lua_State* L)
{
luaW_register<Character>(L, "Character", Character_table, Character_metatable, Character_new);
return 1;
}
Character_table用於靜態函式, 我們在Character類中沒有它們,所以這個結構是空的。
Character_metatable用於設定將在Lua中使用的函式名稱。
luaopen_Character註冊一個類, 第一個引數是lua_State *,第二個引數是如何在Lua指令碼中命名我們的類。 其他引數是靜態表,元表和建構函式。
我們的測試指令碼如下所示:
player = Character.new("Hero", 100)
player:setHealth(80)
hp = player:getHealth()
name = player:getName()
print("Character name: "..name..". HP = "..hp)
最後程式碼下載地址如下所示:
連結:https://pan.baidu.com/s/1Rn3WwXYVLA-t79s0MFFarQ
提取碼:mtgr
參考網址:https://eliasdaler.wordpress.com/2013/10/11/lua_cpp_binder/