玩轉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.cnblogs.com/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 &params, 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步:

  1. 獲取c++對象
  2. 獲取參數, 校驗參數類型
  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 &params, 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混合編程的基本原理和實現方案, 希望對大家幫助.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM