はじめに
2010 年 6 月、地元で出版社である「zine」Boing Boing がゲーム開発コンテストを開催していることが判明しました。これは、JavaScript と <canvas>
ですばやく簡単にゲームを作成する絶好の言い訳だと考えたため、作業に取り掛かりました。コンテスト終了後もまだたくさんのアイデアがあり、始めようとしていました。その事例紹介は「オンスロート!」という小さなゲームですアリーナ。
ピクセル化したレトロな外観
チップチューンに基づいたゲームの開発がコンテストの前提となることを考えると、ゲームはレトロな任天堂エンターテイメント システムのゲームのような外観を持つことが重要でした。ほとんどのゲームにはこの要件はありませんが、アセットの作成が簡単で、懐かしいゲーマーに自然にアピールできるという点で、特にインディー デベロッパーの間では一般的な芸術スタイルです。
これらのスプライトがいかに小さいかを考慮して、ピクセルを 2 倍にすることにしました。つまり、16x16 のスプライトは 32x32 ピクセルなどになります。当初から、手間のかかる作業はブラウザに任せるのではなく、アセットの作成に力を入れてきました。この方法は簡単に実装できましたが、デザイン上のメリットも明らかでした。
次のようなシナリオを検討しました。
<style>
canvas {
width: 640px;
height: 320px;
}
</style>
<canvas width="320" height="240">
Sorry, your browser is not supported.
</canvas>
この方法は、アセットの作成側で 2 倍にするのではなく、1x1 のスプライトで構成されます。そこから、CSS が引き継いでキャンバス自体のサイズを変更します。Google のベンチマークによると、この方法を使用すると、大きい(2 倍に拡大した)画像をレンダリングするよりも約 2 倍高速になります。しかし、残念ながら、CSS のサイズ変更にはアンチ エイリアスが含まれています。これを防ぐ方法は見つかりませんでした。
個々のピクセルが非常に重要であるため、これはゲームにとって大きな問題となりましたが、キャンバスのサイズを変更する必要があり、プロジェクトに対してアンチ エイリアスが適切な場合は、パフォーマンス上の理由からこの方法を検討できます。
キャンバスの楽しい使い方
<canvas>
が話題になっていることは周知ですが、デベロッパーが依然として DOM の使用を推奨していることがあります。どちらを使用すべきか迷っている方のために、<canvas>
を使用して時間とエネルギーを大幅に節約した例を次に示します。
敵に敵が襲いかかるの!アリーナ] の場合、赤色で点滅し、「痛み」を示すアニメーションが簡潔に表示されます。作成する必要があるグラフィックの数を制限するため、敵は下向きの「痛み」がある状態にのみ表示されます。これはゲーム内では許容範囲内で、スプライトの作成にかかる時間を大幅に短縮できます。しかし、ボス モンスターの場合、64x64 ピクセル以上の大きなスプライトが、ペイント フレームに対して左または上向きから突然下向きにスプライト表示されるのを見るのは不快でした。
ボスごとに 8 方向のペイント フレームを描画することは明らかですが、これには非常に時間がかかります。<canvas>
のおかげで、コードのこの問題を解決できました。
まず、モンスターを隠し「バッファ」<canvas>
に描画し、赤色でオーバーレイして、結果を画面にレンダリングします。コードは次のようになります。
// Get the "buffer" canvas (that isn't visible to the user)
var bufferCanvas = document.getElementById("buffer");
var buffer = bufferCanvas.getContext("2d");
// Draw your image on the buffer
buffer.drawImage(image, 0, 0);
// Draw a rectangle over the image using a nice translucent overlay
buffer.save();
buffer.globalCompositeOperation = "source-in";
buffer.fillStyle = "rgba(186, 51, 35, 0.6)"; // red
buffer.fillRect(0, 0, image.width, image.height);
buffer.restore();
// Copy the buffer onto the visible canvas
document.getElementById("stage").getContext("2d").drawImage(bufferCanvas, x, y);
ゲームループ
ゲーム開発は、ウェブ開発とは顕著な違いがいくつかあります。ウェブスタックでは、イベント リスナーを介して発生したイベントに反応するのが一般的です。そのため、初期化コードでは、入力イベントをリッスンする以外は何もしない場合があります。ゲームのロジックは異なります。これは、常に自身を更新する必要があるためです。たとえば、プレーヤーが動いていない場合でも、ゴブリンが彼を捕まえるのを止めることはできません。
ゲームループの例を次に示します。
function main () {
handleInput();
update();
render();
};
setInterval(main, 1);
1 つ目の重要な違いは、handleInput
関数はすぐには何も実行しないということです。一般的なウェブアプリでユーザーがキーを押した場合、目的のアクションをすぐに実行すると合理的です。しかし、ゲームでゲームの流れを正しく処理するには、時系列順に進める必要があります。
window.addEventListener("mousedown", function(e) {
// A mouse click means the players wants to attack.
// We don't actually do that yet, but instead tell the rest
// of the program about the request.
buttonStates[e.button] = true;
}, false);
function handleInput() {
// Here is where we respond to the click
if (buttonStates[LEFT_BUTTON]) {
player.attacking = true;
delete buttonStates[LEFT_BUTTON];
}
};
入力について理解し、残りのゲームルールに従うことを確認したうえで、update
関数でその入力を考慮できます。
function update() {
// Check for collisions, states, whatever else is needed
// If after that the player can still attack, do it!
if (player.attacking && player.canAttack()) {
player.attack();
}
};
最後に、すべての計算が済んだら画面を再描画します。DOM-land では、ブラウザがこのヒーブリフトを処理します。ただし、<canvas>
を使用する場合は、何かが発生するたびに(通常は単一のフレームごとに)手動で再描画する必要があります。
function render() {
// First erase everything, something like:
context.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
// Draw the player (and whatever else you need)
context.drawImage(
player.getImage(),
player.x, player.y
);
};
時間ベースのモデリング
時間ベースのモデリングとは、最後のフレーム更新からの経過時間に基づいてスプライトを移動するという概念です。この手法により、スプライトの移動速度を一定にしつつ、ゲームをできるだけ速く実行できます。
時間ベースのモデリングを使用するには、最後のフレームが描画されてからの経過時間をキャプチャする必要があります。これをトラッキングするには、ゲームループの update()
関数を拡張する必要があります。
function update() {
// NOTE: You'll need to initially seed this.lastUpdate
// with the current time when your game loop starts
// this.lastUpdate = Date.now();
// Calculate elapsed time since last frame
var now = Date.now();
var elapsed = (now - this.lastUpdate);
this.lastUpdate = now;
// Do stuff with elapsed
};
経過時間がわかったので、特定のスプライトが各フレームを移動する距離を計算できます。まず、スプライト オブジェクトで現在位置、速度、方向をトラッキングする必要があります。
var Sprite = function() {
// The sprite's position relative to the top left of the game world
this.position = {x: 0, y: 0};
// The sprite's direction. A positive x value indicates moving to the right
this.direction = {x: 1, y: 0};
// How many pixels the sprite moves per second
this.speed = 50;
};
これらの変数を考慮して、時間ベースのモデリングを使用して上記のスプライト クラスのインスタンスを移動する方法を以下に示します。
// Determine how far this sprite will move this frame
var distance = (sprite.speed / 1000) * elapsed;
// Apply the movement distance to the sprite's current position
// taking into account its direction
sprite.position.x += (distance * sprite.direction.x);
sprite.position.y += (distance * sprite.direction.y);
なお、direction.x
と direction.y
の値は正規化する必要があります。つまり、常に -1
~1
の範囲内にする必要があります。
コントロール
おそらくコントロールは、Onslought!アリーナ。最初のデモではキーボードのみに対応しており、プレーヤーは矢印キーで画面上でメイン キャラクターを動かし、スペースバーの方向に発射しました。やや直感的で理解しやすいものの、難易度の高いレベルではゲームをほとんどプレイできなくなっていました。プレーヤーには何十もの敵や発射体が常に飛んでいくため、どの方向からでも発射しながら悪者間を織り交ぜることが不可欠です。
このジャンルの類似ゲームと比較するために、キャラクターが攻撃の狙いに使用する標的レチクルを制御するマウスのサポートを追加しました。キャラクターはキーボードで動かせますが、この変更後は 360 度の任意の方向に同時に発射できるようになりました。ハードコア プレーヤーはこの機能を高く評価していましたが、トラックパッド ユーザーの不満という副作用が発生しました。
トラックパッドのユーザーに対応するため、矢印キーによるコントロールを戻し、今回は押した方向で起動できるようにしました。あらゆるタイプのプレーヤーに対応できていると感じた一方で、知らないうちにゲームが複雑になりすぎていました。驚いたことに、チュートリアル モーダルは大いに無視されていたものの、攻撃用のオプションのマウス(またはキーボード)コントロールに気づいていなかったプレーヤーもいました。
幸運にもヨーロッパのファンがいますが、一般的な QWERTY キーボードがなく、方向移動に WASD キーを使用できないという不満の声も寄せられています。左利きのプレーヤーも同様の不満を表明しています。
この複雑な制御方式により、モバイル デバイスでの再生の問題も生じます。実は Google からよく寄せられる要望は 「Onslought!Arena が使用可能です。HTML5 の主な強みの 1 つはポータビリティです。そのため、これらのデバイスにゲームを実装することは可能です。多くの問題(特にコントロールとパフォーマンス)を解決する必要があります。
これらの多くの問題に対処するために、Google はマウス(またはタッチ)操作のみを含むゲームプレイの単一入力方法から始めました。プレーヤーが画面をクリックまたはタップすると、メイン キャラクターが押された場所に向かって歩き、最も近い敵を自動的に攻撃します。コードは次のようになります。
// Find the nearest hostile target (if any) to the player
var player = this.getPlayerObject();
var hostile = this.getNearestHostile(player);
if (hostile !== null) {
// Found one! Shoot in its direction
var shoot = hostile.boundingBox().center().subtract(
player.boundingBox().center()
).normalize();
}
// Move towards where the player clicked/touched
var move = this.targetReticle.position.clone().subtract(
player.boundingBox().center()
).normalize();
var distance = this.targetReticle.position.clone().subtract(
player.boundingBox().center()
).magnitude();
// Prevent jittering if the character is close enough
if (distance < 3) {
move.zero();
}
// Move the player
if ((move.x !== 0) || (move.y !== 0)) {
player.setDirection(move);
}
敵を狙うという追加の要素を排除することで、状況によってはゲームが楽になることもありますが、プレーヤーにとっては、物事をシンプルにすることで多くのメリットがあると考えています。その他の戦略として、キャラクターを危険な敵の近くに置いてターゲットにするなどの戦略も見られます。タッチデバイスをサポートする機能は非常に重要です。
音声
制御とパフォーマンスのなかでも、Onslought!Arena は HTML5 の <audio>
タグでした。
おそらく最悪の側面はレイテンシです。ほとんどのブラウザでは、.play()
を呼び出してから音声が実際に再生されるまでに遅延が発生します。特に私たちのゲームのようなテンポの速いゲームをプレイすると、ゲーマーのエクスペリエンスが損なわれる可能性があります。
その他の問題として、「progress」イベントが発生しないことがあり、これによりゲームの読み込みフローが無期限にハングする可能性があります。こうした理由から、Flash の読み込みに失敗した場合に HTML5 オーディオに切り替えるという、いわゆる「フォール フォワード」方式を採用しました。コードは次のようになります。
/*
This example uses the SoundManager 2 library by Scott Schiller:
http://www.schillmania.com/projects/soundmanager2/
*/
// Default to sm2 (Flash)
var api = "sm2";
function initAudio (callback) {
switch (api) {
case "sm2":
soundManager.onerror = (function (init) {
return function () {
api = "html5";
init(callback);
};
}(arguments.callee));
break;
case "html5":
var audio = document.createElement("audio");
if (
audio
&& audio.canPlayType
&& audio.canPlayType("audio/mpeg;")
) {
callback();
} else {
// No audio support :(
}
break;
}
};
また、ゲームでは MP3 ファイルを再生しないブラウザ(Mozilla Firefox など)をサポートすることも重要です。この場合は、次のようなコードを使用してサポートを検出し、Ogg Vorbis などに切り替えることができます。
/*
Note: you could instead use "new Audio()" here,
but the client will throw an error if it doesn't support Audio,
which makes using "document.createElement" a safer approach.
*/
var audio = document.createElement("audio");
if (audio && audio.canPlayType) {
if (!audio.canPlayType("audio/mpeg;")) {
// Here you know you CANNOT use .mp3 files
if (audio.canPlayType("audio/ogg; codecs=vorbis")) {
// Here you know you CAN use .ogg files
}
}
}
データの保存
アーケード スタイルのシューティング ゲームでハイスコアを狙おう!ゲームデータの一部を維持する必要があることはわかっていました。Cookie のような従来の技術を使用することも可能でしたが、楽しい新しい HTML5 テクノロジーについて詳しく知りたいと思いました。ローカル ストレージ、セッション ストレージ、Web SQL データベースなど、さまざまな選択肢があります。
新しく、使いやすくて便利であるため、localStorage
を使用することにしました。基本的な Key-Value ペアの保存をサポートしています。これは、シンプルなゲームに必要なものがすべてです。簡単な使用例を次に示します。
if (typeof localStorage == "object") {
localStorage.setItem("foo", "bar");
localStorage.getItem("foo"); // Value is "bar"
localStorage.removeItem("foo");
localStorage.getItem("foo"); // Value is now null
}
いくつか注意すべき「落とし穴」があります。何を渡すかにかかわらず、値は文字列として保存されるため、予期しない結果が生じる可能性があります。
localStorage.setItem("foo", false);
typeof localStorage.getItem("foo"); // Value is "false" (a string literal)
if (localStorage.getItem("foo")) {
// It's true!
}
// Don't pass objects into setItem
localStorage.setItem("bar", {"key": "value"});
localStorage.getItem("bar"); // Value is "[object Object]" (a string literal)
// JSON stringify and parse when dealing with localStorage
localStorage.setItem("json", JSON.stringify({"key": "value"}));
typeof localStorage.getItem("json"); // string
JSON.parse(localStorage.getItem("json")); // {"key": "value"}
概要
HTML5 は非常に効率的です。ほとんどの実装は、グラフィックからゲーム状態の保存まで、ゲーム デベロッパーが必要とするすべてのものを処理します。<audio>
タグの問題など、頭痛の種は増えてはいますが、ブラウザ デベロッパーの動きは急速に進んでいますが、HTML5 で構築されたゲームの未来は明るい見通しです。