読者です 読者をやめる 読者になる 読者になる

フニゲの開発日記

Electronとか...

Cocos2d-html5の基礎

原稿


2014/02/15修正。v2.2.2対応。


 前章でだいたい書きたいことは書いてしまったような気もしますが、まだいくつか大事なことが残っています。ちょっと基礎を補強しておきましょう。

シーングラフを理解する

 前章で登場した文字やスプライト、レイヤーなどはすべてcc.Nodeを継承していますので、ノードとしての機能を持っています。ノードとしての機能というのはつまり、addChildで他のノードの「親」になったり、逆に他のノードの「子供」になる機能です。文字やスプライトはレイヤーにaddChildされ、レイヤーはシーンに貼り付けられて、ゲーム画面にあるもの全てがシーンからはじまる一本の大きな木になっているわけですね。これがシーングラフです。

f:id:funige:20131208154928p:plain

 Cocos2dは、シーングラフを辿りながら各ノードを順番に描画していきます(各ノードのdrawメソッドを順番に呼び出します)。ここがCocos2dの大きな制限なのですが、親は必ず子供より先に(画面の奥に)描画されます。
 兄弟(同じ階層のノード)は親にaddChildされた順番に描画されるのが基本ですが、addChildにz値をつけて呼ぶことで挿入する順序を変えることもできます。z値の大きい方が手前です。
 addChildした後で順番を変えたいときは、reorder­Childを使います。

var node1 = cc.Node.create();
this.addChild(node1, 10);

// z値が同じなら後にaddChildされた方が上
// node2はnode1より手前に描画される
var node2 = cc.Node.create();
this.addChild(node2, 10);

// z値が小さいときは
// addChildされた順番に関係なく奥に描画される
var node3 = cc.Node.create();
this.addChild(node3, 0); // いちばん奥に

// reorderChildを呼べば重ね順を変更できる
this.reorderChild(node3, 20); // いちばん手前に


 z値は兄弟の間の順番を決めるのに使われるだけです。例えば、同じ階層にある兄弟を2枚描画するときは、必ず「下のノード → 下のノードの子供 → 上のノード → 上のノードの子供」の順に描画が行われます。子供にどんなz値を入れても、この順序を変えることはできません。
 以下に、ノードの簡単な操作方法を示します。

// 新しいノードの作成
var node = cc.Node.create();
var child = cc.Node.create();

// タグを指定して子ノードを追加する
// z値やタグを省略すると、親のz値とタグが代入されます。
var tag = 1234;
node.addChild(child, 0, tag);

// 親ノードを取得する
child.getParent(); // =node

// タグを指定して子ノードを取得する
node.getChildByTag(tag); // =child

// 子ノードを切り離す
node.removeChild(child);

移動やサイズの指定

 ノードの位置を指定するとき使われる「cc.p」。
 これまでも何度も出てきたので、「これは何だろう」と思っていた人もいるかも知れませんが、実は例えば「cc.p(1, 2)」と書くところを普通に「{x:1, y:2}」と書いてもまったく問題ありません。まあcc.pを使った方が短く書けるので、特に理由がない限りcc.pを使うべきでしょう。
 同じようなコンストラクタに、サイズを表すcc.sizeや色を表すcc.c3bなどがあります。

cc.p(1, 2) {x:1, y:2}
cc.size(1, 2) {width:1, height:2}
cc.c3b(0, 100, 200) {r:0, g:100, b:200}

 ノードの移動やサイズの指定には、以下のようなメソッドが使われます。

// 位置の取得。最初createしたときは原点(たぶん画面の左下)に置かれています
var pos = node.getPosition();

// (スプライトなどの)幅と高さの取得
var size = node.getContentSize();

// 平行移動
node.setPosition(cc.p(200, 100)); 
// node.setPosition(200, 100) でもOK

// 回転。時計回りが+です。
node.setRotation(180);

// スケール
node.setScale(2.0);

