玩轉cocos2d-x lua-binding, 實現c++與lua混合編程
引言
城市精靈GO(http://csjl.teamtop3.com/)是一款基于cocos2d-x開發的LBS社交游戲, 通過真實地圖的探索, 發現和抓捕隱匿于身邊的野生精靈, 利用游戲中豐富的玩法提升和進化自己的精靈團隊, 一步一步成為精靈訓練大師.


本游戲的開發混合使用了c++和lua編程, 既發揮了c++高性能, 跨平臺系統兼容的優勢, 又享受了lua敏捷方便的開發效率. cocos2d-x提供了一套完備的lua-binding工具來幫助開發者實現c++和lua的代碼聯合, 可以方便實現兩者之間的數據通信和代碼互調. 本文就從lua-binding入手, 深入介紹c++和lua混合編程的相關細節, 探討其中可能存在的問題和發展. 本文原創發布于博客園(http://www.rzrgm.cn/dabaopku/p/5649294.html), 獨家授權XX轉載.
本游戲開發基于cocos2d-x 3.2.0版本, 其他版本用戶請祥閱官方文檔和源代碼.
原理介紹
lua(https://www.lua.org/)作為一種輕量級的腳本語言, 以其簡單的語法結構, 方便的c++集成能力, 高效的執行效率收到廣大游戲開發者的熱愛, 也是cocos2d-x官方首次引入的腳本語言.
作為一種腳本語言, lua是在一個運行時環境(State)里執行的, 這個運行時環境保存了腳本運行所需的內存空間, 創建的全局變量, 加載的庫文件等. 在這個運行時環境里還有一個棧空間(Stack), 其作用就是在lua和c語言進行數據傳遞和函數調用. lua原生實現了很多c api對棧空間進行操作, 讓開發者能夠方便地實現lua腳本代碼與c編譯代碼的雙向通信.
本文的主題是lua和c++混合編程, 但背后其實是lua和c api的互相調用, 所有c++的功能都要通過一層c函數的包裝, 這點是要牢記在心的, 這也正是lua-binding的核心.
cocos2d-x提供的lua-bingding工具使用libclang分析c++源碼, 提取語法樹, 將c++的類成員函數封裝為c函數, 然后根據參數類型自動調用lua c api, 實現對棧空間的操作, 將c++的數據傳遞給lua. lua腳本加載編譯好的c++庫, 就可以自由調用c++里面的類對象和成員函數了; c++的代碼則可以直接使用lua c api, 執行一段lua腳本, 并通過棧空間獲取返回結果.
操作實戰
本節我們來詳細介紹c++和lua混合編程的具體實現方法, 首先介紹如果利用cocos2d-x的工具自動把項目里的c++代碼導出為lua模塊, 然后介紹如果手動導出特殊類型的函數, 最后介紹實踐中的技巧和潛在隱患.
自動生成lua模塊
cocos2d-x提供的lua-binding工具位于項目 tools/tolua 目錄下, 可以看到里面有 genbindings.py, **.ini, userconf.ini等文件, 這些就是自定義代碼導出策略的配置文件.


- userconf.ini: 這個文件配置了系統運行的環境變量, 比如adk路徑, 使用系統默認值即可
- genbindings.py: 這是生成lua-binding代碼的腳本, 代碼最后的cmd_args參數配置了需要導出的不同lua模塊, 根據需要在這里添加條目即可. 括號里的第一個參數代表了模塊名(下文介紹), 第二個參數代表了生成文件的名字; 如果想要自定義生成目錄, 可以修改output_dir變量
- **.ini: 這里是導出配置的關鍵, 我們以cocos2dx.ini為例, 詳細介紹每一個部分的作用
- [cocos2d-x] 這里對應上文介紹的模塊名, 要和第一個參數保持一致
- prefix 給所有生成的c函數添加前綴, 防止命名重復
- target_namespace 導出的lua模塊的命名空間, 重要
- 接下來是一些編譯參數, 用于輔助libclang尋找頭文件, 設置宏參數, 如果發現clang出錯, 可以考慮修改這里的參數
- headers 這個參數可以設置一組頭文件, 程序根據這個頭文件及其包含的頭文件, 抽取出c++聲明的類, 作為導出對象, 重要
- classes 這個參數配置基于正則表達式的模式列表, 過濾上一個參數提取出的類, 得到最終導出的類列表, 重要
- skip 不想導出的類級函數, 用于處理一些和lua不兼容的c++參數, 比如 std::function, std::pair 等, 這些參數沒法通過棧空間傳遞給lua, 因此也就沒辦法導出給lua使用, 需要排除掉. 重要
- rename_functions, rename_classed 重命名導出的函數和類, 用處不大
- classes_have_no_parents, base_classes_to_skip, abstract_classes 一些小功能, 用處不大
設置完這個文件, 運行 genbindings.py 文件, 就可以看到生成的c++代碼了. 把這個代碼加入到項目中, 就可以在lua腳本里直接使用c++的相關功能了.
比如, c++里有這個一個類:
class GuideManager
{
public:
static GuideManager *getInstance();
public:
bool isAvailable(const std::string &id);
void addGuide(const std::string &id);
void finishGuide(const std::string &id);
bool isGuideFinished(const std::string &id);
void clear();
protected:
std::map<std::string, bool> _finishedGuides;
std::mutex _lock;
};
通過lua-binding得到導出類, 就可以在lua代碼里直接使用:
local manager = pp.GuideManager:getInstance()
if manager:isAvailable("12345") then
-- Show Guide
manager:finishGuide("12345")
end
通過上述簡單的配置, 就可以把項目里上百個類, 幾千個成員函數直接導出, 提供給lua使用. 在生成c++文件的同時, 程序還會生成一系列沒有實際功能的lua文件, 每一個文件對應一個導出類, 列出了這個類導出的所有函數以及參數類型, 方便開發者驗證導出的方法是否滿足預期, 同時可以交給第三方插件來輔助IDE進行代碼高亮與提示.
如果開發者更新了c++代碼, 只需要重新運行腳本, 更新導出文件即可.
以上操作就是cocos2d-x推薦給開發者使用的lua-binding方案, 可以在官方網站和網絡上找到豐富的教程, 這里不再深入展開.
手動導出lua模塊
在實際應用中, 手動導出一個功能模塊也是很重要的需求, 比如在c++里面實現了一個網絡庫, 通過傳遞一個std::function作為回調函數, 函數原型如下:
void get(const std::string &path, Json::Value ¶ms, std::function<void(NetworkResponse *)> callback);
但是lua里面的函數和c++的std::function并不兼容, 不能直接把lua的函數傳遞給c++使用, 因此lua-binding工具就不能自動生成代碼綁定了. 開發者需要手動實現參數的傳遞, 把lua函數轉換為c++的std::function.
為了克服這個困難, 我們先來看一下lua-binding是怎樣自動生成代碼的:
int lua_pocketpet_PetModel_getSkillById(lua_State* tolua_S)
{
// 1
int argc = 0;
PocketPet::PetModel* cobj = nullptr;
bool ok = true;
cobj = (PocketPet::PetModel*)tolua_tousertype(tolua_S,1,0);
// 2
argc = lua_gettop(tolua_S)-1;
if (argc == 1)
{
std::string arg0;
ok &= luaval_to_std_string(tolua_S, 2,&arg0);
if(!ok)
return 0;
// 3
PocketPet::SkillModel* ret = cobj->getSkillById(arg0);
object_to_luaval<PocketPet::SkillModel>(tolua_S, "pp.SkillModel",(PocketPet::SkillModel*)ret);
return 1;
}
return 0;
}
我們可以看出, lua調用c++代碼一共包含3步:
- 獲取c++對象
- 獲取參數, 校驗參數類型
- 調用成員函數
自動生成的代碼支持int, double等數值類型, 指針類型, std::string, std::map, std::vector, cocos2d::Map, cocos2d::Vector等模板類型, 超出這些范圍的, 就需要我們自己實現了. 參考上述代碼, 我們可以先實現以下這個函數:
int lua_pocketpet_NetworkManager_getInLua(lua_State* tolua_S)
{
int argc = 0;
PocketPet::NetworkManager* cobj = nullptr;
bool ok = true;
cobj = (PocketPet::NetworkManager*)tolua_tousertype(tolua_S,1,0);
argc = lua_gettop(tolua_S)-1;
if (argc == 3)
{
std::string arg0;
std::string arg1;
LUA_FUNCTION arg2;
ok &= luaval_to_std_string(tolua_S, 2,&arg0);
ok &= luaval_to_std_string(tolua_S, 3,&arg1);
// 1
arg2 = toluafix_ref_function(tolua_S, 4, 0);
if(!ok)
return 0;
cobj->get(arg0, arg1, arg2);
return 0;
}
return 0;
}
在這里, 我們首先通過 toluafix_ref_function 獲得一個LUA_FUNCTION(也就是 int)類型的lua函數指針, 將這個值作為參數傳遞給業務函數. 在業務函數里, 通過棧空間來回調這個函數指針, 如下所示:
void NetworkManager::get(const std::string &path, const std::string ¶ms, int callback)
{
auto func = [callback](NetworkResponse *response){
auto engine = LuaManager::getInstance()->engine();
engine->getLuaStack()->pushObject(response, "pp.NetworkResponse");
// 1
engine->getLuaStack()->executeFunctionByHandler(callback, 1);
};
Json::Value json;
this->get(path, json, func);
}
我們創建了一個std::function對象func作為lua函數的封裝, 在func內部, 通過lua棧空間調用lua回調函數. 通過這兩層的封裝, 就實現了把lua的函數作為c++的回調函數進行使用.
對于其他的特殊類型, 也都可以用類似的手段來解決.
其他技巧與潛在風險
通過lua-binding方案, 可以方便的把c++開發的功能導入到lua里面進行使用, 可以方便團隊從c++向lua轉型, 提高產品后期快速迭代更新的速度. 雖然現在鼓勵腳本開發, 但c++的應用無可避免, 比如渠道sdk, 比如跨平臺適配, 比如賬號安全維護等等, 都還是需要c++這把瑞士軍刀來應對一切挑戰. 在c++和lua之間, 除了通過棧空間傳遞數據, 我們還可以有多種機制來進行通信, 從而克服lua-binding的局限.
一種方案是開辟一塊專門的內存空間, 通過鍵值對存儲臨時對象, 雙方通過這塊共享空間傳遞必要信息. 這種方式可以靈活的傳遞復雜的數據, 同時可以應對異步調用c++的問題, 防止cocos2d-x對象因為跨幀被autorelease.
另一種方案可以使用消息分發, 通過數據對象的序列化與反序列化, 實現復雜數據的傳遞, 比如json對象, 但需要評估實現的性能損耗.
在使用lua-binding時, 還需要考慮線程執行的問題,如果涉及到多層回調以及ui刷新, 要確保內容的更新在主線程完成.
另外, 在c++中調用lua腳本會創建一個新的運行時環境, 不同運行時環境之間的數據是相互獨立的, 要格外留意腳本文件相關初始化工作是否正確執行.
總結
本文詳細介紹了使用cocos2d-x工具, 實現c++和lua混合編程的基本原理和實現方案, 希望對大家幫助.
浙公網安備 33010602011771號