Cocos2d-JS 触り始めた

以前 NME でゲームを作ってみたりしたことなどもあったが、巷で話題なのは Unity とか Cocos2dx 辺りで、この辺は有名だしもっと色々便利なんだろうなあと思っていた。最近会社を辞めて少し時間ができたので、そんなに情報が多くなくて面白そうな Cocos2d-JS を触ってみようかと思ったので、合わせて日誌的にブログを書いてみることにする。


何も知らない状態から探り探りやっているので、書いている内容が正確ではない可能性があります。
俺っていつもそう。

インストール

ひとまず「ブラウザで動くものが出来上がればいいかな」と思っていたものの、Android デバイスを持っているのでせっかくだし目標としては Web 版と Android 版を書き出す方向で進めてみたい。

作業環境は Windows8.1 64bit 版。まず Cocos2d-JS v3.0rc0 と、Android SDK、Android NDK、Apache Ant、Python 辺りを導入。NME の時はこの辺はインストーラがほとんど自動で全てをやってくれたので、それに比べるとわりと面倒くさい。

それぞれをインストールしたらこちらのページを参考に setup.py を実行し、環境変数の設定について聞かれるので適宜設定し、手順の通り MyGame を作って
cocos run -p web
してみると確かにブラウザが開き簡単なサンプルが動いているのを確認できた。
ブラウザを閉じ今度は Android 端末を USB デバッグを有効にした状態で接続し、
cocos run -p android
すると C++ ソースファイルのコンパイルが始まり、そのまま3年の月日が流れた。それは嘘だが、しばらく待つと端末にアプリが転送され難なく起動した。これ以降はコンパイル済みのライブラリをリンクするだけで済むようになるので、実行までの所要時間もだいぶ減るようだ。

生成されたコードを眺めてみる

生成されたファイル群を見てみると、JavaScript ソースファイルは MyGame/main.js MyGame/src/resource.js MyGame/src/app.js の3つのファイルがあるようだ。

MyGame/main.js

cc.game.onStart = function(){
    cc.view.setDesignResolutionSize(800, 450, cc.ResolutionPolicy.SHOW_ALL);
 cc.view.resizeWithBrowserSize(true);
    //load resources
    cc.LoaderScene.preload(g_resources, function () {
        cc.director.runScene(new HelloWorldScene());
    }, this);
};
cc.game.run();
このファイルはゲームメイン処理実行において一番最初に呼ばれるであろう cc.game オブジェクトが持つ onStart イベントのイベントハンドラを設定し、ゲームのメイン処理が開始されるよう cc.game.run を呼んでいるように見える。
設定したイベントハンドラ内では、画面の大きさを設定し、ブラウザの大きさに合わせて表示領域を拡大縮小するように設定し、更に g_resources を読み込み、恐らくその読み込み完了時に呼ばれるコールバック関数で HelloWorldScene シーンを作り、そのシーンの実行を開始していると思われる。

それを検証するため試しに cc.view.setDesignResolutionSize に渡している数字を逆にすると縦長の画面になったり、cc.view.resizeWithBrowserSizefalse で呼ぶとブラウザの表示領域が狭くても縮小表示されないことを確認できた。
ただし縦長の画面にするだけでは Android のアプリケーションとしては横向き(landscape)から縦向き(portrait)に切り替わるわけではないようだ。また、画面周りの設定は厳密にはここだけに存在するわけではないようなのでもう少し掘り下げて探してみて、一応 MyGame/index.html の canvas タグの幅と高さと、MyGame/frameworks/runtime-src/proj.android/AndroidManifest.xml 内の android:screenOrientation="landscape"android:screenOrientation="portrait" に変更してみたところ、Android アプリ上で縦画面として動くようになった。

canvas タグの大きさはブラウザで cc.game.onStart が呼ばれる前のタイミングでどの程度の大きさで出すのかという話だと思う(ここを変更しなくても JavaScript ファイルの変更だけで縦長にはなっていたから)のであまり重要ではないと思う。
ただこの HTML ファイル内にも <meta name="screen-orientation" content="portrait"/> の記述があったので、この辺も必要に応じて変えたほうがいいのかも知れない(最初から portrait だったけど…)。

MyGame/src/resource.js

