ベーマガ世代、ChatGPTとゼロからRPGを作る旅
第7章:マップ機能の高度化、メニューウィンドウ、会話のサウンド実装
マップ機能の高度化(当たり判定と階層化)
第7章で無事に複数マップ対応ができ、まともに村のマップを作り始めたら、マップチップが増えてきて、番号で当たり判定を指定するのがしんどくなってきた。
それにベッドのヘッドボードだとか、家をとんがり屋根にしたのでその裏側とか、主人公が歩くときに、手前に背景をかぶせたくなってきた。
あまりやるとレトロ感が薄れていってしまうけど、当たり判定と前景マップだけでも導入したい。
ということで、「マップ作成ツール」を相談していたチャットでChatGPTにお願いしてみた。
1)背景マップ(今の状態)
2)当たり判定のマップ(歩行不可エリアを×などで示す。論理上、歩行可能エリアは0、歩行不可エリアは1とする。背景マップの上に透過度50%で重ねる)
3)前景マップ(一部透過した画像を背景マップの上に重ねて描く)
2、3については、表示/非表示を切り替えられる。
1、2、3のマップデータをダウンロードできるようにする。
レイヤー機能を追加しておいたよ!以下の通り拡張された:
✅ 実装内容:
1. 3レイヤー構成
• backgroundMap(背景)
• collisionMap(当たり判定:0=歩ける、1=歩けない)
• foregroundMap(前景)
2. 表示切り替え
• 当たり判定/前景レイヤーをチェックボックスで表示・非表示できる!
3. レイヤー別ダウンロード
• 背景/当たり判定/前景のマップを個別に .txt ファイルとして保存可能!
4. マップアップロード
• 全レイヤーをひとつの JSON にまとめて保存・読み込みOK!
少し画面がごちゃついてきて、どのレイヤーを操作するかの切り替えラジオボックスや、表示/非表示の切り替えなどで、表示バグが出たりで何度かやり取り。
が、無事にjsonが出力できるようになったので、今度はそれをゲーム作成側のチャットに持っていって見てもらう。
map_all_layers.txtにあるようなマップファイルを読み込めるようにして、背景・前景・当たり判定処理をこのRPGに導入したいんだけど、できるかな?
よし、いいぞ。
ただ、前景はほとんどマップチップが指定されておらず、nullという文字がそのまま入っているので、見た目がずれて見にくいのがちょっと気になっていた。
先に一点だけ確認。マップデータでnullと表示されたところを空文字というか、削除しても大丈夫? 可能なら、マップデータを短くしたいんだけど。
マップデータを軽量化・可読性向上のために "null" の代わりに「空」や削除をしたくなる気持ち、めちゃくちゃわかります!
""だと位置ずれも少なく見やすそうなので、採用することにした。
以下のような3層のマップファイルでRPGのマップ実装を進めてもらえるかな
const maps={}; maps["home"] = {
"background": [ ["7","5","5","5","5","5","5","8"],
["6","0","0","2","0","0","0","3"],
["6","22","10","10","10","24","10","3"],
["6","10","10","10","10","10","10","3"],
["6","10","10","10","11","12","13","3"],
["6","10","10","10","10","10","10","3"],
["6","10","10","10","10","10","10","3"],
["9","4","4","4","4","4","4","19"] ], v
"foreground": [ ["","","","","","","",""],
["","","","","","14","",""],
["","","","","","","",""],
["","","","","","","",""],
["","","","","","","",""],
["","18","15","","","","",""],
["","","","","","","",""],
["","","","","","","",""] ],
"collision": [ ["1","1","1","1","1","1","1","1"],
["1","1","0","0","0","1","0","1"],
["1","0","0","0","0","1","0","1"],
["1","0","0","0","0","0","0","1"],
["1","0","0","0","1","1","1","1"],
["1","0","1","0","0","0","0","1"],
["1","0","0","0","0","0","0","1"],
["1","1","1","1","1","1","1","1"] ] }
これにて、当たり判定、前景マップを搭載した主人公のマイルームができあがった!
もちろんゲーム側もあわせて修正。
マップをどんどん広げる基礎が整った。
▲ 当たり判定、前景まで対応した強化版マップツール……!
メニューウィンドウを作ろう
メッセージウィンドウに加え、RPGに欠かせないのがメニューウィンドウだ。
プレイヤーのステータスを見たり、アイテムを使ったり、設定をいじったり、これがないと始まらない。
ステータスメニューを実装するにあたり、そもそもどんなステータスを入れるか考えておかないとならない。
体力(ライフやHPが数字で表されるのがピンとこず、スタミナとすることにした)、攻撃力、防御力、すばやさなどは当然入れるとして、今回は徳を貯めるみたいなシステムを考えていた。
<徳>はどんなパラメータ名にするか。こんな時こそ、言語の天才ChatGPTに聞いてみるべし……!
”徳”を意味する英単語はある?
もし「徳」をステータスとして使いたい場合:
• Virtue:美徳の指標(Ultimaシリーズでも使われていた)
• Karma:善行・悪行を加算して判定するようなカルマ値(やや宗教的)
• Honor(名誉):武士道的な「徳」に近い
• Spirit や Wisdom:ややスピリチュアル/知恵寄り
うーん、この中だったら……やっぱりカルマだろうか。
カルマという単語には昔からなじみがある。
なんせ、生まれて初めて手に入れたゲーム「ロマンシア」に登場したパラメータだからだ。
パソコン、MSX2にバンドリングされていて(もしくは店のおまけ?)、父が「これがついてきたぞ」と誇らしげに見せてくれ、私も初めて手にする自分のゲームカートリッジに興奮を隠せなかったわけだが。
これがまあ……とんでもない死にゲーだった。
スタートしてまっすぐ右に歩いていくと、速攻で池で溺れて死亡(死ぬのは速攻でもなく、水底でじわじわライフが減っていくエグい仕様)
池をやり過ごした後(ジャンプすると泳げるらしい)、ジャンプで溝を渡りきれずマグマに落ちて死亡(落ちたのは私のプレーが下手なだけだが、マグマの中を落ち続け、プレイヤーのライフが減って焼け死ぬまで見守るという鬼畜仕様)
さらに、雲に乗っているおじいさんに話しかけたら、いきなり殺され、天使の輪がつき羽が生えて飛んでいき死亡(分かるかこんなもん……!)
しかも、池には実は潜らないと先に進めず、そこにある杭を特定の順番で押すことで秘密の通路が開く仕様(窒息して溺れ死ぬ前に急いで)。
また、瞬殺してくるおじいさんには、ある条件を満たして話しかけると「天国ステージ」に行けて、ここでも必須アイテムが手に入る仕様。
ぷちアクションでのバトルもあるが、ロード・セーブみたいな気の利いた機能はない。
もはやゲーム雑誌などの攻略情報なくして解くことが不可能と思われる、とんでもない鬼ゲーだった。
(異常な難易度はともかく、ファンタジーな世界観やもろもろの設定、とっても好きでワクワクしたけどね!)
じゃあ、プレイやー(原文ママ)に以下のパラメータを設定しよう。右にあるのは初期値。
レベルごとに上限が決まっている。
===
レベル:1
スタミナ:10
攻撃力:10
防御力:10
魔力:10
カルマ:0
すばやさ:5
経験値:0
ChatGPTが作ってくれたひな形に、ゴールド、アイテム、装備を足してもらう。
なぜかアイテムは配列で、装備はオブジェクトで、以下のような構造を提案してくれた。
const playerStatus = {
level: 1,
stamina: 10,
attack: 10,
defense: 10,
magic: 10,
karma: 0,
speed: 5,
exp: 0,
gold: 0,
items: [],
equipment: {}
};
ESCキーを押すと左上にウィンドウが出て、ステータス確認、持ち物の表示、装備の表示ができるようにしたい
追加完了しました!
あっという間にメニューウィンドウができあがった。
イベントスクリプトからステータスの加減算ができるように処理追加を依頼。
ここからメニューを拡張していく。
・各ウィンドウ内の文字サイズを統一したい
・ESCでウィンドウを開けるのは、"normal"の時だけ(イベント中は押せない)
・ESCを押した時、左上に選択ウィンドウが現れ、「ステータス」「所持品」「装備」が選択できるようにしたい。このウィンドウが出ている間は、プレイヤーの移動など他の処理は行われない
・イベントと同じく青い背景をカーソルとして上下キーで移動、エンターで決定
・ステータスを選ぶと、左上のウィンドウにステータス一覧が表示される。「戻る」だけの選択肢があり、Enterを押すと選択ウィンドウに戻る
・「所持品」を選ぶと、所持品一覧が表示される。所持品が多いときは、左右キーで続きの画面と切り替えられる。何も持っていないときは「戻る」だけが表示される。Enterを押すと「使う」「捨てる」が選べる
・「装備」を選ぶと装備一覧が確認できる。装備品が多いときは、左右キーで続きの画面と切り替えられる。何も持っていないときは「戻る」だけが表示される。現在装備中の装備には装備マークがつく。Enterを押すと、「装備する」「装備を外す」「捨てる」が選べる
ChatGPTが始めに作ってくれたメニューはcanvas内ではなくdivタグで表示されていたようで、ブルーの選択肢も半透過になっていて統一感がない。
そこで実装方法とUI的なトンマナをmessageWindowと合わせるよう依頼した。
また、メニューを表示中にESCを押すとメニューが消えるが、離すとまた現れるというバグが発生し、キーイベントをチェックし直すことに。
div#menu-windowの使用をやめ、drawMenuWindow() 関数でcanvas上に直接メニューを描画させるのにも何度かやり取りが必要だった。
canvasに何度もソースを貼りなおし、どうにかこうにかバグを直した。
それにしても、メニューウィンドウ、ステータスウィンドウ、アイテムウィンドウ、と一個ずつ実装していくのはあまりにも骨が折れるので処理をまとめたい。
メニューウィンドウで選択させる機能がたくさんあるから、まとめたらシンプルになると思うけどどう? クラスと関数とどっちがいいかな?
🎯 おすすめ
今回はメニューウィンドウの種類が今後増える可能性が高く、選択肢の操作、描画、サブメニュー(例: 「装備を外す」「使う」など)を扱う必要があるので、クラスベースの方が拡張性・保守性に優れています。
必要であれば、MenuWindow クラスのひな型を今すぐ導入します!やってみましょうか?
こうしてできあがったMenuWindowクラス。
ページネーションの実装は、AIが瞬殺してくれて助かった。
同じアイテムを「やくそう 3個」みたいにひとまとめにするgetGroupedItemsも瞬く間に作りあげてくれた。
メッセージリファクタ&セリフに「ててててっ」と音をつける!
既存の drawMessageWindow()もMessageWindowクラスにして、「はい、いいえ」等の選択肢をメッセージウィンドウ上に表示して選べるように進めたのだけど、やはりバグが頻発してgameStateの状態、keydownの処理など見直すのに一苦労。
気がつけばグローバルと関数内でふたつイベントリスナーができているし。
どうにもバグを生み出しやすそうな構造なので、グローバルのイベントリスナーに統合するリファクタを行ってもらった。
だいぶすっきりしたね。 イベントで一行読むたびにインスタンスを作っているけど、内部のテキストを書き換えても、visibleのオンオフで表示を切り替えてもいいのかも。 (セリフが消えた時はvisible=falseにする) どっちが効率よいかな。
• イベントの数が少ない/開発段階では現状のままでもOK
• 長編イベント/会話が多くなる/軽量化が必要になったら使い回しに切り替えるのが◎
• 今のMessageWindowがシンプルな分、使い回しにもすぐ対応できます
こんなのはどう?
・messageWindowはグローバルでのインスタンスにしてしまう
・messageWindow.setText()とかのfunctionを作って、テキストがセットされたら表示を開始する(ここにコールバック関数を入れる)
ChatGPTが即座にこの方式でコードを書き直してくれ、これでだいぶ使い勝手のよいmessageWindowができた。
さて、ここでかねてからのロマンがむくむくと頭をもたげてきた。
キャラクターのセリフを表示するとき、ドラクエみたいに文字が順次でてきて、ててててっと音が鳴るようにしたいんだよね
今までのやり取りで、ChatGPTの察知能力の高さを存分に感じ取っていたので、限りなくラフなお願いを書いてみたものの……
「文字が順次出て」「ててててっと音が鳴る」
こんな依頼の仕方で通じるのだろうか。はたして……?
ドラクエ風に「文字が一文字ずつ表示されて、ててててっ♪という音が鳴る」仕様を実装しました!
🎵 効果音はData URIで簡易音を埋め込んでいます(必要に応じて差し替え可)。
通じたあぁーっ!
それどころか、効果音まで作ってくれたの? すごすぎる!
む……待てよ、音が出ないぞ。
以下のエラーが出たんだけど、Edgeだとこのオーディオ形式がサポートされてなかったりする?
Uncaught (in promise) NotSupportedError: The element has no supported sources.
はい、そのエラーは、Edge などの一部ブラウザが 指定された audio データ URI を音声として正しく解釈できなかった 場合に発生します。
✅ 解決策
1. 正式な audio ファイルを用意して使用する方法(推奨)
2. AudioContext を使って短いピッ音を合成する方法(ファイル不要)
案2のほうの合成音声を出すソースも作ってくれた。
function playTypeSound() {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = "square";
osc.frequency.setValueAtTime(800, ctx.currentTime); // 高めの音
gain.gain.setValueAtTime(0.1, ctx.currentTime); // 音量
osc.connect(gain).connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.05); // 50ms
}
素晴らしい。キャラごとに音声の高さを変えたかったんだけど、これなら簡単に対応できそう。
わざわざaudioファイルとか準備したくないし、これは案2で実装でしょう。
音を出す型自体はしっかりできたのだが、メッセージボックス内で動かしてみると選択肢を表示した後だけ音が鳴らないなど、お決まりの「デバッグしまくり」ルート発生。
ChatGPTに相談しつつ、最後はソースを追いかけて頑張って自力で直す。
全体を見直してやっと直せたよ。 正しく動くようになったけど、メッセージ表示を繰り返しているうちに、ある瞬間からタイプ音が聞こえなくなってしまう。 サウンドのバッファの問題とかかな?
音がある瞬間から聞こえなくなるという症状、詳しい情報ありがとう!
おそらくこれは Web Audio API の制限や、AudioContext が頻繁に生成されすぎて破棄されずリソース不足になっている ことが原因です。
なるほど。それならmessageWindowクラスと一緒で、毎度生成せずにグローバルで生成&使いまわし方式にしよう。
拾えるアイテム、毒の沼
いったんイベントのバグフィックスがある程度完了したので、ソースを貼りなおしたよ。
NPCとイベントのところはできたけど、宝箱や地面のキラキラ(アイテム)を拾えるようにするにはどうしたらいいかな。
セリフの「ててててっ」が通じるぐらいだから、地面のキラキラアイテムも、多分これで通じるでしょう。
フィールド上に「やくそう」や「ゴールド」などのアイテムを置いて、プレイヤーがそこに移動すると自動で取得できるようになりました。画面上には以下のように表示されます:
• やくそう(アイテム) → 金色の四角
• ゴールド → 白色の四角
取得すると、メッセージウィンドウに表示されて、プレイヤーの所持品またはゴールドに加算されます。
通じました。
ワンステップでなんとも手際のよい仮実装。
本当に応用力が高いねえ。
ありがとう、うまくいったよ。
・フィールド上に毒の沼みたいのがあって、乗ると体力が3減る
・やくそうを使ったら体力が10回復する というのも入れてみたいな
■ 毒の沼
• プレイヤーが毒の沼に乗ると、スタミナが3減少します。
• 該当の座標:(10,10)と(11,10)(map1)
■ 回復アイテム(やくそう)
• やくそうを使うと、スタミナが10回復します。
• 上限を超えて回復しないよう調整済み。
やくそう使用時にメッセージが表示されないバグがあったがすぐに解消。
勢いで毒の沼を作ったはいいけど、これ、いるかな? どこで使おうかな……?
▲ 自宅を出たら、なんと目の前に適当に配置した毒の沼が広がってました……!
ともあれ、プレイヤーのステータス確認、アイテム使用とRPGの必須要素がそろったゾ。
次は戦闘シーンの実装だ!