HTML5 を使ったシンプルな 2 D ゲームの作り方 (ランダムな動作と FPS の制御)

前回から 1 つ飛んでしまいましたが、HTML5 を使用したシンプルな 2D ゲームの作り方の第 8 回めをお送りします。

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

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

このシリーズの前回となる第 7 回めの記事では、クラスの概念を使用して、落ちてくる雪の結晶を複数生成しましたが、動作させると横一列に並んで落ちてくる状態でした。これは各インスタンスで異なるのが、横位置を表す x プロパティのみであったからでず。今回は縦位置を表すプロパティ y の値を調整し、これにより雪をランダムに降らせる実装を行います。

また、このゲームでは、アニメーションを実現するためのループに requestAnimationFrame 関数を使用していますが、ループのタイミングは良くも悪くも Web ブラウザー任せとなってしまうため Web ブラウザーごとにアニメーションのスピードが異なるという状況が発生します。この問題に対応するたに FPS(Frame Per Second : 秒間フレーム数) を調整するためのコードを追加します。

 

雪の結晶の降るタイミングの不確定化

前回の実装で生成した 6 つの雪の結晶の降るタイミングを変更するには、縦の位置を示すプロパティである y の値を各々変更します。

具体的には、現在初期値が 0 になっている雪の結晶インスタンスの y プロパティに、6 パターンの異なるマイナス値を指定します。なぜマイナス値かというと、プラスの値だと、Canvas 内の、本来なら雪の結晶が落ちていく途中に表示されてしまうからです。

1 ~ 6 間の異なる値は以下の式で得ることができます。

1 ~ 6 間の異なる値 = Math.floor(Math.random() * 6 )

そして、ここに雪の結晶の降るタイミングとして不自然にならないマイナス値を得るために –50 を掛けその結果を雪の結晶の移動開始位置 (プロパティ y の初期値)とします。ちなみにこの –50 という数値は数学的根拠があるものではなく、目分量的なものです。

雪の結晶の y プロパティの初期値 = Math.floor(Math.random() * 6 ) * -50;

この式を使用して以下のように getRandomPosition 関数を定義します。

//雪の結晶の縦位置の初期値をランダムに設定する
function getRandomPosition(colCount, delayPos) {
        return Math.floor(Math.random() * colCount) * delayPos;
};

また開始のマイナス値を得るための係数 –50 も var か const キーワードで適当な場所に宣言しておいてください。

const SNOW_START_COEFFICIENT = -50;

loadAssets 関数と renderFrame 関数内を以下のように getRandomPosition 関数を使用したものに書き換えます。

loadAsset 関数

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/snow.png';

        /*画像読み込み完了のイベントハンドラーに Canvas に
           画像を表示するメソッドを記述 */
        img_snow.onload = function () {
            for (var i = 0; i < SNOWS_COUNT; i++) {
                var sprite_snow = new Sprite(img_snow);
                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);
                snow_sprites.push(sprite_snow);
                sprite_snow = null;
            }
};

renderFrame 関数

function renderFrame() {
        //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 の y 値を増分
            snow_sprite.y += snow_sprite.dy;
            //画像を描画
            ctx.drawImage(snow_sprite.image, snow_sprite.x, snow_sprite.y);

            //当たり判定
            isHit(snow_sprite, sprite_snow_man);
            snow_sprite = null;
        }
        //画像を描画
        ctx.drawImage(sprite_snow_man.image, sprite_snow_man.x, sprite_snow_man.y);
        window.requestAnimationFrame(renderFrame);
}

以上の設定で 6 つの雪の結晶画像がそれぞれ異なるタイミングで降ってくるようになりました。

以下の黒いボックスをクリックして動作を確認してください。(※このサンプルは FPS の制御コードもすでに組み込まれています)

FPS の制御

FPS は Frame Per Second の名前のとおり 1 秒間にいくつのフレームがあるかを示すものです。