var res = {
    HelloWorld_png : "res/HelloWorld.png",
    CloseNormal_png : "res/CloseNormal.png",
    CloseSelected_png : "res/CloseSelected.png"
};

var g_resources = [
    //image
    res.HelloWorld_png,
    res.CloseNormal_png,
    res.CloseSelected_png

    //plist

    //fnt

    //tmx

    //bgm

    //effect
];
これは MyGame/main.js で cc.LoaderScene.preload に渡していた g_resources を定義しているファイルのようだ。
リソースの追加時はここに追記したりすれば良さそうだが、表示頻度の少ないシーン用のリソース(エンディング周りとか?)などはここに定義せずシーンを読み込む際に個別に読み込んだりした方が起動時のローディング時間を減らせるのかも知れない。

MyGame/src/app.js

var HelloWorldLayer = cc.Layer.extend({
    sprite:null,
    ctor:function () {
        //////////////////////////////
        // 1. super init first
        this._super();

        /////////////////////////////
        // 2. add a menu item with "X" image, which is clicked to quit the program
        //    you may modify it.
        // ask director the window size
        var size = cc.director.getWinSize();

        // add a "close" icon to exit the progress. it's an autorelease object
        var closeItem = cc.MenuItemImage.create(
            res.CloseNormal_png,
            res.CloseSelected_png,
            function () {
                cc.log("Menu is clicked!");
            }, this);
        closeItem.attr({
            x: size.width - 20,
            y: 20,
            anchorX: 0.5,
            anchorY: 0.5
        });

        var menu = cc.Menu.create(closeItem);
        menu.x = 0;
        menu.y = 0;
        this.addChild(menu, 1);

        /////////////////////////////
        // 3. add your codes below...
        // add a label shows "Hello World"
        // create and initialize a label
        var helloLabel = cc.LabelTTF.create("Hello World", "Arial", 38);
        // position the label on the center of the screen
        helloLabel.x = size.width / 2;
        helloLabel.y = 0;
        // add the label as a child to this layer
        this.addChild(helloLabel, 5);

        // add "HelloWorld" splash screen"
        this.sprite = cc.Sprite.create(res.HelloWorld_png);
        this.sprite.attr({
            x: size.width / 2,
            y: size.height / 2,
            scale: 0.5,
            rotation: 180
        });
        this.addChild(this.sprite, 0);

        var rotateToA = cc.RotateTo.create(2, 0);
        var scaleToA = cc.ScaleTo.create(2, 1, 1);

        this.sprite.runAction(cc.Sequence.create(rotateToA, scaleToA));
        helloLabel.runAction(cc.Spawn.create(cc.MoveBy.create(2.5, cc.p(0, size.height - 40)),cc.TintTo.create(2.5,255,125,0)));
        return true;
    }
});

var HelloWorldScene = cc.Scene.extend({
    onEnter:function () {
        this._super();
        var layer = new HelloWorldLayer();
        this.addChild(layer);
    }
});
これはゲームのメイン処理を行っているらしいファイルで、少し長い。 全体としては cc.Layer を継承した HelloWorldLayercc.Scene を継承した HelloWorldScene を定義している。少し見てみた限りでは cc.Layercc.Scenecc.Node を継承しているようだ。

HelloWorldScene では onEnter イベントの発生時に HelloWorldLayer を作成し、自身の子として追加している。MyGame/main.js では HelloWorldScene を作成しているコードがあったので、実行順序としては
  1. cc.gameonStart イベントが発生
  2. HelloWorldScene のインスタンスを作成
  3. cc.director.runScene に作成したインスタンスを渡してシーン実行開始
  4. HelloWorldSceneonEnter イベントが発生
  5. HelloWorldLayer のインスタンスを作成
  6. HelloWorldLayer のコンストラクタで定義したアニメーションが順次実行されていく
という感じになっていると思われる。
この辺、行番号付いてた頃の BASIC 辺りとか、HSP とか、DX ライブラリとかのようにメインループを自分で回すのではなく、知らんところで勝手にメインループ的なものは動いているので何かが起きた時に対応するコードだけを書く感じで進めていくようなイメージが基本になるので、こういうイベントドリブン型に慣れる必要があるように思う(まあブラウザで JavaScript を使ったことがあるならば自ずと触ることになったとは思うけど)。
なので HelloWorldLayer のコンストラクタで定義したアニメーションは自分で実行するんじゃなく、あくまでアニメーションの実行計画を渡しておくと、それを元にうまい具合に実行してくれる、というイメージ。

