忍者ブログ

JavaScriptゲーム

JavaScriptでゲームを作る!

画像を移動する方法

  • 次は画像を移動する方法を記します。
  • 人物やモンスターの画像を上下左右斜め方向に移動して描画しています。
  • 前回の「画像を表示する方法」を元に、少し変更を加えています。

01 ファイル構成

それでは処理を見ていきます。
先ずはファイル構成です。
ファイルとそれを入れるフォルダーを以下のような構成します。

02 ファイル全体の流れ

コード全体で行おうとしていることは以下の通りです。

  1. HTMLファイルに埋め込まれた <canvas> 要素に対して、JavaScriptを使って画像を描画します。
  2. 画像管理クラス を使って画像をあらかじめ読み込みます(プリロード処理)。
  3. 読み込んだ画像を スプライトクラス を使って座標を移動して描画します。
  4. レイヤークラス は <canvas> 要素をクリアしてから スプライトクラス の描画処理を実行します。
  5. ループクラス はフレームレートのタイミングで レイヤークラス の描画処理を繰り返し実行します。

これにより、スプライトクラス の描画処理が繰り返し実行され、移動し続ける画像を表示できるようになります。

03 各部分の詳細解説

次は各ファイル毎に解説していきます。
尚、「imageManager.js」については、前回の「画像を表示する方法」から変更がないので割愛します。

a. sample.html - HTMLファイル

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=660,user-scalable=no,shrink-to-fit=yes">
    <title>画像を移動する方法</title>
</head>
<body>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            margin: 0;
            padding: 0;
            background-color: #f4f4f4;
        }
    
        canvas {
            display: block;
            margin: 20px auto;
            border: 1px solid #ccc;
        }
    
        p {
            font-size: 18px;
            color: #333;
        }
    </style>
    
    <p>
        リフレッシュレート:<span class="リフレッシュレート">0</span>Hz
        /フレームレート:<span class="フレームレート">0</span>fps
    </p>
    <canvas class="レイヤー1" width="640" height="640"></canvas>
    <script type="module" src="js/sample.js"></script>
</body>
</html>

このファイルでは、以下のことを行っています。

  • リフレッシュレートとフレームレートの表示:ウェブページに描画処理のパフォーマンス(Hzやfps)を表示するためのテキスト。
  • キャンバスの作成:<canvas>タグを使って、図形や画像を描画する領域を作成しています。ここでは、幅640px、高さ640pxのキャンバスを作っています。
  • JavaScriptの読み込み:<script type="module" src="js/sample.js"></script>で外部のJavaScriptコード(sample.js)を読み込んでいます。

初心者向けのポイント:

  • キャンバスは画面上に見える「お絵かきエリア」みたいなものです。
  • JavaScriptを使うことで、動的に画像を描画したり、動かしたりできます。

b. layer.js - レイヤークラス

export class レイヤークラス {
    constructor(セレクター) {
        this.canvas = document.querySelector(セレクター);
        this.ctx = this.canvas.getContext('2d');
        this.処理リスト = [];
    }

    描画する() {
        this.クリアする();

        // ループ中に処理リストの処理が削除されてもいいように逆順でループします
        for (let i = this.処理リスト.length - 1; i >= 0; i--) {
            this.処理リスト[i].処理();
        }
    }

    クリアする() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }

    処理を追加する(id, 処理) {
        const 既存処理 = this.処理リスト.find(x => x.id == id);
        if (既存処理) {
            既存処理.処理 = 処理; // 既にある場合は差し替える
            return;
        }

        this.処理リスト.unshift({ id, 処理 });
    }

    処理を削除する(id) {
        this.処理リスト = this.処理リスト.filter(x => x.id != id);
    }
}

レイヤークラスは、Canvas要素(キャンバス)で画像や図形を描画するための管理クラスです。

