スクリプティングでCocos2d-x高速開発

お初にお目にかかります。
自宅のエスプレッソマシンの調子が悪く、オーバーホールに出そうか悩み中のカフェマスターです。
舞鶴鎮守府の兼業提督でもあります。

コードの修正を反映するたびに、アプリ再ビルド、転送、起動の待ち時間がかかりますがとっても無駄ですよね?
書いたコードが即反映されれば、集中力も切れないし、生産性向上は間違いなし。

ということで、今回は
iOS環境でシミュレータを起動したまま、コードをガシガシ書いていくためのプロジェクト雛形作成方法
ご紹介したいと思います。

もくじ

  1. 環境構築
  2. プロジェクトセットアップ
  3. C++側でスクリプトを動的リロードする仕組みを作成
  4. スクリプトからC++を呼ぶためのグルーコード生成
  5. スクリプトからC++を呼んで動的リロード

環境構築

前提条件として、MacにHomebrewとcocos2d-x 2系をインストール済みの環境を想定しています。

Luaパッケージマネージャセットアップ

Homebrewを使ってLua関連パッケージをインストールします。
luarocksはrubyのgemに相当するパッケージマネージャです。

brew install lua luajit luarocks

MoonScriptセットアップ

Luaはクラスの概念がなかったり初めて扱うには取っ付きづらいのでここではCofeeScript風の文法で記述でき、
LuaコンパイルできるMoonScriptを使うことにします。

文法はこちらの公式サイトで

luarocks install moonscript

開発

プロジェクトセットアップ

サンプルプロジェクト作成

cd <path_to>/tools/project-creator
./create_project.py -project sample -package com.example -language lua

C++側でスクリプトを動的リロードする仕組みを作成

ここから実際のコーディングを行っていきます。
まずは、C++側でスクリプトを動的にリロードしてシーンを再スタートさせる仕組みを作ります。

スクリプトリロード・シーン管理用クラスを作成

SceneManager.h

class SceneManager {
private:
static void loadScripts();
static void initGameScene();

public:
static void startGameScene();
static void restartGameScene();
};

SceneManager.cpp

#include &quot;SceneManager.h&quot;
#include &quot;CCLuaEngine.h&quot;
#include &quot;cocos2d.h&quot;

using namespace std;
using namespace cocos2d;

static string scriptFiles[] = {
&quot;lua/App.lua&quot;,
&quot;lua/TitleScene.lua&quot;
};

void SceneManager::loadScripts() {
CCLuaEngine* pEngine = CCLuaEngine::defaultEngine();
string path;

for (int i = 0; i &lt; sizeof(scriptFiles) / sizeof(scriptFiles[0]); i++) {
path = CCFileUtils::sharedFileUtils()-&gt;fullPathForFilename(scriptFiles[i].c_str());
pEngine-&gt;executeScriptFile(path.c_str());
}
}

void SceneManager::initGameScene() {
SceneManager::loadScripts();

CCLuaStack*stack = CCLuaEngine::defaultEngine()-&gt;getLuaStack();
lua_State* L = stack-&gt;getLuaState();
lua_pcall(L, 1, 1, 0);
}

void SceneManager::startGameScene() {
CCScene* scene = CCScene::create();
CCDirector::sharedDirector()-&gt;runWithScene(scene);
SceneManager::initGameScene();
}

void SceneManager::restartGameScene() {
CCDirector::sharedDirector()-&gt;popToRootScene();
SceneManager::initGameScene();
}

スクリプトからC++を呼ぶためのグルーコード生成

先ほど作成したSceneManagerをスクリプト側から呼び出せるようにします。

tolua++を使うための定義ファイルを作成します。

Classes/tolua_glue/bind.pkg

$#include &quot;SceneManager.h&quot;

class SceneManager {
static void startGameScene();
static void restartGameScene();
};

定義ファイルを元にtolua++でグルーコードを作成します。 tolua++はCocos2d-xのtoolsディレクトリ下のものをそのまま利用します。

