手把手教你架构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語言編寫的腳本:
我們要使用Lua腳本中的內容,需要執行下面的語句:
LuaScript script("player.lua"); std::string filename = script.get("player.filename"); int posX = script.get("player.pos.X");如何使用Lua與C++綁定,讀者可以參考網址:
http://lua-users.org/wiki/BindingCodeToLua
接下來我們分析一下上面的Lua腳本,Player表是全局的,因此需要通過lua_getglobal方法獲取它, 現在Player表將位于堆棧頂部,使用lua_getfield函數獲取pos表,然后使用變量x,如下圖所示:
對應的代碼下載地址如下所示:
https://github.com/EliasD/unnamed_lua_binder
使用上面的代碼時,不要忘記把Lua的庫加進工程里面。我們可以使用Lua做很多事情,如下所示:
如何清理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++編程。
通過案例的方式給讀者介紹,比如下面代碼:
對應的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只是說“你好”并且什么都不做。我們的交互代碼可能如下所示:
如何實現這個交互方法?我們很容易想到使用枚舉,如下所示:
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類,如下所示:
我們遇到了一個問題, 如果Lua現在沒有關于這種類型,我們如何將Character *作為參數傳遞? 我們如何在Lua中注冊非靜態成員函數并調用它們?Lua包裝器可以參考網址:http://lua-users.org/wiki/BindingCodeToLua
決定使用LuaWrapper,它沒有額外的依賴關系而且不需要構建, 只需將一個頭文件復制到項目中即可開始使用。
使用LuaWrapper,函數的編寫如下所示:
從現在開始,我們將使用checknumber而不是tonumber。 它基本相同,但如果出現問題,它會拋出錯誤信息。
LuaWrapper提供了相同的方法,可以用它來獲取C ++對象,還可以創建對象并調用它們的方法,如下所示:
使用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腳本中命名我們的類。 其他參數是靜態表,元表和構造函數。
我們的測試腳本如下所示:
最后代碼下載地址如下所示:
鏈接:https://pan.baidu.com/s/1Rn3WwXYVLA-t79s0MFFarQ
提取碼:mtgr
參考網址:https://eliasdaler.wordpress.com/2013/10/11/lua_cpp_binder/
總結
以上是生活随笔為你收集整理的手把手教你架构3D引擎高级篇系列八的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 树莓派使用STEP1:装系统
- 下一篇: Keil中的Code,RO,RW,ZI分