このシリーズまだやってたの!?、と声が聞こえてきそうなくらい前回から間が空いてしまいましたが約半年ぶりに HTML5 を使用したシンプルな 2D ゲームの作り方の第 10 回めをお送りします。
どんなゲームを作るのかは 1 回目の記事の中に実際に動作するゲームが埋め込んであるのでぜひ遊んでみてください。なお、開発に必要な画像データは 2 回目の記事からダウンロードできますので、実際にゲーム開発を体験したい方はそちらから入手してください。
- HTML5 を使ったシンプルな 2 D ゲームの作り方(序)
- HTML5 を使ったシンプルな 2 D ゲームの作り方(準備編)
- HTML5 を使ったシンプルな 2 D ゲームの作り方 (画像のロード)
- HTML5 を使ったシンプルな 2 D ゲームの作り方 (アニメーションの実装)
- HTML5 を使ったシンプルな 2 D ゲームの作り方 (矢印キーとタッチによる制御の実装)
- HTML5 を使ったシンプルな 2 D ゲームの作り方 (当たり判定の実装)
- HTML5 を使ったシンプルな 2 D ゲームの作り方 (複数のSpriteの生成)
- HTML5 を使ったシンプルな 2 D ゲームの作り方 (ランダムな動作と FPS の制御)
- HTML5 を使ったシンプルな 2 D ゲームの作り方(スプライトを使った画像の切り替え)
前回までの記事で、1. 入力装置からのプレイヤーの操作、2. 複数のターゲットの生成とランダムな動作、3.当たり判定、4. スプライト(キャラクター)の変更といったおおよそアクションゲームに必要な機能の実装が完了しました。
今回は当たり判定時の効果の追加ということで、雪だるまと雪の結晶がヒットした際に音を出すようにしたいと思います。
HTML5 を使用したオーディオファイルの再生
HTML5 ファイルで MP3 などのオーディオファイルを再生する方法はいくつかありますが、単に再生するだけであれば Audio オブジェクトを使用するのが簡単です。わざわざ audio タグを記述しなくても以下のような JavaScript でオーディオファイルを再生することができます。
audioObj.play();
HTML5 の Audio オブジェクトのより詳しい使い方については以下のドキュメントを参照してください。
オーディオファイルの入手と配置
使用するオーディオファイルは以下の OneDrive のシェアから snow_game_audio.zipをダウンロードしてください。
snow_game_audio.zip を解凍すると audio という名前のフォルダが出てくるので、これを Visual Stuio のソリューションエクスプローラーにドラッグドロップして追加します。
以下のようになれば配置は完了です。
コードの追加
雪の結晶に雪だるまが当たった際に音を鳴らすだけなので、当たり判定処理の中に前述のコード記述すれば良いと思われるかもしれませんが、そう簡単ではありません。
例えば、雪の結晶は複数あるので、それらに同時にぶつかった際にはそれぞれに音を出す必要があります。
また、ゲームの処理はすべてアニメーションのためのフレーム(繰り返し処理)の中で動作しているため、そのまま単に音を鳴らす処理を記述しただけでは同じ処理が何度も繰り返されることになるので、それを防ぐための処理を実装する必要があります。
ここからは、前回までのコードにそういった点を考慮した実装を行っていきます。
Sprite クラスへの Audio プロパティの追加
ゲーム画面内で動作する Sprite (キャラクター) のクラス(※)に Audio オブジェクトのインスタンスを格納するためのプロパティと、フレーム処理の中でオーディオの繰り返し再生を避けるためのプロパティを定義します。
(※厳密にいうと JavaScript に”クラス”は無いので”関数”なのですが、理解しやすさを優先してこう呼びます。)
具体的には以下のように Spriteクラスを定義しているコードの、プロパティとして外部変数を定義している箇所に、Audio オブジェクトのインスタンスを格納するための変数 audioと 繰り返し再生を避けるために再生済みを示すフラグ audioPlayedを定義します。
var Sprite = function (img, width, height) {
this.image = img; //image オブジェクト
this.height = img.height;
this.width = img.width;
this.x = 0; //表示位置 x
this.y = 0; //表示位置 y
this.dx = 0; //移動量 x
this.dy = 0; //移動量 y
this.audio = null; //Audio オブジェクト
this.audioPlayed = false; //音が複数回鳴るのを防ぐ
var _offset_x_pos = 0;
var that = this;
~ 略 ~
Audio オブジェクトのインスタンス生成と格納
雪の結晶用の Sprite クラスのインスタンスが生成されるタイミングで Audio オブジェクトのインスタンス生成してプロパティに格納します。
個別の Sprite クラスに Audio オブジェクトを持たせるのはそれぞれに独立した再生を行わせるためです。
具体的なコードの追加箇所は loadAssets関数内で img_snowオブジェクトの onload イベントハンドラを定義している以下の箇所です。
//HTML ファイル上の canvas エレメントのインスタンスを取得
canvas = document.getElementById('bg');
//アニメーションの開始
canvas.addEventListener("click", loadCheck);
//2D コンテキストを取得
ctx = canvas.getContext('2d');
//image オブジェクトのインスタンスを生成
img_snow = new Image();
//image オブジェクトに画像をロード
img_snow.src = '/img/sp_snow.png';
/*画像読み込み完了のイベントハンドラーに Canvas に
画像を表示するメソッドを記述 */
img_snow.onload = function () {
for (var i = 0; i < SNOWS_COUNT; i++) {
var sprite_snow = new Sprite(img_snow, SNOW_PIC_WIDTH, SNOW_PIC_HEIGHT);
sprite_snow.dy = 1;
sprite_snow.dx = NEIGHBOR_DISTANCE;
sprite_snow.x = i * sprite_snow.dx;
sprite_snow.y = getRandomPosition(SNOWS_COUNT, SNOW_START_COEFFICIENT);
//Audio オブジェクトのインスタンスをセット
sprite_snow.audio = new Audio("audio/kiiiin1.mp3");
snow_sprites.push(sprite_snow);
sprite_snow = null;
}
};
オーディオ オブジェクトの再生
雪の結晶の Sprite オブジェクトの audio プロパティに設定されたオーディオ オブジェクトを再生します。
これは単に当たり判定の処理である isHit関数内で playメソッドを実行し、フレーム処理による繰り返し再生を避けるためのフラグである audioPlayedプロパティを true に設定する処理と if 文を記述するだけです。
具体的には以下のような記述となります。
function isHit(targetA, targetB) {
if ((targetA.x <= targetB.x && targetA.width + targetA.x >= targetB.x)
|| (targetA.x >= targetB.x && targetB.x + targetB.width >= targetA.x)) {
if ((targetA.y <= targetB.y && targetA.height + targetA.y >= targetB.y)
|| (targetA.y >= targetB.y && targetB.y + targetB.height >= targetA.y)) {
ctx.font = "bold 20px 'MS ゴシック'";
ctx.fillStyle = "red";
ctx.fillText("ヒットしました", getCenterPostion(canvas.clientWidth, 140), 160);
targetA.imageIndex = SNOW_CLASH;
//衝突音を鳴らす
if (!targetA.audioPlayed) {
targetA.audio.play();
targetA.audioPlayed = true;
}
}
}
}
再生済みフラグのリセットと再生の終了
雪の結晶が画面から消え、表示位置をリセットするタイミングで再生済みフラグである audioPlayedプロパティを fase に変更します。
また同時に、今回は再生時間の短いオーディオファイルであるためあまり必要ないのですが、再生を停止するための pauseメソッドを実行します。
これらの処理はフレーム毎に呼び出される renderFrame 関数内に以下のように記述します。
if (timeKeeper.nextFrameJob()) {
//canvas をクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
//sprite_snow_man の x 値が動作範囲内かどうか
if ((sprite_snow_man.x < sprite_snow_man.limit_rightPosition && key_value > 0)
|| (sprite_snow_man.x >= 3 && key_value < 0)) {
//img_snow_man の x 値を増分
sprite_snow_man.x += key_value;
}
var length = snow_sprites.length;
for (var i = 0; i < length; i++) {
var snow_sprite = snow_sprites[i];
//snow_sprite の y 値(縦位置) が canvas からはみ出たら先頭に戻す
if (snow_sprite.y > canvas.clientHeight) {
snow_sprite.y = getRandomPosition(SNOWS_COUNT, SNOW_START_COEFFICIENT);
snow_sprite.imageIndex = SNOW_BLUE;
//オーディオ再生を停止
snow_sprite.audio.pause();
//オーディオ再生済フラグのリセット
snow_sprite.audioPlayed = false;
}else {
if (loopCounter == SWITCH_PICTURE_COUNT
&& snow_sprite.imageIndex != SNOW_CLASH) {
snow_sprite.imageIndex = (snow_sprite.imageIndex == SNOW_BLUE)
? SNOW_WHITE : SNOW_BLUE;
}
};
//snow_sprite の y 値を増分
snow_sprite.y += snow_sprite.dy;
//画像を描画
snow_sprite.draw();
//当たり判定
isHit(snow_sprite, sprite_snow_man);
snow_sprite = null;
}
//画像を描画
sprite_snow_man.draw();
//処理数のカウント
if (loopCounter == SWITCH_PICTURE_COUNT) { loopCounter = 0; }
loopCounter++;
window.requestAnimationFrame(renderFrame);
} else {
window.requestAnimationFrame(renderFrame);
}
}
ここまでの作業で、雪の結晶が雪だるまに衝突した際に音ができるようになりました。
以下の黒いボックスをクリックして、実際の動作を確認してください。(矢印キー[←][→]で雪だるまが左右に移動します。スマートフォンの場合はタッチで操作が可能です)
今回作業を行った main.js 全体のコードは以下のとおりです。
//矢印キーのコード
var LEFT_KEY_CODE = 37;
var RIGHT_KEY_CODE = 39;
var key_value = 0;
//全体で使用する変数
var canvas = null;
var ctx = null;
var img_snow = null;
var img_snow_man = null;
//画面の書き換え数をカウントする
var loopCounter = 0;
//雪の結晶画像を切り替える閾値
const SWITCH_PICTURE_COUNT = 24;
//1 秒間に実行されるフレーム数
const GAME_FPS = 48;
//表示する雪の結晶の数
const SNOWS_COUNT = 6;
//移動開始位置を得るための係数
const SNOW_START_COEFFICIENT = -50;
//隣り合う 雪の結晶画像の x 位置の差分
const NEIGHBOR_DISTANCE = 58;
//雪だるまの Sprite のインスタンスを格納する配列
var sprite_snow_man = null;
//雪の Sprite のインスタンスを格納する配列
var snow_sprites = [];
//スプライト画像のインデックス
const SNOW_BLUE = 0;
const SNOW_WHITE = 1;
const SNOW_CLASH = 2;
//雪の結晶の画像サイズ
const SNOW_PIC_HEIGHT = 32;
const SNOW_PIC_WIDTH = 32;
//雪ダルマの画像サイズ
const SNOW_MAN_PIC_HEIGHT = 80;
const SNOW_MAN_PIC_WIDTH = 80;
//Sprite クラスの定義
var Sprite = function (img, width, height) {
this.image = img; //image オブジェクト
this.height = img.height;
this.width = img.width;
this.x = 0; //表示位置 x
this.y = 0; //表示位置 y
this.dx = 0; //移動量 x
this.dy = 0; //移動量 y
this.audio = null; //Audio オブジェクト
this.audioPlayed = false; //音が複数回鳴るのを防ぐ
var _offset_x_pos = 0;
var that = this;
//使用するインデックスを設定するための Setter/Getter
var _imageIndex = 0;
Object.defineProperty(this, "imageIndex", {
get: function () {
return _imageIndex;
},
set: function (val) {
_imageIndex = val;
_offset_x_pos = width * _imageIndex;
}
});
//Sprite を描画するメソッド
this.draw = function () {
ctx.drawImage(img, _offset_x_pos, 0, width, height, that.x, that.y, width, height);
};
}
//FPS をコントロールするための timeKeeper クラス
function TimeKeeper(frameCount) {
var bofore_animation_time = 0;
var frameInterval = (600 / frameCount);
//window.performance オブジェクトに対応していないブラウザへの対応
var getNow = (window.performance.now) ?
function () { return window.performance.now(); }
: function () { return (new Date()).getTime(); }
//FPS として指定したフレームごとの時間が経過したら true を返す
this.nextFrameJob = function () {
var now_the_time = getNow();
var renderFlag = !(((now_the_time - bofore_animation_time) < frameInterval)
&& bofore_animation_time);
if (renderFlag) bofore_animation_time = now_the_time;
return renderFlag;
};
}
//TimeKeeper クラスのインスタンスを格納
var timeKeeper = new TimeKeeper(GAME_FPS);
//DOM のロードが完了したら実行
document.addEventListener("DOMContentLoaded", function () {
loadAssets();
setHandlers();
});
function setHandlers() {
//キーイベントの取得 (キーダウン)
document.addEventListener("keydown", function (evnt) {
if (evnt.which == LEFT_KEY_CODE) {
key_value = -3;
} else if (evnt.which == RIGHT_KEY_CODE) {
key_value = 3;
}
});
//雪だるまが進みっぱなしにならないように、 キーが上がったら 0 に
document.addEventListener("keyup", function () {
key_value = 0;
});
//Canvas へのタッチイベント設定
canvas.addEventListener("touchstart", function (evnt) {
if ((canvas.clientWidth / 2) > evnt.touches[0].clientX) {
key_value = -3;
} else {
key_value = 3;
}
});
//雪だるまが進みっぱなしにならないように、 タッチが完了したら 0 に
canvas.addEventListener("touchend", function (evnt) {
key_value = 0;
});
}
function loadAssets() {
//HTML ファイル上の canvas エレメントのインスタンスを取得
canvas = document.getElementById('bg');
//アニメーションの開始
canvas.addEventListener("click", loadCheck);
//2D コンテキストを取得
ctx = canvas.getContext('2d');
//image オブジェクトのインスタンスを生成
img_snow = new Image();
//image オブジェクトに画像をロード
img_snow.src = '/img/sp_snow.png';
/*画像読み込み完了のイベントハンドラーに Canvas に
画像を表示するメソッドを記述 */
img_snow.onload = function () {
for (var i = 0; i < SNOWS_COUNT; i++) {
var sprite_snow = new Sprite(img_snow, SNOW_PIC_WIDTH, SNOW_PIC_HEIGHT);
sprite_snow.dy = 1;
sprite_snow.dx = NEIGHBOR_DISTANCE;
sprite_snow.x = i * sprite_snow.dx;
sprite_snow.y = getRandomPosition(SNOWS_COUNT, SNOW_START_COEFFICIENT);
//Audio オブジェクトのインスタンスをセット
sprite_snow.audio = new Audio("audio/kiiiin1.mp3");
snow_sprites.push(sprite_snow);
sprite_snow = null;
}
};
//雪だるま画像のロード
img_snow_man = new Image();
img_snow_man.src = '/img/snow_man.png';
img_snow_man.onload = function () {
sprite_snow_man = new Sprite(img_snow_man,
SNOW_MAN_PIC_WIDTH, SNOW_MAN_PIC_HEIGHT);
sprite_snow_man.x = getCenterPostion(canvas.clientWidth, img_snow_man.width);
sprite_snow_man.y = canvas.clientHeight - img_snow_man.height;
sprite_snow_man.limit_rightPosition = getRightLimitPosition(canvas.clientWidth,
img_snow_man.width);
};
};
//ゲームで使用する Splite オブジェクトが準備されたかどうかを判断
function loadCheck() {
if (snow_sprites.length && sprite_snow_man) {
//準備ができたらアニメーションを開始
window.requestAnimationFrame(renderFrame);
} else {
//まだの場合はループして待機
window.requestAnimationFrame(loadCheck);
}
}
function renderFrame() {
if (timeKeeper.nextFrameJob()) {
//canvas をクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
//sprite_snow_man の x 値が動作範囲内かどうか
if ((sprite_snow_man.x < sprite_snow_man.limit_rightPosition && key_value > 0)
|| (sprite_snow_man.x >= 3 && key_value < 0)) {
//img_snow_man の x 値を増分
sprite_snow_man.x += key_value;
}
var length = snow_sprites.length;
for (var i = 0; i < length; i++) {
var snow_sprite = snow_sprites[i];
//snow_sprite の y 値(縦位置) が canvas からはみ出たら先頭に戻す
if (snow_sprite.y > canvas.clientHeight) {
snow_sprite.y = getRandomPosition(SNOWS_COUNT, SNOW_START_COEFFICIENT);
snow_sprite.imageIndex = SNOW_BLUE;
//オーディオ再生の終了
snow_sprite.audio.pause();
//オーディオ再生済フラグのリセット
snow_sprite.audioPlayed = false;
}else {
if (loopCounter == SWITCH_PICTURE_COUNT
&& snow_sprite.imageIndex != SNOW_CLASH) {
snow_sprite.imageIndex = (snow_sprite.imageIndex == SNOW_BLUE)
? SNOW_WHITE : SNOW_BLUE;
}
};
//snow_sprite の y 値を増分
snow_sprite.y += snow_sprite.dy;
//画像を描画
snow_sprite.draw();
//当たり判定
isHit(snow_sprite, sprite_snow_man);
snow_sprite = null;
}
//画像を描画
sprite_snow_man.draw();
//処理数のカウント
if (loopCounter == SWITCH_PICTURE_COUNT) { loopCounter = 0; }
loopCounter++;
window.requestAnimationFrame(renderFrame);
} else {
window.requestAnimationFrame(renderFrame);
}
}
//中央に配置する画像の X 座標を求める関数
function getCenterPostion(containerWidth, itemWidth) {
return (containerWidth / 2) - (itemWidth / 2);
};
//Player (雪だるまを動かせる右の限界位置)
function getRightLimitPosition(containerWidth, itemWidth) {
return containerWidth - itemWidth;
}
//雪の結晶の縦位置の初期値をランダムに設定する
function getRandomPosition(colCount, delayPos) {
return Math.floor(Math.random() * colCount) * delayPos;
};
//当たり判定
function isHit(targetA, targetB) {
if ((targetA.x <= targetB.x && targetA.width + targetA.x >= targetB.x)
|| (targetA.x >= targetB.x && targetB.x + targetB.width >= targetA.x)) {
if ((targetA.y <= targetB.y && targetA.height + targetA.y >= targetB.y)
|| (targetA.y >= targetB.y && targetB.y + targetB.height >= targetA.y)) {
ctx.font = "bold 20px 'MS ゴシック'";
ctx.fillStyle = "red";
ctx.fillText("ヒットしました", getCenterPostion(canvas.clientWidth, 140), 160);
targetA.imageIndex = SNOW_CLASH;
if (!targetA.audioPlayed) {
targetA.audio.play();
targetA.audioPlayed = true;
}
}
}
}
})();
まとめ
ここまでの機能実装で、アクションゲームに最低限必要なアニメーション、キャラクターの操作、当たり判定、キャラクター画像の変更、オーディオ再生といった機能が実装できました。
ゲームの終了、リセット、点数の加算や表示といった機能は、これまで応用で実装可能だと思います。
今回作成したこの単純なゲームでも画像を入れかえることによって、例えば降ってくるお金を拾うゲームや、逆に爆弾をよけ続けるゲーム、タッチ機能を使い、蟻の群れを指でつぶすといったゲームを作成可能です。
以下にこれまで作業した内容のサンプルプロジェクトをダウンロード可能としましたので、ぜひ改造していろいろなものを作成してみてください。
