HTML5 を使ったシンプルな 2 D ゲームの作り方 (アニメーションの実装)
去年末に出演した schoo (スクー) さんの授業で使用したサンプルアプリをもとにした、HTML5 を使ったシンプルな 2 D ゲームの作り方を紹介しています。
どんなゲームを作るのかは 1 回目の記事の中に実際に動作するゲームが埋め込んであるのでぜひ遊んでみてください。なお、開発に必要な画像データは 2 回目の記事からダウンロードできまるので、実際にゲーム開発を体験したい方はそちらから入手してください。
- HTML5 を使ったシンプルな 2 D ゲームの作り方(序)
- HTML5 を使ったシンプルな 2 D ゲームの作り方(準備編)
- HTML5 を使ったシンプルな 2 D ゲームの作り方 (画像のロード)
今回の記事では、前回の記事で Canvas に表示した画像を動くようにしていきます。
Canvas のアニメーション
Canvas でのアニメーションは、描画したオブジェクトのプロパティに現在位置と移動先の位置、時間あたりの移動量を指定で云々….というものではなく、もっと原始的な、わかりやすくいうとパラパラ漫画のようなものです。
描画要素をオブジェクトとして扱える SVG とは違い Canvas で描画された画像は、単なるビットマップ (絵) なので描画された要素を個別に指定することはできません。つまり、Canvas 上に UFO を描画した場合、表示の済んだ UFO をオブジェクトとして指定することはできません。たとえその UFO がアダムスキー型であろうが葉巻型であろうが、はたまたオーソドックスな円盤型であろうが、それだけを選択することはできないのです。なぜならそれは Canvas 上にビットマップで書かれた模様に過ぎないからです。
ではどのようにしてアニメーションを実現するのかというと、Canvas 全体を描画してはクリアし描き換えていくという方法で実現します。
フレームとアニメーション
アニメーションの原理は、微妙に描写の異なる絵を高速に描き換えていくことで動画を実現しています。
この描き換えられる絵のことをフレームといい、単位時間で描画されるフレームの数をフレームレートと言います。単位としては、1 秒あたりのフレームレートを fps (frames per second) という単位で表します。
Canvas でアニメーションを実現するには、Canvas の描画とクリアを繰り返していく必要かありますが、ここで重要になってくるのが時間あたりの処理をさせるフレーム管理です。JavaScript には setInterval 関数があり、1 秒あたりの処理数を管理するのに以前から使用されてきました。しかし、HTML5 からは window オブジェクトによりアニメーションに向いた requestAnimationFrame メソッドが追加され、これを使用することができます。
setInterval と requestAnimationFrame
setInterval は単位時間当たりの処理を繰り返し行うための関数ですが、もともとがアニメーション用に作られているわけではないので、非効率な点がいろいろとありました。例えば、Web ブラウザーがバッググラウンドにあり、描画の必要がないときでも setInterval 関数は動作しつづけます。このためCPU サイクルの浪費、電力の無駄使いという問題が発生しました。また、モニターのリフレッシュレートとも合わせられず、過剰な描画呼び出しが発生し、バッテリの駆動時間や、他のアプリのパフォーマンスにも悪影響が及ぼす可能性がありました。
いっぼう、requestAnimationFrame メソッドは、最初からアニメーションで使用されることが前提で作られているため、ブラウザーがページの表示を更新する必要のあるタイミングで (のみ) アプリは通知を受け取ることができるので、CPU やメモリを効率的に使用することができます。
requestAnimationFrame メソッドの使い方
requestAnimationFrame メソッドは、引数にフレームを描画するための関数をコールバック関数として渡し、継続して描画を行う際にはコールバック関数内でその関数をさらに呼ぶという使い方をします。
例えは、以下は HTML エレメントの style.left を呼び出し毎に加算し、HTML エレメントを右方向へ動かす処理を記述したものですが、関数内の requestAnimationFrame メソッドに、呼び出し元と同じ関数名が指定されています。
function renderLoop() {
elm.style.left = (iPos += 3) + "px";
handle = window.requestAnimationFrame(renderLoop);
}
requestAnimationFrame メソッドと FPS
requestAnimationFrame メソッドはブラウザーの負荷に合わせ、60 fps 以内で実行されます。実行は描画の準備が整った段階で行われるため、setInterval 関数を使ったアニメーションのような過剰な呼び出しによるコマ落ちや描画の乱れも発生せず、滑らかに動作します。
ただし、1 秒間当たりの処理数は明示的に指定できないため、厳密な fps を設定するには独自でその仕組みを実装する必要があります。例えば、現在時刻から前回実行時刻をマイナスし、その差分で描画を行うかどうかという判断する、といった具合です。
実際のところ、使用する Web ブラウザーごとに処理の発生するタイミングはまちまちなので、アニメーションさせるアイテムの数が増えてくるとその差が開き、Web ブラウザーごとに動作スピードが異なるということが発生します。(体験談)
よって、fps を制御するコードを実装することをお勧めしますが、今回の実装では複雑になるので除きます。(※後の回で実装します。)
requestAnimationFrame メソッドを使用したアニメーションの実装の詳細については以下のドキュメントを参照してください。
- HTML5 ゲームで setInterveral を requestAnimationFrame に置き換える
- スクリプトによるアニメーションのタイミング制御 ("requestAnimationFrame")
requestAnimationFrame メソッドを使用した
アニメーションの実装
前置きが長くなりましたが前回まで作ったコードにアニメーションを実装し、雪の結晶の画像が繰り返し降ってくるようにします。
原理としては、requestAnimationFrame メソッドに引数として渡される関数内で Canvas をクリアし、drawImage メソッドで画像を描画する際に縦の位置を表す引数 Y を加算していき、Y の値が Canvas の縦のサイズを超えたら 0 に戻すという単純なものです。
それでは実際にコード書いていきましょう。
まず、renderFrame という名前の新しい関数を定義します。Visual Studio を使用している方は func とタイプしてキーボードの [tab] キーを 2 回押下し、生成された myfunction 関数の名前を renderFrame に変更してください。
renderFrame 関数内に Canvas をクリアするための context オブジェクトが持っている clearRect 関数を記述します。第一引数、第二引数は、クリアする領域の左上端の座標なので両方とも 0 を指定します。第三引数、第四引数は幅と高さを指定するので、canvas の width と height を指定します。
//canvas をクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
次に、雪の結晶の画像の現在の縦の座標を保持しているプロパティ _y を 2 つ増分し、画像を描画します。前の処理で Canvas 全体がクリアされているので雪だるまの画像も描画する必要があります。
//雪の画像の縦位置を増分
img_snow._y += 2;
//画像を描画
ctx.drawImage(img_snow, img_snow._x, img_snow._y);
ctx.drawImage(img_snow_man, img_snow_man._x, img_snow_man._y);
最後に requestAnimationFrame メソッドの引数に renderFrame 関数を指定します。
//ループを開始
requestId = window.requestAnimationFrame(renderFrame);
これで適切な場所から renderFrame 関数を呼び出すと、雪の結晶の画像が上からゆっくりと降りてきます。ただし、このままだと雪の結晶の画像の _y プロパティが加算され続けるため雪の結晶の画像は Canvas からはみ出して消えてしまいます。
雪の結晶の画像が Canvas の表示領域をはみ出したら先頭に戻るように以下のコードを関数内の先頭に追加します。
//img_snow の y 値(縦位置) が canvas からはみ出たら先頭に戻す
if (img_snow._y > canvas.clientHeight) { img_snow._y = 0 };
renderFrame 関数の全体のコードは以下のようになります。
function renderFrame() {
//img_snow の y 値(縦位置) が canvas からはみ出たら先頭に戻す
if (img_snow._y > canvas.clientHeight) { img_snow._y = 0 };
//canvas をクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
//img_snow の y 値を増分
img_snow._y += 2;
//画像を描画
ctx.drawImage(img_snow, img_snow._x, img_snow._y);
ctx.drawImage(img_snow_man, img_snow_man._x, img_snow_man._y);
//ループを開始
requestId = window.requestAnimationFrame(renderFrame);
}
renderFrame 関数を呼び出しを記述します。
renderFrame 関数を呼び出しは、Canvas への画像のロード処理と同様に画像が完全に読み込まれるのを待つ必要があるので、本来であればチェックルーチンを作って走らせる必要がありますが、まだ実装の初期段階なので今回は Canvas をクリックした際に呼びだすようにしましょう。
loadAssets 関数の先頭部分で、変数 canvas に document.getElementById メソッドで HTML 上の Canvas のインスタンスを格納している箇所の直後に、以下のように addEventListener メソッドでイベントハンドラとして設定します。
function loadAssets() {
//HTML ファイル上の canvas エレメントのインスタンスを取得
canvas = document.getElementById('bg');
//アニメーションの開始
canvas.addEventListener("click", renderFrame);
default.html を選択し、キーボードの [F5] キーを押下してページを実行し、画像が表示されたら Canvas 部分をクリックしてください。
以下の画像のように雪の結晶画像が、繰り返し上から下へとアニメーションしていきます。
main.js ファイル全体の JavaScript コードは以下のようになります。
(function () {
//全体で使用する変数
var canvas = null;
var ctx = null;
var img_snow = null;
var img_snow_man = null;
//DOM のロードが完了したら実行
document.addEventListener("DOMContentLoaded", function () {
loadAssets();
});
function loadAssets() {
//HTML ファイル上の canvas エレメントのインスタンスを取得
canvas = document.getElementById('bg');
//アニメーションの開始
canvas.addEventListener("click", renderFrame);
//2D コンテキストを取得
ctx = canvas.getContext('2d');
//image オブジェクトのインスタンスを生成
img_snow = new Image();
//image オブジェクトに画像をロード
img_snow.src = '/img/snow.png';
/*画像読み込み完了のイベントハンドラーに Canvas に
画像を表示するメソッドを記述 */
img_snow.onload = function () {
img_snow._x = getCenterPostion(canvas.clientWidth, img_snow.width);
img_snow._y = 0;
//canvas 上で image を描画
ctx.drawImage(img_snow, img_snow._x, img_snow._y);
};
//雪だるま画像のロード
img_snow_man = new Image();
img_snow_man.src = '/img/snow_man.png';
img_snow_man.onload = function () {
img_snow_man._x = getCenterPostion(canvas.clientWidth, img_snow_man.width);
img_snow_man._y = canvas.clientHeight - img_snow_man.height;
ctx.drawImage(img_snow_man, img_snow_man._x, img_snow_man._y);
};
};
function renderFrame() {
//img_snow の y 値(縦位置) が canvas からはみ出たら先頭に戻す
if (img_snow._y > canvas.clientHeight) { img_snow._y = 0 };
//canvas をクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
//img_snow の y 値を増分
img_snow._y += 2;
//画像を描画
ctx.drawImage(img_snow, img_snow._x, img_snow._y);
ctx.drawImage(img_snow_man, img_snow_man._x, img_snow_man._y);
//ループを開始
requestId = window.requestAnimationFrame(renderFrame);
}
//中央に配置する画像の X 座標を求める関数
function getCenterPostion(containerWidth, itemWidth) {
return (containerWidth / 2) - (itemWidth / 2);
};
})();
まとめ
今回は Canvas を使用してアニメーションを実現する方法と、requestAnimationFrame メソッドを使用したアニメーションのの実装方法について紹介しました。
次回はキーボードの矢印キーと画面のタッチで雪だるまを左右に動かせるように実装します。