忍者ブログ

JavaScriptゲーム

JavaScriptでゲームを作る!

場面を遷移する方法

前回の「選択肢を表示する方法」のコードでは、選択肢毎にコードを書く必要がありif文だらけのコードになっていました。
今回はこの問題を解消する方法を記します。
以下の例では、見た目では分かりませんが、画像、メッセージ、選択肢情報といったパラメータを変えただけで、全て共通のコードを実行しています。
そのため、どれだけ画像やメッセージ、選択肢が増えても、if文等のコードが増えることはありません。

ファイル構成

ファイルとそれを入れるフォルダーを以下のような構成します。
※背景と人物の画像はBing Image Creatorで生成しています(AI生成)。

scene.js - 場面情報クラス

class 選択肢情報クラス {
    constructor(ar) {
        this.id = ar[0];
        this.名称 = ar[1];
    }
}

class 画像情報クラス {
    constructor(ar) {
        this.スプライトid = ar[0];
        this.レイヤーid = ar[1];
        this.画像id = ar[2];
        this.x1 = ar[3];
        this.y1 = ar[4];
        this.x2 = ar[5];
        this.y2 = ar[6];
        this.表示メソッド = ar[7];
        this.p1 = ar[8];
        this.p2 = ar[9];
        this.p3 = ar[10];
    }
}

export class 場面情報クラス {
    constructor(ar) {
        this.id = ar[0];
        this.メッセージ = ar[1];

        this.選択肢情報リスト = [];
        ar[2].forEach(x => this.選択肢情報リスト.push(new 選択肢情報クラス(x)));

        this.画像情報リスト = [];
        if (ar[3]) ar[3].forEach(x => this.画像情報リスト.push(new 画像情報クラス(x)));
    }
}

1. 選択肢情報クラス

このクラスは「選択肢」を管理するためのものです。

  • コンストラクタ:クラスを作るときに、配列 ar を受け取ります。
    • ar[0] は選択肢の「id」。
    • ar[1] は選択肢の「名称」(例えば「はい」や「いいえ」など)。

このクラスを使うことで、選択肢のデータを簡単に扱えるようになります。

2. 画像情報クラス

このクラスは、プログラム内で表示する画像の情報を管理します。

  • コンストラクタ:配列 ar を受け取り、画像に関する情報を格納します。
    • スプライトid:画像の識別ID。
    • レイヤーid:どの層(レイヤー)に画像を表示するか。
    • 画像id:実際の画像データの識別子。
    • x1, y1:画像の開始位置(左上の座標)。
    • x2, y2:画像の終了位置(右下の座標)。
    • 表示メソッド:画像をどのように表示するか(例えば拡大・縮小)。
    • p1, p2, p3:追加のパラメータ(用途に応じて設定)。

このクラスにより、画像を座標やレイヤーに基づいて管理できます。

3. 場面情報クラス

このクラスは、「場面」を管理するためのメインクラスです。

  • コンストラクタ:配列 ar を受け取り、以下の情報を設定します。
    • id:場面の識別子。
    • メッセージ:この場面で表示する文章。
    • 選択肢情報リスト:ar[2] に含まれる選択肢のデータを 選択肢情報クラス に変換して保存します。
    • 画像情報リスト:ar[3] が存在する場合、画像データを 画像情報クラス に変換して保存します。

このクラスを利用すると、場面に対応するメッセージ、選択肢、画像を一括して扱えます。

sceneTransition.js - 場面遷移クラス

import { スプライトクラス } from './sprite.js'; // クラスをインポート

export class 場面遷移 {
    static レイヤー管理 = {};
    static スプライト管理 = {};
    static 場面情報管理 = {};
    static 場面情報 = null;
    static 画像管理 = null;
    static メッセージウィンドウ = null;
    static コマンドウィンドウ = null;

    static async 開始する() {
        if (!場面遷移.場面情報) throw new Error("場面情報が初期化されていません");

        while (1) {
            await 場面遷移.画像を表示する();
            await 場面遷移.メッセージを表示する();
            const 選択肢情報 = 場面遷移.場面情報.選択肢情報リスト[0];
            if (!選択肢情報.名称) {
                // 選択肢情報に名称が無い場合は場面を遷移する
                場面遷移.場面情報 = 場面遷移.場面情報管理[選択肢情報.id];
                continue;
            }
            await 場面遷移.コマンドウィンドウを表示する();
        }
    }

