Fork me on GitHub

スペースインベーダーのチュートリアル

このチュートリアルでは、スペースインベーダーのクローンを作成します。このチュートリアルでは、コードを通じてより多くのゲームエレメントを作ることと、プラットフォームゲームのチュートリアル が取り上げなかった MelonJS が提供する他の API を使うことに主な焦点を当てます。

はじめに

このチュートリアルを実際に行うには、以下が必要になります:

  • melonJS のボイラープレート: このチュートリアルではデフォルトのテンプレートプロジェクトとして使用します。
  • チュートリアルの画像アセット:ボイラープレートのデータディレクトリの中に解凍します。解凍すると、以下の内容が含まれています:
  • data/img/player.png
    data/img/ships.png
    js/game.js
    etc
    
  • melonJS ライブラリ:ボイラープレートをダウンロードした場合には、すでに手元にあります。 /lib ディレクトリの下にコピーしてください。 ボイラープレートはミニフィケーションされたタスクを提供するため、開発バージョンをコピーしても大丈夫です。
  • より詳細な内容がわかる melonJS のドキュメンテーション

テスト/デバッグ:

melonJS ボイラープレートの README にも詳細が書いてあるように、`grunt connect` ツールを使うことでローカルのウェブサーバーを使うのが最適で、そうすることによって http://localhost:8000 URL を使ってブラウザ内でゲームのテストができるようになります。

そのファイルシステムを使いたいだけの場合には、"cross-origin request" のセキュリティエラーに直面することが問題となります。Chrome の場合は、"--disable-web-security" パラメータを使う必要がある、または、ブラウザの起動時に"--allow-file-access-from-files" した方が良いことになります。ローカルのコンテンツをテストするためにはこれを行わねばならず、これを行わない場合には XHR を通じてアセットをロードしようとする時にブラウザがエラーを出します。ただし、この方法は推奨されるものではありません。このオプションを有効にしている限りは、セッションにセキュリティ上の脆弱性を追加することになります。

宇宙船の設定

ボイラープレートにあるデータストラクチャはこのような感じになっているはずです:

data/
  img/
    player.png
    ships.png
js/
  game.js
  resources.js
  entities/
    HUD.js
    entities.js
  screens/
    play.js
    title.js
index.html
index.css

ボイラープレートにはデフォルトのコードが大量に含まれています。このチュートリアルでは必要のないファイルもあります。ファイル screens/title.js を削除して、フォルダ the entire js/entities を削除しても大丈夫です。その後、それらを含まないようにするために index.html ファイルをアップデートして、ファイル game.js からTitleScreen の参照を削除します。

var game = {
    // Run on page load.
    onload : function () {
        // Initialize the video.
        if (!me.video.init(640, 480, {wrapper : "screen", scale : 'auto'})) {
            alert("Your browser does not support HTML5 canvas.");
            return;
        }

        // add "#debug" to the URL to enable the debug Panel
        if (me.game.HASH.debug === true) {
            window.onReady(function () {
                me.plugin.register.defer(this, me.debug.Panel, "debug", me.input.KEY.V);
            });
        }

        // Initialize the audio.
        me.audio.init("mp3,ogg");

        // Set a callback to run when loading is complete.
        me.loader.onload = this.loaded.bind(this);

        // Load the resources.
        me.loader.preload(game.resources);

        // Initialize melonJS and display a loading screen.
        me.state.change(me.state.LOADING);
    },

    // Run on game resources loaded.
    loaded : function () {
        // set the "Play/Ingame" Screen Object
        this.playScreen = new game.PlayScreen();
        me.state.set(me.state.PLAY, this.playScreen);

        // start the game
        me.state.change(me.state.PLAY);
    }
};

ゲームがブートストラップされる場所は Game.js です。index.html は、window ready イベントの中の全ての js ファイルがロードされた後に、game.onload 関数を呼び出します。 me.video.init ビットは canvas タグを作成し、映像をセットアップします。

12〜16行目はデバッグパネルを使うためのものです。

その後で、音声エンジンを初期化し、このゲームに対応させるフォーマットを指示します。

loaded 関数に対する me.loader のコールバックを設定し、その後、アレイ経由で、ロードする必要のあるアセットが何なのかを指示します。

このプロセスの最後のステップは、ゲームのステートをロード中に設定することです。

loaded : function () {
  this.playScreen = new game.PlayScreen();
  me.state.set(me.state.PLAY, this.playScreen);

  // start the game
  me.state.change(me.state.PLAY);
}

loaded 関数はその後プレイ画面をセットアップし、play ステートに対して screen オブジェクトを使用するようにゲームに指示します。

その後、ゲームステートは PLAY に設定されます。

スペースインベーダーに戻って

最初に、画像を resources.js ファイルに追加します。

game.resources = [
    { name: "player", type: "image", src: "data/img/player.png" },
    { name: "ships", type: "image", src: "data/img/ships.png" }
];