主な機能:

  • キャンバスの初期化:指定されたセレクター(例: .レイヤー1)からキャンバス要素を取得し、2D描画コンテキスト(ctx)を準備します。
  • 描画処理:登録された処理(処理リスト)を順番に実行して、キャンバスに描画します。
  • キャンバスをクリアする:クリアするメソッドでキャンバスの中身を消去します。
const レイヤー = new レイヤークラス('.レイヤー1'); 
// 上記はキャンバス(.レイヤー1)を管理する新しいレイヤーを作成します。

c. sprite.js - スプライトクラス

export class スプライトクラス {
    constructor(レイヤー, 画像要素, x1, y1, x2, y2) {
        this.レイヤー = レイヤー;
        this.ctx = レイヤー.ctx;
        this.画像要素 = 画像要素;
        this.レイヤー.処理を追加する(this, this.描画する.bind(this));
        this.座標を設定する(x1, y1, x2, y2);
    }

    座標を設定する(x1, y1, x2, y2) {
        const 画像要素 = this.画像要素;
        const 表示幅 = x2 - x1;
        const 表示高さ = y2 - y1;

        const 画像縦横比 = 画像要素.width / 画像要素.height;
        const 表示縦横比 = 表示幅 / 表示高さ;

        let 描画幅, 描画高さ, 位置調整X, 位置調整Y;

        if (画像縦横比 > 表示縦横比) {
            描画幅 = 表示幅;
            描画高さ = 表示幅 / 画像縦横比;
            位置調整X = 0;
            位置調整Y = (表示高さ - 描画高さ) / 2; // 垂直中央揃え
        } else {
            描画高さ = 表示高さ;
            描画幅 = 表示高さ * 画像縦横比;
            位置調整X = (表示幅 - 描画幅) / 2; // 水平中央揃え
            位置調整Y = 0;
        }

        this.x = x1 + 位置調整X;
        this.y = y1 + 位置調整Y;
        this.幅 = 描画幅;
        this.高さ = 描画高さ;
    }

    描画する() {
        this.ctx.drawImage(this.画像要素, this.x, this.y, this.幅, this.高さ); // 画像を描画
    }

    移動して描画する(移動幅x, 移動幅y, 停止位置x, 停止位置y) {
        return new Promise(resolve => {
            this.レイヤー.処理を追加する(this, () => {
                if (移動幅x) this.x += 移動幅x;
                if (移動幅y) this.y += 移動幅y;
                this.描画する();

                let 停止フラグ = false;
                if (移動幅x) {
                    if (移動幅x > 0) {
                        if (this.x >= 停止位置x) 停止フラグ = true;
                    } else if (移動幅x < 0) {
                        if (this.x <= 停止位置x) 停止フラグ = true;
                    }
                }
                if (移動幅y) {
                    if (移動幅y > 0) {
                        if (this.y >= 停止位置y) 停止フラグ = true;
                    } else if (移動幅y < 0) {
                        if (this.y <= 停止位置y) 停止フラグ = true;
                    }
                }
                if (!停止フラグ) return;

                // 停止位置に来たら描画だけの処理に切替える
                this.レイヤー.処理を追加する(this, this.描画する.bind(this));
                resolve();
            });
        })
    }
}

スプライトクラスは、画面上の画像(スプライト)を管理するためのクラスです。たとえば、キャラクターや背景画像を表示したり動かしたりする処理を提供します。

主な機能:

  • 座標を設定:画像の位置(x, y)やサイズ(幅、高さ)を設定します。
  • 描画:キャンバス上に画像を描画します。
  • 移動アニメーション:指定した速さや方向に画像を移動しながら描画します。
const 人物01 = new スプライトクラス(レイヤー, 画像, x1, y1, x2, y2);
// 人物01というスプライトを作成し、レイヤー上に表示します。

d. loop.js - ループクラス