requestAnimationFrame メソッドは、従来の setTimeout 関数よりも処理効率などの面で優れていますが、時間あたりの実行数を指定することができません。時間当たりに実行される回数も、デバイスや Web ブラウザー任せな部分があるので、Web ブラウザーごとにアニメーションのスピードが若干異なったりということがあります。今回の FPS の制御コードを追加することにより、時間単位のフレームの実行数を明示的にコードで管理できるようになるので、Web ブラウザー間のアニメーションのスピードの違いなどをある程度吸収することができます。

1 秒あたりの実行数を指定する方法

1 秒あたりの実行数を指定するには、前後のフレームが実行される時間の間隔 A を求め、それと現在の時間から前回実行時の時間を引いた値 B を比較し、B が A と等しいか大きい場合に次のフレームを実行します。

たとえは 1 秒間に 50 フレーム動作させる場合には以下のようになります。

if((現在時刻 - 前回の実行時の時刻) >= (1 秒 / 50 ){
     次のフレームの実行
}

この処理を行うにはフレーム間の時間間隔と、前回の実行時間を保持する必要があるので function でインスタンスを生成しそれらの値とメソッドを保持します。具体的には以下のように TimeKeeper クラスを定義してインスタンスを生成します。

//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;
    };
}

//1 秒間に実行されるフレーム数
const GAME_FPS = 48;
//TimeKeeper クラスのインスタンスを格納
var timeKeeper = new TimeKeeper(GAME_FPS);

TimeKeeper クラスの nextFrameJob メソッドを使用して renderFrame 関数の処理を分岐させます。

この記述を行ったあとの renderFrame 関数のコードは以下です。追記した部分を太字で示しています。

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 の y 値を増分
            snow_sprite.y += snow_sprite.dy;
            //画像を描画
            ctx.drawImage(snow_sprite.image, snow_sprite.x, snow_sprite.y);

            //当たり判定
            isHit(snow_sprite, sprite_snow_man);
            snow_sprite = null;
        }
        //画像を描画
        ctx.drawImage(sprite_snow_man.image, sprite_snow_man.x, sprite_snow_man.y);
        window.requestAnimationFrame(renderFrame);
   } else {
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;

    //表示する雪の結晶の数
    const SNOWS_COUNT = 6;

    //移動開始位置を得るための係数
    const SNOW_START_COEFFICIENT = -50;

    //隣り合う 雪の結晶画像の x 位置の差分
    const NEIGHBOR_DISTANCE = 58;

    //雪だるまの Sprite のインスタンスを格納する配列 
    var sprite_snow_man = null;

    //雪の Sprite のインスタンスを格納する配列 
    var snow_sprites = [];

    //Sprite クラスの定義
    var Sprite = function (img) {
        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
    }

    //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;
        };
    }

    //1 秒間に実行されるフレーム数
    const GAME_FPS = 48;
    //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/snow.png';

        /*画像読み込み完了のイベントハンドラーに Canvas に
           画像を表示するメソッドを記述 */
        img_snow.onload = function () {
            for (var i = 0; i < SNOWS_COUNT; i++) {
                var sprite_snow = new Sprite(img_snow);
                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);
                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);
            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 の y 値を増分
                snow_sprite.y += snow_sprite.dy;
                //画像を描画
                ctx.drawImage(snow_sprite.image, snow_sprite.x, snow_sprite.y);

                //当たり判定
                isHit(snow_sprite, sprite_snow_man);
                snow_sprite = null;
            }
            //画像を描画
            ctx.drawImage(sprite_snow_man.image, sprite_snow_man.x, sprite_snow_man.y);
            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);
            }
        }
    }
})();

 

まとめ

今回は複数の雪の結晶を各々ランダムに降らすロジックを追加し、他の Web ブラウザーで実行した際にもアニメーションのスピードに違いが出ないように制御コードを追加しました。

次回は画像処理における「スプライト」を使用して、キャラクターの移動時や、あたり判定時に画像を切り替える処理を追加します。

Real Time Analytics

Clicky