ベーマガ世代、ChatGPTとゼロからRPGを作る旅

第9章:フラグ判定、売買スクリプト、開発環境整備

イベントでのフラグ判定

イベント機能を拡張して、アイテムを持っているかどうかなどフラグ判定でイベントの進行を変えられるようにした。
たとえば、以下のように書くと、"gotSpecialItem"フラグがtrueなら2へ、falseなら4へ分岐できるようになった。


  "1": { type: "if", flag: "gotSpecialItem", then: "2", else: "3" },
"2": { type: "message", text: "おお、そのアイテムを持っているのか!", next: "4" },
"3": { type: "message", text: "まだそのアイテムは持っていないようだな。", next: "4" },
"4": { type: "end" }

フラグ判定についてはこれで無事動くようになったのだが、類似の機能として以下のように数値比較を入れてみたところ、うまく判定されない。


  "5": { type: "if", check: playerStatus.gold >= 10, then: 6 , else: 8 },
  

ChatGPTにうまくいかない理由を聞いてみた。

AIアイコン

この playerStatus.gold >= 10 は イベント定義時点で一度評価されて、結果(true/false)が固定されてしまう んだ。
🔧 解決策:checkを関数にする(または式文字列を評価)
✅ 方法①:check を関数にする
✅ 方法②:check を文字列として eval() する

なるほど。
イベントスクリプトは、変数でなくconst checkScript = {...}のように定数になっているので、初期値で固定されてしまうんだね。
方法①の方式は以下。evalを使うより、こちらのほうが安全だというので採用。


    "5": { type: "if", check: () => playerStatus.gold >= 10, then: 6 , else: 8 },
  

    if (cmd.type === "if") {
let result = false;
if (typeof cmd.check === "function") {
result = cmd.check(); // 関数なら実行
} else {
result = !!cmd.check; // それ以外(true/false固定ならそのまま)
}
currentEventId = result ? cmd.then : cmd.else;
currentStep();
}

JavaScriptのアロー関数 ()=> みたいな書き方って、個人的になんとなくしっくりこないんだけど、慣れるより仕方ないね……

売買スクリプト

お店での売買については、以前作ったイベントスクリプトだと特定のアイテムしか買えなかったので拡張を行った。
まず、店ごとの販売・買取リストを準備する。


  const defaultShopItems = {
"やくそう": { price: 10, sell: 8 },
"小石": { price: 3, sell: 1 },
"ハンカチ": { price: 10, sell: 5 },
"鏡": { price: 30, sell: 20 }
};

もとはスクリプトの中でif文で分岐していたのだが、分かりやすいようにChatGPTにお願いして販売用の関数に切り出してもらった。
以下が売却用の関数。
ここでもメッセージウィンドウを使うため、イベントスクリプトを再利用している。


  function shopSellItem(itemData = defaultShopItems, returnScript = null, returnId = "1") {
const items = getGroupedItems(playerStatus.items);
if (items.length === 0) {
runScript({
"1": { type: "message", text: "売れるものがないようだね。", next: "2" },
"2": { type: "end" }
}, "1", returnScript, returnId);
return;
}
gameState = "eventmenu";
menuWindow = new MenuWindow(["売りたいアイテムを選んでくれ"], [...items, "戻る"], (selected) => {
if (selected === "戻る") {
if (returnScript) runScript(returnScript, returnId);
} else {
const itemName = selected.split(" ")[0];
if (playerStatus.items[itemName]) {
removeItem(itemName);
const price = itemData[itemName]?.sell || 1;
playerStatus.gold += price;
runScript({
"1": { type: "message", text: `${itemName}を売った! ${price}G受け取った`, next: "2" },
"2": { type: "end" }
}, "1", returnScript, returnId);
}
}
});
}