// アンカーポイント
node.setAnchorPoint(cc.p(0.5, 0.5)); 
// node.setAnchorPoint(0.5, 0.5) でもOK

 アンカーポイントは説明が必要でしょう。ラベルやスプライトなどを表示するとき、「ノードの座標に対してその文字や画像をどう配置するか」を決めるのがアンカーポイントです。

f:id:funige:20131208154957p:plain

 デフォルトでは、ノードの座標がラベルやスプライトの中心に来るように描画されます。これは setAnchorPoint(cc.p(0.5, 0.5)) を指定したのと同じです。
 ノードが回転や拡大縮小するときは、常にアンカーポイントが中心点になります。


 レイヤーはこの原則からちょっと外れているように見えるかもしれません。レイヤーはその座標がコンテンツの左下になるように描画されますが、これはアンカーポイントの位置を (0, 0) にずらしているのではありません。レイヤーを回転したりスケールすれば、ちゃんとコンテンツの中心に対して回転したりスケールすることが確認できます。
 この動作は ignore­AnchorPoint­For­Position という別のフラグをセットすることで実現されています。

 cc.Layerには背景色のあるcc.LayerColorや、背景をグラデーションにできるcc.LayerGradientという姉妹がいます。画面に四角形を表示したいときにスプライトの代わりに使うと便利なのですが、レイヤーをこういう目的で使う時は

layer.ignoreAnchorPointForPosition(false);

 みたいな感じでオフにしておけば、他のスプライトと同じ動作になるので扱いやすくなると思います。


 ノードに子供をaddChildするときの動作も間違いやすいポイントです。
 親ノードが幅や高さを持たない「点」の場合はもちろん親と同じ座標にaddChildされますが、親が幅や高さを持っている場合は、そのコンテンツの左下端が子ノードの原点になります。

var sprite1 = cc.Sprite.create("me0.png");
var sprite2 = cc.Sprite.create("me0.png");
var sprite3 = cc.Sprite.create("me0.png");
this.addChild(sprite1);
sprite1.addChild(sprite2);
sprite2.addChild(sprite3);

f:id:funige:20131231185703p:plain

 親のアンカーポイントが左下端 (0, 0) のときは親と子の原点は一致しますが、そうでないときは原点は一致しません。
 子供を左下以外の場所、例えば親と同じ原点に移動したいケースが多いと思うのですが、アンカーポイントを使って見た目をずらしても回転や拡大の中心はずれません。素直にsetPositionを使って移動するか、ダミーのcc.Nodeを挟みましょう。
 

// sprite1のアンカーポイントが (0.5, 0.5) の場合
var sprite1 = cc.Sprite.create("me0.png");
var sprite2 = cc.Sprite.create("me0.png");

var size = sprite1.getContentSize();
sprite2.setPosition(size.width * 0.5, size.height * 0.5);
this.addChild(sprite1);
sprite1.addChild(sprite2);
...

// またはcc.Nodeを使って以下のように書きます
var sprite1 = cc.Sprite.create("me0.png");
var sprite2 = cc.Sprite.create("me0.png");
var node = cc.Node.create();

this.addChild(node);
node.addChild(sprite1);
node.addChild(sprite2);
...

アクション

 位置やスケールをアニメーションさせる方法には、updateメソッドで少しずつ動かす方法とcc.Actionを使う方法の2種類があります。updateメソッドを使う方法は前の章で紹介したので、ここではcc.Actionを使う方法について補足しておきましょう。
 cc.Nodeを継承したクラスはすべてアクションを使うことができます。例えばspriteを3秒かけて (0, 100) まで移動させたいなら、以下のように2行書くだけです。

var move = cc.MoveTo.create(3, cc.p(0, 100));
sprite.runAction(move);

 同じ方法で、ノードの様々なパラメータをアニメーションさせることができます。

// 移動
cc.MoveTo.create(3, cc.p(0, 100)); // (0, 100) へ移動
cc.MoveBy.create(3, cc.p(0, 100)); // 現在位置から100ピクセル相対移動