この変数は game.js 内で me.loader.preload に渡されたものです。

このアセットの構造は:

name ゲームの中で使いたいアセットの名称。string キー。
type アセットのタイプ。有効なタイプには audio、binary、image、json、tmx、tsx があります。そのままのテキストやリストに含まれない他のフォーマットを読み込むには、バイナリが良い方法です。TMX & TSX は Tiled ファイルのフォーマットです。 xml フォーマットでも json フォーマット大丈夫です。
src アセットに対する index.html からの相対パス。音声の場合は、直接パスの代わりにフォルダを指定する必要があります。

js/screens/play.js を開いて、 onResetEvent と onDestroyEvent の2つの方法でコードを空にします。その後保存して、そしてウェブブラウザでゲームを開きます。

ここではあまり説明することがありません。実際に変更してみましょう。

最初にすることは、プレイヤーエンティティの作成です。

js フォルダの下に新しいファイルを追加して、player.js と名前をつけます。index.html ファイルにも必ず追加してください。

game.Player = me.Sprite.extend({
  init : function () {
      var image = me.loader.getImage("player");
      this._super(me.Sprite, "init", [
          me.game.viewport.width / 2 - image.width / 2,
          me.game.viewport.height - image.height - 20,
          { image : image }
      ]);
  }
});

ここでは me.Sprite を拡張する window.game オブジェクトに関数を追加しようとしています。これでプレイヤー画像を捕捉する init メソッドをセットアップします。

x 座標については、単にちょうど中心を捕捉し、宇宙船の半分を引いて、中心に位置するようにします。そして、y プロパティを一番下から 20 ピクセル上に設定します。そして、最後に画像インスタンスをそれに渡します。

this._super の呼び出しは、親クラスのメソッドを参照する方法です。この場合、me.Sprite コンストラクタを呼び出します。

それでは、エンティティプールの中にこのプレイヤーをセットアップしましょう。game.js を開いて、loaded メソッドの上に以下のコードを追加します:

me.pool.register("player", game.Player);

ここで js/screens/play.js を開き、onResetEvent メソッドを以下のように編集します:

game.PlayScreen = me.ScreenObject.extend({
  /**
   * action to perform on state change
   */
  onResetEvent : function () {
      me.game.world.addChild(me.pool.pull("player"));
  },

  /**
   * action to perform when leaving this screen (state change)
   */
  onDestroyEvent : function () {
  }
});

このステートがロードされる時に、onResetEvent が呼び出されます。そのため、呼び出す時に


    me.state.change(me.state.PLAY);
    

ファイル game.js の中で、onResetEvent が次に呼び出されます。

やった、宇宙船が画面の下に表示されました!

しかし、まだロード中のバーが見えてしまい、かっこよくありません。こうなる理由は、MelonJS がする必要のないオペレーションをしたがらないからです。再描画される背景画像があるときもありますが、そのときには元々のロード中バーは覆い隠されます。しかし、このゲームには背景画像がないため、次にカラーレイヤーを追加します。

    me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
    

これをプレイ画面のプレイヤーを追加した行の上に追加します。最初のパラメーターは単にレイヤーの名前ですので、必要に応じて後でゲーム世界からフェッチすることは簡単です。

2つ目のパラメーターは描画する色を hex で与えます。

addChild 関数に渡される2つ目のパラメーターは、z インデックスです。最初に描画して、そしてゼロに設定します。

これでやっかいなロード中バーはなくなったはずです。次は敵を追加します。js フォルダの下に enemy.js という名前の新しいファイルを作成して、ファイル index.html に追加しましょう。

敵はプレイヤーのレーザーのようなものと衝突しなければならないので、me.Sprite と反対に me.Entityを拡張すべきで、以下のようになります:

game.Enemy = me.Entity.extend({
  init: function (x, y) {
      this._super(me.Entity, "init", [x, y, {
          image : "ships",
          width : 32,
          height : 32
      }]);
  },

  update: function (dt) {
      this._super(me.Entity, "update", [dt]);
      return true;
  }
});

この敵と追加するとともに、様々な場所に配置する必要があるため、x & y がコンストラクタに追加され、その後、the me.Entity のコンストラクタに渡されます。 アレイの3つ目のパラメーターは設定のハッシュです。この設定はその画像を "ships" と特定し、game.resources アレイを参照します。幅と高さは 32x32 のサイズに設定されます。

ここでカスタマイズされた update メソッドを作成し、ゲームエンジンに再描画を指示しています。melon がゲームループを通り過ぎると、結果に or をします。 与えられたフレームに何も変更がない場合には、再ペイントされません。true を返し、super メソッドを呼び出すことで、敵が実際にレンダリングされることが保証されます。

game.js の中で、敵をエンティティプールに追加します:

me.pool.register("enemy", game.Enemy);
    

