読者です 読者をやめる 読者になる 読者になる

プログラミングの魔物

エラー、バグ、仕様変更と戦うブログ

cocos2d-x LuaでCCNodeにLuaのコールバック関数を登録する方法

cocos2d-xでLua関数をコールバックする時の参考用にメモ。

ソースを読んでみるとCCNodeは最初からLua関数をコールバックできるように作られている。
ここに説明が書いてある(しかしサンプルは古い様子)
http://www.cocos2d-x.org/projects/cocos2d-x/wiki/New_Lua_Engine_Documents

//nHandlerは関数を識別するための値でtoluaがつける。lua側は普通に関数を渡せる
void CCNode::registerScriptHandler(int nHandler)

//コールバックが呼ばれる際に渡される列挙型(呼ばれるタイミング)
//スクリプトエンジンによって呼ばれる値が異なる。m_eScriptType == kScriptTypeJavascriptなどで判別されている
enum {
    kCCNodeOnEnter,
    kCCNodeOnExit,
    kCCNodeOnEnterTransitionDidFinish,
    kCCNodeOnExitTransitionDidStart,
    kCCNodeOnCleanup
};

//コールバックを呼び出してる場所
CCScriptEngineManager::sharedManager()->getScriptEngine()->executeNodeEvent(this, kCCNodeOnEnter);

シーンのonEnterとonExitはシーンの開始時、シーンの終了時に呼ばれる
CCNodeの場合onEnterはシーンの子に入った時、onExitはシーンから削除された時にも呼ばれる(タイミングは複数あるものの、呼ばれるのはそれぞれ1度だけ)

local function createScene()
    local scene = CCScene:node()

    --コールバックを用意して
    local function sceneEventHandler(eventType)
        --呼び出されたイベントのタイプを判別して実行
        --if eventType == kCCNodeOnEnter then  --サンプルのままだと認識できないのでコメントアウト
        if eventType["name"] == "enter" then
            if scene.onEnter then scene:onEnter() end
        else
            if scene.onExit then scene:onExit() end
        end
    end

    --コールバックの登録
    scene:registerScriptHandler(sceneEventHandler)

    return scene
end

local scene = createScene()
function scene:onEnter()
    print("on scene enter")
end
fucntion scene:onExit()
    print("on scene exit")
end

CCDirector:sharedDirector():runWithScene(scene)

新しくコールバックの機構を用意しなくても列挙型の最高値である4をインクリメントした値でイベントを呼び出せば(あるいは列挙型そのものを書き換えれば)CCNodeにコールバック関数を登録して任意のタイミングで呼び出すことが出来そう。


(サンプル修正前に)実際に呼び出してみるとeventTypeはテーブルになっていて、
調べてみると"name"フィールドにenterという値が入っていた。

print(eventType["name"])  --> enter

どこかでkCCNodeOnEnterと"enter"という文字列の関連付けをしているだろうと思い検索してみるとCCLuaEngine.cppのexecuteNodeEventで行われていた。

int CCLuaEngine::executeNodeEvent(CCNode* pNode, int nAction)
{
    int ret = 0;
    do 
    {
        int nScriptHandler = pNode->getScriptHandler();
        CC_BREAK_IF(0 == nScriptHandler);

        cleanStack();
        CCLuaValueDict dict;
        if (nAction == kCCNodeOnEnter)
        {
            dict["name"] = CCLuaValue::stringValue("enter");
            pushCCLuaValueDict(dict);
            ret = executeFunctionByHandler(nScriptHandler, 1);
        }
        else if (nAction == kCCNodeOnExit)
        {
            dict["name"] = CCLuaValue::stringValue("exit");
            pushCCLuaValueDict(dict);
            ret = executeFunctionByHandler(nScriptHandler, 1);
        }
    } while (0);
    return ret;
}

CCLuaEngineを拡張しないとenterとexit以外のイベントはスルーされる。

コールバックを追加したCCNode派生クラスを作る

  1. kCCNodeOnEnterを含むenumに適当な列挙子を追加する
  2. CCLuaEngineのexecuteNodeEventに上記の列挙子の処理を追加する(kCCNodeOnExitのelseifをコピーして書き換え)
  3. tolua++でCCNode派生クラスを作成し、処理したいイベントでexecuteNodeEventを呼ぶ
  4. LuaエンジンからLuaステートを取得して上記の派生クラスをオープンする

そもそもLua関数をコールバックしようとした理由は、デバッグ用にvisit()でLua関数を呼んでccDrawRect()で当たり判定を描画するためである。
しかし、これLuaへ移植したほうがCCLuaEngineやCCNodeを書き換えない分楽そう。


追記:DebugDrawを移植してみたが、リアルタイムに当たり判定を描画するのには向かないので、結局DebugDrawのdrawにコールバックを追加することにした。

cocos2d-xがLua関係をビルドするときのPHPスクリプトについて触れているトピックスを見つけた。
http://www.cocos2d-x.org/boards/11/topics/13736
build.phpを見てみたところ、LuaCocos2dを作成するためのスクリプトのようだった。
cocos2d-xのLuaスクリプトエンジンをビルドする時に、Luaの関数とCのint型を関連付けたりする箇所など、手作業で1つずつ修正すると面倒な部分を自動化しているらしい。

cocos2d-xの修正を必要としないLua関数コールバックの作成

CCNodeのregisterScriptHandlerを使わずにコールバックを実装する場合、build.phpによる修正も考慮する必要がある。
とりあえずテストとしてDebugDrawで独自のコールバックを実装してみた。
LuaCocos2dはビルドに時間がかかりそうなことと、バージョンアップ時の手間を考えて、独自のライブラリは個別にオープンする形にしている。

今回はbuild.phpによる修正部分を手作業で行った。
スクリプトハンドラを登録する関数の引数の型はヘッダ側はintでpkg側はLUA_FUNCTIONにする。
グルーコードを生成し、以下の部分を書き換える。

/* グルーコード */
//     (tolua_isvaluenil(tolua_S,2,&tolua_err) || !tolua_isusertype(tolua_S,2,"LUA_FUNCTION",0,&tolua_err)) ||
     (tolua_isvaluenil(tolua_S,2,&tolua_err) || !toluafix_isfunction(tolua_S,2,"LUA_FUNCTION",0,&tolua_err)) ||
//LUA_FUNCTION funcID = *((LUA_FUNCTION*)  tolua_tousertype(tolua_S,2,0))
  LUA_FUNCTION funcID = (  toluafix_ref_function(tolua_S,2,0));

※pkgの先頭に$#include "LuaCocos2d.h"をつけておくことにより、toluafix_ref_functionを呼んでも怒られずに済む。

コールバックの呼び出し部分は以下の様な形。
なんとなくCCLuaEngine::defaultEngine()より良い呼び出し元があるような気がしないでもない。

  if (handler){
    CCLuaEngine::defaultEngine()->cleanStack();
    CCLuaEngine::defaultEngine()->executeFunctionByHandler(handler, 0);
  }

これでcocos2d-xを変更せずにLua関数のコールバックで当たり判定を描画できるようになった。