// 回転
cc.RotateTo.create(3, 90);

// スケール
cc.ScaleTo.create(3, 2.0);

 cc.EaseInなどのイーズアクションを使うと、速度を途中で変化させることもできます。

// じわっと動きはじめる
var move = cc.MoveTo.create(3, cc.p(100, 200));
var easeIn = cc.EaseIn.create(move, 4);
sprite.runAction(easeIn);

 cc.EaseInの他にも、速度を変化させるアクションはたくさん用意されています。

  • cc.EaseOut
  • cc.EaseInOut
  • cc.EaseSineInOut
  • cc.EaseErasticInOut
  • cc.EaseBounceInOut
  • cc.EaseBackInOut

 Cocos2dのアクションが素晴らしいのは、アクションを引数にして新しいアクションを作る方法が豊富に用意されていることです。
 例えば、cc.RepeatForeverを使えば同じアクションを無限に繰り返すことができます。 

// 回り続ける
var rotate = cc.RotateBy.create(3, 90);
var repeat = cc.RepeatForever.create(rotate);
sprite.runAction(repeat);

 複数のアクションを続けて行うには、cc.Sequenceを使います。
 複数のアクションを並列に行うには、cc.Spawnを使います。

// 移動した後で回転
var sequence = cc.Sequence.create(
  cc.MoveBy.create(3, cc.p(100, 200)),
  cc.RotateBy.create(3, 90));
sprite.runAction(sequence);

// 移動しながら回転しながら点滅しながらフェードアウト
var spawn = cc.Spawn.create(
  cc.MoveBy.create(3, cc.p(100, 200)),
  cc.RotateBy.create(3, 90),
  cc.Blink.create(3, 16),
  cc.FadeTo.create(3, 0));
sprite.runAction(spawn);

 アクションの途中や最後に何か処理を挟むのも簡単です。

var callAfterMove = cc.Sequence.create(
  cc.MoveBy.create(3, cc.p(100, 200)),
  cc.CallFunc.create(function () {
    cc.log("(・ω・)やあ");
  }, this));
sprite.runAction(callAfterMove);

アニメーション

 ではスプライトのアニメーションはどう書いたらいいでしょうか。
 ここで言うアニメーションというのは、パラパラ漫画の要領で静止画像を次々に表示して、画像を動いているように見せる技法です。

 例えばme0.pngとme1.pngの2枚を交互にぱたぱた表示するコードは、cc.Animationクラスを使って以下のように書けます。

var animation = cc.Animation.create();
animation.addSpriteFrameWithFile("me0.png");
animation.addSpriteFrameWithFile("me1.png");
animation.setDelayPerUnit(0.1); // 0.1秒ずつ表示
var action = cc.Animate.create(animation);

var me = cc.Sprite.create();
me.runAction(cc.RepeatForever(action));

 runActionメソッドやcc.RunForeverのはたらきについてはさっき学んだとおりです。cc.EaseInなどを使ってアニメーションのタイミングを調整することもできますし、cc.Spawnやcc.Sequenceを使って他のアクションと組み合わせるのも自由自在です。

クラスの継承を理解する

 次の章ではCocos2dの主な登場人物、cc.Nodeを継承したクラスを順番に見ていこうと思うのですが、そもそもCocos2dの継承ってどうなってるんでしょう。このへんの仕組みも勉強しておきましょう。
 cc.Layerとcc.Sceneを継承して新しいクラスを作る方法は、前に紹介しましたね。

// sample.js
var Sample = cc.Layer.extend({
  init:function () {
    this._super();
    var size = cc.Director.getInstance().getWinSize();

    this.player = cc.LabelTTF.create("Hello World", "Arial", 38);
    this.player.setPosition(cc.p(size.width / 2, 0));
    this.addChild(this.player, 5);

    this.setTouchEnabled(true);
    return true;
  },

  onTouchesBegan:function (touches, event) {},
  onTouchesMoved:function (touches, event) {},
  onTouchesEnded:function (touches, event) {},
  onTouchesCancelled:function (touches, event) {},
});