play.js に戻って、ゲーム世界に敵を追加します。play.js はこんな風になるはずです:

game.PlayScreen = me.ScreenObject.extend({
  /**
   * action to perform on state change
   */
  onResetEvent : function () {
      me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
      me.game.world.addChild(me.pool.pull("player"), 1);
      me.game.world.addChild(me.pool.pull("enemy", 50, 50), 2);
  },

  /**
   * action to perform when leaving this screen (state change)
   */
  onDestroyEvent : function () {
  }
});

敵を任意の x & y に置いて、試してみましょう。ブラウザからページを保存 & 更新してください。

宇宙船の見え方が変化し続けていることに気づくと思います。data/img の下にあるファイル ships.png を開くと、スプライトシートに4つの異なる宇宙船が含まれていることがわかります。レンダリング可能なもののための me.Entity は me.AnimationSheet クラスを使用します。レンダリング可能なプロパティにアニメーションを追加したり設定したりしていないため、すべてのフレームを1つずつループしている状態です。これを修正しましょう。

敵に新しいメソッドを追加します:

chooseShipImage: function () {
    var frame = ~~(Math.random() * 3);
    this.renderable.addAnimation("idle", [frame], 1);
    this.renderable.setCurrentAnimation("idle");
}

最初の行は使いたいフレームを単にランダムにしているだけです。宇宙船は 32x32 で、画像は 64x64 なので、4フレームあります。 ~~ は 数字がゼロまたはプラスであるときの Math.floor のショートカットです。マイナスの数字の場合は、 Math.ceil のように機能します。

2行目はアニメーションシートインスタンス(this.renderable)にアクセスしていて、addAnimation 関数を使って、新しい idle フレームを追加します。こうして、ランダムに作成された index を単に指定します。

最後の行では、現在のアニメーションを idle に設定します。

ここで以下のようにコンストラクタの一番下にある関数を呼び出します:

game.Enemy = me.Entity.extend({
    init: function (x, y) {
        this._super(me.Entity, "init", [x, y, {
            image: "ships",
            width: 32,
            height: 32
        }]);
        this.chooseShipImage();
    },

    chooseShipImage: function () {
        var frame = ~~(Math.random() * 3);
        this.renderable.addAnimation("idle", [frame], 1);
        this.renderable.setCurrentAnimation("idle");
    },
});

ここでページを更新すると、宇宙船はそのうちの1つとしてのみ表示されるはずです。変更されたことを確かめるに、何度か更新してみてください。

動きの適用

宇宙船が画面に表示されたら、実際にインタラクションを始めましょう。

play.js に戻って、キー機能設定を追加しましょう:

game.PlayScreen = me.ScreenObject.extend({
  /**
   * action to perform on state change
   */
  onResetEvent : function () {
      me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
      me.game.world.addChild(me.pool.pull("player"), 1);
      me.game.world.addChild(me.pool.pull("enemy", 50, 50), 2);

      me.input.bindKey(me.input.KEY.LEFT, "left");
      me.input.bindKey(me.input.KEY.RIGHT, "right");
      me.input.bindKey(me.input.KEY.A, "left");
      me.input.bindKey(me.input.KEY.D, "right");
  },

  /**
   * action to perform when leaving this screen (state change)
   */
  onDestroyEvent : function () {
      me.input.unbindKey(me.input.KEY.LEFT);
      me.input.unbindKey(me.input.KEY.RIGHT);
      me.input.unbindKey(me.input.KEY.A);
      me.input.unbindKey(me.input.KEY.D);
  }
});

このメソッドがここで呼び出すのは、かなり単純です。キー操作をアクション名に結びつけます。1つのアクション名に複数のキーを割り当てることもできます。

良いゲーム設計がなされると、複数のキー操作設定が提供されることが一般的です。もっと良い場合は、設定が変更可能になっています。左利きの人や異なるキー配置を使用している人々のことを常に気にとめておく必要があります。

addChild の呼び出しに z index オプションが追加されていることに気づいたかもしれません。これはかなり良いやり方で、こうすることで描画の順番を確定することができます。

onDestroyEvent は、ステートの変更時にイベントを削除します。play ステートになるのはロード後だけなので、実際に必要なことではありません。しかし、覚えておくと良いでしょう。

キー操作が設定できたら、プレイヤーの動きを実装しましょう。player クラスに以下の update 関数を追加しましょう:

次に、init メソッドの中でプレイヤーに velx プロパティを追加し、画面上で行ける最も遠い x の位置(maxX)も追加します:

init : function () {
    var image = me.loader.getImage("player");
    this._super(me.Sprite, "init", [
        me.game.viewport.width / 2 - image.width / 2,
        me.game.viewport.height - image.height - 20,
        image
    ]);
    this.velx = 450;
    this.maxX = me.game.viewport.width - this.width;
},

次に、キーイベントを確かめる update メソッドを修正し、それに従ってプレイヤーを動かします。

