JavaScriptでゲームを作る!
前回の「選択肢を表示する方法」のコードでは、選択肢毎にコードを書く必要がありif文だらけのコードになっていました。
今回はこの問題を解消する方法を記します。
以下の例では、見た目では分かりませんが、画像、メッセージ、選択肢情報といったパラメータを変えただけで、全て共通のコードを実行しています。
そのため、どれだけ画像やメッセージ、選択肢が増えても、if文等のコードが増えることはありません。
ファイルとそれを入れるフォルダーを以下のような構成します。
※背景と人物の画像はBing Image Creatorで生成しています(AI生成)。
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)));
}
}
このクラスは「選択肢」を管理するためのものです。
このクラスを使うことで、選択肢のデータを簡単に扱えるようになります。
このクラスは、プログラム内で表示する画像の情報を管理します。
このクラスにより、画像を座標やレイヤーに基づいて管理できます。
このクラスは、「場面」を管理するためのメインクラスです。
このクラスを利用すると、場面に対応するメッセージ、選択肢、画像を一括して扱えます。
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];
}
}
このクラスは、プログラムの場面を管理するためのメインクラスです。例えば、特定の画像やメッセージを表示したり、場面を切り替えたりします。
場面遷移クラスでは、多くのデータを静的プロパティで管理しています。
このメソッドは、場面遷移の主要な処理を行います。
処理手順:
画面に画像を表示するための処理を行います。
スプライトは、画像を画面上に表示するための単位です。
画像の表示方法を指定して実行します。
場面のメッセージを画面に表示します。
選択肢情報リストに基づいて選択肢を表示します。
このプログラムでは、次のような流れで「場面」を管理します:
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);
最初に、import 文で外部ファイルに定義されたクラスを読み込んでいます。各クラスの役割は以下の通りです:
サンプルクラスは、このプログラムのセットアップと管理を行います。
重要な静的プロパティ:
プログラムの全体的な流れを準備する主なメソッドです。以下の処理を行います。
必要な画像(背景やキャラクター)を読み込む処理。
const 画像情報リスト = [
{ id: '町', src: 'img/bg02.jpg' },
{ id: '宿屋', src: 'img/bg03.jpg' },
{ id: 'プレイヤー', src: 'img/chara01.png' },
{ id: '受付', src: 'img/chara03.png' }
];
このように、画像の識別IDとファイルパスを指定して管理します
プログラム全体で利用する「繰り返し処理」を初期化し、開始します。
プレイヤーにメッセージを表示するためのウィンドウを用意します。
プレイヤーが選択肢を選べるウィンドウを作成します。
描画を行うための「レイヤー」を作成。画面の背景やキャラクターの表示を管理します。
遷移データリスト をもとに、各場面を管理するオブジェクトを作成します。
遷移データリスト には各場面の情報が含まれています。
["町の広場へ行く", "プレイヤーは町の広場に来た<w>10</w>", [["町の広場",]], [["背景01", "レイヤー1", "町", 0, 0, 640, 640, "フェードインする", 1, ,]]]
これは、「町の広場へ行く」という選択肢を選んだときに表示する情報を表します。
プログラムは addEventListener('load', サンプル.main) でスタート。