    static async 画像を表示する() {
        if (場面遷移.場面情報.画像情報リスト.length == 0) return; // 画像情報が無い場合は何もしない

        let スプライト, 画像要素;
        const プロミスリスト = [];
        場面遷移.場面情報.画像情報リスト.forEach(画像情報 => {
            スプライト = 場面遷移.スプライト管理[画像情報.スプライトid];
            if (!画像情報.画像id) {
                if (!スプライト) return; // 画像idが無い場合はスプライトを作成できないので何もしない
            } else {
                画像要素 = 場面遷移.画像管理.取得する(画像情報.画像id);
                if (スプライト) {
                    // 指定したスプライトに別の画像を設定したい場合は新たにスプライトを作り直す
                    if (スプライト.画像要素 != 画像要素) スプライト = 場面遷移.スプライトを作成する(画像要素, 画像情報);
                } else {
                    // 指定したスプライトが存在しない場合は新たに作成する
                    スプライト = 場面遷移.スプライトを作成する(画像要素, 画像情報);
                }
            }

            場面遷移.表示メソッドに従って画像を表示する(画像情報, スプライト, プロミスリスト);
        });

        if (プロミスリスト.length == 0) return;
        await Promise.all(プロミスリスト);
    }

    static スプライトを作成する(画像要素, 画像情報) {
        const スプライト = new スプライトクラス(
            場面遷移.レイヤー管理[画像情報.レイヤーid],
            画像要素,
            画像情報.x1,
            画像情報.y1,
            画像情報.x2,
            画像情報.y2
        );
        場面遷移.スプライト管理[画像情報.スプライトid] = スプライト;
        return スプライト;
    }

    static 表示メソッドに従って画像を表示する(画像情報, スプライト, プロミスリスト) {
        if ((画像情報.表示メソッド == "フェードインする") || (画像情報.表示メソッド == "フェードアウトする")) {
            プロミスリスト.push(スプライト[画像情報.表示メソッド](画像情報.p1));
        } else if (画像情報.表示メソッド == "移動してフェードインする") {
            const 正位置 = { x: スプライト.x, y: スプライト.y };
            const 移動距離 = { x: 0, y: 0 };
            場面遷移.移動距離を取得する(画像情報, 移動距離, -640);
            スプライト.座標を設定する(
                画像情報.x1 + 移動距離.x,
                画像情報.y1 + 移動距離.y,
                画像情報.x2 + 移動距離.x,
                画像情報.y2 + 移動距離.y
            );
            プロミスリスト.push(スプライト[画像情報.表示メソッド](画像情報.p1, 画像情報.p2, 正位置.x, 正位置.y, 画像情報.p3));
        } else if (画像情報.表示メソッド == "移動してフェードアウトする") {
            const 移動距離 = { x: 0, y: 0 };
            場面遷移.移動距離を取得する(画像情報, 移動距離, 640);
            プロミスリスト.push(スプライト[画像情報.表示メソッド](画像情報.p1, 画像情報.p2, 移動距離.x, 移動距離.y, 画像情報.p3));
        }
    }

    static 移動距離を取得する(画像情報, 移動距離, 基本移動距離) {
        if (画像情報.p1 > 0) 移動距離.x = 基本移動距離;
        else if (画像情報.p1 < 0) 移動距離.x = -基本移動距離;

        if (画像情報.p2 > 0) 移動距離.y = 基本移動距離;
        else if (画像情報.p2 < 0) 移動距離.y = -基本移動距離;
    }

    static async メッセージを表示する() {
        if (!場面遷移.場面情報.メッセージ) return; // 表示するメッセージが無い場合は何もしない
        await 場面遷移.メッセージウィンドウ.表示する();
        await 場面遷移.メッセージウィンドウ.メッセージを表示する(場面遷移.場面情報.メッセージ);
    }

    static async コマンドウィンドウを表示する() {
        const id = await 場面遷移.コマンドウィンドウ.表示する(場面遷移.場面情報.選択肢情報リスト);
        場面遷移.場面情報 = 場面遷移.場面情報管理[id];
    }
}

1. 場面遷移クラス全体の役割

