HTML5 ゲーム ガイド

はじめに

キャンバスと HTML5 を使ってゲームを作りたいですか?このチュートリアルに沿って操作すれば、すぐに使い始めることができます。

このチュートリアルは、JavaScript に関する中級レベル以上の知識があることを前提としています。

まずゲームをプレイするか、記事に直接移動してゲームのソースコードを表示できます。

キャンバスの作成

描画するには、キャンバスを作成する必要があります。このガイドは「No Tears」ガイドであるため、jQuery を使用します。

var CANVAS_WIDTH = 480;
var CANVAS_HEIGHT = 320;

var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
                      "' height='" + CANVAS_HEIGHT + "'></canvas>");
var canvas = canvasElement.get(0).getContext("2d");
canvasElement.appendTo('body');

ゲームループ

スムーズで連続的なゲームプレイをシミュレートするには、人間の心と目が認識できるよりも速くゲームを更新して画面を再描画する必要があります。

var FPS = 30;
setInterval(function() {
  update();
  draw();
}, 1000/FPS);

現時点では、update メソッドと draw メソッドは空白のままにしておきます。重要なのは、setInterval() が定期的に呼び出しを行うことです。

function update() { ... }
function draw() { ... }

Hello World

ゲームループが機能するようになったので、draw メソッドを更新して、実際に画面にテキストを描画しましょう。

function draw() {
  canvas.fillStyle = "#000"; // Set color to black
  canvas.fillText("Sup Bro!", 50, 50);
}

静止したテキストには非常に便利ですが、ゲームループがすでに設定されているため、簡単に移動させることができます。

var textX = 50;
var textY = 50;

function update() {
  textX += 1;
  textY += 1;
}

function draw() {
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

試してみましょう。動いているはずですが、画面に描画された以前の回数も残っています。なぜそうなのか、少し考えてみてください。これは、画面を消去していないためです。画面を消去するコードを draw メソッドに追加しましょう。

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

これで、画面上でテキストが動くようになりました。これで、実際のゲームの半分まで完成しました。コントロールを強化し、ゲームプレイを改善し、グラフィックを調整するだけです。実際のゲームの 7 分の 1 程度の完成度ですが、チュートリアルにはまだまだ続きがあります。

プレーヤーの作成

プレーヤーデータを保持し、描画などの処理を行うオブジェクトを作成します。ここでは、すべての情報を保持する単純なオブジェクト リテラルを使用して、プレーヤー オブジェクトを作成します。

var player = {
  color: "#00A",
  x: 220,
  y: 270,
  width: 32,
  height: 32,
  draw: function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  }
};

現時点では、プレーヤーを表すために単純な色付きの長方形を使用しています。ゲームを描画するときに、キャンバスをクリアしてプレーヤーを描画します。

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  player.draw();
}

キーボードの操作

jQuery ホットキーの使用

jQuery Hotkeys プラグインを使用すると、ブラウザ間でキーの処理を簡単に行うことができます。解読不能なクロスブラウザの keyCodecharCode の問題に泣き暮らすのではなく、次のようにイベントをバインドできます。

$(document).bind("keydown", "left", function() { ... });

どのキーにどのコードが割り当てられているかについて細かく考える必要がなくなるのは大きなメリットです。「プレーヤーが上ボタンを押したら何かする」などのように、簡単に記述できるようにしたいのです。jQuery Hotkeys は、それを簡単に実現できます。

プレーヤーの移動

JavaScript がキーボード イベントを処理する方法は、完全にイベントドリブンです。つまり、キーが停止しているかどうかを確認する組み込みクエリがないため、独自のクエリを使用する必要があります。

「キーの処理にイベントドリブン方式を使わないのはなぜですか?」と思われるかもしれません。これは、キーボードの繰り返し速度がシステムによって異なり、ゲームループのタイミングにバインドされていないため、ゲームプレイがシステムによって大きく異なる可能性があるためです。一貫したエクスペリエンスを実現するには、キーボード イベント検出をゲームループと緊密に統合することが重要です。

イベント クエリを実行できる 16 行の JS ラッパーが含まれています。これは key_status.js と呼ばれ、keydown.left などをチェックすることで、いつでもキーのステータスをクエリできます。

キーが押されているかどうかをクエリできるようになりました。この簡単な更新方法を使用して、プレーヤーを移動できます。

function update() {
  if (keydown.left) {
    player.x -= 2;
  }

  if (keydown.right) {
    player.x += 2;
  }
}

ぜひお試しください。

プレーヤーを画面外に移動できることに気付くかもしれません。プレーヤーの位置を制限して、境界内に収まるようにしましょう。また、プレーヤーが少し遅いように見えるので、速度も上げましょう。