var SampleScene = cc.Scene.extend({
  onEnter:function () {
    this._super();
    var layer = new Sample();
    layer.init();
    this.addChild(layer);
  }
});

 そうです。このリストです。もう3回目ですね。自分でもちょっとしつこいと思うのですが、ここではクラスの継承方法に注目して、もう一度見てみましょう。
 Sampleはcc.Layerを継承したクラスです。initメソッドをオーバーライドしています。後半のSampleSceneはcc.Sceneを継承したクラスで、onEnterメソッドをオーバーライドして、SampleのインスタンスをaddChildしています。
 このSampleSceneがシーングラフの始点です。ここにaddChildされたノードが親から子へ次々に描画されていくことになります。

 実際にゲームを作るときは、シーンやレイヤー以外にも、いろいろ新しいクラスを作ることになるでしょう。まあ前章でやったようにJavaScriptは柔軟なので、クラスなんて使わなくてもゲームはできるんですけど、ShipクラスとかEnemyクラスとか作って別ファイルに切り離すことで、プログラムが長大なスパゲッティになるのを防ぐことができます。

 新しいクラスを作るときのテンプレートはこんな感じです。

var Enemy = cc.Sprite.extend({
  init:function () {
    this._super();
    // 位置や速度の初期化など
    ... 
    this.scheduleUpdate();
  },

  update:function (dt) {
    // 更新処理
    ... 
  },
});

Enemy.create = function () {
  var enemy = new Enemy();
  enemy.init();
  return enemy;
};

 scheduleUpdateを呼ぶことで、オブジェクトがスケジューラに登録されて、インスタンスのupdateメソッドが毎フレーム呼ばれるようになります。オブジェクトを使わなくなった時はunscheduleUpdateでスケジューラから削除してください。
 初期化や更新をオブジェクトに任せてしまえば、オブジェクトを使う側のコードは簡単になります。

  var enemy = Enemy.create();
  this.addChild(enemy);

 オブジェクトがスプライト1枚だけの場合は、上記のようにcc.Spriteを直接継承すればいいでしょう。複数の部品でできた複雑なオブジェクトの場合はcc.Nodeを継承して、内部で部品をcc.NodeにaddChildするようにすればいいと思います。cc.Node自体は画面に描画されませんが、多くのノードを束ねてまとめて動かす時にcc.Nodeを使うのは大変便利なテクニックです。

var ComplexEnemy = cc.Node.extend({
  init:function () {
    this._super();

    var sprite1 = cc.Sprite.create(...);
    this.addChild(sprite1);
    var sprite2 = cc.Sprite.create(...);
    this.addChild(sprite2);
    ...
    this.scheduleUpdate();
 },

  update:function (dt) {
    this._super();
    ... 
 },
});

ComplexEnemy.craete = function () {
  var enemy = new ComplexEnemy();
  enemy.init();
  return enemy;
};

 継承したクラスでメソッドをオーバーライドするときは、必ず1行目にthis._super()を書いて、親クラスのメソッドが実行されるようにします。
 initの他にも同じ方法でオーバーライドできるメソッドが幾つかあります。

// コンストラクタ。newされた時に自動的に呼ばれる
ctor:function ()

// 画面に表示された(親にaddChildされた)時に呼ばれる
onEnter:function ()

