ゲーム開発

Web 向け 2D ゲーム エンジン

Michael Oneppo

コード サンプルのダウンロード

ゲームを作ろうとしている 1 人の開発者がいるとします。この開発者がゲーム開発に使えるツールを探したところ、見つかった選択肢の多さに落胆してしまいます。そこで、今のゲーム コンセプトのニーズをすべて満たすカスタム エンジンを作ることにしました。このエンジンは、これまでにはなく、将来のゲーム開発の要件になりそうなこともすべて盛り込みます。でも、実際にゲームを製作したことは一度もありません。

ツールにまつわるこうした悲劇はしばしば起こります。さいわいなことに、考えられるすべてのプラットフォームに対してたくさんのゲーム開発エンジンが生まれています。その結果、方法論、ツール、およびライセンス モデルは多種多様に拡散してる状態です。たとえば、JavaScripting.com (英語) では 14 種類の分野に分けてオープン ソースのゲーム ライブラリが掲載されています。そこには、物理法則のエンジン、オーディオ ライブラリ、ゲーム専用の入力システムはまだ含まれていません。こうした選択肢の中に、検討中のゲーム プロジェクトに適切なゲーム エンジンが必ず存在します。

今回は、Web 向けに人気のある 3 種類のオープン ソース 2D ゲーム エンジンを紹介します。紹介するのは、Crafty、Pixi、Phaser の 3 つです。3 つのライブラリを対比するために、前回のコラムで製作した "Ping (ピン)" ゲーム (msdn.microsoft.com/ja-jp/magazine/dn913185.aspx) を各エンジンに移植し、どのような感触が得られるかを確かめます。ここでは 2D ゲームに限定し、多くの Web 向け 3D ゲーム エンジンは扱いません。3D については、いずれ紹介する予定です。差し当たり、2D に重点を置き、そのチャンスに注目します。

Crafty

Crafty (craftyjs.com、英語) は、主にタイルベースのゲームを対象にしたエンジンですが、本コラムのピンも含め、多様な 2D ゲームに対して適切に機能します。Crafty の基盤となるのは、コンポーネントと呼ばれるオブジェクト モデルと依存関係のシステムです。

コンポーネントはクラスに似ており、具体的な属性やアクションを定義します。コンポーネントの各インスタンスをエンティティと呼びます。コンポーネントは、大量に組み合わせることで、複雑で機能豊富なエンティティを作成できるようになっています。たとえば、Crafty でスプライト シートを作成する場合、sprite 関数を使用して目的のスプライトを表すコンポーネントを生成します。今回は、ピン ゲームの画像から、以下の種類のスプライトを定義します。

Crafty.sprite("sprites.png", {
  PlayerSprite: [0, 128, 128, 128],
  OpponentSprite: [0, 0, 128, 128],
  BallSprite: [128, 128, 64, 64],
  UpSprite: [128,0,64,64],
  DownSprite: [128,64,64,64],
  LeftSprite: [192,0,64,64],
  RightSprite: [192,64,64,64]});

このスプライト シート上のボールの画像を使用するボール エンティティを作成する場合、以下のように "e" 関数を使ってエンティティを作成する際に、BallSprite コンポーネントを含めます。

Crafty.e("Ball, BallSprite");

これだけでは、まだ画面に描画されません。このエンティティを 2D 内に登場させ (つまりエンティティに "x" と "y" の位置を持たせ)、DOM 要素を使って描画することを、Crafty に指示します。

Crafty.e("Ball, 2D, DOM, BallSprite");

要素をキャンバスに描画する場合は、ドキュメント オブジェクト モデル (DOM) コンポーネントをキャンバスに置き換えるだけです。