function update() {
  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

入力を追加するのも同様に簡単です。次に、なんらかの投射物を追加しましょう。

function update() {
  if (keydown.space) {
    player.shoot();
  }

  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

player.shoot = function() {
  console.log("Pew pew");
  // :) Well at least adding the key binding was easy...
};

ゲーム オブジェクトの追加

投射物

次に、実際に射出物を追加しましょう。まず、これらをすべて格納するコレクションが必要です。

var playerBullets = [];

次に、箇条書きのインスタンスを作成するコンストラクタが必要です。

function Bullet(I) {
  I.active = true;

  I.xVelocity = 0;
  I.yVelocity = -I.speed;
  I.width = 3;
  I.height = 3;
  I.color = "#000";

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.active = I.active && I.inBounds();
  };

  return I;
}

プレイヤーが撃つと、弾丸のインスタンスを作成して弾丸のコレクションに追加する必要があります。

player.shoot = function() {
  var bulletPosition = this.midpoint();

  playerBullets.push(Bullet({
    speed: 5,
    x: bulletPosition.x,
    y: bulletPosition.y
  }));
};

player.midpoint = function() {
  return {
    x: this.x + this.width/2,
    y: this.y + this.height/2
  };
};

次に、更新ステップ関数に箇条書きの更新を追加する必要があります。箇条書きのコレクションが無限に増えないように、箇条書きのリストはフィルタされ、アクティブな箇条書きのみが含まれます。また、敵に衝突した弾丸を削除することもできます。

function update() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.update();
  });

  playerBullets = playerBullets.filter(function(bullet) {
    return bullet.active;
  });
}

最後のステップは、箇条書きを描画することです。

function draw() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.draw();
  });
}

次は、弾丸を追加したときと同じ方法で敵を追加します。

  enemies = [];

function Enemy(I) {
  I = I || {};

  I.active = true;
  I.age = Math.floor(Math.random() * 128);

  I.color = "#A2B";

  I.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2;
  I.y = 0;
  I.xVelocity = 0
  I.yVelocity = 2;

  I.width = 32;
  I.height = 32;

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.xVelocity = 3 * Math.sin(I.age * Math.PI / 64);

    I.age++;

    I.active = I.active && I.inBounds();
  };

  return I;
};

function update() {
  ...

  enemies.forEach(function(enemy) {
    enemy.update();
  });

  enemies = enemies.filter(function(enemy) {
    return enemy.active;
  });

  if(Math.random() < 0.1) {
    enemies.push(Enemy());
  }
};

function draw() {
  ...

  enemies.forEach(function(enemy) {
    enemy.draw();
  });
}

画像の読み込みと描画

箱が飛び交う様子を見るのは楽しいですが、画像があればさらに楽しいでしょう。通常、キャンバスに画像を読み込んで描画するのは大変な作業です。このような苦痛を回避するには、シンプルなユーティリティ クラスを使用します。

player.sprite = Sprite("player");

player.draw = function() {
  this.sprite.draw(canvas, this.x, this.y);
};

function Enemy(I) {
  ...

  I.sprite = Sprite("enemy");

  I.draw = function() {
    this.sprite.draw(canvas, this.x, this.y);
  };

  ...
}

衝突検出

画面上にはさまざまな要素が飛び交っていますが、それらは相互に作用していません。爆発するタイミングをすべてに知らせるには、なんらかの衝突検出を追加する必要があります。

シンプルな長方形の衝突検出アルゴリズムを使用しましょう。

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

確認すべき競合がいくつかあります。

  1. プレーヤーの弾丸 => 敵の船
  2. プレーヤー => 敵の宇宙船

更新メソッドから呼び出せる、衝突を処理するメソッドを作成しましょう。

function handleCollisions() {
  playerBullets.forEach(function(bullet) {
    enemies.forEach(function(enemy) {
      if (collides(bullet, enemy)) {
        enemy.explode();
        bullet.active = false;
      }
    });
  });

  enemies.forEach(function(enemy) {
    if (collides(enemy, player)) {
      enemy.explode();
      player.explode();
    }
  });
}

function update() {
  ...
  handleCollisions();
}

次に、プレイヤーと敵に爆発メソッドを追加する必要があります。これにより、削除対象としてフラグが立てられ、爆発が追加されます。

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // Extra Credit: Add an explosion graphic
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // Extra Credit: Add an explosion graphic and then end the game
};

サウンド

エクスペリエンスをさらに充実させるため、効果音も追加する予定です。画像と同様に、HTML5 で音声を使用するのは少し面倒ですが、Google の魔法の公式 sound.js のおかげで、音声を非常に簡単に作成できます。

player.shoot = function() {
  Sound.play("shoot");
  ...
}

function Enemy(I) {
  ...

  I.explode = function() {
    Sound.play("explode");
    ...
  }
}

この API は現在ティアフリーですが、現在のところ、音声を追加することはアプリをクラッシュさせる最も早い方法です。音声が途切れたり、ブラウザのタブ全体が停止したりすることは珍しくありません。ティッシュを用意しておいてください。

Farewell

動作する完全なゲームのデモもご覧ください。ソースコードを ZIP としてダウンロードすることもできます。

JavaScript と HTML5 でシンプルなゲームを作成する基本を学習できたでしょうか。適切なレベルの抽象化でプログラミングすることで、API のより難しい部分から自分自身を保護し、将来の変更に備えて復元力を高めることができます。

参照