はじめに
Claude Code + MCP + Godot 4.xで脱出ゲームを作る連載の第13回です。
- 第1回:環境構築とプロジェクト作成
- 第2回:Claude Code + godot-mcpのセットアップ
- 第3回:Claude CodeとMCPでスプラッシュ画面を実装する
- 第4回:Claude CodeとMCPでタイトル画面を実装してシーンを遷移する
- 第5回:背景の特定領域をタップしてフェード遷移する
- 第6回:再利用可能なモーダルポップアップを作る
- 第7回:スプライトシートを使ったダイヤル錠ギミックを実装する
- 第8回:戻るボタン・正解判定・開封演出・ローカル保存を実装する
- 第9回:アイテム管理:シーン常駐のインベントリシステムを実装する
- 第10回:アイテムで扉を開ける
- 第11回:BGM・効果音を実装する
- 第12回:左右移動・時計ギミック・アイテム合成・パーティクルを実装する
- 第13回:暖炉の金庫ギミックとバッドエンディング演出を実装する(今回)
前回までにホール周辺の移動・時計ギミック・アイテム合成・炎の演出が完成しました。今回は暖炉に火を灯すと出現する「金庫」、そして金庫を開けて手に入るアイテムを使うと辿り着くバッドエンディングまでを実装します。
今回は一連の実装をClaude Codeへのプロンプトだけで進めた記録です。「AIコーディングエージェントに機能追加を任せると、どのくらいの粒度の指示でどこまで実装が進むのか」の実例として、プロンプトと実際の実装内容をそのまま記録しています。
この記事で作るもの
暖炉点火状態に連動した背景の切り替え、金庫・額縁のタップ領域とズームシーンへの遷移を実装します。既存の時計ギミック・額縁ギミックを参考にした金庫のダイヤル錠(回転式)、金庫から入手できる新規アイテム「怪しい装置」、アイテム使用→ホワイトフラッシュ→バッドエンディングの演出一式も扱い、実行時に出たGDScriptの型推論エラーの修正も記録します。
前提条件
Claude Code + godot-mcpのセットアップ完了(第2回参照)、scripts/global.gd(オートロード)に FIRE_LIT・インベントリ・使用済み管理が実装済み(第10・12回参照)、scenes/room_fireplace.tscn・room_fireplace_closeup.tscn が実装済み(第12回参照)、scenes/room_clock_gimmick.tscn(時計ギミック)が実装済み(第12回参照)、scenes/components/item_popup.tscn が実装済み(第9回参照)という前提で進めます。
設計方針
各部屋は「暖炉」「時計」「額縁」のように独立したギミックが並列に存在しており、既存ギミックの実装パターンを参考にしながら新規ギミックを実装するという進め方をしています。今回のプロンプトにも「時計のギミックの処理を参考に」といった、既存実装への参照を明示した指示が含まれており、これによってプロジェクト全体で一貫したコードスタイルを保てています。
1. 暖炉に火を灯したら背景を切り替える
暖炉に火が入っている状態になったら、bg_stage_005_b.png、近接はbg_stage_005_2_2.pngを背景として表示
最初の指示は非常にシンプルな「状態に応じた背景差し替え」です。Global.FIRE_LIT という永続フラグ(第12回でセーブデータにも保存済み)がすでに存在していたため、これを参照します。room_fireplace.gd(部屋全体)と room_fireplace_closeup.gd(暖炉近接)の双方で、_ready() 時に FIRE_LIT が true なら preload しておいた点火後テクスチャに差し替えるようにしました。近接シーンではランタンでその場で点火した瞬間にも即座に背景を切り替える必要があったため、点火処理(_on_hearth_area_gui_input)の中でも同じ切り替えを実行するよう修正しています。
状態に応じた見た目の切り替えは、シーンを開いた瞬間(_ready())だけでなく、状態が変化した瞬間にも別途反映が必要でした。プロンプト自体にはこの点が明記されていませんでしたが、既存コードを読み込むことでこの抜け漏れに気づき、両方に対応しています。
2. 金庫と額縁のタップ領域を追加し、金庫ズームシーンへ遷移
暖炉の近接で、暖炉に火が入っている状態の時、金庫と額縁領域をタップしたら、bg_stage_005_4.pngを背景にしたシーンに遷移。
この指示の時点では「金庫」「額縁」の絵はまだ画面上のどこにあるか指定されていません。そのため、まず暖炉近接シーンの背景画像(bg_stage_005_2_2.png)と、遷移先の背景画像(bg_stage_005_4.png)を実際に画像として読み込み、額縁とダイヤル金庫が描かれている位置を目視で確認しています。
第12回の room_clock.tscn → room_clock_zoom.tscn のズーム遷移パターン(ClockArea → _go_to())をそのまま踏襲し、PictureFrameArea と SafeArea の2つの Control を新規配置します。
# scripts/room_fireplace_closeup.gd(抜粋)
func _on_safe_area_gui_input(event: InputEvent) -> void:
if not Global.FIRE_LIT:
return
if not (event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT):
return
_go_to("res://scenes/room_fireplace_safe.tscn")
未点火時はタップ判定自体を無効化(if not Global.FIRE_LIT: return)しています。新規シーン room_fireplace_safe.tscn / room_fireplace_safe.gd は、既存の room_clock_zoom.tscn と同じ「背景+戻るボタンのみ」の最小構成で新規作成し、Global._SCENE_BGM(第11回)にも新シーンを登録しています。
「画像を実際に見て座標を判断する」フェーズが発生するのがこの手のゲーム実装タスクの特徴で、テキストの指示だけでは解決できない部分です。この連載でも第7回のダイヤル位置調整など、たびたび同じ確認作業が発生しています。
3. 金庫のダイヤル錠ギミック(ポップアップ)を実装
金庫近接で金庫をタップしたら、safeのbase.pngをベースにしたポップアップのギミックを表示。sage/dial.pngをbase.pngのダイヤルに被せるように表示。ダイヤルの左側にbtn_turn_left.png、右側にbtn_turn_right.pngのボタンをそれぞれ設置して、左側のボタンをタップすればdial.pngを左へ30度回転、右側のボタンをタップすればdial.pngを右へ30度回転させる。時計のギミックの処理を参考に、まとめられればまとめてください。左へ3回、右へ5回、左へ7回、右へ2回、が正解。正解したら、金庫近接シーンで金庫をタップすると、金庫が開くシーンに遷移して、bg_stage_005_5.pngを背景表示
今回の実装の中で最もロジックが複雑な部分です。指示の中に「時計のギミックの処理を参考に、まとめられればまとめてください」という既存実装への参照と抽象化の指示が含まれていたため、room_clock_gimmick.gd(第12回、ボタンを押すたびに _hour_count * 30.0 のようにカウント×角度で針を回転させる方式)、room_picture_frame_gimmick.gd(タップした値を配列に記録し、正解の配列と一致するかを判定する、額縁の4隅を順番にタップするギミック)、post.gd / popup.gd(第6・8回、「フラグが立っていればシーン遷移、立っていなければポップアップを開く」という分岐パターン)を読み込んでから設計しています。
これらを組み合わせて、以下のように実装しました。左ボタン=-1、右ボタン=1 という同じ値を回転角の符号にもタップ履歴の識別子にも使うことで、方向判定用の配列と回転角度計算を1つの値で表現し、コードの重複を削減しています。正解列 [左×3, 右×5, 左×7, 右×2] を [-1,-1,-1, 1,1,1,1,1, -1,-1,-1,-1,-1,-1,-1, 1,1] という配列に変換し、額縁ギミックと同じ「配列同士の比較」で正誤判定するようにしました。不正解時は額縁ギミックと同様、リセット音を鳴らして reload_current_scene() を呼び、正解時は Global.SAFE_SOLVED = true を立てて成功音を鳴らし、ポップアップを閉じるのみ(すぐには遷移しない)にしています。金庫近接シーン側のタップ処理は post.gd(第5回)の郵便受け開閉と同じ構造にし、「SAFE_SOLVED が真ならシーン遷移、偽ならポップアップを開く」という分岐にしました。
safe/base.png(1448×1086)に対して safe/dial.png(108×108)をダイヤル位置に拡大重ね表示し、左右に btn_turn_left.png / btn_turn_right.png を配置する座標も、画像を実際に確認しながら算出しています。
-1/1 という単一の値を「回転させる角度の符号」と「タップした方向を記録する配列の要素」の両方に使うことで、別々に管理するコードを1本化できました。今回の金庫ギミックは、実は完全に新しいロジックではなく、「時計の角度計算」+「額縁の配列比較判定」+「郵便受けの分岐パターン」という既存の3つのパターンの組み合わせでできています。連載を通して確立してきたパターンの資産性が、新しいギミックの実装速度に直結しています。