このクラスは、プログラムの場面を管理するためのメインクラスです。例えば、特定の画像やメッセージを表示したり、場面を切り替えたりします。

  • 静的プロパティ:各種データ(レイヤー管理、スプライト管理、場面情報管理など)を一括で管理します。
  • 場面遷移:開始する メソッドを呼び出すことで、プログラムの場面遷移が始まります。
  • ループ構造:無限ループ (while(1)) を使って、場面表示の処理を繰り返します。

2. 静的プロパティ

場面遷移クラスでは、多くのデータを静的プロパティで管理しています。

  • レイヤー管理:レイヤー(表示する層)を管理します。
  • スプライト管理:スプライト(プログラム内の画像要素)を管理します。
  • 場面情報管理:複数の場面に対応するデータを保存します。
  • 場面情報:現在の場面のデータを保持します。
  • 画像管理:画像を取得したりするための機能を提供します。
  • メッセージウィンドウ & コマンドウィンドウ:メッセージや選択肢を表示するための管理要素です。

3. 場面を開始する: 開始する メソッド

このメソッドは、場面遷移の主要な処理を行います。

処理手順:

  1. 初期化チェック:場面情報 が設定されていない場合はエラー (throw new Error) を発生させます。
  2. 画像とメッセージの表示:画像を表示する メソッドで画像を表示し、次に メッセージを表示する メソッドでメッセージを表示します。
  3. 場面遷移:選択肢情報に基づいて次の場面を設定します。選択肢に名称がない場合は、自動的に次の場面に進みます。
  4. 選択肢の表示:コマンドウィンドウを表示する メソッドで選択肢をユーザーに提示します。

4. 画像を表示する: 画像を表示する メソッド

画面に画像を表示するための処理を行います。

  • 画像情報の確認:現在の場面情報に画像情報がない場合は何もしません。
  • スプライトの作成:必要に応じて新しいスプライト(画像要素)を作成します。
  • 表示メソッド:画像を表示する際の方法(フェードインや移動など)に従って処理します。
  • 非同期処理:Promise.all を利用して画像表示の処理が完了するのを待ちます。

5. スプライトの管理: スプライトを作成する メソッド

スプライトは、画像を画面上に表示するための単位です。

  • パラメータ:画像要素、座標 (x1, y1, x2, y2) を指定してスプライトを作成します。
  • レイヤー設定:スプライトを配置するレイヤーを管理します。

6. 画像表示メソッドの適用: 表示メソッドに従って画像を表示する メソッド

画像の表示方法を指定して実行します。

  • フェードイン/フェードアウト:画像を徐々に表示したり消したりします。
  • 移動して表示:画像を指定した位置に移動させながら表示します。
  • 座標計算:必要に応じて移動距離や座標を計算します。

7. メッセージ表示: メッセージを表示する メソッド

場面のメッセージを画面に表示します。

  • メッセージウィンドウの表示:メッセージウィンドウを表示します。
  • メッセージ内容の更新:現在の場面のメッセージを設定して表示します。

8. 選択肢の表示: コマンドウィンドウを表示する メソッド

選択肢情報リストに基づいて選択肢を表示します。

  • 選択肢の取得:ユーザーが選んだ選択肢のIDを取得します。
  • 場面遷移:選択肢に応じて次の場面情報を設定します。

sample.js - サンプルクラス

このプログラムでは、次のような流れで「場面」を管理します:

  1. 必要な画像やレイヤー、ウィンドウを準備。
  2. 「場面」の情報(テキスト、画像、選択肢など)を管理する。
  3. 実際に選択肢を選びながら場面を遷移する仕組みを実現。
import { 画像管理クラス } from './imageManager.js'; // クラスをインポート
import { レイヤークラス } from './layer.js'; // クラスをインポート
import { ループ } from './loop.js'; // クラスをインポート
import { メッセージウィンドウクラス } from './messageWindow.js'; // クラスをインポート
import { コマンドウィンドウクラス } from './commandWindow.js'; // クラスをインポート
import { Data } from './data.js'; // クラスをインポート
import { 場面情報クラス } from './scene.js'; // クラスをインポート
import { 場面遷移 } from './sceneTransition.js'; // クラスをインポート