// 画面から消えた(removeChildされた)時に呼ばれる
onExit:function ()

 ctorについては……どうしようかな。あんまり説明したくないのですが、まあ説明しないわけにもいかないか。
 ctorはインスタンスができたときいちばん最初に自動的に呼ばれるメソッドです。便利なのですが、ちょっと注意が必要なメソッドでもあります。
 奇妙に感じる人もいると思うのですが、Cocos2d-html5のクラスには、ctorとinitの2つの初期化関数があるのです。これはたぶんObjective-Cからの移植という歴史的な事情のせいでしょう。
 注意しなければいけないのは、ctorが呼ばれるタイミングではまだ親クラスの初期化が終わっていない(可能性がある)ということです。
 ctorがインスタンスを作るときに暗黙的に呼ばれるのに対し、initはインスタンスを作った後に誰かが責任を持って呼ばなければなりません。Cocos2dがJavaScriptに移植された当初はたぶん必ずinitを呼ぶ規約だったと思うのですが、どうも最近のバージョンではそのへんが緩んできたようで、HelloWorldのサンプルでもcc.Sceneはinitを呼ばないまま使用されています。しかし一方で、cc.Layerやcc.Spriteなどはinitが必須で、initを呼ぶまで初期化が終わりません。
 このへんの仕様は今後のバージョンアップで変わる可能性もありますので、ユーザーがクラスを作るときの方針としてはとりあえず

  • ctorは使わない。使うときは注意して使う。
  • 初期化はinitで行う。インスタンスを作ったら必ずinitを呼ぶ。

というのが安全だと思います。

Cocos2d-xでも動くコードを書くために

 Cocos2d-html5で書かれたゲームは、多くの場合そのままCocos2d-xを使ってAndroidiOSで動かすことができますが、いくつか注意が必要な点もあります。


 Cocos2d-xのメモリ管理はちょっと前までiPhoneのプログラミングで主流だった「参照カウンタ」の考え方で行われています。インスタンスを所有するオブジェクトはカウンタを+1して、もう使わなくなったら-1します。誰にも所有されていないインスタンスはカウンタが0になって、メモリから削除されます。
 この仕組みをうっかり忘れてコードを書いていると、Cocos2d-xに持って行ったときにエラーが出ます。例えば、以下のコードは、Cocos2d-html5では動きますが、Cocos2d-xでは動きません。

init:function () {
  this._super();
  this.pauseMenu = cc.Node.craete();
  ... // ポーズボタンが押されたときに表示するメニューを作っておく
},
onPause:function () {
  this.addChild(this.pauseMenu); // Invalid Native Objectエラー
},

 インスタンスを作ったあと、誰もaddChildしなければ、そのノードは誰にも所有されていませんから、たぶんinit関数を抜けるタイミングでメモリから消えてしまうのです。
 同じ理由で、一度releaseChildしたノードを後でaddChildしてもう一度使おうとするとエラーになります。
 対策としてはまあ、ノードを作ったらすぐに必ずaddChildすればいいのですが、retain()とrelease()を使って、参照カウンタを手動で制御することもできます。

init:function () {
  this._super();
  this.pauseMenu = cc.Node.craete();
  this.pauseMenu.retain(); // <- 追加
  ...
},
onPause:function () {
  this.addChild(this.pauseMenu); // エラーは出ない
},
onExit:function () {
  this.pauseMenu.release(); // <- 追加
  this.pauseMenu = null;
  this._super();
},

v2.2.2以降の注意点

 Cocos2d-html5 v2.2.2から、getPosition や get­Anchor­Point、get­Content­Size は定数 (cc.­_Point­Const や cc.­_Size­Const) を返すようになりました。つまり、取得した値を上書きすることはできません。

// これまでの書き方
var pos = enemy.getPosition();
pos.x += 10; // posを変更しようとすると警告が出る
pos.y -= 5;
enemy.setPosition(pos); 

// v2.2.2以降はこう書く
var pos = enemy.getPosition();
enemy.setPosition(pos.x + 10, pos.y - 5);  

 これはまあ、慣れちゃえばどうってことないでしょう。

この章のまとめ

 この章では、Cocos2dの基礎について、絶対必要と思われる部分に絞って簡単に説明しました。

  • ノードの描画順序。
  • ノードの位置やアンカーポイントの設定方法。
  • アクションの使い方。
  • cc.Spriteとcc.Nodeを継承したクラスを作る方法。
  • Cocos2d-xでも動くコードを書くための注意点。
広告を非表示にする