5.1さらうどん

@giginetの技術ブログ。ゲーム開発、iOS開発、その他いろいろ

cocos2d-xのLuaバインディングについて解説してみる

はじめに

最近、cocos2d-xというゲームフレームワークを利用してゲーム開発を行っています。


このフレームワークWindows/Linux/iOS/Android向けのゲームをC++で記述できる優れもの。

プラットフォーム間の差異をほとんど吸収してくれるので、何も考えなくとも移植性に優れたゲーム開発が行えます。

国内ではあんまり流行っていない感じですが、そのうちデファクトスタンダードになりそうな予感。


命名規則などはcocos2dを参考にしていたり、CocoaのNSObjectやNSArrayと似たような挙動をするクラスが移植されていたりと、Objective-Cの文化を知らないと書きにくいのが難点ですが、クロスプラットフォームのゲーム開発をしたいのであれば現状ではこれ一択な気がします。

特にObjective-C未経験者はCCObjectのメモリ管理周りでハマりそう。


cocos2d-xには、レベルデザインLuaで記述できるようなLuaエンジンが搭載されているのですが、公式ドキュメントや公式フォーラムはおろか、国内外でほとんど情報がない感じだったので弄ってみた知見をまとめてみた。おそらく国内初で国外でもほとんど情報ないです。


凄く凄くニッチな記事ですが、誰かの役に立てば幸いです。

Lua-C++連携の基本

LuaスクリプトC++で値をやりとりするためにはlua_Stateと呼ばれるモノを利用して、値のやりとりを行います。

lua_Stateは1つのスタックを持っていて、そのスタックに値をpush, popすることのみで値のやりとりが行えます。

以下の記事を読んで、なんとなく内容を把握しておくと良いでしょう。

Luaスタックの操作 - karetta.jp
Luaスタック上のテーブルの格納と参照 - karetta.jp
その2 Luaスクリプト事始め

cocos2d-xに実装されているLua Wrapper

cocos2d-xには以下のクラスが定義されています。(バージョン2.0.3時点)

CCLuaEngine

基本的なLua連携をラップしたエンジンです。

単にスクリプトを実行したり、Stateにpushしたり基本的なことがそれなりに行えます。詳しくはドキュメントを読んでください。

cocos2d-x: CCLuaEngine Class Reference

CCLuaValue

Luaの変数をラップしたオブジェクトです。

CCLuaValue::intValue()などを実行することで、型に応じて中身を取り出すことができます。

また、CCObjectを持たせることもできて、Luaと簡単にCCObjectのやりとりもできて良い。


詳しくはドキュメントを読んでください。

cocos2d-x: CCLuaValue Class Reference

CCLuaValueArray

std::listへのエイリアス。Arrayって名前なのにlistなのが使いにくい

CCLuaValueDict

std::mapへのエイリアス

Luaのテーブルを参照する

例えばLua側に以下のようなテーブルを定義したとします。

someTable = {
  hoge = 10,
  piyo = "string"
}

C++側からは以下のように取り出してあげると良いでしょう

CCLuaEngine* engine = CCLuaEngine::defaultEngine(); // デフォルトのengineを取り出す
string path = CCFileUtils::sharedFileUtils()->fullPathFromRelativePath("filename.lua"); // Luaスクリプトの絶対パスを取り出す
engine->executeScriptFile(path.c_str()); // Luaファイルを実行する
lua_State* L = engine->getLuaState(); // 現在のStateを取り出す
lua_getglobal(L, "someTable"); // someTableを取り出す
lua_getfield(L, -1, "hoge"); // someTableからfield:hogeを取り出す
int hoge = lua_tointeger(L, -1); // hogeの中身をintとして取り出す
lua_getglobal(L, "someTable"); 
lua_getfield(L, -1, "piyo"); // someTableからfield:piyoを取り出す
string piyo = lua_tostring(L, -1); // piyoの中身をstringとして取り出す

まず、lua_getglobalで"someTable"がStateの一番上にpushされます。


lua_getfield(L, -1, "hoge");で、State Lの一番上(-1)に積んであるtableからフィールド"hoge"を取り出し、Stateの一番上にpushします。


最後にlua_tointeger(L, -1)で、State Lの一番上(-1)に積んである値をintとして取り出しています。


テーブルの中にテーブルが含まれていたとき、再帰的にテーブルを読んでくれないみたいなので自分で実装してください。参考までに、僕はObjective-Cで書かれたKobold2DのKKLua::loadLuaTableFromFileをcocos2d-x向けに移植して使っています。

Luaの関数をC++から呼ぶ

基本的にやることは2つで

  • C++からLuaに渡す引数はLuaStateにpushしてあげる
  • 関数実行後に戻り値はStateにpushされるのでC++からpopする

例えば、Lua側でこのような関数があったとすると

function add(a, b)
   return a + b
end

cocos2d-x側からはこのように呼んでやればOK

CCLuaEngine* engine = CCLuaEngine::defaultEngine(); // デフォルトのengineを取り出す
string path = CCFileUtils::sharedFileUtils()->fullPathFromRelativePath("filename.lua"); // Luaスクリプトの絶対パスを取り出す
engine->executeScriptFile(path.c_str()); // Luaファイルを実行する
lua_State* L = engine->getLuaState(); // 現在のStateを取り出す
lua_getglobal(L, "add"); // globalからaddを取り出して、Stateにpushする
lua_pushinteger(L, 10); // 第1引数(a)に10をpush
lua_pushinteger(L, 20); // 第2引数(b)に20をpush
if (lua_pcall(L, 2, 1, 0)) { // 関数を実行
  // もし、実行時にエラーがあれば、エラーを出力
  cout << lua_tostring(L, lua_gettop(L)) << endl;
}
int returnValue = lua_tointeger(L, lua_gettop(L)); // 戻り値を取り出す
cout << returnValue << endl; // 30