export class サンプル {
    static 遷移データリスト = [
        ["スタート", , [["町の広場へ行く",]],]
        , ["町の広場へ行く", "プレイヤーは町の広場に来た<w>10</w>", [["町の広場",]], [["背景01", , , , , , , "フェードアウトする", 1, ,], ["背景01", "レイヤー1", "町", 0, 0, 640, 640, "フェードインする", 1, ,], ["人物01", , , , , , , "移動してフェードアウトする", -20, 0, 1], ["人物01", "レイヤー2", "プレイヤー", 0, 0, 640, 640, "移動してフェードインする", -20, 0, 1]]]
        , ["町の広場", "どうする?", [["話す", "話す"], ["持物を見る", "持物を見る"], ["移動する", "移動する"]],]
        , ["話す", "話が聞けそうな人は誰もいない<w>10</w>", [["町の広場",]],]
        , ["持物を見る", "今のところ、何も持っていない<w>10</w>", [["町の広場",]],]
        , ["移動する", "どこへ行く?", [["町の広場", "やめる"], ["武器屋へ行く", "武器屋へ行く"], ["防具屋へ行く", "防具屋へ行く"], ["道具屋へ行く", "道具屋へ行く"], ["宿屋へ行く", "宿屋へ行く"], ["町を出る", "町を出る"]],]
        , ["武器屋へ行く", "武器屋は開いていないようだ<w>10</w>", [["町の広場",]],]
        , ["防具屋へ行く", "防具屋は開いていないようだ<w>10</w>", [["町の広場",]],]
        , ["道具屋へ行く", "道具屋は開いていないようだ<w>10</w>", [["町の広場",]],]
        , ["宿屋へ行く", "プレイヤーは宿屋に来た<w>10</w>", [["宿屋01",]], [["背景01", , , , , , , "フェードアウトする", 1, ,], ["背景01", "レイヤー1", "宿屋", 0, 0, 640, 640, "フェードインする", 1, ,], ["人物01", , , , , , , "移動してフェードアウトする", 20, 0, 1]]]
        , ["宿屋01", "受付:<br>いらっしゃいませ<br>一泊100カーネですが<br>お泊りになりますか?", [["宿屋02", "泊まる"], ["宿屋05", "やめる"]], [["人物01", "レイヤー2", "受付", 0, 0, 640, 640, "移動してフェードインする", 20, 0, 1]]]
        , ["宿屋02", "受付:<br>それではお休みください<w>10</w>", [["宿屋03",]],]
        , ["宿屋03", "プレイヤーはしばし眠りについた<w>10</w>", [["宿屋04",]], [["背景01", , , , , , , "フェードアウトする", 1, ,], ["人物01", , , , , , , "移動してフェードアウトする", -20, 0, 1], ["人物01", "レイヤー2", "プレイヤー", 0, 0, 640, 640, "移動してフェードインする", -20, 0, 1]]]
        , ["宿屋04", "受付:<br>お早うございます<br>またのお越しをお待ちしております<w>10</w>", [["町の広場へ行く",]], [["背景01", , , , , , , "フェードインする", 1, ,], ["人物01", , , , , , , "移動してフェードアウトする", 20, 0, 1], ["人物01", "レイヤー2", "受付", 0, 0, 640, 640, "移動してフェードインする", 20, 0, 1]]]
        , ["宿屋05", "受付:<br>やめるのですね<br>またのお越しをお待ちしております<w>10</w>", [["町の広場へ行く",]],]
        , ["町を出る", "まだ装備が整っていない<w>10</w>", [["町の広場",]],]];

    static async main() {
        await サンプル.使用する画像を読み込む();
        サンプル.ループ処理を作成する();
        サンプル.メッセージウィンドウを作成する();
        サンプル.コマンドウィンドウを作成する();
        サンプル.レイヤーを作成する();
        サンプル.場面情報管理を作成する();
        場面遷移.開始する();
    }

    static async 使用する画像を読み込む() {
        const 画像情報リスト = [
            { id: '町', src: 'img/bg02.jpg' },
            { id: '宿屋', src: 'img/bg03.jpg' },
            { id: 'プレイヤー', src: 'img/chara01.png' },
            { id: '受付', src: 'img/chara03.png' }
        ];
        場面遷移.画像管理 = new 画像管理クラス();
        await 場面遷移.画像管理.画像を読み込む(画像情報リスト);
    }

    static ループ処理を作成する() {
        ループ.初期化する();
        ループ.開始する();
    }

    static メッセージウィンドウを作成する() {
        場面遷移.メッセージウィンドウ = new メッセージウィンドウクラス(".スクリーン", ".メッセージウィンドウ");
        Data.オブジェクト管理["サンプル"] = サンプル;
    }