左へ3回、右へ5回、左へ7回、右へ2回、ダイヤルを回すと・・・
4. 入手アイテム「怪しい装置」の追加
アイテムに「怪しい装置」を追加して、暖炉の上の金庫が空いてる状態の時に取得できるように。未所持の場合だけ、item_005.pngを被せて表示。
resources/items/ を確認したところ、アイコン用・拡大表示用の画像(item_05_1.png / item_05_1_large.png)がすでに用意されていたため、命名規則(item_0X_1)に沿って item_05_1.tres を新規作成します。
全画面オーバーレイ用の item_005.png も既に金庫内部の位置に合わせて作られていたため、room_clock_cabinet.tscn(第12回)や room_picture_frame_open.tscn と同じ「ignore_texture_size = true の TextureButton を背景いっぱいに重ねる」パターンをそのまま踏襲します。
# scripts/room_fireplace_safe_open.gd(抜粋)
func _update_item_button_visibility() -> void:
$ItemButton.visible = not Global.has_item(_ITEM.id) and not Global.is_used(_ITEM.id)
Global.has_item() / Global.is_used()(第10回)を組み合わせた「未所持かつ未使用の場合のみ表示」の可視性制御も、既存の2ギミックと完全に同じロジックを流用しています。
このステップは新規ロジックがほぼゼロで、既存パターンの当てはめだけで完結しました。あらかじめアセットが命名規則通りに用意されていたことも大きく寄与しています。プロジェクト初期(第9回)で ItemData のリソース化と命名規則を決めておいた効果がここに表れています。
5. アイテム使用→ホワイトフラッシュ→バッドエンディング演出
アイテム「怪しい装置」の詳細表示時に、btn_use.png ボタンをフレーム枠内右下に表示して、タップされたら、アイテムを使用済みにします。使用されたら、アイテム詳細ポップアップは閉じて、演出用のフルサイズの下にタップなどが伝搬しないノードを用意します。そのノードで「画面が真っ白に光ってフェードアウトする」を実装してください。フェードアウト後、「ending」ステージに遷移してください。bgはending.pngです。ending前のフェードアウトは画面が白い時点でendingに遷移してください。endingは黒からフェードインして、フェードインが終わったらend_bad.pngを画面右側中央に表示。透過から始まってフェードインしつつ、下にアニメーションで移動してください。endingではBGMを停止してください。end_bad.pngを表示したタイミングで、se_end_bad.mp3を鳴らしてください。
今回のシリーズで最も要素の多い指示で、いくつかの要素に分解して実装しています。
共通コンポーネント item_popup.gd(第9回、インベントリから呼び出される汎用ポップアップ)に btn_use.png を追加しました。特定アイテム(item_05_1)の詳細表示時のみ表示されるよう、アイテムIDでの出し分けを実装し、タップで Global.use_item() → ポップアップを閉じる → 次の演出を呼び出す、という流れです。
「演出用のフルサイズの下にタップなどが伝搬しないノード」という指示から、既存の Global オートロードが管理する CanvasLayer(第9回でインベントリバー用に既に layer = 10 で1枚存在)よりさらに上の layer = 20 に、ColorRect 1枚だけの新しいオーバーレイを追加しました。
# scripts/global.gd(抜粋)
func _setup_white_flash_overlay() -> void:
var layer := CanvasLayer.new()
layer.layer = 20
add_child(layer)
_white_flash = ColorRect.new()
_white_flash.color = Color(1, 1, 1, 0)
_white_flash.mouse_filter = Control.MOUSE_FILTER_IGNORE
_white_flash.set_anchors_preset(Control.PRESET_FULL_RECT)
layer.add_child(_white_flash)
func play_bad_end_transition() -> void:
_white_flash.mouse_filter = Control.MOUSE_FILTER_STOP
var tween := create_tween()
tween.tween_property(_white_flash, "color:a", 1.0, 0.6)
tween.tween_callback(func():
get_tree().change_scene_to_file("res://scenes/ending.tscn")
)
tween.tween_property(_white_flash, "color:a", 0.0, 0.6)
tween.tween_callback(func(): _white_flash.mouse_filter = Control.MOUSE_FILTER_IGNORE)
平常時は mouse_filter = IGNORE(操作を素通しする)にしておき、演出開始時のみ mouse_filter = STOP に切り替えて入力をブロックします。透明→白へアルファをアニメーションさせ、画面が完全に白くなった瞬間に change_scene_to_file() を呼ぶことで、シーン切り替えのカクつきを白フラッシュの裏に隠しています。シーン切り替え後は白→透明へフェードアウトし、裏で進行している遷移先シーン自身のフェードインと自然にクロスフェードするように設計しました。
ending.tscn は新規実装です。
# scripts/ending.gd(抜粋)
func _ready() -> void:
modulate.a = 0.0
var tween := create_tween()
tween.tween_property(self, "modulate:a", 1.0, 0.6)
tween.tween_callback(_on_fade_in_complete)
func _on_fade_in_complete() -> void:
$SeEndBad.play()
var end_bad := $EndBad as TextureRect
end_bad.modulate.a = 0.0
var end_bad_target_y: float = end_bad.position.y
end_bad.position.y = end_bad_target_y - end_bad_slide_distance
var tween := create_tween()
tween.set_parallel(true)
tween.tween_property(end_bad, "modulate:a", 1.0, 0.8)
tween.tween_property(end_bad, "position:y", end_bad_target_y, 0.8)
ルート Control の modulate.a を 0→1 でトゥイーンする、他シーンと同じ「黒からのフェードイン」パターンを流用しています。フェードイン完了後のコールバックで se_end_bad.mp3 を再生すると同時に、end_bad.png を「透過(alpha 0) → 不透明(alpha 1)」「60px上のオフセット位置 → 定位置」へ同時にトゥイーンさせ、「フェードインしながら下に移動してくる」演出を実装しました。Global._SCENE_BGM(第11回)に "Ending" をあえて登録しないことで、既存のBGM切り替えロジック(未登録シーン=空文字列のパス)がそのままBGM停止として機能する設計にし、専用コードを書かずに要件を満たしています。
「フェードアウト後に遷移」と「白い時点で遷移」という一見矛盾する2つの指示は、「白へのフラッシュ」と「白からのフェードアウト」を切り替えの前後に分けることで両立させています。指示の文面をそのままロジックに落とすのではなく、意図を汲み取って設計に反映することが重要でした。ending シーンをBGM辞書に登録しないことでBGM停止を実現しているのは、第11回で作った「辞書にキーがなければ無音になる」という仕様を逆手に取った実装です。新しい分岐を追加せず、既存ロジックの「何もしない」パスを活用しています。
動作確認
F5 でゲームを実行し、暖炉に火を灯すと room_fireplace.tscn/room_fireplace_closeup.tscn の背景が切り替わることを確認します。火が灯った状態で金庫・額縁の領域をタップするとそれぞれのズームシーンに遷移し、金庫をタップするとダイヤル錠ポップアップが開きます。「左×3、右×5、左×7、右×2」の順にダイヤルを回すと正解になり、正解後に金庫をタップすると開いたシーンに遷移して「怪しい装置」を取得できます。インベントリで「怪しい装置」の詳細を開き「使う」ボタンをタップすると、画面が白くフラッシュしてバッドエンディングに遷移します。ending.tscn は黒からフェードインし、その後 end_bad.png がフェードインしながら下にスライドしてきて、エンディング中はBGMが鳴っていなければ、一通りの動作確認は完了です。
金庫・額縁のタップが反応しない場合は Global.FIRE_LIT が true になっているか(未点火時は判定を無効化している)を確認してください。ダイヤルの正解判定が合わない場合は方向値の配列([-1,-1,-1,1,1,1,1,1,...])が指示通りの回数分だけ並んでいるかを見ます。ホワイトフラッシュ中に裏の操作ができてしまう場合は _white_flash.mouse_filter が演出中は STOP になっているか、エンディングでBGMが鳴ってしまう場合は Global._SCENE_BGM に "Ending" キーが誤って登録されていないかを確認してください。$EndBad 関連で型エラーが出る場合は as TextureRect などの明示キャストと型注釈が入っているかを見ます。
成果物
まとめ
暖炉の点火→金庫・額縁の出現→ダイヤル錠ギミック→アイテム入手→使用によるバッドエンド、という一連のストーリーラインを実装しました。
| やりたいこと | 実装 |
|---|---|
| 状態に応じた背景の即時切り替え |
_ready() 時と状態変化のタイミングの両方で切り替え処理を実行 |
| 既存ギミックを参考にした新規ギミック | 時計(角度計算)+額縁(配列比較)+郵便受け(分岐パターン)を組み合わせる |
| 命名規則に沿ったアイテム追加 | 既存の2ギミックと同じ可視性制御ロジックをそのまま流用 |
| 「白い時点で遷移」の実現 | ホワイトフラッシュとフェードアウトを切り替えの前後に分けて両立 |
| BGMを止めずに専用コードを書かない | シーンをBGM辞書に登録しないことで既存の「無登録=無音」ロジックを活用 |
| 型推論エラーの修正 |
as によるノードの明示キャストと変数の型注釈 |
「既存のXを参考にして」という指示は非常によく効きました。時計ギミックや額縁ギミック、郵便受けポップアップなど、既存の類似実装を明示的に参照させることで、プロジェクト全体で一貫したコードスタイル・設計パターンを保ったまま新機能を実装できています。画像アセットは実際に読み込んで目視確認するフェーズが必須で、タップ領域やオブジェクトの重ね位置など、テキストの指示だけでは解決できない座標決定は、画像を都度確認しながら進める必要がありました。一見矛盾する指示も、実装を分割することで両立できます。ユーザーの説明が推敲途中で変化するケースも、意図を汲み取って設計に反映することが重要でした。微調整やエラー修正は最小差分で対応可能で、座標の微調整やGDScriptの型推論エラーなど、ピンポイントな指摘に対しては、全体を作り直すことなく該当箇所のみを修正できました。




