はじめに
最近、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時点)
CCLuaValue
Luaの変数をラップしたオブジェクトです。
CCLuaValue::intValue()などを実行することで、型に応じて中身を取り出すことができます。
また、CCObjectを持たせることもできて、Luaと簡単にCCObjectのやりとりもできて良い。
詳しくはドキュメントを読んでください。
CCLuaValueArray
std::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つで
例えば、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の全APIがLuaに公開されており、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を覚えて貰ってレベルデザインをして貰っているのですが、比較的簡単に習得して貰えたようで良かった。試行錯誤もかなりやりやすくなるのでオススメです。
こういう技術を使って、レベルデザインとシステムを上手く分離して設計できると、プログラマの仕事が飛躍的に少なくなるのでプランナとプログラマ、お互いにとって幸せだと思います。