    static コマンドウィンドウを作成する() {
        場面遷移.コマンドウィンドウ = new コマンドウィンドウクラス(".スクリーン", ".コマンドウィンドウ");
    }

    static レイヤーを作成する() {
        場面遷移.レイヤー管理["レイヤー1"] = new レイヤークラス('canvas.レイヤー1'); // 背景用のレイヤ
        場面遷移.レイヤー管理.レイヤー1.描画処理をループ処理に追加する();
        場面遷移.レイヤー管理["レイヤー2"] = new レイヤークラス('canvas.レイヤー2'); // 前景用のレイヤ
        場面遷移.レイヤー管理.レイヤー2.描画処理をループ処理に追加する();
    }

    static 場面情報管理を作成する() {
        サンプル.遷移データリスト.forEach(x => 場面遷移.場面情報管理[x[0]] = new 場面情報クラス(x));
        場面遷移.場面情報 = 場面遷移.場面情報管理['スタート'];
    }
}

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

1. 必要なクラスのインポート

最初に、import 文で外部ファイルに定義されたクラスを読み込んでいます。各クラスの役割は以下の通りです:

  • 画像管理クラス:使用する画像を読み込み、管理する。
  • レイヤークラス:背景やキャラクターなどの表示エリア(レイヤー)を作成する。
  • ループクラス:処理を繰り返すための仕組み(主にアニメーション)。
  • メッセージウィンドウクラス:メッセージを表示するウィンドウを制御。
  • コマンドウィンドウクラス:選択肢(コマンド)を表示するウィンドウ。
  • 場面情報クラス:各場面のデータ(テキストや画像など)を管理。
  • 場面遷移:現在の場面を変更してストーリーを進める。

2. サンプルクラスの役割

サンプルクラスは、このプログラムのセットアップと管理を行います。

重要な静的プロパティ:

  • 遷移データリスト:各場面の情報(テキスト、画像、選択肢)を定義した配列です。
    • 例えば、"町の広場へ行く" という選択肢を選んだときのシーンが定義されています

3. サンプルクラスのメソッド

1. main メソッド

プログラムの全体的な流れを準備する主なメソッドです。以下の処理を行います。

  • 画像の準備(使用する画像を読み込む)。
  • レイヤーやウィンドウの作成。
  • 場面遷移.開始する を呼び出してプログラムを開始。

2. 使用する画像を読み込む メソッド

必要な画像(背景やキャラクター)を読み込む処理。

const 画像情報リスト = [
    { id: '町', src: 'img/bg02.jpg' },
    { id: '宿屋', src: 'img/bg03.jpg' },
    { id: 'プレイヤー', src: 'img/chara01.png' },
    { id: '受付', src: 'img/chara03.png' }
];

このように、画像の識別IDとファイルパスを指定して管理します

3. ループ処理を作成する メソッド

プログラム全体で利用する「繰り返し処理」を初期化し、開始します。

4. メッセージウィンドウを作成する メソッド

プレイヤーにメッセージを表示するためのウィンドウを用意します。

5. コマンドウィンドウを作成する メソッド

プレイヤーが選択肢を選べるウィンドウを作成します。

6. レイヤーを作成する メソッド

描画を行うための「レイヤー」を作成。画面の背景やキャラクターの表示を管理します。

7. 場面情報管理を作成する メソッド

遷移データリスト をもとに、各場面を管理するオブジェクトを作成します。

4. 各場面のデータ構造(遷移データリスト)

遷移データリスト には各場面の情報が含まれています。

["町の広場へ行く", "プレイヤーは町の広場に来た<w>10</w>", [["町の広場",]], [["背景01", "レイヤー1", "町", 0, 0, 640, 640, "フェードインする", 1, ,]]]

これは、「町の広場へ行く」という選択肢を選んだときに表示する情報を表します。

  • メッセージ:プレイヤーは町の広場に来た。
  • 選択肢情報:次に選べる選択肢。
  • 画像情報:背景やキャラクターの画像をどのように表示するか(例えば「フェードイン」)。

5. プログラムの開始

プログラムは addEventListener('load', サンプル.main) でスタート。

  • ページが読み込まれると、main が実行され、セットアップが行われます。

拍手[0回]

PR

コメント

P R

プロフィール

HN:
No Name Ninja
性別:
非公開