以下が購入用の関数。


  function shopBuyItem(itemData = defaultShopItems, returnScript = null, returnId = "1") {
const options = Object.entries(itemData).map(([name, info]) => `${name} ${info.price}G`).concat(["戻る"]);
changeGameState("eventmenu");
menuWindow = new MenuWindow(["買いたいアイテムを選んでくれ"], options, (selected) => {
if (selected === "戻る") {
if (returnScript) runScript(returnScript, returnId);
} else {
const itemName = selected.split(" ")[0];
const price = itemData[itemName]?.price || 99;
if (playerStatus.gold >= price) {
playerStatus.items[itemName] = (playerStatus.items[itemName] || 0) + 1;
playerStatus.gold -= price;
runScript({
"1": { type: "message", text: `${itemName}を買った!`, next: "2" },
"2": { type: "end" }
}, "1", returnScript, returnId);
} else {
runScript({
"1": { type: "message", text: `お金が足りないようだ!`, next: "2" },
"2": { type: "end" }
}, "1", returnScript, returnId);
}
}
menuWindow = null;
});

イベント内からは以下のように呼び出した。


  // 売る
{ type: "custom", func: () => shopSellItem(defaultShopItems, shopScript, "1") }
// 買う
{ type: "custom", func: () => shopBuyItem(defaultShopItems, shopScript, "1") }

ちなみにこちらの関数、装備しているアイテムが分かりやすいよう(装備中)の文字をくっつけたところうまく動かなくなった。
そこで末尾の(装備中)の文字を削ってから処理するように修正した。
「なんで表示用の日本語文字列で処理するの? 一意のID使いなさいよ」いう突っ込みが聞こえてくるようであるが、まあ行き当たりばったりで作るとこうなる。
わざわざ半角英数字のIDをつけなくても動作するので、このまま進める。

なお、上記の関数では引数として戻り先のスクリプトを渡しているが、これを動かすにはイベントキューでのPUSH/POPを無効にする必要があり、いったんオフにした。
(でも後でやっぱりイベントキューも必要になり、イベントシステムが複雑化していくにつれ、また直す羽目に。ビルド&スクラップ&ビルド)

ソースファイル分割

さて、増築に増築を重ねて開発を進めてきたが、そろそろソースが長すぎて辛くなってきた。
ChatGPTとソースをやり取りしているcanvasにもコピペしきれない。
プログラムをどこからどこまで、どういう風に切り出したらいいだろう。
ChatGPTに相談してみよう。

ユーザーアイコン

ソースが長くなっていてここのcanvasに貼る時エラーが出たから、サウンドやイベントの設定を書いているjsファイルを分けようと思うんだけど、何て名前がお勧め?

AIアイコン

おお〜完璧な整理のタイミングですね!🎉
JSファイルを分割して整理するの、めちゃくちゃナイスです!

分割案

なるほど、いい感じ。
名前づけのセンスもないので、地味に助かる。
提案してくれた案を参考にしつつ、ちょっとずつjsに切り出していくことにした。

マップデータも、ベタ打ちで定数を書きこんでいたけど、今後どんどん長くなるだろうし、そろそろ外部のjsonファイルを読み込むような形式に変えておくべきかもね。

開発環境を整える json外部化、ローカルWEBサーバ、Git導入

ユーザーアイコン

マップを以下のようにプログラムに直接書いているんだけど、{}範囲のjsonファイルから読みだして初期設定するにはどうしたらいいかな?
maps["myhome"]= {"map":"tileset_villageroom", "background":[[8,7,7,7,7,7,8,7,7,7,7,7,7,7,7,8],....}

AIアイコン

いいね!マップデータを外部JSONファイルに分けておくと整理しやすくなるし、マップの追加も簡単になります。


  const maps = {};

function loadMap(name, filePath) {
return fetch(filePath)
.then(response => response.json())
.then(data => {
maps[name] = data;
console.log(`マップ「${name}」を読み込みました。`);
})
.catch(err => console.error(`マップ「${name}」の読み込みに失敗`, err));
}

おっと、関数を作ってくれたのはいいけど、もともと第5章で作ってもらっていたマップ切り替えの関数名(loadMap)とバッティングしてるじゃないですか。
これだからソースが複雑になってくると嫌なんだよなあ。
しかし、ChatGPTも別の機能に対して同じ関数名を出してくるところを見ると、何かのテンプレに沿ってアドバイスしているわけでもなさそうだな。

ユーザーアイコン

今すでに同じ名前の関数が存在してるんだけど、既存の関数をsetCurrentMapかなんかにリネームしたほうがいいかな?

ChatGPTに同意してもらえたので、既存の関数側をリネームした。
なお、jsonファイルは以下のようにしてしておき、順次呼び出すようにした。


  const mapFiles = {
"myhome": "maps/myhome.json",
"map1": "maps/map1.json",
};
Promise.all(Object.entries(mapFiles).map(([key, path]) => loadMap(key, path)))
.then(() => {
console.log("すべてのマップを読み込み完了");
// 初期マップの描画など
});

読み込みは非同期で行われるらしく、ChatGPTは上記の通り、Promise.allを使って、マップをすべて読み込み終わってから初期マップの描画を入れるようなプログラムを渡してきた。
この//の部分だが、現時点では何も入れていない。
というのも、今回のプログラムでは、HTMLファイル側のbodyタグ内で以下のように全体初期化のinit()関数を呼び出すように作っているのだ。
<body onload="init();">
このinitが呼ばれた後でないと、画面の描画も始まらない。
そしてbody onloadイベントは、HTMLのすべてのリンク先のファイルをメモリに読み込み、べた書きされたjsも<すべて実行が終わってから>呼び出されるのである。

さて、jsonが無事呼ばれたかというと……エラーになった。
えっと……エラーの原因は、なんとなく検討がつくのだが……

ユーザーアイコン

敵のことはまたいろいろ相談したいんだけど、マップのjson読み込みでこんなエラーが出た。
WEBサーバにアップしないとダメとか?
===
rpgtest.html:53 Access to fetch at 'file://hogehoge...myhome.json' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome-extension, chrome-untrusted, data, edge, http, https, isolated-ap

AIアイコン

はい、そのエラーは**「CORS(クロスオリジン制限)」によるもので、file:// プロトコルで直接開いている HTML ファイルから、ローカルの JSON を fetch しようとするとブロックされる**のが原因です。

ですよねええ。
あなた「神谷翔のサイバー事件簿」ってサイバーセキュリティ系のミステリ書いてたでしょ、って怒られそうですな。

しかし、実行の都度FTPでWebサーバにアップするなんてやっていられない。
わざわざローカルPC上にWebサーバたてるのもめんどくさいなぁ……
……といっても、json外部化を避けて通るのはきつすぎるし、ここは観念してWEBサーバを立てるしかないかな……
ChatGTPのお勧めによると……

開発テスト環境お勧め度

Pythonは入れてるけど、JavaScriptで開発しているのにいちいちPython開いてサーバを立ち上げるのは嫌だ。
ちょうどvscodeで開発を進めていたので、お勧めされたLive Serverを入れることにした。

いざ使ってみると、めちゃくちゃ使い勝手がよかった。
vscode上でhtmlファイルを開き、画面の右下に出てくる"Go Live"を押すだけでブラウザが起動。
URLも勝手に入った状態で開くので、ブラウザに127.0.0.1……などと打ち込む必要もない。
しかも、関連ファイルを書き換えるたびに読み込んでくれるので、その都度サーバキャッシュを消して……みたいな手間も不要だった。
むしろフォルダから立ち上げた時のほうが、リロードしなければならない分手間かも……というぐらい。
これを機に、もろもろ環境を整えたほうがよさそうだな。

ユーザーアイコン

イベント周りって結構センシティブで、ソースちょっと書き換えて動かなくなると、前に戻したりが面倒なんだけど、githubみたいなものも入れたほうがいいかな。

うっかりGitHub(=ソース共有・管理サイト)と書いてしまったが、ここで導入したかったのはバージョン管理システムである「Git」。
間違えてソースを書き換えてしまっても前に戻せる安心感が欲しくなってきたんだよね。

AIアイコン

めちゃくちゃいい着眼点です!
イベント周りのような繊細な処理を扱うプロジェクトでは、Git/GitHubなどのバージョン 管理を導入するのは 大正解 です。
実は思っているほど難しくないです。

ユーザーアイコン

Windowsでvscode使ってる場合はどうしたらいい?

AIアイコン

Windows + VSCode 環境での Git 導入はめちゃくちゃスムーズにできます!
以下の手順に沿えば、5分で導入&初回コミットまで可能です💪

AIアイコン

✅ 1. Gitをインストール(初回だけ)
まだインストールしていなければ、以下から Git を入れてください:
👉 https://git-scm.com/download/win
「次へ」連打でOK(基本はデフォルト設定のままで問題なし)
インストールが終わったら、一度PCを再起動すると安心

AIアイコン

✅ 2. VSCode で Git を有効にする
🎯 任意のプロジェクトフォルダで操作:
VSCodeでプロジェクトを開く(例: C:\Users\あなた\Documents\MyRPG)
左のサイドバーから「ソース管理(🟦マーク)」をクリック
「リポジトリを初期化する」をクリック
これだけで .git フォルダが作られて、Gitの管理下に入りました!

AIアイコン

✅ 3. 最初のコミットを作成 1. 上に「変更されたファイル」がリストされている 2. すべてをステージ(+マーク)する 3. 下の入力欄にコメントを書く(例:「初回コミット」) 4. 「✔(チェックマーク)」ボタンを押してコミット! これで履歴が保存されました🎉

至れりつくせりの説明。
環境設定って苦手なんだけど、「5分で導入できる」「「次へ」連打でOK」というアドバイスは絶妙。やる気が出る。
ということで、さっそく導入してみた。

結果……めちゃくちゃ便利になりました!
前のソースと比較できるのはもちろんなのだが、書き換えている最中に「書き換えた箇所」が分かるようになっているのが強い。
ソースを書き換える時、念のためコメントアウトして前のソースを残しておいたり、別ファイルに保存したりしていたのだが、「あれ、どこ行ったっけ?」「どこ直したっけ?」とだいぶ混乱し始めていたのだ。
いやぁ、もっと早く導入しておけばよかった。
複数人開発の時は必須だと思っていたけど、個人開発でもある程度の規模のソースを書くときには入れておいたほうがよそそうだ。

vscodeのソース

▲ 変更したソースの左に青いバーが出て、ファイル名も色が変わる。

vscodeのソース比較画面

▲ 古いソースと比較してマージできるのはもちろんのこと、コミット時にコメントが残せるので、いつ何をしたかも追いやすい。

次回は、前回見送った戦闘シーンでの「照準システム」。
今度こそちゃんと実装します!