update : function (time) {
    this._super(me.Sprite, "update", [time]);
    if (me.input.isKeyPressed("left")) {
        this.pos.x -= this.velx * time / 1000;
    }

    if (me.input.isKeyPressed("right")) {
        this.pos.x += this.velx * time / 1000;
    }

    this.pos.x = this.pos.x.clamp(0, this.maxX);

    return true;
}

ゲームオブジェクトの update 関数は、常にデルタタイム(ミリ秒単位)を受け取ります。親のクラスアップデートにそれを受け渡すことが大切です。

this._super(me.Sprite, "update", [time]);

その後は、left アクションが現在押されているかどうかを確認することになります。以前に設定された velocity の値を使って、velocity の値を単に引いて、秒単位のデルタとかけ合わせます。

if (me.input.isKeyPressed("left")) {
    this.pos.x -= this.velx * time / 1000;
}

右に動かすには、right アクションを確認して、x 座標の位置に速度の値を追加します。

if (me.input.isKeyPressed("right")) {
    this.pos.x += this.velx * time / 1000;
}

次に、clamp を使って、x の値が画面の外に行かないことを保証します。

this.pos.x = this.pos.x.clamp(0, this.maxX);

返された値は、再描画が必要かどうか melon に伝えます。アニメーションシートが与えられたフレームの上でアニメーション化する必要がある時に、これが指示するために役立てることができます。 しかし、これは単一のスプライトなので、単に再描画するように指示することができます。

return true;

ファイルを保存して、ブラウザを更新します。A/D キー または左右の矢印キーを使って、動かしてみましょう。

敵の動き

スペースインベーダーの大きな特徴は、すべての宇宙船が1つの方向に動き、下に降りて、そして反対側に動くことです。すべての宇宙船が一体となって動きます。プレイヤーに使った velocity ロジックを使って、それを enemy クラスに適用します。しかし、自分たちでやった方が、MelonJS をより活用することができます。ここでは me.Container の自作したサブクラスを使いましょう。

コンテナ内のオブジェクトはその親に対する相対的なものです。コンテナを動かす時には、その中の全てのオブジェクトが一緒に動きます。これは回転やサイズ変更のオペレーションにも適用されます。それでは、1つ作ってみましょう。

新しいファイル js/enemy_manager.js を作成し、index.html に追加します。

game.EnemyManager = me.Container.extend({
  init : function () {
      this._super(me.Container, "init", [0, 32,
          this.COLS * 64 - 32,
          this.ROWS * 64 - 32
      ]);
      this.COLS = 9;
      this.ROWS = 4;
      this.vel = 16;
  }
});

本質的には、ここで設定しているものがスタート位置と基本となる幅です。コンテナを 32 ピクセル下げて始め、0 で左 (または x)です。

幅および高さとして、宇宙船に 64 ピクセルを割り当てています。そこから、最後の行と列では横のパッドが必要ないため、32 ピクセルを引きます。

コンテナに敵を追加するには、別のメソッドが必要になります:

createEnemies : function () {
  for (var i = 0; i < this.COLS; i++) {
      for (var j = 0; j < this.ROWS; j++) {
          this.addChild(me.pool.pull("enemy", i * 64, j * 64));
      }
  }
  this.updateChildBounds();
}

9列 x 4列で、36 機の宇宙船を作成しました。すべての追加された子を考慮に入れるために適切なサイズに変更されることを保証する "updateChildBounds" 関数が呼び出されていることも確認してください。同様に、子を削除する時にも、あとで同様に行います。

ここで、play.js の中で、敵のための addChild を削除し、enemy マネージャーにプロパティを設定します。以下、createEnemies を呼び出し、ゲーム世界に追加します。

onResetEvent : function () {
    me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
    me.game.world.addChild(me.pool.pull("player"), 1);

    this.enemyManager = new game.EnemyManager();
    this.enemyManager.createEnemies();
    me.game.world.addChild(this.enemyManager, 2);

    me.input.bindKey(me.input.KEY.LEFT, "left");
    me.input.bindKey(me.input.KEY.RIGHT, "right");
    me.input.bindKey(me.input.KEY.A, "left");
    me.input.bindKey(me.input.KEY.D, "right");
    me.input.bindKey(me.input.KEY.SPACE, "shoot", true);
}

保存して更新したら、たくさんの宇宙船がランダムに表示されるはずです。

動かすために、シンプルなままにし、1秒に1回コンテナを動かします。これには melonjs タイマーを使うことができます。

これら2つのメソッドを enemy_manager.js に追加します。

onActivateEvent : function () {
    var _this = this;
    this.timer = me.timer.setInterval(function () {
        _this.pos.x += _this.vel;
    }, 1000);
},

onDeactivateEvent : function () {
    me.timer.clearInterval(this.timer);
}

その後、init メソッドの vel プロパティを 16 に設定します。