export class ループ {
    static 初期化する(フレームレート) {
        ループ.処理リスト = [];
        ループ.リフレッシュ時刻;
        ループ.処理時刻;
        ループ.リフレッシュカウンタ = 0;
        ループ.処理カウンタ = 0;
        ループ.フレームレート = フレームレート | 1000 / 60;
        ループ.リフレッシュレート要素 = document.querySelector(".リフレッシュレート");
        ループ.フレームレート要素 = document.querySelector(".フレームレート");
    }

    static 開始する() {
        ループ.処理時刻 = ループ.リフレッシュ時刻 = Date.now();
        ループ.する();
    }

    static する() {
        ループ.処理を実行する();
        requestAnimationFrame(ループ.する);
    }

    static 処理を実行する() {
        const 現時刻 = Date.now();
        if (現時刻 - ループ.リフレッシュ時刻 < 1000) {
            ループ.リフレッシュカウンタ++;
        } else {
            ループ.リフレッシュ時刻 = 現時刻;
            ループ.リフレッシュレート要素.innerHTML = ループ.リフレッシュカウンタ;
            ループ.フレームレート要素.innerHTML = ループ.処理カウンタ;
            ループ.リフレッシュカウンタ = 0;
            ループ.処理カウンタ = 0;
        }

        if (現時刻 - ループ.処理時刻 < ループ.フレームレート) return;
        ループ.処理時刻 += ループ.フレームレート;
        ループ.処理カウンタ++;

        // ループ中に処理リストの処理が削除されてもいいように逆順でループします
        for (let i = ループ.処理リスト.length - 1; i >= 0; i--) {
            ループ.処理リスト[i].処理();
        }
    }

    static 処理を追加する(id, 処理) {
        const 既存処理 = ループ.処理リスト.find(x => x.id == id);
        if (既存処理) {
            既存処理.処理 = 処理; // 既にある場合は差し替える
            return;
        }

        ループ.処理リスト.push({ id, 処理 });
    }

    static 処理を削除する(id) {
        ループ.処理リスト = ループ.処理リスト.filter(x => x.id != id);
    }
}

ループクラスは、アニメーションを管理するためのメインループを提供します。アニメーションとは、画像を動かしたり、複数の処理を一定の時間間隔で繰り返すことを指します。

主な機能:

  • 処理を管理:アニメーションに必要な複数の処理をリストとして登録し、順番に実行します。
  • 一定のフレームレートで実行:滑らかな動きを実現するために、1秒間に60回(または指定のレート)処理を呼び出します。
ループ.初期化する();
ループ.開始する();
ループ.処理を追加する(レイヤー, レイヤー.描画する.bind(レイヤー));
// アニメーションを開始し、レイヤーの描画をループ内で実行します。

e. sample.js - サンプルクラス

import { 画像管理クラス } from './imageManager.js'; // クラスをインポート
import { スプライトクラス } from './sprite.js'; // クラスをインポート
import { レイヤークラス } from './layer.js'; // クラスをインポート
import { ループ } from './loop.js'; // クラスをインポート