HelloWorldLayer では右下に表示されている電源ボタン、中央で回転する画像、下から上に移動していくテキストをそれぞれ作成し、自身の子として追加している。また、アニメーションの実行計画を作成し、それぞれのオブジェクトの runAction で実行開始しているようだ。
スプライトに対しては cc.Sequence.create で回転と拡大のアニメーションを連続実行するようにして登録し、テキストに対しては cc.Spawn.create でテキスト移動と色変化のアニメーションを並列実行しているようだ。

電源ボタンである closeItem を作っているところのコードでは関数を渡しているが、内容から察するにボタンがタップされた時にログメッセージを出力する処理を行っている。
確かにブラウザのデバッグコンソールを表示したり、Android ではコマンドプロンプトで adb logcat をした状態で端末でタップするとログメッセージが表示されるのを確認できた。
ちょっとした変数の内容確認などはここでも行えるようだ。

自動で生成されたプロジェクトの全体像が少し見えたような気がする。

ちょっと書き換えてみる

ひとまず cc.Scene や cc.Layer などのクラスがあることがわかった。シーンというのは恐らくタイトル画面とかプレイヤーセレクト画面などの大きな単位で、レイヤーはその画面内に登場するパーツを表現している気がする。

例えばアクションゲームを作ったとして、起動した瞬間にいきなり動き出すのではなくタイトル画面でゲームスタートボタン的なものを押してからゲームを始めるような流れを作るなら、タイトル画面のシーンとゲーム本編のシーンの2つを作り、それらを切り替えることで実現できそうに思える。

いきなりシーンを2つ作るのはちょっと面倒臭いので、既にある HelloWorldScene から HelloWorldScene に切り替えるのをやってみることにする。また、 closeItem というボタンがあるので、役割は違うものの、これをそのまま流用してログメッセージを吐く代わりにシーン切り替えを行わせてみることにする。

シーンを実行させる処理は MyGame/main.js で cc.director.runScene(new HelloWorldScene()); というコードを見かけているので、これをこのまま持ち込めば同じシーンに画面切り替えすると思われる。
        var closeItem = cc.MenuItemImage.create(
            res.CloseNormal_png,
            res.CloseSelected_png,
            function () {
                cc.log("Menu is clicked!");
                cc.director.runScene(new HelloWorldScene());
            }, this);
要するに、こんな感じでそのまま持ち込んでみる。

これを試しに実行してみると、確かにボタンを押されたタイミングでまた回転アニメーションなどが最初から始まり、シーンが1から実行されなおしている感じになった。

ただし、このままだと本当に単純に切り替わるだけで物足りない。完成度の高いゲームはほとんど全ての動作で細かい動きなどを入れていたりするし、画面切り替えにはやっぱりエフェクトが欲しい。

適当に見て回っているとこの辺cc.TransitionFade というそれっぽいものを見つけた。このコードは Cocos2d-JS の v2.x 系のものなので少し実装が違うようなのだが、v3.0 系でも存在しているようなので、これを使ってみることにする。
cc.TransitionFadecc.TransitionScene を継承していて、それは cc.Scene を継承しているので、画面切り替え用のシーンをシーンとシーンの間に挟み込むことで切り替えエフェクトを実現するんだろうか。取りあえずシーンだということはまたcc.director.runScene に渡せばいいということだと思った。

cc.TransitionScene のコンストラクタは第1引数に切り替え所要時間を秒で、第2引数に切り替え先シーンを指定すればいいようなので、少しコードを変えて以下のようにした。
        var closeItem = cc.MenuItemImage.create(
            res.CloseNormal_png,
            res.CloseSelected_png,
            function () {
                cc.log("Menu is clicked!");
                cc.director.runScene(cc.TransitionFade.create(0.5, new HelloWorldScene()));
            }, this);
これで、0.5秒でフェードアウト、フェードインしてシーンが切り替わるようになった。

今後

この辺にわりと丁寧なチュートリアルっぽいのがあるようなので、そっちも目を通してみたい。