this.vel = 16;

このオブジェクトがゲーム世界に追加されると、onActivateEvent が呼び出されます(定義されている場合)。 これは、コンテナ上で addChild にどんなオブジェクトが渡された場合にも実行されます。同様に、オブジェクトがゲーム世界から削除されると、 onDeactivateEvent が呼び出されます。

MelonJS バージョンの setInterval (ゲームループの中に内蔵され、window.setInterval を使わない)を使うことで、x 座標上の位置を大きくすることができます。

保存して、ブラウザを更新します。敵の宇宙船はこれですべて一体となって動くようになります。

次に、removeChildNow のカウンターパートを追加します:

removeChildNow : function (child) {
    this._super(me.Container, "removeChildNow", [child]);
    this.updateChildBounds();
}

removeChild の代わりに removeChildNow を拡張する理由は、removeChild が呼び出されるのが、現在のフレームが終わった後だからです。removeChildNow は実際にオブジェクトを削除するメソッドで、オブジェクトが削除された後に境界のサイズを変更したいのです。

onActivateEvent : function () {
    var _this = this;
    this.timer = me.timer.setInterval(function () {
        var bounds = _this.childBounds;

        if ((_this.vel > 0 && (bounds.right + _this.vel) >= me.game.viewport.width) ||
            (_this.vel < 0 && (bounds.left + _this.vel) <= 0)) {
            _this.vel *= -1;
            _this.pos.y += 16;
            if (_this.vel > 0) {
              _this.vel += 5;
            }
            else {
              _this.vel -= 5;
            }
        }
        else {
            _this.pos.x += _this.vel;
        }
    }, 1000);
}

これはかなり長いコードなので、分解して行きましょう。

子の境界を使うことで、世界座標における左 & 右の値を得ることができます。

var bounds = _this.childBounds;

if の最初の部分では、コンテナが右に動いているかどうかを確かめ、その後、右の境界 + 速度がビューポートの外に出ていないかを確かめています。

(_this.vel > 0 && (bounds.right + _this.vel) >= me.game.viewport.width)

2つ目の部分では、コンテナが左に動いているかどうかと、左側の境界がゼロ未満かどうかを確かめています。

(_this.vel < 0 && (bounds.left + _this.vel) <= 0)

このブロックでは、速度の向きを反対にし、16 ピクセル下げて、その後速度を上げています。

_this.vel *= -1;
_this.pos.y += 16;
if (_this.vel > 0) {
    _this.vel += 5;
}
else {
    _this.vel -= 5;
}

そして、最後の所では、コンテナがまだ左にも右にも動いてない場合に、速度を増加させています。

else {
    _this.pos.x += _this.vel;
}

ここで保存・更新すれば、画面を左右に動きながら、プレイヤーに近づいてくるはずです。しかし、ここで問題があります!境界に達した時に引き返さないのです!何が起きたのでしょうか?

ここでは、http://localhost:8000/#debug のようにURL に #debug を追加して、デバッグパネルの中の "hitbox" の隣にあるチェックボックスをタップします。これによって hitbox のレンダリングが有効になり、オブジェクトの内部ストラクチャーが視覚化されます。

hitbox は、子の境界(EnemyManager の本来の位置を占める大きな紫色の長方形)がコンテナと一緒に動いていないことを示します。これは、子の境界のアップデートが自動的に計算されないためです。update メソッドをオーバーロードすることで、子の境界の再計算を簡単に行うことができます:

update : function (time) {
    this._super(me.Container, "update", [time]);
    this.updateChildBounds();
}

保存して更新すると、期待していた通りに動くようになったのがわかります!

レーザーの追加、ピューピュー!

ここで、このゲームに実際の“ゲーム” を追加しましょう。

最初にすることは、ファイル play.js を開いて、新しいキー操作の設定 & 設定解除を追加することです:

me.input.bindKey(me.input.KEY.SPACE, "shoot", true);
me.input.unbindKey(me.input.KEY.SPACE);

bindKey の呼び出しがブーリアン型になる理由は、キープレスごとの登録だけを許可するためです。そのため、2回打つには、プレイヤーはスペースバーを押して、離して、もう一度押さなければなりません。

プレイヤーが打てるようにする前に、レイザーが必要です。ファイル laser.js を作成し、以下のコードを追加します。いつもと同様に、laser.js のスクリプトタグをファイル index.html に追加することをお忘れ無く。

game.Laser = me.Entity.extend({
    init : function (x, y) {
        this._super(me.Entity, "init", [x, y, { width: game.Laser.width, height: game.Laser.height }]);
        this.z = 5;
        this.body.setVelocity(0, 300);
        this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;
        this.renderable = new (me.Renderable.extend({
            init : function () {
                this._super(me.Renderable, "init", [0, 0, game.Laser.width, game.Laser.height]);
            },
            destroy : function () {},
            draw : function (renderer) {
                var color = renderer.getColor();
                renderer.setColor('#5EFF7E');
                renderer.fillRect(0, 0, this.width, this.height);
                renderer.setColor(color);
            }
        }));
        this.alwaysUpdate = true;
    },

    update : function (time) {
        this.body.vel.y -= this.body.accel.y * time / 1000;
        if (this.pos.y + this.height <= 0) {
            me.game.world.removeChild(this);
        }

        this.body.update();
        me.collision.check(this);

        return true;
    }
});

