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

フニゲの開発日記

Electronとか...

はじめてのゲーム (2)

原稿


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

キャラクターを動かす

 はじめてのゲームはできるだけ単純にしたいので、平凡ですが、宇宙船が隕石を避けて進む横スクロールのゲームにしました。

f:id:funige:20131207135723p:plain

 宇宙船と、隕石です。心の目で見てくださいね。赤いコメント隕石が右から左に激しく流れています。
 まず隕石をたくさん表示して、右から左に動かしたいので、initメソッドを以下のように変更しましょう。

  init:function () {
    this._super();
    var size = cc.Director.getInstance().getWinSize();

    // 宇宙船
    this.ship = cc.LabelTTF.create("(・ω・)", "Arial", 38);
    this.ship.setPosition(cc.p(size.width / 2, size.height / 2));
    this.addChild(this.ship, 5);

    // 隕石
    this.enemies = [];
    for (var i = 0; i < 15; i++) {
      var enemy = cc.LabelTTF.create("w", "Arial", 38);
      enemy.v = cc.p(-5, (Math.random() - 0.5) * 3); // 隕石の速度
      enemy.setColor(cc.c3b(255, 0, 0)); // 赤くする
      enemy.setPosition(cc.p((Math.random() + 1) * size.width, 
                             Math.random() * size.height));

      this.addChild(enemy, 10);
      this.enemies.push(enemy);
    }

    this.scheduleUpdate(); // これについては後述

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

 Math.random()は0以上1未満の乱数を返す数学関数ですが、……まあ説明不要ですね。
 ここで注目して欲しいのは、「隕石の速度」って書いてあるところの「enemy.v」という表記です。enemy.vって何でしょう。enemyはcc.LabelTTFのインスタンスですよね。でもcc.LabelTTFにはvなんてプロパティはありません。
 これは筆者がいま勝手に作ったプロパティです。JavaScriptは柔軟なので、インスタンスに後からいくらでも自由に変数を追加することができるのです。これは便利ですね。C++で同じことをしようとすれば、クラスを継承するか、とりあえず速度の配列でも作るか。どちらにせよゲームに直接関係のないコードをいっぱい書かなければならないでしょう。
 addChildの「5」とか「10」という引数が何を意味しているのか、気になる人もいるでしょうか。これは「高さ」です。数値の大きいほうが上に描画されます。z=10の隕石はz=5の宇宙船より手前に表示されることになります。Z座標を省略すると、Cocos2dは下から上に、addChildされた順番に描画します。
 
 さて、Cocos2dでキャラクターを「動かす」方法は大まかに分けて2種類あります。updateメソッドで少しずつ位置をずらす方法と、cc.Actionを使う方法です。どちらも大事なのですが、ここではupdateを使うクラシックな方法を使って隕石を動かしてみましょう。
 隕石の速度はenemy.vにセットしましたが、なにしろいま勝手に作ったプロパティですから、フレームワークは何もしてくれません。enemy.vを使って隕石の位置を更新するのは自分の責任です。
 initメソッドの最後の方にあるthis.scheduleUpdate()というのが、updateメソッドを呼ぶためのおまじないになります。このおまじないをプログラムのどこかに書いておけば、1フレーム(1/60秒)ごとにupdateメソッドが呼ばれるようになります。フレームレートの設定はcocos2d.jsの中にありますので、1/60秒が気に入らない人はもっと遅くすることもできます。

 updateメソッドは以下の通りです。これをinitメソッドとonTouchBeganメソッドの間に挿入してください。

  update:function (dt) {
    var size = cc.Director.getInstance().getWinSize();

    for (var i = 0; i < this.enemies.length; i++) {
      var enemy = this.enemies[i];
      var pos = enemy.getPosition();
      pos.x += enemy.v.x;
      pos.y += enemy.v.y;

      // 画面から出ないないように
      if (pos.x < 0) pos.x = size.width;
      if (pos.y < 0) pos.y = size.height;
      if (pos.y > size.height) pos.y = 0;
      enemy.setPosition(pos);
    }
  },

 簡単だと思いました? 
 でもブラウザでテストするとエラーが出るんです。Warning of _PointConst: Modification to const or private property is forbidden……? はあ?


 これは Cocos2d-html5 v2.2.2 からの仕様なのですが、getPositionで取得した値を書き換えようとするとエラーが出るようになりました。上のコードは、getPositionのところを以下のように修正すれば動きます。

      // このposを後から変更するとエラーが出る
//    var pos = enemy.getPosition(); // このposはconst

      // 他のオブジェクトにコピーしてやれば自由に変更できる
      var pos = enemy.getPosition();
      pos = cc.p(pos.x, pos.y); // このposは変更可能
      ...

 どうも高速化のためらしいのですが、初心者には意味不明ですね。
 まあ、エラーが出ないように書き直すのは簡単なので、スルーして先に進みます。

プレイヤーの操作に反応させる

 そろそろかっこいい宇宙船をスプライトで表示したいところですが、まだ表示しませんよ。
 次は宇宙船を操作して、上下に動かせるようにします。onTouchesBegan以降の4つのメソッドを以下のように書き換えてください。

  onTouchesBegan:function (touches, event) {
    this.touched = touches[0].getLocation();
  },
  onTouchesMoved:function (touches, event) {
    this.touched = touches[0].getLocation();
  },
  onTouchesEnded:function (touches, event) {
    this.touched = null;
  },
  onTouchesCancelled:function (touches, event) {
    this.touched = null;
  },

 こうすれば、this.touchedに現在の指の位置(ブラウザではマウスカーソルの位置)が入りますね。値が何も入っていないときは、指がタッチしていないということです。
 touchesはマルチタッチに対応するために配列になっていますが、本書では最初の指、touches[0]の位置だけを使います。
 updateメソッドの末尾に以下のコードを追加して、宇宙船を上下に動かしてみましょう。

    var pos = this.ship.getPosition();
    if (this.touched) {
      var k = 0.7;
      pos.y = (pos.y * k) + (this.touched.y * (1.0 - k));
      this.ship.setPosition(pos);
    }

 宇宙船が滑らかに動くように、線形補間して動きを調整しています。線形補間はタッチ入力や、加速度計の値を扱うときによく使う式ですから、なじみがない人はkの値をいろいろ変えて試してみるといいでしょう。kが1に近いほど宇宙船の動きはゆっくりになります。

当たり判定

 当たり判定も簡単に実装できます。updateメソッドの末尾に、さらに以下のコードを追加します。

    for (var i = 0; i < this.enemies.length; i++) {
      var enemy = this.enemies[i];
      var distance = cc.pDistance(pos, enemy.getPosition());
      if (distance < 25) { // まあ適当に調整します
        cc.log("HIT!!");
      }
    }

 cc.logというのはプログラマならお馴染みのデバッグプリントです。ブラウザのJavaScriptコンソールを開けば、宇宙船が隕石にぶつかるたびに「HIT!!」という文字列が何度も出力されることがわかるはずです。(同じ文字列を続けて出力すると、Chromeはまとめて表示します)

f:id:funige:20131207135759p:plain

 せっかくJavaScriptコンソールを開いたので、コンソールの使い方にも触れておきましょう。まだJavaScriptコンソールをあまり使ったことが無いなら、使い方に慣れるいい機会です。
 上のソースコードではcc.pDistanceという関数を使っています。この関数について調べるために、コンソールに以下のように入力してみましょう。「>」で始まる行が、筆者が入力した部分です。コンソールの機能はエラーやログを表示するだけではありません。式を入力すれば結果を返してくれることは知っていますよね?

f:id:funige:20131207135817p:plain

 ……うーん「HIT!!」がじゃまですね。紹介する順番を間違えたかな。

Chromeデバッグ機能を使う

 cc.logを使ったデバッグは手軽でどんな環境でも動くのが利点ですが、もっと便利な方法がChromeにはたくさん用意されています。

f:id:funige:20140215072004p:plain

 デベロッパーツールのメニューで「Sources」タブを選ぶと、ソースコードを開くことができます。Mac OS Xだと「Hit Cmd+O to open a file」と表示されると思います。Cmd+Oを押してファイルを選択してみましょう。


 ソースの左側の行番号の所をクリックすれば、ブレークポイントを設定して、プログラムを途中で止めて調べることができます。

f:id:funige:20140215072049p:plain

 ブレークポイントでプログラムの実行が止まったら、ソースの右側にある「Scope Variables」という欄で(下の方にスクロールしないと見えないかも知れません)、変数の内容を確認したり修正することができます。

 デベロッパーツールのメニューで今度は「Console」タブに戻って、コンソールにいろいろ入力してみましょう。

f:id:funige:20140215072131p:plain

 ブレークポイントでプログラムが止まっている間は、止めた場所で使っていた変数(thisとかiとか)がコンソールでもそのまま使えますので、好きなだけ状態を調べたり、メソッドを実行することができます。


 プログラムの実行を再開したいときはSourceタブ右上の「|►」ボタン(またはF8)を押します。
 ブレークポイントを解除したいときはソースの左の行番号のところをもう一回クリックしてもいいですし、「Scope Variables」の下の「Breakpoints」の欄で一時的に無効にすることもできます。

ゲームオーバーの画面を追加する

 背景の星空は紫のグラデーションがいいですね。ちょっとメルヘンなかんじで。
 でもその前に、ゲームオーバーの画面とスコアの計算を追加しましょう。sample.jsと同じフォルダに、今度はresult.jsというファイルを新規作成します。新しいファイルを作るのは2回目ですね。

// result.js
var g = {
  score:0,
};

var Result = cc.Layer.extend({
  init:function () {
    this._super();
    var size = cc.Director.getInstance().getWinSize();

    var resultLabel = cc.LabelTTF.create("今回のスコア " + g.score, "Arial", 20);
    resultLabel.setPosition(cc.p(size.width / 2, size.height / 2));
    this.addChild(resultLabel);

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

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

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

 最初に作ったsample.jsとほとんど同じです。似たようなコードを何度も貼って申し訳ないのですが、このテンプレートは何度も使います。こういうのはエディタの機能か何か使って、ぱっと作成できるようにしておくといいですね。
 あと、もう忘れているかも知れませんが、新しいJSファイルを追加した時は必ずcocos2d.jsに登録しなければなりません。

// cocos2d.js 42行目あたり
  appFiles:[
    'src/resource.js', 
    // 'src/myApp.js'//add your own files in order here
    'src/sample.js',
    'src/result.js',
 ]

 result.jsができたら、sample.jsに戻って、ゲームが終わったとき画面をフェードアウトしてResultシーンにジャンプするように修正します。HIT!!のところは何度も通るので、2回目以降の衝突は無視するように工夫しました。

// sample.js
  update:function (dt) {
    ...
      if (distance < 25) {
        cc.log("HIT!!");
        if (!this.gameover) {
          this.gameover = true;
          this.onGameover();
        }
      }
    }
    g.score++; // ついでにこれも追加
  },

  onGameover:function () {
    var transition = cc.TransitionFade.create(1.0, new ResultScene());
    cc.Director.getInstance().replaceScene(transition);
  },

 updateの最後の行の「g.score++」は、スコアの計算です。
 updateを通るたびに1点ずつ加算しているので、プレイ時間が長いほど得点が高くなります。g.scoreはresult.jsの先頭で定義されていますが、グローバル変数なので、sample.jsからも普通にアクセスできます。
 

 えっ? ゲーム画面にスコアを表示したいですか? しょうがないなあ。画面の左上にスコア表示を追加しましょう。

// sample.js initメソッドの中に追加
    this.scoreLabel = cc.LabelTTF.create("", "Arial", 17);
    this.scoreLabel.setPosition(cc.p(20, size.height - 20));
    this.scoreLabel.setAnchorPoint(cc.p(0, 1));
    this.addChild(this.scoreLabel, 10);

 それから

// sample.js updateメソッドの中に追加
    this.scoreLabel.setString("SCORE " + g.score);

 こんな感じですね。ラベルの位置を調整するために、setAnchorPointを使いました。
 アンカーポイントを指定しないと、setPositionで決められた位置が「ラベルの中心点」になるように描画されます。これはsetAnchorPoint(cc.p(0.5, 0.5))を指定したのと同じです。
 setAnchorPoint(cc.p(0, 1))を指定すれば、setPositionで決められた位置が「ラベルの左上の角」になるように描画してくれます。スコアは画面の左上隅に描画していますから、こうしておけば文字列のサイズが変わっても左上の角の位置はずれません。画面の左上に注目。

f:id:funige:20131207135844p:plain

 蛇足ですが、最後にハイスコアの管理も追加しておきましょう。ハイスコアの保存にはlocal­Storageを使います。local­Storageは、任意の文字列に名前をつけてセーブしてくれる便利なサービスです。保存するアイテムにつける名前は何でもいいのですが、ここでは「Sample/­highScore」という名前にしました。

 result.jsのinitメソッド後半に、以下のコードを追加してください。

// result.js initメソッドの中に追加
    var highScore = parseInt(sys.localStorage.getItem("Sample/highScore") || 0);
    if (g.score > highScore) {
      highScore = g.score;
      sys.localStorage.setItem("Sample/highScore", highScore);
    }
    var highLabel = cc.LabelTTF.create("ハイスコア " + highScore, "Arial", 20);
    highLabel.setPosition(cc.p(size.width / 2, size.height / 2 - 50));
    this.addChild(highLabel);

    // いちいちリロードするのは面倒なので、
    // 2.0秒経ったら画面をタップしてゲームに戻れるようにします。
    this.scheduleOnce(function () {
      this.onTouchesBegan = function (touches, event) {
         g.score = 0;
         cc.Director.getInstance().replaceScene(new SampleScene());
      };
    }, 2.0);

 これではじめてのゲームは完成です。

リソースを使う

 ……ああ、そうでした。画像ファイルの表示方法はまだ説明してなかったですね。でもcc.LabelTTFをcc.Spriteに差し替えるだけですから、そんなに難しくありません。

  //this.ship = cc.LabelTTF.create("(・ω・)", "Arial", 38); // これの代わりに
  this.ship = cc.Sprite.create("res/ship.png"); // これを使う

 隕石も画像に変えて、背景画像も貼ってみましょう。
 ゲームに使う画像はできれば自分で描いて欲しいのですが、急いでる人は筆者のWebサイトからダウンロードすることもできます。


 deteruyo.appspot.com/test/apps/Sample/res/ship.png


 ここまで黙っていて申し訳ないのですが、ここまで書いてきたソースコードもすべてサポートページからダウンロードできるようになっていますので、もしどこかで詰まったらこのページで実際に動くコードを確認してください。

f:id:funige:20131207135912p:plain

 簡単に画像が表示できるのはCocos2d-html5の本当に素晴らしい所なのですが、あまり便利すぎるのも考えものです。便利な環境に慣れると、非同期処理の本質的な難しさを見落としがちです。
 画像の読み込みには時間がかかりますし、どれくらい時間がかかるかは予想できません。Cocos2d-html5は画像が読み込まれるまで次の行を実行せずに待つのでしょうか? 待たないとしたら、例えば画像のサイズがまだわからないときスプライトのサイズを取得しようとすると、どんな値が返ってくるのでしょうか? 答えられますか?
 
 cc.Spriteのように、使いたい場所でリソースを読み込んで使えるのはCocos2d-html5の世界では「むしろ例外」と考えた方がいいでしょう。後の章でスプライトによく似たcc.­Scale9­Spriteやcc.­Menu­Item­Imageを紹介しますが、スプライトの時と同じ感覚でこれらのクラスを使おうとすると、何も画面に表示されなくてびっくりすることになります。
 使いたいリソースは、src/resource.jsで全部宣言して、ゲームがはじまる前に先読みしておくのが正式な手順です。スプライトに使う画像も、リソースとして先読みしておくことをおすすめします。
 
 リソースとは、画像や効果音のデータのことです。
 本章の前半では、プロジェクトを構成するindex.html・cocos2d.js・main.jsの3つのファイルのはたらきについて説明しました。順番が遅くなってしまいましたが、このresource.jsも、この3つと同じくらい重要なファイルだと考えていいでしょう。
 resource.jsに追加されたリソースは、main.jsのapplication­Did­Finish­Launchingメソッドの中で先読み (preload) されます。先読みしたリソースはメモリ上に存在することが保証されますから、読み込みのタイムラグを意識せずにすぐにサイズを取得したり使ったりすることができます。
 宇宙船の画像 (res/ship.png) をsrc/resource.jsに登録してみましょう。

// resouce.js
var s_ship = "res/ship.png"; // 追加
//var s_HelloWorld = "res/HelloWorld.png";
//var s_CloseNormal = "res/CloseNormal.png";
//var s_CloseSelected = "res/CloseSelected.png";

var g_resources = [
    //image
    {src:s_ship}, // ここも追加
    //{src:s_HelloWorld},
    //{src:s_CloseNormal},
    //{src:s_CloseSelected}

    //plist

    //fnt

    //tmx

    //bgm

    //effect
];

 sample.jsの方は、何も変更する必要がありません。先読みされたファイルと同じ名前がプログラム中に出てきたら勝手にメモリ上のキャッシュが利用されますし、キャッシュに無ければその場で画像が読み込まれます。

// sample.js
  //this.ship = cc.LabelTTF.create("(・ω・)", "Arial", 38); // これの代わりに
  //this.ship = cc.Sprite.create("res/ship.png"); // このままでもいいんだけど
  this.ship = cc.Sprite.create(s_ship); // こっちのほうがいいかな?

 まあ気分の問題ですが、直接「res/ship.png」と書くより、定数s_shipを使って書き直したほうがいいのかな。後で別の画像に差し替えたくなったときに、修正する場所が減って楽ですからね。

効果音とBGM

 オーディオファイルもリソースのひとつです。データは自分で作ってもいいのですが、インターネットで公開されている商用利用可の素材をダウンロードして使うのが簡単です。
 商用利用可能な素材を無料で公開しているサイトは幾つかありますが、今回は個人開発者に人気の魔王魂さん (maoudamashii.­joker­sounds.­com) を例にして、ゲームに組み込む方法を紹介します。

f:id:funige:20131207135940p:plain

 BGM素材「サイバー」のページから「サイバー07」、効果音素材「戦闘」のページから「爆発06」をダウンロードしましょう。もちろん他の素材を選んでもいいです。
 ファイル形式はmp3とoggとか選べるようになっていますが、両方ダウンロードしてください。FireFoxはmp3が再生できないし、IESafarioggが再生できません。Chromeならどっちも再生できるんですけど、どのブラウザでも音が出るようにしたいときは複数のフォーマットを用意するのが基本のようです。

 ダウンロードしたファイルは、画像ファイルと同じようにresフォルダに置いて、resource.jsから読み込みます。

// resource.js
var s_cyber07 = "res/bgm_maoudamashii_cyber07.mp3"; 
var s_cyber07Ogg = "res/bgm_maoudamashii_cyber07.ogg"; 
var s_explosion06 = "res/se_maoudamashii_explosion06.mp3"; 
var s_explosion06Ogg = "res/se_maoudamashii_explosion06.ogg"; 

var g_resources = [
    ...
    //bgm
    {src:s_cyber07},
    {src:s_cyber07Ogg},

    //effect
    {src:s_explosion06},
    {src:s_explosion06Ogg},
];

 ゲームに組み込むのも簡単です。sample.jsのonEnterとonGameoverにそれぞれ以下のようなコードを追加してください。

// sample.js
onEnter:function () {
    var audioEngine = cc.AudioEngine.getInstance();
    audioEngine.playMusic(s_cyber07, true); // BGM
    ...
},

onGameover:function () {
    var audioEngine = cc.AudioEngine.getInstance();
    audioEngine.playEffect(s_explosion06); // 爆発音
    audioEngine.stopMusic(s_cyber07);
    ...
},

 s_cyber07とs_explosion06はどちらもmp3ファイルですが、mp3が再生できないブラウザではうまい具合に同名の他のファイル(s_cyber07Oggとか)を探して再生してくれるようです。

この章のまとめ

 この章ではたくさんのことを学びました。

  • 新しいプロジェクトを作る方法。
  • 各ファイル(index.html・cocos2d.js・main.js)のはたらき。
  • シーンの書き方。
  • updateメソッドを使ってキャラクターを動かす方法。
  • プレイヤーの操作に反応させる方法。
  • 複数のシーンを切り替える方法。
  • ハイスコアの取得と保存。
  • リソースの先読み。

演習課題

  • 隕石と衝突した時、音だけでなく動きで表現するようにすれば、もっとゲームらしくなります。ぶつかった時に画面全体を揺らしてみましょう。(ヒント:レイヤー自体の座標をthis.setPosition()でランダムに変えてやれば画面全体を動かすことができます)
  • 隕石の大きさを乱数で決めるようにしましょう。スプライトの表示サイズはenemy.setScale()で変えることができますが、当たり判定はどう書いたらいいでしょうか?
  • ゲームは簡単な難易度からはじめて、だんだん難しくなるようにするべきです。難易度を調整する方法を考えてみましょう。
広告を非表示にする