城市精靈GO(http://csjl.teamtop3.com/)是一款基於cocos2d-x開發的LBS社交遊戲, 經過真實地圖的探索, 發現和抓捕隱匿於身邊的野生精靈, 利用遊戲中豐富的玩法提高和進化本身的精靈團隊, 一步一步成爲精靈訓練大師. html
本遊戲的開發混合使用了c++和lua編程, 既發揮了c++高性能, 跨平臺系統兼容的優點, 又享受了lua敏捷方便的開發效率. cocos2d-x提供了一套完備的lua-binding工具來幫助開發者實現c++和lua的代碼聯合, 能夠方便實現二者之間的數據通訊和代碼互調. 本文就從lua-binding入手, 深刻介紹c++和lua混合編程的相關細節, 探討其中可能存在的問題和發展. 本文原創發佈於博客園(http://www.cnblogs.com/dabaopku/p/5649294.html), 獨家受權XX轉載.c++
本遊戲開發基於cocos2d-x 3.2.0版本, 其餘版本用戶請祥閱官方文檔和源代碼.正則表達式
lua(https://www.lua.org/)做爲一種輕量級的腳本語言, 以其簡單的語法結構, 方便的c++集成能力, 高效的執行效率收到廣大遊戲開發者的熱愛, 也是cocos2d-x官方首次引入的腳本語言.編程
做爲一種腳本語言, lua是在一個運行時環境(State)裏執行的, 這個運行時環境保存了腳本運行所需的內存空間, 建立的全局變量, 加載的庫文件等. 在這個運行時環境裏還有一個棧空間(Stack), 其做用就是在lua和c語言進行數據傳遞和函數調用. lua原生實現了不少c api對棧空間進行操做, 讓開發者可以方便地實現lua腳本代碼與c編譯代碼的雙向通訊.json
本文的主題是lua和c++混合編程, 但背後實際上是lua和c api的互相調用, 全部c++的功能都要經過一層c函數的包裝, 這點是要牢記在心的, 這也正是lua-binding的核心.api
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模塊, 而後介紹若是手動導出特殊類型的函數, 最後介紹實踐中的技巧和潛在隱患.網絡
cocos2d-x提供的lua-binding工具位於項目 tools/tolua 目錄下, 能夠看到裏面有 genbindings.py, **.ini, userconf.ini等文件, 這些就是自定義代碼導出策略的配置文件.異步
設置完這個文件, 運行 genbindings.py 文件, 就能夠看到生成的c++代碼了. 把這個代碼加入到項目中, 就能夠在lua腳本里直接使用c++的相關功能了.ide
好比, 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方案, 能夠在官方網站和網絡上找到豐富的教程, 這裏再也不深刻展開.
在實際應用中, 手動導出一個功能模塊也是很重要的需求, 好比在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步:
自動生成的代碼支持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混合編程的基本原理和實現方案, 但願對你們幫助.