game.Laser.width = 5;
game.Laser.height = 28;

それでは、部分ごとに見て行きましょう。一番下には、レイザーの幅と高さとして2つのプロパティを設定したので、簡単に再利用できます。

game.Laser.width = 5;
game.Laser.height = 28;

ここは従来と同じやり方です。パラメーターから x & y の位置を設定して、幅+高さのプロパティを設定します。他のオブジェクトとは少し違いますが、オブジェクトの z 座標は個別に設定します。これは、addChild の呼び出しで z 座標を渡す代わりとなる方法です。

this._super(me.Entity, "init", [x, y, { width: game.Laser.width, height: game.Laser.height }]);
this.z = 5;

次のメソッドは、物理ボディを設定するためのもので、画面全体にレーザーを動かすために使います。

this.body.setVelocity(0, 300);
this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;

デフォルトでは、me.Body はシェイプをセットアップしません。しかし、me.Entity は、位置・幅・高さに基づいてシェイプを作成します。

まず、速度を設定します。速度はベクトルで、レイザーは上に向けて打ちたいと思っています。そのため、速度を 300 に設定します。方向を指示するため、速度はマイナスの値にならないように注意してください。

this.body.setVelocity(0, 300);

次に、衝突のタイプを設定します。これは衝突コールバックの中で役に立ちます。

this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;

ここはかなりボリュームのあるコードです。me.Renderable は melon の中でベースとなる描画クラスです。ゲーム世界の中で正しい描画オブジェクトとなるために必要な最低限の設定が与えられます。

this.renderable = new (me.Renderable.extend({
    init : function () {
        this._super(me.Renderable, "init", [0, 0, game.Laser.width, game.Laser.height]);
    },
    destroy : function () {},
    draw : function (renderer) {
        var color = renderer.getColor();
        renderer.setColor('#5EFF7E');
        renderer.fillRect(0, 0, this.width, this.height);
        renderer.setColor(color);
    }
}));

この init はかなり昔からある形で、laser エンティティと相対化されるため、x & y をゼロに設定します。エンティティ自体と同じ幅と高さを使用します。

init: function () {
    this._super(me.Renderable, "init", [0, 0, game.Laser.width, game.Laser.height]);
},

この空の destroy メソッドは、レンダリング可能なものが削除された時のコールバックです。callback ロジックを定義する必要はありませんが、関数の定義はそれでも必要です。

destroy : function () {},

次に描画メソッドを実装します。ここまで使ってきたクラスは、目的に合った実行中の描画メソッドを提供してきましたが、ここでレンダラーを使って、何かを手で描画します。

draw : function (renderer) {
    var color = renderer.getColor();
    renderer.setColor('#5EFF7E');
    renderer.fillRect(0, 0, this.width, this.height);
    renderer.setColor(color);
}

レンダラーは me.CanvasRenderer か me.WebGLRenderer のどちらかを使えばよく、me.video.init の中の設定によります。レンダラーは基本的な描画オペレーションを提供します。

それでは、まず、元々の色を参照します。getColor() から返される値は me.Color のインスタンスです。

var color = renderer.getColor();

色をきれいなレーザーらしい緑色に設定します。

renderer.setColor('#5EFF7E');

次に、fill rect ナンバーを使います。繰り返しになりますが、0, 0 は相対的なものです。次に、幅と高さを使って、塗る長方形のサイズを指示します。

renderer.fillRect(0, 0, this.width, this.height);

次に、色を元に戻します。これは大切なことで、他の描画呼び出しは色の変更に影響を受けません。

renderer.setColor(color);

一般的に言うと、純粋なキャンバス描画の呼び出しを使うよりは、画像を使ってゲームを作成するべきですが、キャンバスをいつどのように使うべきかを知っておくと、かなり役に立ちます。

次が、レイザーの init メソッドの最後のステップです:

init : function (x, y) {
    // ...
    this.alwaysUpdate = true;
}

alwaysUpdate プロパティはできる限り避けるべきです。これは、オブジェクトがビューポートの外側にあるときに、オブジェクトをアップデートします。このゲームで使う理由は、レーザーが画面の外に出るまでレーザーを消したくないからです。画面の外に出るまで待って、alwaysUpdate が false の場合は、レーザーが消えることはありません。

update メソッドと言えば。