ここでのミソは関数の実行を司るlua_pcallで、第1引数にState, 第2引数に引数の数、第3引数に戻り値の数を指定します。第4引数はエラー時に実行する関数を指定するようですが、0で大丈夫です。

lua_pcallは、Luaの実行時にエラーが出たときに、エラー文をpushして、戻り値1を返します。

そのため、上記のように記述しておくとエラーを出力することができます。


また、Luaは関数から複数個の戻り値を返すことができます。
例えば、3つの引数を全てintで返す場合、pcallの第3引数に3を指定した後、

int returnValue0 = lua_tointeger(L, -1);
int returnValue1 = lua_tointeger(L, -2);
int returnValue2 = lua_tointeger(L, -3);

のように受け取ってやれば良いです。


例えば、敵やアイテムの出現率、経験値テーブルなど、レベルデザインに直結するちょっとした計算を行いたいときに、関数をLuaで記述して、C++から呼んでやると良いでしょう。

C++のクラスやメソッドを公開し、Luaから使用する

cocos2d-xのLuaエンジンには、予めtolua++と呼ばれるC++Luaバインディングを良い感じでやってくれるライブラリが搭載されています。これを利用することで簡単にC++のクラスやメソッドをAPIとして公開して利用することができます。


cocos2d-xではLuaCocos2d.cppというファイルで、cocos2d-xの全APILuaに公開されており、Lua側から自由に利用することができるようになっているようです。ソースコードはバージョン2.0.3の時点で58000行にも及んでおり、力業でゴリゴリ書いてる感じ。すごい。

cocos2d-x/scripting/lua/cocos2dx_support/LuaCocos2d.cpp at gles20 · cocos2d/cocos2d-x


公開の方法はLuaCocos2d.cppとかを読めば良いですが、簡単にまとめておきます。細かい挙動はよくわかってないので察してください

class UserClass :public CCObject {
 public: 
  int someMethod(int arg) {
    return arg + 1;
  }
};

static void tolua_reg_types (lua_State* tolua_S) {
  tolua_usertype(tolua_S, "UserClass"); // UserClassクラスをregisterする
}

static int tolua_UserClass_someMethod(lua_State* tolua_S) {
  UserClass* self = (UserClass*)tolua_tousertype(tolua_S, 1, 0); // オブジェクトを取り出す
 int arg0 = (int)tolua_tonumber(tolua_S, 2, 0); // Luaから渡された1つめの引数を取り出す
  int return = self->someMethod(arg0); // UserClassオブジェクトのsomeMethodを実行する
  tolua_pushnumber(tolua_S, (int)return); // someMethodの実行結果をpushする
  return 1;
}

TOLUA_API int tolua_userdefine_open(lua_State* tolua_S) {
  tolua_open(tolua_S);
  tolua_reg_types(tolua_S);
  tolua_module(tolua_S, NULL, 0);
  tolua_beginmodule(tolua_S, NULL);
   tolua_constant(tolua_S, "SOME_CONSTANT", 10); // enumや定数を定義する
   tolua_cclass(tolua_S, "UserClass", "UserClass", "CCSprite", NULL); // UserClassをクラスとして割り当てる。第4引数にはスーパークラスのクラス名を記述する
   tolua_beginmodule(tolua_S, "UserClass"); // UserClassのメソッド定義開始
    tolua_function(tolua_S, "someMethod", tolua_UserClass_someMethod); // someMethodを登録する
   tolua_endmodule(tolua_S); // メソッド定義終了
  tolua_endmodule(tolua_S);
  return 1;
}

CCLuaEngine* engine = CCLuaEngine::defaultEngine(); // デフォルトのengineを取り出す
tolua_userdefine_open(engine->getLuaState()); // クラスを登録する

Lua側ではこういう関数を定義しておきます。

function executeSomeMethod(obj)
  -- objにはUserClass型のオブジェクトが渡される
  print(obj:someMethod(10)) -- 11
end

最後に、上記同様にC++側からexecuteSomeMethodを実行してやります

CCLuaEngine::pushCCObject(CCObject* obj);でCCObjectを簡単にLuaに渡せるため、関数の引数としてオブジェクトを簡単に渡すことができます。


これを上手く利用することで、アイテムの使用時の効果を全てLua側で記述するなど、レベルデザインとシステムの分離が行えます。良い。

まとめ

この記事に書いたことを大体理解しておけば、Lua側から基本的に何でもできるので良いです。あんまり頻繁にLuaを実行するとオーバーヘッドが気になる感じなのですが、それなりに高速に動作してくれるのであんまり気になりません。毎フレーム呼び出したりとかしなければ大丈夫かと。


今開発中のゲームでは、非プログラマLuaを覚えて貰ってレベルデザインをして貰っているのですが、比較的簡単に習得して貰えたようで良かった。試行錯誤もかなりやりやすくなるのでオススメです。


こういう技術を使って、レベルデザインとシステムを上手く分離して設計できると、プログラマの仕事が飛躍的に少なくなるのでプランナとプログラマ、お互いにとって幸せだと思います。