class サンプル {
    static async main() {
        const レイヤー = new レイヤークラス('canvas.レイヤー1');
        const 画像管理 = new 画像管理クラス();
        ループ.初期化する(1000/60);
        ループ.開始する();

        // 事前に読み込む画像の配列
        const 画像情報リスト = [
            { id: '草原', src: 'img/bg01.jpg' },
            { id: '人物01', src: 'img/chara01.png' },
            { id: '炎竜', src: 'img/mon01.png' },
            { id: 'デビル', src: 'img/mon02.png' }
        ];

        // ページ初期化時に画像をプリロード
        if (!await 画像管理.画像を読み込む(画像情報リスト)) return;

        // 画像の描画位置を設定
        const 背景01 = new スプライトクラス(レイヤー, 画像管理.取得する('草原'), 0, 0, 640, 640);

        const 人物01 = new スプライトクラス(レイヤー, 画像管理.取得する('人物01'), 0, 100, 640, 640);
        const 人物01正位置 = { x: 人物01.x, y: 人物01.y };
        人物01.座標を設定する(640, 100, 1280, 640); //初期表示位置を設定する

        const モンスター01 = new スプライトクラス(レイヤー, 画像管理.取得する('炎竜'), 0, 0, 640, 540);
        const モンスター01正位置 = { x: モンスター01.x, y: モンスター01.y };
        モンスター01.座標を設定する(-640, 0, 0, 540); //初期表示位置を設定する

        const モンスター02 = new スプライトクラス(レイヤー, 画像管理.取得する('デビル'), 0, 320, 320, 640);
        const モンスター02正位置 = { x: モンスター02.x, y: モンスター02.y };
        モンスター02.座標を設定する(640, -640, 960, 320); //初期表示位置を設定する

        const モンスター03 = new スプライトクラス(レイヤー, 画像管理.取得する('デビル'), 320, 320, 640, 640);
        const モンスター03正位置 = { x: モンスター03.x, y: モンスター03.y };
        モンスター03.座標を設定する(-320, -640, 0, 320); //初期表示位置を設定する

        ループ.処理を追加する(レイヤー, レイヤー.描画する.bind(レイヤー));

        let プロミスリスト;
        while (1) {
            await 人物01.移動して描画する(-40, 0, 人物01正位置.x, 0);
            await サンプル.一時停止(2000);

            プロミスリスト = [];
            プロミスリスト.push(人物01.移動して描画する(40, 0, 640, 0));
            プロミスリスト.push(モンスター01.移動して描画する(5, 0, モンスター01正位置.x, 0));
            プロミスリスト.push(モンスター02.移動して描画する(-20, 20, モンスター02正位置.x, モンスター02正位置.y));
            プロミスリスト.push(モンスター03.移動して描画する(10, 10, モンスター03正位置.x, モンスター03正位置.y));
            await Promise.all(プロミスリスト);
            await サンプル.一時停止(2000);

            プロミスリスト = [];
            プロミスリスト.push(モンスター01.移動して描画する(0, 10, 0, 640));
            プロミスリスト.push(モンスター02.移動して描画する(0, 5, 0, 640));
            プロミスリスト.push(モンスター03.移動して描画する(0, 20, 0, 640));
            await Promise.all(プロミスリスト);
            await サンプル.一時停止(2000);

            モンスター01.座標を設定する(-640, 0, 0, 540); //初期表示位置を設定する
            モンスター02.座標を設定する(640, -640, 960, 320); //初期表示位置を設定する
            モンスター03.座標を設定する(-320, -640, 0, 320); //初期表示位置を設定する
        }
    }

    static 一時停止(時間) {
        return new Promise(resolve => setTimeout(resolve, 時間));
    }
}

addEventListener('load', サンプル.main);

サンプルクラスは、これらのクラスを組み合わせて具体的な処理(ゲームやアニメーション)を実現する部分です。

主な処理:

  • 画像のプリロード:画像を事前に読み込むことで、スムーズな描画を実現します。
  • スプライトの作成とアニメーション:スプライトをレイヤーに追加し、一定の方向に動かすアニメーションを行います。
  • 一時停止と再開:awaitを使用して、一定時間停止した後に次のアニメーションを実行します。
await サンプル.一時停止(2000); 
// 2000ミリ秒(2秒)待ってから次の処理を実行します。

f. 初心者向けまとめ

  • キャンバスは「描画用の黒板」のようなものです。
  • レイヤーは「黒板を管理する管理人」で、黒板消しを持っています。
  • スプライトは「黒板の上を動くキャラクターや背景」で、黒板消しは持っていないため、他のスプライトが描いたものに上書きすることはあっても、消すことはありません。
  • ループは「描画を永遠に繰り返す仕組み」です。
  • サンプルクラスで全てをつなぎ、画像の移動や停止、再開をコントロールします。

拍手[0回]

PR

コメント

P R

プロフィール

HN:
No Name Ninja
性別:
非公開