update : function (time) {
    this.body.vel.y -= this.body.accel.y * time / 1000;
    if (this.pos.y + this.height <= 0) {
        me.game.world.removeChild(this);
    }

    this.body.update();
    me.collision.check(this);

    return true;
}

最初の部分が、レイザーを動かす方法です。宇宙船は位置を直接操作することで動きます。ここには衝突ボディがあるため、y 方向の加速度を引くことで、y 方向の速度を操作します。

this.body.vel.y -= this.body.accel.y * time / 1000;

レーザーの位置に高さを足した値(つまり、レーザーの一番下)がゼロ未満である場合には、レーザーをゲーム世界から削除できます。繰り返しになりますが、alwaysUpdate が true に設定されているため、この関数がここで実行されます。

if (this.pos.y + this.height <= 0) {
    me.game.world.removeChild(this);
}

ボディのアップデートはとても重要で、これはこのフレームに設定した速度を適用します。

this.body.update();

次に、me.collision.check(this) でこのオブジェクトに対する衝突をチェックするように、MelonJS に指示します。

me.collision.check(this);

この機能に対する次のステップは、レーザーをエンティティプールに追加することです。Player & Enemy オブジェクトと同様に、game.js へ以下のコードを追加します。

me.pool.register("laser", game.Laser);

そして、ファイル player.js に戻り、update メソッドの中にレーザーの発射を追加します:

if (me.input.isKeyPressed("shoot")) {
    me.game.world.addChild(me.pool.pull("laser", this.pos.x - game.Laser.width, this.pos.y - game.Laser.height))
}

ゲームをリロードして、レーザーを撃ってみましょう。レーザーが発射されるのが見えるはずです。しかし、何かと衝突することはありません。

衝突

まず、Enemy に物理ボディを与えましょう。enemy.js の init メソッドにこれを追加します。

this.body.setVelocity(0, 0);
this.body.collisionType = me.collision.types.ENEMY_OBJECT;

そして、update メソッドを追加して、ボディを更新できるようにします:

update : function (time) {
    this._super(me.Entity, "update", [time]);

    this.body.update();

    return true;
}

ここで、laser.js ファイルに衝突ハンドラーを追加しましょう。

onCollision : function (res, other) {
    if (other.body.collisionType === me.collision.types.ENEMY_OBJECT) {
        me.game.world.removeChild(this);
        game.playScreen.enemyManager.removeChild(other);
        return false;
    }
}

この使われていない res パラメーターは、単なる衝突の結果です。そのため、オーバーラップの程度や衝突の場所などに関する詳細が含まれています。

Enemy のボディにおける衝突タイプを ENEMY_OBJECT に設定したため、レーザーが衝突したオブジェクトのタイプを確認することができます。