エンティティは、イベントに反応し、画面上で独立したオブジェクトのように動作します。多くの JavaScript ライブラリと同様、Crafty のエンティティにはイベントに反応するための bind 関数があります。イベントは Crafty 独自のものなので、注意が必要です。たとえば、エンティティが各フレーム内で何らかの反応を示すようにする場合は、EnterFrame イベントに反応させます。本稿のゲームで考えると、このイベントは、ボールを保持しているプレイヤーと適切な物理法則に基づいてボールを動かすために必要です。図 1 は、ボールの動きを扱う EnterFrame イベント関数によるボール エンティティの初期化を示しています。

図 1 ボール エンティティの定義

Crafty.e("Ball, 2D, DOM, BallSprite, Collision")
  .attr({ x: width/2, y: height/2, velocity: [0,0], owner: 'User' })
  .bind('EnterFrame', function () {
    // If there's an owner, have the ball follow the owner.
    // Nudge the ball to the left or right of the player
    // so it doesn't overlap.
    if (this.owner) {
      var owner = Crafty(this.owner);
      switch(this.owner) {
        case 'User':
          this.x = owner.x + 128;
          break;
        case 'Opponent':
          this.x = owner.x - 64;
            }
            this.y = owner.y + 32;
            return;
    }
    // Bounce the ball off the ceiling or floor.
    if (this.y <= 0 || this.y >= (height - 64))
      this.velocity[1] = -this.velocity[1];
    // Move the ball based on its velocity, which is defined in
    // pixels per millisecond.
    var millis = 1000 / Crafty.timer.FPS();
    this.x += this.velocity[0] * millis;
    this.y += this.velocity[1] * millis;
  })

詳しく見てみると、エンティティのプロパティを参照するために、"this" を頻繁に使用しているのがわかります。Crafty 組み込みの "this.x" と "this.y" は、ボールの位置を定義します。2D コンポーネントを使ってボールを作成するときに、この機能を追加します。ステートメント "this.velocity" は、今回のためのカスタム コードです。attr 関数を使用して、プロパティとして定義しているのがわかります。どちらのプレイヤーがボールを保持しているのかを示すために使用している this.owner も同じです。

Crafty にはいくつか組み込みコンポーネントがありますが、ピンの例ではあまり使用していません。以下に組み込みコンポーネントの例を挙げます。

  • Gravity: Crafty には、エンティティを自動的に下方向へ引っ張るシンプルな重力エンジンがあります。この動きを止めるために、任意の数の要素型を定義することができます。
  • TwoWay/FourWay/MultiWay Motion: これらのコンポーネントにより、さまざまなアーケードスタイルの動きを入力方式に利用できるようになります。TwoWay の場合は、左右の動きとジャンプのオプションを任意のキーに結び付けることができます。FourWay の場合は、基本方向に上下の動きが加わります。MultiWay では、任意の方向への動きにユーザー定義の入力を結び付けることができます。
  • Particles: Crafty には高速粒子系があり、キャンバス描画にのみ使用できます。粒子は、滑らかな円形の単色画像なので、煙、炎、爆発の表現に適しています。

Crafty で実装したピンのコードは、本稿付属のダウンロードで入手できます。コードの移植方法と、特に対戦相手の AI に関する部分を確認して、全体像を把握してください。

Pixi.js

Pixi.js (pixijs.com、英語) はハードウェアと密接に関係し、必要最低限の機能を持つ 2D Web レンダラーです。Pixi.js は 2D キャンバスを利用しないで WebGL を直接操作できるため、パフォーマンスが大きく向上します。Pixi.js のゲームには 2 つの重要な要素があります。1 つはシーン グラフでゲーム レイアウトを表現するステージ、もう 1 つはキャンバスまたは WebGL を使用して実際にステージの要素を画面に描画するレンダラーです。WebGL が利用可能な場合、Pixi.js は既定で WebGL を使用します。

$(document).ready(function() {
  // The stage to play your game.
  stage = new PIXI.Stage(0xFFFFFFFF);
  lastUpdate = 0;
  // autoDetectRenderer() finds the fastest renderer you've got.
  renderer = PIXI.autoDetectRenderer(innerWidth, innerHeight);
  document.body.appendChild(renderer.view);
});

