AdventCalender2016 15日目を担当するyuuです。
今回は、制作したADVゲームをAndroidに移植するにあたり、cocos2d-xを使ってみて感じた厄介な点や便利な点を記したいと思います。
お品書き
- cocos2d-xとは
- 環境構築
- Cocos2d-xのプロジェクトを作成する
- Cocos2d-xのプロジェクトをEclipseを使って編集・実行する
- cocos2d-xの仕様と、移植する上での留意点
- ADVゲームを移植した話
この記事で説明すること
- cocos2d-xのWindowsでの環境構築のやり方
- cocos2d-xのプロジェクトの作り方
- cocos2d-xの簡単な仕様と仕組み
- Javaのゲームを移植した時の体験談と考え方
この記事で説明しないこと
- cocos2d-xの詳細な仕様
- cocos2d-xの便利関数の紹介
cocos2d-xとは
Cocos2d-xは世界中の何千人もの開発者が使用するオープンソースのクロスプラットフォームのゲーム開発ツールです。
主にC++によって記述され、Windows、Mac、Android、iOSのいずれにも対応します。また、オープンソースのため、処理の流れを実際に追える上、必要とあらば改造することも可能です。
他方で、Android SDKなどによる機種固有の処理をC++から呼び出すのは割と難儀します。
それでは、以下でWinodows環境でのCocos2d-x開発環境の構築の手順を見ていきましょう。必要でない方は飛ばしてください。
環境構築
これが七面倒です。環境構築の方が、実際にゲームを作るよりも時間がかかった程度には。
まず、cocos2d-xそのものの環境構築には、以下のものが必要です。
cocos2d-xの環境構築に必要なもの
- cocos2d-x本体
- Python 2.x系
まず、cocos2d-xの本体は、ここでダウンロードしてください。なお、筆者のバージョンは3.13系になります。ダウンロードしたcocos2d-x-3.13.1.zipを解凍して出来たcocos2d-x-3.13.1フォルダをC:の直下に置きましょう。Cドライブ直下にするのは、パスに日本語名が混じらないようにするためです。
続いて、cocos2d-xのセットアップに必要なPythonをここのDownload Python 2.7xの方のボタンを押して、ダウンロードしてください。Download Python 3.xではうまくいきません。ダウンロードしたインストーラーを起動して、以下の画面が出るまでNextを押します。
この画面が出たら、赤い×が表示されている所をクリックして、「Will be installed on local hard drive」を選びます。その後、再びNextを押してインストールを開始します。インストールが終わったら、コマンドプロンプトで「python --version」と打ってみてください。「Python 2.7.x」と表示されていれば成功です。
ここまでで、一応cocos2d-xの環境は作れます。しかし、Androidの開発環境を構築するために、さらに以下の行程を踏む必要があります。(Android開発環境は必要ないという方は飛ばしてください)
Android開発環境に必要なもの
- JDK
- Android SDK
- Android NDK
- Apache Ant
まず、Javaの開発環境であるJDKが必要です。ここからダウンロードしてください。インストール後、環境変数に、JAVA_HOMEを追加し、C:\Program Files\Java(インストールしたJDKのフォルダ名)を設定してください。
次は、Android SDKをダウンロードします。このページの一番下までスクロールして、「コマンドライン ツールのみ入手する」にある「installer_r(バージョン)-windows.exe」をダウンロードします。ダウンロードしたインストーラーを起動して、指示通りに進めます。ただ、パスの設定では、Cドライブの直下にそろえた方が分かりやすいかもしれません。
続いて、Android NDKをダウンロードします。ここからダウンロードできます。注意していただきたいのは、最新版ではなく、r10cが必要だということです。筆者の環境では、r12bやr9dでは動きませんでした。ダウンロードしたNDKのr10cを解凍し、Cドライブ直下に置きます。
最後に、Apache Antをダウンロードします。ここから「apache-ant-(バージョン)-bin.zip」をダウンロードしましょう。後は、Android NDKと同じく、ダウンロードした圧縮ファイルを解凍し、Cドライブ直下に置きます。
以上で、Android開発環境は整いました。では、実際にcocos2d-xをセットアップしていきます。
cocos2d-xのセットアップ
まず、コマンドプロンプトでcocos2d-x本体のフォルダに入ります。指示通りならば、C:\cocos2d-x-3.13に入ることになります。ここで、「setup.py」と打ち込みます。
「Please enter the path of (環境変数名) (or press Enter to skip):」と表示されるので、それぞれ、以下のように入力してください。
環境変数名 | 入力する値 | 入力例 |
---|---|---|
NDK_ROOT | ダウンロードしたAndroid NDKのフォルダ | C:\android-ndk-r10c |
ANDROID_SDK_ROOT | ダウンロードしたAndroid SDKのフォルダ | C:\android-sdk |
ANT_ROOT | ダウンロードしたApache Antのbinフォルダ | C:\apache-ant-1.9.7-bin\apache-ant-1.9.7\bin |
Cocos2d-xのプロジェクトを作成する
コマンドプロンプトで、以下のコマンドを入力してください。「cocos new [GameName] -p [Package] -l cpp -d [Directoy]」
GameNameには、そのゲームの名前を入力します。
Packageには、そのゲームのパッケージ名を入力します。特に思いつかないのであれば、「com.company.GameName」のようにすればいいでしょう。
Directoryには、そのプロジェクトを格納するフォルダを入力します。「C:\Cocos」と入力した場合、「C:\Cocos\GameName」というプロジェクトが作成されます。
作成されたプロジェクトの中身
作成されたプロジェクトには、様々なフォルダができています。この中で、必要そうなものだけ紹介しておきます。
[Classes]:ゲームのコードが格納します。基本的に、ソースはここで統一されています。
[cocos2d]:cocos2d-xに必要なファイルが格納されています。このフォルダ内のcocosサブフォルダに、cocos2d-xのソースが格納されています。必要であれば、このソースを書き換えることで、cocos2d-xを改造することができます。
[proj.android]:Android開発のためのファイルが格納されています。正常にビルドするために、「jni/Android.mk」と「bin/AndroidManifest.xml」を書き換える必要があるかもしれません。
[proj.win32]:Windowsソフト開発のためのファイルが格納されています。Visual Studioを使って「GameName.sln」を開くことができます。
[Resources]:画像や音声ファイルなどのリソースファイルを格納します。
それでは、最後にIDEを開発環境として用意してみましょう。
Cocos2d-xのプロジェクトをEclipseを使って編集・実行する
Eclipseを使って、cocos2d-xのプロジェクトを編集し、実行してみます。Eclipseが無い方は、ここからダウンロードしてください。
まず、EclipseにおけるAndroid開発環境を整えましょう。
Help -> Install New Software.. (ヘルプ -> 新規ソフトウェアのインストール)から、Work with(作業対象)の右側にあるAdd(追加)ボタンをクリックします。
Name(名前)に「Android Development Tools Plugin」、
Location(ロケーション)に「「http://dl-ssl.google.com/android/eclipse/」を入力して、OKを押してください。
Developer Tools(開発ツール)と表示されたら、全てを選択します。その後、Next(次へ)を押して、そのまま同意画面を経てインストールまで進みます。
続いて、Window -> Preferences(ウィンドウ -> 設定)のAndroidに移動します。SDK Location(SDK ロケーション)に、先ほどダウンロードしたSDKのパス(例:C:\android-sdk)を入れておきます。
その後、Window -> Android SDK Manager(ウィンドウ -> Android SDK Manager)を選択し、Android SDK Managerを起動します。ここで、Toolsと最新のAndroid SDKを選択し、右下のInstall n packages(nはインストールするパッケージ数)をクリックします。後は同意して、インストールしてください。
では始めに、プロジェクトのビルドは基本的にコマンドプロンプトで行うということに注意してください。コマンドプロンプトで、プロジェクトのフォルダまで入った後、「cocos compile -p android」と打ち込むと、プロジェクトがビルドされます。これを行わないと、Eclipseでソースを書き換えたのに、変更が反映されないといった症状が発生します。
それでは、Eclipseでプロジェクトをインポートしてみましょう。
ツールバーから、File -> New -> Project...(ファイル -> 新規 -> プロジェクト)を選びます。現れたダイアログで、Android -> Android Project from Existing Code (Android -> 既存コードからのAndroidプロジェクト)を選びます。
Root Directory(ルート・ディレクトリー)に、プロジェクトのパスを入力すると、Projectsにたくさんのプロジェクトが出ると思います。その中から、「cocos2d\cocos\platform\android\java」、「cocos2d\cocos\platform\android\libcocos2dx」、「proj.android」を選んでください。完了して、パッケージエクスプローラーに二つのファイルが追加されていれば成功です。
アプリケーションの実行には、パッケージエクスプローラーのlibcocos2dxでない方(GameNameかproj.android)を右クリックし、Debug as -> Android Application (デバッグ -> Androidアプリケーション)を選択してください。
ここで、Androidアプリケーションを実行しようとすると、エラーが出るか、変更が反映されないことがあります。そういう時は、以下のことを試してみてください。
proj.android/jniディレクトリにある「Android.mk」ファイルを開き、LOCAL_SRC_FILESに、ソースファイルをすべて記述します。
//BaseScene.cppを追加する場合
LOCAL_SRC_FILES := hellocpp/main.cpp \
../../Classes/AppDelegate.cpp \
../../Classes/HelloWorldScene.cpp \
../../Classes/BaseScene.cpp
続いてproh.androidディレクトリにある「AndroidManifest.xml」ファイルを開く、以下の部分を修正します。
//修正前
android:configChanges="orientation|keyboardHidden|screenSize">
//修正後
android:configChanges="keyboard|keyboardHidden|orientation">
cocos2d-xの仕様と、移植する上での留意点
それでは、cocos2d-xの開発環境が構築できたところで、cocos2d-xの仕様と移植する際の留意点を話しておきます。移植した話だけを見たい方は飛ばしてください。
まず、JavaからC++11への移植な上、ツールを使用してのものであったため、コードは全換装されることになります。また、バリバリのオブジェクト指向型言語でありGC付きでもあるJavaから、C++への移行は、設計思想にも大きな変更を迫ることになります。
それを踏まえて、JavaからC++へ移植した際の注意点を挙げておきます。
JavaとC++11の相違点
- GCがないので、オブジェクトの寿命を管理する必要がある。やたらめったらnewを使えない。
- unsigned int と signed intをきっちり区別しておかないと、思わぬところでバグる。
- Javaの癖でクラスを乱発するとコードが散らばって醜くなる。
- Javaの癖で関数を細分化して作りまくるとコードが散らばって醜くなるうえ、inlineを明示しないとオーバーヘッドがある
- 静的変数の扱いが微妙に違う。
- auto(型推論)は便利。
- コールバック関数を渡せるのは非常に便利。
筆者はJavaの癖で細かい部分まで関数化したり、オブジェクト化したりしていたため、コードが分かりづらくなってしまいました。
ともかく、これらの点を念頭に置いてゲームを設計すれば、多少なりとも見やすくわかりやすいコードになると思われます。
それでは、次にCocos2d-xの仕様について触れたいと思います。
Cocos2d-xの仕様
Cocos2d-xの仕様で注意する点は以下の通りです。
- すべての基本はシーン
- 座標が左下原点。上に行くほどy座標が大きい。
- 画像は置くもの、載せるもの、動かすもの。
- 画面はノードを載せまくって管理
- 画像やノードは中心基点で表示
- ただし、ノード自身の基点は中心なのに、ノードの子から見た親ノードの基点は左下
- イベントはリスナーで管理
- どうしても定期的に実行するイベントがあるならば、updateで管理
- ゲーム画面と処理の一纏めはシーンで管理。シーンはスタック構造。
それでは、順に説明していきます。
すべての基本はシーン
後で詳しく話しますが、とにかくcocos2d-xの基本はシーンと呼ばれるものです。このシーンの上に、レイヤーと呼ばれるものがあり、その上に実際に色々と置いていくことになります。シーンはクラスで表現されているので、この辺りはJavaと親和性が高いかと思われます。
class TitleScene : public cocos2d::Layer // Layerを継承
{
private:
protected:
TitleScene(); // コンストラクタ
virtual ~TitleScene(); // デストラクタ(このオブジェクトがdeleteされた時に呼ばれる)
bool init() override; // 初期化処理
public:
static cocos2d::Scene* createScene(); // このシーンを作成するメソッド。これを用いてシーンを作成する(後述)
void onEnterTransitionDidFinish() override; // このシーンに入り、初期化が終わった後に呼ばれる
CREATE_FUNC(TitleScene); // TitleSceneの部分に、クラス名を入れておけばCococs2d-xに必要な関数を勝手に作ってくれる
};
これが、シーンのクラスです。ヘッダーファイルにこのように書き、ソースファイルにその実装を書くことになります。シーンの説明のはずなのに、レイヤーを継承している点については、これはそういうものだと思ってください。形式的に、このようにして、createScene()で実際にシーンを作ることになります。createScene()の実装は、大概以下のようになります。
Scene* TitleScene::createScene()
{
auto scene = Scene::create();
auto layer = TitleScene::create();
scene->addChild(layer);
return scene;
}
座標が左下原点
Javaでゲームを作っている方が一番最初に躓く点だと思われますが、座標が左下原点です。数学的にはこっちの方がメジャーなんですが、プログラム的にはマイナーなんじゃないでしょうか。
実際、移植する際もこれを失念して難儀しました。
画像は置くもの、載せるもの、動かすもの
Javaでは、画像は常にdrawImageし続けるものですが、cocos2d-xではそうではありません。cocos2d-xでは、Spriteというオブジェクトで画像を管理します。これは、以下のように使用します。
//生成・設定
Sprite* sprite = Sprite::create("ファイル名");//生成
Sprite* sprite = Sprite::create("ファイル名", Rect(x, y, width, height)); //(x, y, x+width, y+height)の四角形で切り取って生成
sprite->setPosition(x, y); //座標を設定
sprite->setScale(scale); //拡大縮小率を設定
sprite->setOpacity(opacity); //透明度を設定
sprite->setAnchorPoint(v); //アンカーポイント(後述)を設定
sprite->setTag(tagNum); //タグ番号(親からこのspriteを見つけるためのユニークな定数)を設定
node->addChild(sprite); //nodeの上にspriteを載せる(nodeがあらかじめ定義されていると仮定)
//消去
node->removeChild(sprite, true); //オブジェクトを指定して消去
node->removeChildByTag(tagNum); //オブジェクトにつけたタグ番号で指定して削除
sprite->removeFromParent(); //オブジェクト自体から消去
このようにして生成し、nodeの上にspriteを載せてしまえば、取り去るまでずっと表示され続けます。つまり、画像は置くものなわけです。
さらに、画像はnodeの上に載せますが、これは画像の上に載せることも可能です。
Sprite* s1 = Sprite::create("A.png");
s1->setPosition(100, 100);
node->addChild(s1);
Sprite* s2 = Sprite::create("B.png");
s2->setPosition(100, 100);
s1->addChild(s2);
このように記述すれば、s1の上にs2が載ります。つまり、画像は載せるものです。
加えて、setPosition(x, y)などは、画像を表示した後にも設定することができます。つまり、画像は動かすものです。
また、runAction()関数を使うことで、これらの画像に様々なエフェクトやアニメーションをかけることができます。これを紹介すると紙面を食いすぎるので、ここでは割愛します。
画面はノードを載せまくって管理
それでは、そういった画像で構成された画面はどのように構成するのでしょうか。その構成に、ノードという概念を使います。
ノードというのは、一枚の透明なガラス板のようなものです。その上に、再びガラス板を打ち付けることも、絵を載せることもできます。加えて言うならば、絵の上に絵を載せることもできるわけです。先の図を見ると、Aの上に、Bが載っています。このように、階層構造を作ることで画面を管理することができます。
ちなみに、この時、BはAから見た相対座標で表示されます。ただし、この相対座標が曲者なので、これは後述します。
階層構造になっているおかげで、画面から一纏まりの画像を消しやすくなっています。例えば、メニュー画面などを作った場合、メニュー画面全体をあるノードの上に置いておけば、そのノードを取り去るだけで、メニュー画面全てを消去することができます。
移植の際は、セーブ画面や、設定画面をノードで一纏めにすることで、管理を簡易化しました。
画像やノードは中心基点で表示
それらの画像ですが、これは中心基点が基本になります。要するに、(0, 0)に表示させようとすると、画像の中心が左下の端に一致します。また、画像の回転なども、中心基点で行われます。
Javaの場合は、左上基点で考えるので、この辺りを注意しなくては思わぬバグの原因になります。
なお、先述したアンカーポイントを使うと、基点を変更することができます。setAnchorPoint(Vec2(0.0, 0.0))で左下基点、setAnchorPoint(Vec2(0.5, 0.5))で中心基点、setAnchorPoint(Vec2(1.0, 1.0))で右上基点という風に変えることになります。これは、setAnchorPoint(Vec2::ANCHOR_MIDDLE)というエイリアスもあります(下表参照)。
ANCHOR_TOP_LEFT | ANCHOR_MIDDLE_TOP | ANCHOR_TOP_RIGHT |
ANCHOR_MIDDLE_LEFT | ANCHOR_MIDDLE | ANCHOR_MIDDLE_RIGHT |
ANCHOR_BOTTOM_LEFT | ANCHOR_MIDDLE_BOTTOM | ANCHOR_BOTTOM_RIGHT |
ただし、ノード自身の基点は中心なのに、ノードの子から見た親ノードの基点は左下
しかし、ここで注意しておきたいのが、ノード自体の基点と、ノードに画像などを載せた時の基点はまた別だということです。
Sprite* s1 = Sprite::create("A.png");
s1->setPosition(100, 100);
node->addChild(s1);
Sprite* s2 = Sprite::create("B.png");
s2->setPosition(100, 100);
s1->addChild(s2);
もう一度この図を使って説明します。Aの表示する座標を定める時は、中心を基点としています。そして、Bの表示する座標はAから見た相対座標です。
ここで、Bの中心座標が(200, 200)となっていと考えていては、誤りです。なぜならば、Aから見た相対座標は、Aの中心から見た相対座標ではなく、Aの左下から見た相対座標だからです。従って、Aの四角形の大きさが(100, 100)だったとすると、Bの中心座標は(150, 150)となります。
この点に留意しないと、画像の上に画像を重ねる時、ずれてしまうことが多々あります。この解決法は、以下の二つがあります。
s2->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT); //アンカーポイントを左下にする(表示する基点を左下にする)
s2->setPosition(s1->getContentSize().width/2, s1->getContentSize().height/2); //重ねる先の画像の縦幅と横幅の半分だけ画像を右上に動かす
s1->addChild(s2);
このようにすれば、画像はきちんと重なって表示されます。
移植の際も、この点は多いに悩まされました。
上記のようなシステムメニューを作る際、ノードで重ねて表示すると非常に便利です。しかし、左下基点のため配置がおかしくなって悩みました。分かってみれば簡単な話なのですが、こういう簡単な抜けほど、意外と気づかないものです。
ここまでで、画像の表示や、それによって構成される画面の作り方は何となくわかったと思います。ですが、当然ゲームは画像の表示だけでは成り立ちません。それだけでは、紙芝居も作れません。そこで、次はイベントの移植についての話になります。
イベントはリスナーで管理
イベントと一口に言いますが、Javaで作ったゲームのイベントは、主に二つに分かれます。KeyListenerを用いたものか、MouseListenerを用いたものです。もしかしたら、WindowsListenerやらActionListenerやらを使うこともあるかもしれませんが、大体は先の二つで管理されていると思います。
しかし、Androidにキーを使った処理を組み込むのは得策ではありません。その点を踏まえた上で、cocos2d-xにおけるイベント管理を見ていきます。
cocos2d-xのイベントはほとんどリスナーで管理します。このリスナーには以下のような種類があります。
- EventListenerAcceleration
- EventListenerController
- EventListenerCustom
- EventListenerFocus
- EventListenerKeyboard
- EventListenerMouse
- EventListenerTouchOneByOne
- EventListenerTouchAllAtOnce
しかし、先だって言ったように、キーボードやフォーカスという発想は、Android移植ではあまり用いたくはありません。従って、恐らくは下の二つ(特にEventListenerTouchOneByOne)を使うことになると思われます。その使い方は以下の通りです。
auto lis = EventListenerTouchOneByOne::create();
lis->setSwallowTouches(true); //タッチの非透過処理(後述)
lis->onTouchBegan = [(処理の中で使う変数名(thisも含む))] (Touch* touch, Event* event) { (タッチされた瞬間の処理。return true or falseを行う必要がある) };
//以下の処理は、onTouchBegan()に格納した「タッチされた瞬間の処理」の帰り値がtrueの場合のみ実行される。
lis->onTouchMoved = [(処理の中で使う変数名(thisも含む))] (Touch* touch, Event* event) { (タッチされた後、移動している最中の処理) };
lis->onTouchEnded = [(処理の中で使う変数名(thisも含む))] (Touch* touch, Event* event) { (タッチが終わった処理) };
lis->onTouchesCancelled = [(処理の中で使う変数名(thisも含む))] (Touch* touch, Event* event) { (タッチが何らかによって邪魔されて終わった処理) };
this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(lis, target);
上のように設定して、targetに紐づけてイベントリスナーを設置します。このtargetには、イベントが発生する画像やノードを指定します。ボタンの画像を置いて、そのボタンの画像にボタンを押した処理を紐づける感覚です。targetに紐づけることで、色々な恩恵が得られます。
まず、それぞれの処理の引数のeventに、targetへのポインタが紐づけられます。このため、処理の中で、event経由でtargetの操作を行うことができます。
次に、タッチイベントの実行順序が分かりやすくなります。タッチイベントの実行順序は、その紐づけられたtargetの階層に依存します。要するに、targetが画面上で前にあればあるほど、先に判定が行われるようになります。
再びこれを参考にすると、AにイベントA、BにイベントBを紐づけ、AとBが重なっている場所をタッチした場合、BのonTouchBeganに入れられた処理が先に実行されます。さらに便利なことに、setSwallowTouches(true)としておけば、タッチの非透過処理が行われます。これは、BのonTouchBeganがtrueを返した場合、Aのタッチイベントが実行されないというものです。
ボタンの裏に隠れたボタンまで同時に押されてしまうのは、自然に考えてもおかしいです。この非透過処理を活用すれば、そういった事態を簡単に防げます。
どうしても定期的に実行するイベントがあるならば、updateで管理
しかし、やはりリスナーだけの駆動には限界があります。定期的にチェックする処理などは、リスナーやrunActionだけで管理するのは難しいですし、コードが分かりにくくなります。こういった場合は、update関数を使うことができます。
bool Scene::init()
{
this->scheduleUpdate();
return true;
}
void Scene::update(float dt)
{
// updateの処理。なお、dtは以前updateを呼んでから経過したフレーム数(60より少ないくらいになる)
}
以上のように、init(初期化処理)の中で、updateを呼びだすことを宣言しておくと、以後updateが一定間隔で呼ばれるようになります。この中で、一定間隔で呼びたいイベントを記述することになります。
ゲーム画面と処理の一纏めはシーンで管理。シーンはスタック構造。
最後に、これらの一纏めが先述したシーンで管理されます。
シーンは、基本的には初期化処理と更新処理からなります。先述したように、画像は置くものなので初期化処理で置けばいいです。また、イベントリスナーも初期化処理で置けます。その後、ボタンを押して新たにイベントや画像が作成される時は、そのリスナーの中に記述することになります。要するに、初期化処理でcocos2d-xは大体完結します。後は、更新処理でそれらを制御する必要があるかどうかを議論すればよいでしょう。
こういったシーンは、以下のように運営します。
// シーンをスタックで管理する場合
Director::getInstance()->pushScene(TitleScene::createScene());
Director::getInstance()->popScene();
// シーンを置き換える場合
Director::getInstance()->replaceScene(TitleScene::createScene());
以上のように、シーンはスタックとして管理することも、置き換えて管理することもできます。スタックの場合は、メニュー画面など、以前のシーンの情報を保持しておきたいシーン遷移に使えます。
反対に、以前のシーンの情報が必要でない遷移をする場合(タイトルからゲーム画面へなどの場合)は、置き換えを利用した方がリソースに優しいです。
まとめ
cocos2d-xはまとめると、簡単には以下のような構成になります。右がノード、左がイベントの構成です。
このように、ノードは重ねて表現します。基本的には、座標は中心基点で、子ノードから親ノードを見た相対座標は、左下基点になります。
このように、イベントはinitで作成したり、設定したりして、そうして作ったものがそれぞれの処理を行います。
なお、これはあくまでもっとも簡単な例です。runActionからupdateの呼び出しを設定することもできます。また、EventListenerから画像の配置、イベントの設置を行うこともできます。ただ、そういった処理もあくまでinitから始まるものであるということは頭に入れておくと設計しやすいかと思います。
ADVゲームの移植
それでは、いよいよ本題に入ります。前座が長すぎるきらいがありますが悪しからず。まずは、Javaで以下のADVゲームを作りました。
ジャンルとしては推定ADVとして、オホーツクに消ゆとは違いますが、ノベルを読んで推理しようというコンセプトのものです。
移植する際、まずはこのゲームのシーンを考えてみました。結論から言うと、以下のシーンに分かれます。
- タイトル
- 物語の選択
- ロード
- システム設定
- ノベル
- 推理
- メニュー
- ログ
このようにゲームをシーン別に区別すると、cocos2d-xで作りやすくなります。しかし、ここで一つ問題が噴出しました。
上のように、システム設定は後ろのシーンを透過した状態で表示させたいという要望がありました。しかし、後ろのシーンを透過表示するという設定は、どうやらできないようです。そこで、透過する要望があった、ロード、システム設定、メニュー、ログはノードという形で実現しました。
以下に、ヘッダーファイルを例示します。
class ConfigScene : public cocos2d::Layer
{
private:
cocos2d::Label* _font_text;
cocos2d::Label* _color_text;
cocos2d::Label* _test_text;
float _words;
int _textOffset;
int _pre_words;
protected:
ConfigScene();
virtual ~ConfigScene();
bool init() override;
public:
std::function<void()> outSystem;
static ConfigScene* createLayer();
CREATE_FUNC(ConfigScene);
void changeColor(cocos2d::Ref*);
void changeFont(cocos2d::Ref*);
void setTestTextRunAction();
};
// 呼び出す方のソースコード
auto layer = ConfigScene::createLayer();
layer->outSystem = std::bind(&TitleScene::outSystem, this);
this->addChild(layer);
前述では、createSceneとしているところを、createLayerとしてシステム設定の諸々が載ったレイヤーとして提供しています。つまり、処理と画像の一纏めをレイヤーとして提供するという方針にしたというわけです。
これで、シーンレベルでの構成は完成しました。続いて、各シーンの画面構成と役割を整理して、cocos2d-xの仕様に則って構成していきます。例えば、タイトルでは、以下のような画面構成で、各画像をタップすると、それぞれのシーンに遷移する、といった風になります。
この実装については上記の留意点を見れば大体問題なくできるでしょう。例えば、以下のようなイベントが典型的です。
//表示の基準になるウィンドウサイズを取得
auto director = Director::getInstance();
auto winSize = director->getWinSize();
//システム画面を出すための画像を設定
auto system = Sprite::create("system/system_config.png");
system->setPosition(Vec2(winSize.width / 2.0 + 210, winSize.height / 2.0 - 100));
this->addChild(system);
//システム画面をタップした時の処理を設定
auto systemListener = EventListenerTouchOneByOne::create()
systemListener->setSwallowTouches(true); //タッチ処理の非貫通設定
//タッチが始まった時の処理
systemListener->onTouchBegan = [this] (Touch* touch, Event* event)
{
//自作関数。タッチされた位置が紐づけた画像の範囲内にあるかどうかを判定する
if(MyUtils::getInstance()->isTouchInEvent(touch, event))
{
//後述しますが、システムはレイヤーで実装しています
auto layer = ConfigScene::createLayer();
layer->outSystem = std::bind(&TitleScene::outSystem, this);
this->addChild(layer);
//trueを返せばタッチ処理はここで止まる
return true;
}
//falseを返せば、タッチ処理は継続する
return false;
};
//systemに紐づけたので、systemより上にある画像に紐づけたリスナーより後にタッチ処理が行われるようになる
this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(systemListener, system);
ちなみに、タッチされた位置が紐づけた画像の位置にあるかどうかを判定するのは結構面倒です。グローバル座標とローカル座標の差異のせいです。それを解決した自作関数を以下に示しておきます。入用でしたら使ってやってください。英語ガバガバなのは仕様です。
シングルトンで実装しましたが、「MyUtils::」の部分を抜いて、上記の呼んでいる部分の「MyUtils::getInstance()->」部分を抜けば、クラス関係なく使えるようになると思います。
このあたり、Javaの癖が抜けてないのが如実に表れてますね。わざわざクラス化してます。
/*
* Checking event range contains touch point by touch and event data.
*/
bool MyUtils::isTouchInEvent(Touch* touch, Event* event)
{
return isTouchInEvent(touch->getLocation(), event->getCurrentTarget());
};
/*
* Checking event range contains touch point by touch and event data.
* down_x or y is extension of left or lower.
* append_x or y is extension of right or upper.
*/
bool MyUtils::isTouchInEvent(Touch* touch, Event* event, int down_x, int down_y, int append_x, int append_y)
{
return isTouchInEvent(touch->getLocation(), event->getCurrentTarget(), down_x, down_y, append_x, append_y);
};
/*
* Checking event range contains point by point and event data.
*/
bool MyUtils::isTouchInEvent(Vec2 point, Event* event)
{
return isTouchInEvent(point, event->getCurrentTarget());
};
/*
* Checking node range contains point by point and node data.
*/
bool MyUtils::isTouchInEvent(Vec2 point, Node* t)
{
return isTouchInEvent(point, t, 0, 0, 0, 0);
};
/*
* Checking node range contains point by point and node data.
* down_x or y is extension of left or lower.
* append_x or y is extension of right or upper.
*/
bool MyUtils::isTouchInEvent(Vec2 point, Node* t, int down_x, int down_y, int append_x, int append_y)
{
//NULL check
if(!t)
{
CCLOG("[ERROR] isInTocuh node is NULL");
return false;
}
Rect targetBox;
//if ignore anchorpoint, not multiple anchorpoint.
if(t->isIgnoreAnchorPointForPosition())
targetBox = Rect(t->getParent()->convertToWorldSpace(t->getPosition()).x - t->getContentSize().width - down_x,
t->getParent()->convertToWorldSpace(t->getPosition()).y - t->getContentSize().height - down_y,
t->getContentSize().width + append_x, t->getContentSize().height + append_y);
else
targetBox = Rect(t->getParent()->convertToWorldSpace(t->getPosition()).x - t->getContentSize().width * t->getAnchorPoint().x - down_x,
t->getParent()->convertToWorldSpace(t->getPosition()).y - t->getContentSize().height * t->getAnchorPoint().y - down_y,
t->getContentSize().width + append_x, t->getContentSize().height + append_y);
return targetBox.containsPoint(point);
};
このようにして、こういった移行自体は、割とスムーズに進みました。先述したように、画像を設置するという感覚と、画像にイベントを紐づけるという感覚があれば、さほど問題は持ち上がりません。しかし、それでもいくつかの特殊な問題が生じました。
- 動的なフレーム画像生成
- ちょいちょい起こる不正参照
- ノベルのためのファイル読み込み
- 一文字時ずつ表示
- セーブのためのファイル書き込み
動的なフレーム画像生成
動的なフレーム画像生成というのは、ウディタをやってる方には「お手軽ウィンドウ」のことと言えば大体把握してくれると思います。ウディタをやっていない方に向けて説明しますと、9分割可能な枠の画像を用意して、それを任意の大きさの枠にすることを指します。
左のような画像から、先のシステムメニューのような枠を生成する処理です。
これを実現するには、純粋に拡大縮小するだけでは不十分です。実際にやってみるとわかりますが、違和感のある枠が出来上がります。そのため、フレーム用のノードを作成し、その上に九分割した枠用の画像をそれぞれ適切に拡大縮小する必要があります。
さらに、スキン変更がしたかったため、こうして作成したフレームをどこかにまとめて保存しておく必要がありました。
以上の実現には、結局以下のようなコードが必要になりました。
/*
* create frame by skin data.
*/
Node* MyUtils::createCutSkin(int w, int h, int cut_mask)
{
int top = 10;
int under = 10;
int right = 10;
int left = 10;
//create base node.
auto node = Node::create();
node->setContentSize(Size(w, h));
//convert skin type to integer.
int skinType = static_cast(VariableManager::SKIN);
if(!(cut_mask & CUT_MASK_UP))
{
//Upper left (not cut up and left)
if(!(cut_mask & CUT_MASK_LEFT))
{
auto addSkin = Sprite::create("system/window.png", Rect(30 * skinType, 0, 10, 10));
addSkin->setPosition(Vec2(5, h-5));
addSkin->setTag(0);
node->addChild(addSkin);
_skins.pushBack(addSkin);
}
//Upper right (not cut up and right)
if(!(cut_mask & CUT_MASK_RIGHT))
{
auto addSkin = Sprite::create("system/window.png", Rect(30 * skinType + 20, 0, 10, 10));
addSkin->setPosition(Vec2(w-5, h-5));
addSkin->setTag(1);
node->addChild(addSkin);
_skins.pushBack(addSkin);
}
}
else
{
//top is zero for cut up.
top = 0;
}
if(!(cut_mask & CUT_MASK_DOWN))
{
//Lower left (not cut down and left)
if(!(cut_mask & CUT_MASK_LEFT))
{
auto addSkin = Sprite::create("system/window.png", Rect(30 * skinType, 20, 10, 10));
addSkin->setPosition(Vec2(5, 5));
addSkin->setTag(2);
node->addChild(addSkin);
_skins.pushBack(addSkin);
}
//Lower right (not cut down and right)
if(!(cut_mask & CUT_MASK_RIGHT))
{
auto addSkin = Sprite::create("system/window.png", Rect(30 * skinType + 20, 20, 10, 10));
addSkin->setPosition(Vec2(w-5, 5));
addSkin->setTag(3);
node->addChild(addSkin);
_skins.pushBack(addSkin);
}
}
else
{
//under is zero for cut down.
under = 0;
}
//In the following, scaling in some cases.
//Left (not cut left)
if(!(cut_mask & CUT_MASK_LEFT))
{
auto addSkin = Sprite::create("system/window.png", Rect(30 * skinType, 0 + 10, 10, 10));
addSkin->setScaleY((h - top - under) /10.0f);
addSkin->setPosition(Vec2(5, (h - top + under) / 2));
addSkin->setTag(4);
node->addChild(addSkin);
_skins.pushBack(addSkin);
}
else
{
//left is zero for cut left.
left = 0;
}
//Right (not cut right)
if(!(cut_mask & CUT_MASK_RIGHT))
{
auto addSkin = Sprite::create("system/window.png", Rect(30 * skinType + 20, 0 + 10, 10, 10));
addSkin->setScaleY((h - top - under) /10.0f);
addSkin->setPosition(Vec2(w-5, (h - top + under) / 2));
addSkin->setTag(5);
node->addChild(addSkin);
_skins.pushBack(addSkin);
}
else
{
//right is zero for cut right.
right = 0;
}
//Upper (not cut up)
if(!(cut_mask & CUT_MASK_UP))
{
auto addSkin = Sprite::create("system/window.png", Rect(30 * skinType + 10, 0, 10, 10));
addSkin->setScaleX((w - left - right) /10.0f);
addSkin->setPosition(Vec2((w + left - right) / 2, h-5));
addSkin->setTag(6);
node->addChild(addSkin);
_skins.pushBack(addSkin);
}
//Lower (not cut down)
if(!(cut_mask & CUT_MASK_DOWN))
{
auto addSkin = Sprite::create("system/window.png", Rect(30 * skinType + 10, 20, 10, 10));
addSkin->setScaleX((w - left - right) /10.0f);
addSkin->setPosition(Vec2((w + left - right) / 2, 5));
addSkin->setTag(7);
node->addChild(addSkin);
_skins.pushBack(addSkin);
}
//main skin(center)
auto skin = Sprite::create("system/window.png", Rect(30 * skinType + 10, 0 + 10, 10, 10));
skin->setScaleX((w - left - right) /10.0f);
skin->setScaleY((h - top - under) /10.0f);
skin->setPosition(Vec2((w + left - right) / 2, (h - top + under) / 2));
skin->setTag(8);
node->addChild(skin);
_skins.pushBack(skin);
//return node
return node;
}
/*
* recreate skin
*/
void MyUtils::recreateCutSkin()
{
int x = 0;
int y = 0;
std::vector removeList;
for(auto skin : _skins)
{
if(skin->getReferenceCount() == 1)\
{
removeList.push_back(skin);
continue;
}
switch(skin->getTag()){
case 0:
x = 0;
y = 0;
break;
case 1:
x = 20;
y = 0;
break;
case 2:
x = 0;
y = 20;
break;
case 3:
x = 20;
y = 20;
break;
case 4:
x = 0;
y = 10;
break;
case 5:
x = 20;
y = 10;
break;
case 6:
x = 10;
y = 0;
break;
case 7:
x = 10;
y = 20;
break;
case 8:
x = 10;
y = 10;
break;
}
skin->setSpriteFrame(SpriteFrame::create("system/window.png", Rect(30 * static_cast(VariableManager::SKIN) + x, y, 10, 10)));
}
//Remove skin data
for(Sprite* sprite : removeList)
_skins.eraseObject(sprite);
}
/*
* clear list of skin
*/
void MyUtils::removeCutSkin()
{
_skins.clear();
_skins.shrinkToFit();
}
/*
* clear number of list of skin
*/
void MyUtils::removeCutSkin(int number)
{
while(number--)
if(!_skins.empty())
_skins.popBack();
_skins.shrinkToFit();
}
createCutSkinで目的のフレームが載ったノードを返し、recreateCutSkinでスキンの変更がなされます。作ったフレームは、removeCutSkinを呼び出さないと、メモリを占有し続けます。
ちょいちょい起こる不正参照
cocos2d-xでは、オブジェクトをcreate()の呼び出しで作ることを推奨しています(auto node = Node::create()
など)。これは、リファレンスカウンタという仕組みを円滑に動かすためなのですが、リファレンスカウンタについてはここを見てもらうとして、作っていて起きた不正参照について話したいと思います。
作っていてよく起きる不正参照は、init()で作ったはいいが、retain()していないくて、リスナーのラムダ式内で呼んでしまうパターンと、メンバ関数として保持したかったが、CC_SYNTHESIZE_RETAINマクロで作った関数を使わずにポインタを格納したため、オブジェクトが破棄されてしまうパターンがあります。
前者は、その場で作ったうえ、何にも登録していないとしばしば発生します。ラムダ式内での不正参照エラーはまずこれを疑った方がいいでしょう。
後者は、リファレンスカウンタの仕様に依拠する問題です。例えば、_spriteという画像を格納するメンバを用意したとします。これは、_spriteにあたる画像をいろんな場所で動かすために必要だったとすればありがちでしょう。この時、以下のように書くと、問題が生じます。
//ヘッダーファイル内
class Test
{
private:
cocos2d::Sprite* _sprite;
}
//ソースファイル内
_sprite = Sprite::create();
詳しい説明は省きますが、この状態だと、retain()されていないため不正参照のリスクが増します。大概の場合はこれでも正常に動作するのですが、一応下記のように修正しておいた方が無難でしょう。
//ヘッダーファイル内
class Test
{
private:
CC_SYNTHESIZE_RETAIN(coocs2d::Sprite*, _sprite, Sprite);
}
//ソースファイル内
setSprite(_sprite);
このほかにも、ラムダ式でキャプチャした変数などが不正参照の恐れがあります。ラムダ式でキャプチャするのはthisにとどめ、できるだけメンバとして上記のように持っておいた方が安全かもしれません。
ノベルのためのファイル読み込み
これは、cocos2d-xというよりは、C++のstringとJavaのStringの差異なのだと思いますが、ノベルのためのテキストファイル読み込みに手間取りました。
cocos2d-xでファイルを読み取るためには、FileUtils::getInstance()->getStringFromFile(filePath)
を用います。これで、std::string型のデータを取得できるのですが、ここから一ページごとのイベントにパースするのに難儀しました。難航した点は、主に以下の三つです。
- 改行問題
- テキストスクリプト読み取りのためのstartWithに絡むBOM問題
- ディレクトリ内のファイル一括取得問題
なお、BOMを回避しつつ、改行ごとにstd::vector型のデータを返す関数は以下の通りです。(C++に慣れていないため、若干記法がおかしいかもしれません)
/*
* Sprite input file data by '\n' and store result.
* Only for UTF-8 file data. BOM is supported.
* Clear result in this function.
*/
void MyUtils::splitFile(std::vector &result ,const std::string& input)
{
CCLOG("[Log]Split file is %s", input.c_str());
//load file text
std::string fileText;
loadText(fileText, input);
//clear result data
result.clear();
std::string item;
//for BOM
bool first = true;
bool bom = false;
int bom_count = 0;
for(char ch : fileText)
{
//BOM check
if(first)
{
unsigned char unch = (unsigned char)ch;
if(unch == 0xEF)
bom = true;
first = false;
}
if(bom && bom_count < 3)
{
bom_count++;
continue;
}
//'\r' is skip
if(ch=='\r')
continue;
///'\n' is delimiter
if(ch=='\n')
{
result.push_back(item);
item.clear();
}
else
item += ch;
}
result.push_back(item);
}
一文字ずつ表示
cocos2d-xにおいて、文字を一文字ずつ表示するには、二つの方法があります。
一つは、一文字ずつをSpriteとして透明のアニメーションを駆使する方法です。こちらは、Spriteとして捉えることで、他の種々のアニメーションも適用できるなどの利点があります。しかし、実用上は結構遅いです。特に、ノベルゲームに必要なSkipを実装したところ、あまりに遅すぎて使えませんでした。また、この方法では、あらかじめフォントファイルを用意する必要があり、こちらも面倒です。
もう一つは、表示する文字列をstd::stringに格納し、そこから切り取って、適宜表示する文字を挿げ替える方法です。こちらは、恐らくメモリに全く優しくありませんが、前者より高速でした。今回は、こちらを採用しました。下記にその処理のための関数を示します。
/*
* Showing text
*/
void NovelScene::showText(Label* label)
{
CCLOG("[Log]showingText");
//reset text offset
_textOffset = 0;
_words = 0;
_pre_words = 0;
//Run Action (showing text)
label->runAction(RepeatForever::create(Sequence::create(
CallFunc::create([this, label]
{
CCLOG("[Log]text showing(%d) : %p", getCancelListener()->isEnabled(), label);
int prepos = _textOffset;
//Set number of loop
int loop = (int)(_words - _pre_words);
_pre_words = (int)_words;
//Calculate text offset
for(int i=0; i<loop; i++)
_textOffset += MyUtils::getInstance()->getOffset(_text, _textOffset);
//Concat label
if(label)
label->addString(_text.substr(prepos, _textOffset - prepos));
// label->setString(_text.substr(0, _textOffset));
else
CCLOG("[ERROR]Label is not exist");
//Checking complete
if(_text.size() < _textOffset)
{
CCLOG("[Log]Complete text. Add listener : %d", SystemFlag::getReadPage(_storyNumber, _storyName, getStory()->getEventHead()->getStoryNumber()));
if(_skipping && SystemFlag::getReadPage(_storyNumber, _storyName, getStory()->getEventHead()->getStoryNumber()))
{
//if skipping, next
getNextListener()->setEnabled(false);
getCancelListener()->setEnabled(false);
label->removeAllChildrenWithCleanup(true);
this->removeChild(label);
nextEvent(false);
}
else
{
getNextListener()->setEnabled(true);
getCancelListener()->setEnabled(false);
label->stopAllActions();
}
}
CCLOG("[Log]text showed : %p", label);
}),
DelayTime::create(0.05f),
NULL)));
//Set cancel listener
getCancelListener()->setEnabled(true);
}
この関数は、渡されたラベルに、逐次文字の書き換えをするアニメーションを設定する関数です。ついでに、タップされたときに全ての文字を一気に表示する機能などもつけています。ただし、それらの文字数や以前の文字数、以前のオフセットなどを記憶しているのは外部変数なので、これだけでは動きませんのであしからず。
また、addStringという関数は、cocso2d::Labelの中にはありません。少しでもメモリ効率を良くしようと、中身を若干改造するに至りました。こういった邪道も割とスムーズにできるのが、cocos2d-xの魅力です。
セーブのためのファイル書き込み
cocos2d-xでは、簡単な情報の保存のためのツールがあります。これは、以下のように用いられます。
//以下はUserDefault(簡単なデータを保存するための方法)
UserDefault *userDef = UserDefault::getInstance();
//データを設定する
userDef->setBoolForKey(key, boolValue);
userDef->setIntegerForKey(key, intValue);
userDef-setFloatForKey(key, floatValue);
userDef->setStringForKey(key, stringValue);
//データを取得する
auto _bool = userDef->getBoolForKey("key");
auto _int = userDef->getIntegerForKey("key");
auto _float = userDef->getFloatForKey("key");
auto _string = userDef->getStringForKey("key");
//以下はplist(データをファイルに書き込む方法。xml形式で保存される)
//パスを設定
auto path = FileUtils::getInstance()->getWritablePath();
auto file = path + "storyData.plist";
//データを取得
ValueMap data = FileUtils::getInstance()->getValueMapFromFile(file);
//データに書き込む
data["key"] = stringValue;
if (FileUtils::getInstance()->writeToFile(data, file))
CCLOG("Write data to %s.", file.c_str());
else
CCLOG("[ERROR] Not Saving !");
//データを取得する
if(!data["key"].isNull())
{
auto _string = data["key"].asString();
auto _int = data["key"].asInt();
auto _bool = data["key"].asBool();
auto _valueMap = data["key"].asValueMap();
}
移植する際のまとめ
以上のような点を考慮すれば、JavaのADVゲームの移植はさして難しくはありません。重要なのは、
- ゲームをシーンに分けること
- 画面の構成を考えること
- イベントは殆どタッチリスナーだけで構成すること
この三点でしょう。
最後に、自分なりに作ったシーンやイベントのテンプレートを示して、この章を終わりたいと思います。
//ヘッダーファイル
#include "cocos2d.h"
class BaseScene : public cocos2d::Layer
{
private:
protected:
BaseScene(); //コンストラクタ
virtual ~BaseScene(); //デストラクタ
bool init() override; //初期化処理
public:
void update(float dt); //更新処理(必要な場合)
void onEnterTransitionDidFinish() override; // シーンに入った後すぐに呼ばれるメソッド(必要な場合)
static cocos2d::Scene* createScene(); // シーンを作成するメソッド
CREATE_FUNC(TitleScene); // cocos2d-xのオブジェクトに必要なcreate()関数を作るマクロ
CC_SYNTHESIZE(Type, _name, Name); // リファレンスカウンタのないオブジェクトのメンバ変数とセッターとゲッターを作るマクロ
CC_SYNTHESIZE_RETAIN(Type, _name2, Name2); // リファレンスカウンタのあるオブジェクトのメンバ変数とセッターとゲッターを作るマクロ
};
//ソースファイル
#include "BaseScene.h"
USING_NS_CC; // cocos2d-xのオブジェクトを使う時、一々cocos2d::のネームスペースを使わないためのマクロ
Scene* BaseScene::createScene()
{
auto scene = Scene::create();
auto layer = BaseScene::create();
scene->addChild(layer);
return scene;
}
BaseScene::BaseScene()
{
_name2 = NULL; // リファレンスカウンタ付きのメンバ変数は、NULLセットしないとエラーを吐く時がある
}
/*
* Destructor
*/
BaseScene::~BaseScene()
{
CC_SAFE_RELEASE_NULL(_name2); //リファレンスカウンタ付きのメンバ変数は、このマクロで安全に解放しておく
}
/*
* Initialize
*/
bool BaseScene::init()
{
if(!Layer::init())
return false;
//Size型の画面サイズ取得
auto winSize = Director::getInstance()->getWinSize();
//背景画面を設定(画面中心に来るように設定)
auto background = Sprite::create("backround.png");
background->setPosition(Vec2(winSize.width / 2.0, winSize.height / 2.0));
this->addChild(background);
//画像を設定
auto sprite = Sprite::create("ファイル名");//生成
auto sprite = Sprite::create("ファイル名", Rect(x, y, width, height)); //(x, y, x+width, y+height)の四角形で切り取って生成
sprite->setPosition(x, y); //座標を設定
sprite->setScale(scale); //拡大縮小率を設定
sprite->setOpacity(opacity); //透明度を設定
sprite->setAnchorPoint(v); //アンカーポイントを設定
sprite->setTag(tagNum); //タグ番号(親からこのspriteを見つけるためのユニークな定数)を設定
back->addChild(sprite); //背景画像の上にspriteを載せる
//文字列を設定(上記にある拡大縮小率の設定などもspriteと同じ要領で可能)
auto text = Label::createWithSystemFont("Text", "Font Name", fontSize);
text->setPosition(node->getPositionX(), node->getPositionY() + node->getContentSize().height/2 + 25);
this->addChild(text);
//イベントリスナーを設定
auto listener = EventListenerTouchOneByOne::create();
listener->setSwallowTouches(true); //タッチの非貫通処理をしたい場合
listener->onTouchBegan = [this](Touch* touch, Event* event)
{
return true;
}
listener->onTouchMoved = [this](Touch* touch, Event* event)
{
}
listener->onTouchEnded = [this](Touch* touch, Event* event)
{
}
listener->onTouchCancelled = [this](Touch* touch, Event* event)
{
}
//spriteに対してイベントを設置
this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, sprite);
return true;
}
void update(float dt)
{
}
void BaseScene::onEnterTransitionDidFinish()
{
}
まとめ
- cocos2d-xはソースコードを一つ書けばクロスプラットフォームで便利
- cocso2d-xのソースコードは全部見えるからいざとなったら解析できる
- 環境構築は面倒
- 座標関連がややこしいので注意(左下基点とか座標系とか)
- ゲームはシーンに分ける
- ノードに重ねまくるという発想を持っていれば、画面構成は楽
- イベントは地雷みたいに設置できる。不正参照には注意
後記
というわけで、cocos2d-xの導入から移植までの話は以上になります。自分でもまさか30000文字を越えようとは思ってもみませんでした(大体ソースコードの引用で本文自体はそんなにないとは思いますが……)。この記事がcocos2d-xの導入の一助となれば幸いです。
cocos2d-xは、個人的には便利なツールだと思います。始めの環境構築さえ乗り切れば、後は楽なもんです。画像とイベント設置してビルドすれば良いだけですから。現に、ADVゲームの移植は一週間で終わりました。その後に移植したパズルゲームは、慣れもあってわずか一日です。
皆様も面倒な環境構築を乗り越えて、cocos2d-xの素晴らしさを体感してみてください。
ちなみに、明日12月16日はHikkyさんと、Kato_Kaoruさんの記事が公開になります。双方面白そうなので、是非に。