unzip <path_to>/tools/tolua++/tolua++.Mac.zip
cd <path_to>/projects/sample
../../tools/tolua++/tolua++ -n bridge -o Classes/tolua_glue/Bridge.cpp -H Classes/tolua_glue/Bridge.h Classes/tolua_glue/bind.pkg

AppDelegateでアプリのエントリポイントを切り替えます。

AppDelegate.cpp

- std::string path = CCFileUtils::sharedFileUtils()-&gt;fullPathForFilename(&quot;hello.lua&quot;);
- pEngine-&gt;executeScriptFile(path.c_str());
+ SceneManager::startGameScene();

グルーコードについての詳細はこちらのサイトでご覧ください。

スクリプトからC++を呼んで動的リロード

SceneManager.cppで読み込んでいた "App.lua" の元ファイルを作成します。

App.moon

export App
export start

SCENE_STATE = {
TITLE: 0
GAME: 1
}

class App
@sceneState = nil

startTitleScene: =>
@sceneState = SCENE_STATE.TITLE
CCDirector\sharedDirector()\pushScene TitleScene\create()

changeGameScene: =>
@sceneState = SCENE_STATE.GAME
CCDirector\sharedDirector()\replaceScene GameScene\create()

restartGame: =>
// SceneManager.cppを呼び出し
SceneManager\restartGameScene()

start = ->
App\startTitleScene()

restartGameのSceneManager\restartGameScene()でスクリプトをリロードし、 App\startTitleScene()が再度呼ばれるようになります。

TitleScene.moon

export TitleScene

class TitleScene
@layer = nil

create: =&gt;
scene = CCScene\create()
@layer = @initLayer()
scene\addChild @layer

initLayer: =&gt;
layer = CCLayer\create()
bg = CCSprite\create “images/title.png”
size = CCDirector\sharedDirector()\getWinSize()
bg\setPosition size.width/2, size.height/2
layer\addChild bg

reloadButton= CCMenuItemImage\create “images/reload.png&quot;, “images/reload.png”
reloadButton\setPosition 0, 0
// スクリプト再読み込みボタン
reloadButton\registerScriptTapHandler -&gt;
App\restartGame()

startButton= CCMenuItemImage\create &quot;images/start.png&quot;, &quot;images/start.png&quot;
startButton\setPosition size.width/2, size.height/2 + 100
// ゲーム開始ボタン
startButton\registerScriptTapHandler -&gt;
App\changeGameScene()

buttons = CCArray\createWithObject startButton
buttons\addObject reloadButton
menu = CCMenu\createWithArray buttons
layer\addChild menu

layer

MoonScriptをLuaコンパイル

最後にMoonScriptをLuaコンパイルして完成です。

moonc -w -t /Users/<username>/Library/Application\ Support/iPhone\ Simulator/6.1/Applications/<app_uuid>/Documents/lua .

moonc -w でMoonScriptの更新を検知して自動でコンパイルしてくれます。
Luaの出力先としてiOSシミュレータのアプリケーションのDocuments下に直接書きだします。
こうすることでScenemanager.cppの動的リロード時に最新のスクリプトファイルを読み込むことになります。
実機で動的リロードする仕組みはHTTP serverをアプリ内で動かして
postで差分を投げつける方法で対応可能です。応用編として試してみてください。

まとめ

さて、気になるのはどうれくらい効率化されるか?ですが、
当方が担当したプロジェクトでは直前の素のcocos2d-xで作ったプロダクト(両方エンジニアは私1名)に比べて
1.5倍~2倍の成果が出せていました。

今回は触れていませんが、コールバックが簡潔に書けるため、イベントハンドラなどを記述しだすとさらに威力が発揮されます。
設定ファイルをyaml形式でさくっと記述し、即リロードしてゲームに反映することでレベルデザインも捗りますよ。
さらにチーム開発を加速するためにディレクターが設定値を編集出来るように仕組み化していくと面白いですね。