Pixi.js にはシーン グラフと変換がありますが、シーンの表示は開発者自身が行う必要があります。本稿のピン ゲームでは、以下のように requestAnimationFrame を使用してレンダリング ループを実行しています。

function update(time) {
  var t = time - lastUpdate;
  lastUpdate = time;
  ball.update(t);
  ai.update();
  // New: You actually have to render the scene
  renderer.render(stage);
  requestAnimationFrame(update);
}

Crafty とは異なり、Pixi.js はゲーム構造の作成方法について指図することはあまりありません。そのため、少しの変更は加えますが、オリジナルのピン ゲームのコードの大部分をそのまま使用することができます。

Pixi.js での大きな違いは、スプライトを画像としてファイルから 1 つずつ読み込んでも、TexturePacker という特別なツールを使用してスプライトをタイル画像として読み込んでもかまわない点です。このツールは個別の画像から成る 1 つグループを、1 つの最適化されたファイルに自動的にマージし、画像の場所を示す JSON ファイルを添付します。今回は TexturePacker を使ってスプライト シートを作成しました。図 2 は、スプライト画像を読み込むように更新した ready 関数を示しています。

図 2 Pixi.js でのスプライト シートの読み込み

$(document).ready(function() {
  // Create an new instance of a Pixi stage.
  stage = new PIXI.Stage(0xFFFFFFFF);
  lastUpdate = 0;
  // Create a renderer instance.
  renderer = PIXI.autoDetectRenderer(innerWidth, innerHeight);
  document.body.appendChild(renderer.view);
  var images = ["sprites.json"];
  var loader = new PIXI.AssetLoader(images);
  loader.onComplete = startGame;
  loader.load();
});

loader は非同期です。そのため、読み込み完了時に呼び出す関数が必要です。プレイヤー、対戦相手、ボールの作成など、残りのゲームの初期化は新しい startGame 関数に含めます。新しく初期化したこれらのスプライトを使用するには、PIXI.Sprite.fromFrame メソッドを使用します。このメソッドは、JSON ファイル内のスプライトの配列のインデックスを受け取ります。

Pixi.js の最後の違いは、タッチとマウスの入力システムが用意されている点です。任意のスプライトを入力に対応できる状態にするには、interactive プロパティを "true" に設定し、イベント ハンドラーをいくつかスプライトに直接追加します。たとえば、今回はボタンの上下に対応してプレイヤーを上下に動かすためにイベントをバインドしています (図 3 参照)。

図 3 プレイヤーが上下に動くようにイベントをバインド

up = new PIXI.Sprite.fromFrame(3);
up.interactive = true;
stage.addChild(up);
up.touchstart = up.mousedown = function() {
  player.move(-distance);
}
down = new PIXI.Sprite.fromFrame(4);
down.interactive = true;
stage.addChild(down);
down.touchstart = down.mousedown = function() {
  player.move(distance);
}

各スプライトを表示するには、スプライトをシーンに手動で追加する必要があります。本稿付属のコード ダウンロードで、今回の Pixi.js でのピンのビルドを確認できます。

Phaser

Phaser (phaser.io、英語) は、機能満載のゲーム エンジンで、すべてのレンダリング タスクの実行に内部で Pixi を使用します。Phaser には、フレームベースのスプライト アニメーション、BGM と SE、ゲーム ステート システム、3 つの異なる補助ライブラリによる物理法則など、多くの機能があります。

既に Pixi.js にピンを移植しているので、Phaser への移植は簡単です。Phaser は Pixi.js に基づくオブジェクトの作成を効率化します。

ball = game.add.sprite(x, y, 'sprites');

Phaser は簡潔になっていますが、複雑になっている部分もあります。たとえば、上記のコードの最後のパラメーター "sprite" は起動時に読み込む画像を表します。

