HTML5 を使ったシンプルな 2 D ゲームの作り方 (当たり判定の実装)

去年末に出演した schoo (スクー) さんの授業で使用したサンプルアプリをもとにした、HTML5 を使ったシンプルな 2 D ゲームの作り方を紹介しています。

どんなゲームを作るのかは 1 回目の記事の中に実際に動作するゲームが埋め込んであるのでぜひ遊んでみてください。なお、開発に必要な画像データは 2 回目の記事からダウンロードできますので、実際にゲーム開発を体験したい方はそちらから入手してください。

  1. HTML5 を使ったシンプルな 2 D ゲームの作り方(序)
  2. HTML5 を使ったシンプルな 2 D ゲームの作り方(準備編)
  3. HTML5 を使ったシンプルな 2 D ゲームの作り方 (画像のロード)
  4. HTML5 を使ったシンプルな 2 D ゲームの作り方 (アニメーションの実装)
  5. HTML5 を使ったシンプルな 2 D ゲームの作り方 (矢印キーとタッチによる制御の実装)

前回の記事では雪だるまをキーボードの矢印 (カーソル) キーで操作する機能を実装しました。

今回の記事では、降ってきた雪の結晶と雪だるまが当たったかどうか判断する処理を実装します。

当たり判定

「当たり判定」とは、文字どおりゲームにおいてキャラクター同士が衝突したかどうかを判断するもので、シューティングゲームのような、ユーザーがゲーム内のキャラクターを操作するタイプのゲームでは不可欠なものです。

当たり判定の方法はいろいろあり、形状が複雑なものに対し厳密に行おうとすると非常に大変です。また、実際のそこまでは必要がないということもあります。例えば、以下の図のアミカケの部分は、当たり判定をする単純な 3 つのパターンを示してしますが、X と Y の値で判断できる 1 や 2 が簡単です。

image

今回は最も単純な、1 のタイプで、2 つの画像が重なったら当たりと判定する方法で実装します。

image

この判断は以下の式で行うことができます。

当たり/はずれ = ((targetA.x <= targetB.x and targetA.width + targetA.x >= targetB.x)
                or  (targetA.x >= targetB.x and targetB.x + targetB.width >= targetA.x) )
                and ( (targetA.y <= targetB.y and targetA.height + targetA.y >= targetB.y)
                or (targetA.y >= targetB.y and targetB.y + targetB.height >= targetA.y))

この式を JavaScript の if 文の書式に直し、ヒットと判断されたら画面に文字を出力する関数を定義します。名前は isHit としましょう。

//当たり判定
    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);
            }
        }
    }

この関数を、アニメーションのフレームを処理している renderFrame 関数内の window.requestAnimationFrame メソッドを呼び出しの手前に記述します。

   //当たり判定
  isHit(img_snow, img_snow_man);

    //ループを開始
    requestId = window.requestAnimationFrame(renderFrame);
}

ここまでの main.js 全体のコードは以下のとおりです。

(function () {
    //矢印キーのコード
    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;

    //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", 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;
            img_snow_man.limit_rightPosition = getRightLimitPosition(canvas.clientWidth,
                                                                                                                     img_snow_man.width);
            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;
        //img_snow_man の x 値が動作範囲内かどうか
        if ((img_snow_man._x < img_snow_man.limit_rightPosition && key_value > 0)
         || (img_snow_man._x >= 3 && key_value < 0)) {
            //img_snow_man の x 値を増分
            img_snow_man._x += key_value;
        }
        //画像を描画
        ctx.drawImage(img_snow, img_snow._x, img_snow._y);
        ctx.drawImage(img_snow_man, img_snow_man._x, img_snow_man._y);

        //当たり判定
        isHit(img_snow, img_snow_man);
        //ループを開始
        window.requestAnimationFrame(renderFrame);
    }

    //中央に配置する画像の X 座標を求める関数
    function getCenterPostion(containerWidth, itemWidth) {
        return (containerWidth / 2) - (itemWidth / 2);
    };

    //Player (雪だるまを動かせる右の限界位置)
    function getRightLimitPosition(containerWidth, itemWidth) {
        return containerWidth - itemWidth;
    }

    //当たり判定
    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);
            }
        }
    }
})();

default.html を選択し、キーボードの [F5] キーを押下してページを実行し、画像が表示されたら Canvas 部分をクリックしてください。

アニメーションのが開始されたらカーソルキー(矢印キー) で雪だるまを動かし、雪だるまが雪の結晶とぶつかったときだけ画面に文字が表示されるのを確認してください。

以下は、実際に動作するサンプルです。ゲーム画面をクリックして動作を確認してみてください。

まとめ

今回は、雪の結晶画像と雪だるまが当たったかどうかを判定する機能を組み込みました。これで基本的なアクションゲームの仕組みはそろったことになります。次回は、雪の結晶画像を増やし、ランダムに降らせる処理を追加します。

ただし、そろそろ de:code 2015 の準備に取り掛からなければならないので、更新には時間があくかもしれません。

ある程度ゲームができた以降は、Azure Web Apps へのデプロイや、Univarsal Windows Apps への展開、Apache Cordova を使用した iOS、Android アプリ化 と Firefox OS 用アプリ、Chrome Apps 化なども考えていますので、お楽しみに。(ほんとに全部書けるんだろうか)

Real Time Analytics

Clicky