こんにちは、エンジニアの成田です。
Cocos2d-xはiOS、Android、WindowsPhoneなどクロスプラットフォームのゲームをC++で開発できることが大きな特徴ですが、C++以外にもJavascriptバインディングやLua>バインディングがあることはご存知でしょうか?
今回はC++からLuaを呼び出し、Cocos2d-xのAPIを操作してみます。
1.なぜスクリプト言語を用いるのか
実装に入る前に、Luaの生い立ちなど細かいことはWikipediaに譲るとしまして、JavascriptもLuaもスクリプトです。なぜゲームでスクリプト言語を用いるのでしょうか。
第一は、ゲームから変数やゲームデータ(e.g.シナリオ、ステージ毎のオブジェクト配置)を外部に分離することができます。ゲーム本体側からはXMLやJSONを読み込む際に必要になるパーサを書く必要はなく、スクリプト言語を呼び出すだけでOKです。
第二に生産性の向上があります。開発が進んでいくとゲームは通常巨大化していき、ビルドに時間を要するようになっていきます。少しパラメータを変更しただけなのに毎度ビルドを行わなければいけないのは大変な苦痛ですが、パラメータがスクリプトという形で外部化されていれば再ビルドの必要はなくなります。
第三には、書いたり理解するのが比較的簡単です。C++のような汎用的な低級言語と比べ、スクリプト言語は自由で理解しやすく、また別段コンパイラもIDEも必要ないため、デザイナー・プロデューサーなど非プログラマが調整を行いたい時に自分で触ることもできます。
これらのメリットのため、スクリプト言語はコンシューマゲームで利用実績が多数あります。
逆にデメリットとしては、C/C++のようなネイティブコードと比較して実行速度が遅い、スクリプト部分がアプリケーション中にテキストファイルとして含まれるため、ハックされやすい等が挙げられます。
2.実装
それではCocos2d-xのLuaバインディングを試してみましょう。
今回は、まずC++ベースのCocos2d-xプロジェクトを作成してから、そこへライブラリを追加することでLuaプログラムを動せるようにします。
これにより、C++でのCocos2d-x環境からLuaを動かせるようになるまでの要件が明白になるようにしたいと思います。
では C++ベースのCocos2d-xプロジェクトを作成します。「cocos2dx」テンプレートを選択してください。
下に「cocos2dx_Lua」というのが見えますが今回は選択しません。
C++のHello worldアプリケーションが作成されたと思います。
Luaプログラムを呼び出すためには、まずCocos2d-xのディレクトリからLuaヘッダ群を持ってくる必要があります。
以下ではCocos2d-xのインストールディレクトリを[cocos2dx]、プロジェクトのルートディレクトリを[project-root]とします。
- [project-root]/libs 下に、新たにLuaディレクトリを作成します。
- ヘッダ群をコピーします。
- これらのディレクトリをプロジェクトに追加します。ただし、cocos2dx_supportディレクトリに関しては、次のファイルだけを追加するようにします。
[cocos2dx]/scripting/Lua/cocos2dx_support を [project-root]/libs/Lua/cocos2dx_support へコピー
[cocos2dx]/scripting/Lua/luajit/include を [project-root]/libs/Lua/luajit/include へコピー
[cocos2dx]/scripting/Lua/luajit/iOS を [project-root]/libs/Lua/luajit/iOS へコピー
[cocos2dx]/scripting/Lua/tolua を [project-root]/libs/Lua/tolua へコピー
CCLuaBridge.cpp
CCLuaBridge.h
CCLuaEngine.cpp
CCLuaEngine.h
CCLuaStack.cpp
CCLuaStack.h
CCLuaValue.cpp
CCLuaValue.h
Cocos2dxLuaLoader.cpp
Cocos2dxLuaLoader.h
LuaCocos2d.cpp
LuaCocos2d.h
platform/iOS/CCLuaObjcBridge.h
platform/iOS/CCLuaObjcBridge.mm
tolua_fix.c
tolua_fix.h
これで準備ができました。
最終的にこのようなディレクトリ構造になっているかと思います。
それではLuaプログラムを書いてみましょう。まずは簡単な足し算を行ってみます。
[project-root]/Resources に hello.Lua ファイルを追加し、次のように記述します。
function add(x, y) return x + y end
引数xとyを加算して返すだけの単純な関数addです。
HelloWorldScene.cpp に#include文を追記し、init()に関数addの呼び出しを追加します。
#include "script_support/CCScriptSupport.h" #include "CCLuaEngine.h"
// on "init" you need to initialize your instance bool HelloWorld::init() { ////////////////////////////// // 1. super init first if ( !CCLayer::init() ) { return false; } ///////////////////////////// // 2. add a menu item with "X" image, which is clicked to quit the program // you may modify it. // add a "close" icon to exit the progress. it's an autorelease object CCMenuItemImage *pCloseItem = CCMenuItemImage::create( "CloseNormal.png", "CloseSelected.png", this, menu_selector(HelloWorld::menuCloseCallback) ); pCloseItem->setPosition( ccp(CCDirector::sharedDirector()->getWinSize().width - 20, 20) ); // create menu, it's an autorelease object CCMenu* pMenu = CCMenu::create(pCloseItem, NULL); pMenu->setPosition( CCPointZero ); this->addChild(pMenu, 1); ///////////////////////////// // 3. add your codes below... // add a label shows "Hello World" // create and initialize a label CCLabelTTF* pLabel = CCLabelTTF::create("Hello World", "Thonburi", 34); // ask director the window size CCSize size = CCDirector::sharedDirector()->getWinSize(); // position the label on the center of the screen pLabel->setPosition( ccp(size.width / 2, size.height - 20) ); // add the label as a child to this layer this->addChild(pLabel, 1); // add "HelloWorld" splash screen" CCSprite* pSprite = CCSprite::create("HelloWorld.png"); // position the sprite on the center of the screen pSprite->setPosition( ccp(size.width/2, size.height/2) ); // add the sprite as a child to this layer this->addChild(pSprite, 0); //////// ここまでサンプルコードのまま //////// // Luaスクリプトの呼び出し CCLuaEngine* pEngine = CCLuaEngine::defaultEngine(); CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine); std::string path = CCFileUtils::sharedFileUtils()->fullPathForFilename("hello.lua"); pEngine->executeScriptFile(path.c_str()); lua_State* L = pEngine->getLuaStack()->getLuaState(); lua_getglobal(L, "add"); // グローバル関数addをgetしてpush lua_pushinteger(L, 1); // 1をpush lua_pushinteger(L, 2); // 2をpush if (lua_pcall(L, 2, 1, 0)) { // 戻り値は関数呼び出しが成功するとき0、エラーは0以外 CCLOG("error=%s", lua_tostring(L, lua_gettop(L))); // エラーメッセージをget } else { CCLOG("return=%d", lua_tointeger(L, lua_gettop(L))); // 戻り値をget } return true; }
実行してみましょう。問題がなければコンソールに次のような出力が得られます。
Cocos2d: return=3
これで C++からLuaへ引数を渡し、Luaからの戻り値を C++で取得できることが確認できました。
ですがまだLuaスクリプトではプリミティブ型の計算しか行っていません。今度こそCocos2d-xのLuaバインディングを使用してみましょう。
hello.Lua ファイルに次のような関数を追加します。
function helloLua(layer) -- CCLabelTTFを生成して… local label = CCLabelTTF:create("Hello Lua!", "Thonburi", 34) -- CCLayerの中央へ配置 label:setPosition(layer:getContentSize().width/2, layer:getContentSize().height/2) layer:addChild(label) end
HelloWorldScene.cpp も変更します。
#include "script_support/CCScriptSupport.h" #include "CCLuaEngine.h" #include <tolua++.h> // tolua_pushusertypeに必要
// サンプルコード部分は省略 //////// ここまでサンプルコードのまま //////// // Luaスクリプトの呼び出し CCLuaEngine* pEngine = CCLuaEngine::defaultEngine(); CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine); std::string path = CCFileUtils::sharedFileUtils()->fullPathForFilename("hello.lua"); pEngine->executeScriptFile(path.c_str()); lua_State* L = pEngine->getLuaStack()->getLuaState(); lua_getglobal(L, "helloLua"); // グローバル関数helloLuaをgetしてpush tolua_pushusertype(L, this, "CCLayer"); // CCLayerもpushできる! if (lua_pcall(L, 1, 1, 0)) { // 引数は1つ、戻り値も1つ、エラーハンドラはなし(0) CCLOG("error=%s", lua_tostring(L, lua_gettop(L))); } return true; }
正常に実行できれば次のような画面になります。
シーンは C++側で生成し、画面中央のテキストラベルをLua側で生成できました。
このように C++とLuaで密接に連携することもできます。
いかがでしたでしょうか。
ゲームの生産性や拡張性向上に寄与するスクリプト言語。みなさんも積極的に使ってみてください。