game.load.image('sprites', 'sprites.png');

Phaser での画像に対するスプライト シート システムは、Pixi.js とは異なり、1 つの画像をタイルに分割することができます。これを利用するには、次のようにします。

game.load.spritesheet('sprites', 'mySprites.png',
  spriteWidth, spriteHeight, totalSprites);

実は、今回の例ではこの関数を使っていません。上記のコードではすべてのスプライトが同じサイズだと想定していますが、ピンのスプライト シートには 64 ピクセルと 128 ピクセルのスプライトがあります。そのため、各スプライト画像を手動でトリミングする必要があります。ボールの画像を縮小するため、スプライトにトリミング用の四角形を設定します。

ball.cropRect = new Phaser.Rectangle(0, 64, 64, 64);
ball.updateCrop();

このコードで、Phaser の柔軟性をある程度感じていただければさいわいです。この柔軟さは、他の方法でも明らかになります。ゲームをイベントベースにすることができます。つまり、イベントをチェックして、update 関数でそのイベントにシーケンシャルに反応することができます。たとえば、キー入力を処理するためにイベントに応答することができます。

game.input.keyboard.onDownCallback = function(key) {
  console.log(key + " was pressed.");
}

または、update ループ内でキーが押下されたかどうかを確認します。

function update() {
  if (game.input.keyboard.isDown('a'.charCodeAt(0)) {
    console.log("'A' key was pressed");
  }
}

この手法は好みや必要性に合わせて、シーケンシャルや非同期のコーディングを使って Phaser の多くのイベントに適用できます。場合によっては、Phaser で後者のイベント処理形式しか使えないことがあります。これを示す良い例がアーケード型の物理法則システムです。この場合は、更新のたびに明示的に関数呼び出しを行い、オブジェクトどうしの衝突をチェックする必要があります。

game.physics.arcade.collide(player, ball, function() {
  ballOwner = player;
});

上記のコードではボールがプレイヤーのスプライトと接触したかどうかを判別し、接触している場合はプレイヤーにボールのコントロールを渡します。本稿付属のコード ダウンロードで、Phaser でのピンの実装を確認してください。

まとめ

2D 専用の JavaScript ゲーム エンジンを探している場合は、ニーズを満たせる選択肢がたくさんあります。最後に、フレームワークを選ぶ際に心に留めておきたい 2 つの重要な要素を紹介します。

必要な機能だけを使う: 大量の機能、包括的で巨大なフレームワーク、および他のオプション機能に惑わされないようにします。オプション機能によって作成するゲームの特定の側面に効果が出る場合を除き、そういった機能はおそらく邪魔になります。また、機能をコンポーネント化する方法も検討します。物理法則システムを取り除いてもシステム全体はそのまま機能するでしょうか。他の機能を提供するためにフレームワークが物理法則システムを利用していないでしょうか。

エンジンのしくみを理解する: フレームワークをカスタマイズまたは拡張する方法を理解します。通常とは異なるゲームにする場合、それでもおそらくは選択したフレームワークに大きく依存するカスタム コードを記述しなければなりません。そのようなときは、独立した機能を新しく作成するか、フレームワークの機能を拡張します。フレームワークに追加するのではなく、それ自体を拡張すると、最終的にコードの保守性が向上します。

今回紹介したフレームワークは多くの問題に対処できるため、間違いなく調べてみる価値があります。周りには数々のエンジン、ライブラリ、フレームワークが存在します。次のゲーム開発の冒険に踏み出す前に、少し調べてみてはいかがでしょう。


Michael Oneppo はクリエイティブな技術者で、マイクロソフトの Direct3D チームで前プログラム マネージャーを務めていました。近年では、非営利技術団体 Library For All の CTO としての業務、さらにニューヨーク大学の Interactive Telecommunications Program で修士号の取得などに努めました。​

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Justin Garrett に心より感謝いたします。