if (other.body.collisionType === me.collision.types.ENEMY_OBJECT) {

次に、レイザーから敵を取り除くとともに、enemyManager コンテナから敵を取り除きます。

me.game.world.removeChild(this);
game.playScreen.enemyManager.removeChild(other);

この場合に false を返すことは厳密には必要ではありませんが、それを指摘することは重要です。MelonJS の中で衝突ハンドラーから false が返される時には、オブジェクトは通過します。true が返される場合には、ハードストップになります。

変更を保存して、ブラウザをリロードします。これで敵の宇宙船を排除することができるようになったはずです。

次のステップでは、勝敗の条件を追加します。

勝敗の条件

このゲームの最後のステップでは、実際に勝敗の条件を追加します。条件自体はかなり単純になります。プレイヤーのエリアの中に宇宙船が入ると、プレイヤーが負けになります。プレイヤーが敵の宇宙船をすべて破壊すると、プレイヤーが勝ちになります。

ゲームが終わるとどうなるのでしょうか? プレイヤーの勝敗のような何かを画面に表示したいと思うことが多いでしょう。シンプルなままで、また別の小さなトリックを見せるために、一度単にゲームをリセットしましょう。そして、起動し直します。

まず、負けの条件から行きます。

ここで使うコードのようなものは、こうなります:

if enemy manager overlaps player
  then end game
else
  continue
end

PlayScreen が現在のゲームのステートです。プレイヤーを参照していて、ステートをリセットすることができます。そこで、そこに負けの条件を確認するロジックを追加しましょう。

checkIfLoss : function (y) {
    if (y >= this.player.pos.y) {
       this.reset();
    }
},

上の onResetEvent メソッドを追加します。Y の値を受け取り、それがプレイヤーを超えたかどうかを確認します。そして、reset メソッドを呼び出します。reset は、ゲーム世界のすべてのオブジェクトを排除し、ステートをリロードします。そこで、onResetEvent を再度呼び出し、敵とプレイヤーを再度配置します。

ここで、条件の確認を呼び出すために、enemy manager の中に interval に対するメソッドの呼び出しを単に追加します:

this.timer = me.timer.setInterval(function () {
    var bounds = _this.childBounds;

    if ((_this.vel > 0 && (bounds.right + _this.vel) >= me.game.viewport.width) ||
        (_this.vel < 0 && (bounds.left + _this.vel) <= 0)) {
        _this.vel *= -1;
        _this.pos.y += 16;
        if (_this.vel > 0) {
          _this.vel += 5;
        }
        else {
          _this.vel -= 5;
        }
        game.playScreen.checkIfLoss(bounds.bottom); // <<<
    }
    else {
       _this.pos.x += _this.vel;
    }
}, 1000);

checkIfLoss メソッドの中で、受け渡された数字がプレイヤーの Y 軸上の位置よりも大きいかどうかを確認しているため、コンテナの下の境界を渡す必要がありますが、これは単に bounds.bottom です。

しかしながら、ここでの問題は、それを実行した時に、this.player が未定義になることです。これは playScreen 上のこのプロパティをまだ設定していないからです。以下の方法でこのプロパティを設定することができます:

onResetEvent のこの行を入れ替えます:

me.game.world.addChild(me.pool.pull("player"), 1);

このように入れ替えます:

this.player = me.pool.pull("player");
me.game.world.addChild(this.player, 1);

保存して、ブラウザを更新します。これで this.player が適切に設定されたため、新しいメソッドの呼び出しが機能するようになりました。 敵を1分間自由に動かした後で、ゲームがリセットすることを確認しましょう。

勝ちの条件

同様に、プレイヤーが勝ったら、ゲームをリセットさせます。すべての宇宙船がいなくなったら、勝ちとしたいので、enemy manager 上の子の length を確認します。

最初に、createEnemies メソッドの一番下にこのブーリアン演算を追加します:

this.createdEnemies = true;

以下の update メソッドを enemy manager に追加します:

update : function (time) {
    if (this.children.length === 0 && this.createdEnemies) {
        game.playScreen.reset();
    }
    this._super(me.Container, "update", [time]);
    this.updateChildBounds();
}

これはかなりシンプルです。子たちはアレイなので、length がゼロかどうか確認して、最初に敵を作成したことを確認します。ブーリアンによる確認をしないと、まだ子がいないことで、ゲームがリセットを続ける可能性があります。

保存して、ブラウザを更新します。時間内にすべての宇宙船を倒して、ゲームがリセットするのを確認しましょう。

チャレンジ

このチュートリアルでは一部をチャレンジとして残したので、自分でやり方を見つけてください。これは、プログラミングやゲーム開発の重要な部分です。

以下のチャレンジやチュートリアルのどこかでつまづいた場合には、その問題を検索するか、フォーラム@html5gamedevs で質問してください。

チャレンジ その1

適切な勝敗画面を追加する

  1. これらの画面は、ゲームにさらに ScreenObjects を追加して、それを game.js に登録して、ステートを変更することでできるかもしれません。勝敗画面でどのステートを使うべきかは、このステートを確認してください:http://melonjs.github.io/melonJS/docs/me.state.html
  2. 勝敗画面には、スプライトやテキスト、またはその両方を含むことができます。あなたの希望次第です。me.Font と me.Sprite を必ず確認してください。me.Font オブジェクトを表示するには、me.Font のインスタンスを含む me.Renderable のインスタンスを使い、me.Font#draw を呼び出す描画関数を実行してください。
  3. checkIfLoss メソッドを調整して、代わりに新しい負けの画面を表示しましょう。
  4. Adjust the if block in the update method on EnemyManager の update メソッドの中の if ブロックを調節し、ステートを勝ちのスクリーンオブジェクトに変えます。
  5. さらにボーナスとして、プレイヤーに遊び方を伝えるメニュー画面を追加しましょう。

チャレンジ その2

UI の追加

  1. 画面の右上や左上に、敵のカウンターや敵の速度を追加します。これらのプロパティは以下から取得することができます: game.playScreen.enemyManager.children.length game.playScreen.enemyManager.vel
  2. 再び、me.Font を見て、テキストを描画するためにレンダリング可能なものを実行します。両方の UI で使えるレンダリング可能なものを拡張を、1つのクラスだけを使ってやってみましょう。
  3. スコアのエレメントを追加します。プレイ画面でスコアを常に確認します。敵が倒されるたびにアップデートされます。レーザーとの衝突ハンドラーによって、敵が削除されることを思い出してください。

チャレンジ その3

レベルの概念を追加する

  1. 敵の艦隊を倒した後に、同じ艦隊に更新する代わりに、スタートがもっと速い新しい敵の艦隊を作りましょう。ここでの主要なロジックは、game.js で何番目の艦隊かを覚えておき、勝つたびにその数を増やして行きます。そして、その数を enemy manager で使って、速度を調節します。
  2. また、それぞれの艦隊をどんどん速く前進させましょう (例えば、Y が増加するたびに、速度を +5 ではなく +8 にする)。良さそうな感じになるまで、少し数字を調整してプレイしてみましょう。