はじめに
この記事は、Godot 4.xで脱出ゲームを作る連載の第7回です。
- 第1回:環境構築とプロジェクト作成
- 第2回:Claude Code + godot-mcpのセットアップ
- 第3回:Claude CodeとMCPでスプラッシュ画面を実装する
- 第4回:Claude CodeとMCPでタイトル画面を実装してシーンを遷移する
- 第5回:背景の特定領域をタップしてフェード遷移する
- 第6回:再利用可能なモーダルポップアップを作る
- 第7回:スプライトシートを使ったダイヤル錠ギミックを実装する(今回)
前回(第6回)ではポップアップコンポーネントを実装しました。今回はそのポップアップ内にダイヤル錠を組み込む謎解きギミックを実装します。
今回の実装は若干の手抜きで、以前Axmol Engineで作成したダイヤル錠(参考記事)をGodotに移植する形で進めます。Claude Codeへの指示もその参考記事のURLを渡す形で始めています。
スプライトシートの作成に興味のあるお方もご覧ください!
この記事で作るもの
- スプライトシート(
spritesheet.png)からAtlasTextureで各数字フレームを切り出す - タップするたびに 0→1→…→9→0 とループする単体ダイヤルコンポーネント(
dial.tscn) -
base.pngの上に3つのダイヤルを並べたダイヤル錠コンポーネント(dial_lock.tscn) - ポップアップ内に組み込んで謎解きギミックとして動作させる
前提条件
- Claude Code + godot-mcpのセットアップ完了(第2回参照)
-
scenes/components/popup.tscnが作成済み(第6回参照) - 以下の素材が
assets/images/dial/に配置済み
| ファイル | 内容 |
|---|---|
spritesheet.png |
数字0〜9の画像をまとめたスプライトアトラス |
spritesheet.json |
フレーム定義(TexturePacker JSON Array形式)参考記事 |
base.png |
ダイヤル錠の背景画像(640×480px)前回配置済み |
1枚の画像のスプライトアトラス
spritesheet.png

設計方針
今回も段階的に追加指示を出しながら調整しています。その試行錯誤の過程もそのまま記録しています。
ファイル構成
scenes/components/
dial.tscn ← 今回作成(単体ダイヤル、再利用可能)
dial_lock.tscn ← 今回作成(base.png + 3ダイヤルのセット)
popup.tscn ← 前回作成(ダイヤル錠を組み込む)
scripts/components/
dial.gd ← 今回作成
dial_lock.gd ← 今回作成
assets/images/dial/
spritesheet.png ← 0〜9の数字フレームをまとめたアトラス
spritesheet.json ← フレーム定義
base.png ← ダイヤル錠の背景
1. スプライトシートの構造を把握する
たまには楽をしましょう!ということで自分の書いた Axmol Engineでのギミック実装を参考に進めることにしました
私が書いたaxmol engine での実装、https://qiita.com/OnuuuumaX/items/2ca44f4be64fbe9d1745 を参考にgodotで実装してください
まずClaude Codeが spritesheet.json を読み込み、フレーム座標を確認しました。
{
"frames": [
{ "filename": "btn_1_0.png", "frame": { "x": 1, "y": 1, "w": 146, "h": 369 } },
{ "filename": "btn_1_1.png", "frame": { "x": 149, "y": 1, "w": 146, "h": 369 } },
{ "filename": "btn_1_2.png", "frame": { "x": 297, "y": 1, "w": 146, "h": 369 } },
{ "filename": "btn_1_3.png", "frame": { "x": 445, "y": 1, "w": 146, "h": 369 } },
{ "filename": "btn_1_4.png", "frame": { "x": 593, "y": 1, "w": 146, "h": 369 } },
{ "filename": "btn_1_5.png", "frame": { "x": 741, "y": 1, "w": 146, "h": 369 } },
{ "filename": "btn_1_6.png", "frame": { "x": 1, "y": 372, "w": 146, "h": 369 } },
{ "filename": "btn_1_7.png", "frame": { "x": 149, "y": 372, "w": 146, "h": 369 } },
{ "filename": "btn_1_8.png", "frame": { "x": 297, "y": 372, "w": 146, "h": 369 } },
{ "filename": "btn_1_9.png", "frame": { "x": 445, "y": 372, "w": 146, "h": 369 } }
]
}
各フレームは 146×369px。2段構成で 0〜5 が1行目、6〜9 が2行目に並んでいます。
1-1. AxmolとGodotの対応関係
Axmol ではカスタム JsonArraySpriteSheetLoader を実装してフレームキャッシュに登録し、setSpriteFrame() でUV座標のみを切り替えていました。Godot では AtlasTexture が同じ役割を果たします。
| やりたいこと | Axmol | Godot |
|---|---|---|
| フレームの切り替え | setSpriteFrame() |
AtlasTexture.region を差し替え |
| タップ検出 | タッチリスナー + convertToNodeSpace
|
TextureButton.pressed シグナル |
| 数字のループ | (_dialDigits[i] + 1) % 10 |
(_digit + 1) % 10 |
| ダイヤル間隔 | 手動座標指定 |
HBoxContainer の separation
|
| 描画順(z軸) | zOrder プロパティ | シーンツリーの並び順で制御 |
2. 単体ダイヤルコンポーネントの実装(dial.tscn)
2-1. 生成されたシーン(dial.tscn)
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/components/dial.gd" id="1_dial"]
[node name="Dial" type="TextureButton"]
custom_minimum_size = Vector2(146, 369)
script = ExtResource("1_dial")
ルートを TextureButton にすることで、タップの検出を pressed シグナルに任せられます。custom_minimum_size でスプライトシートのフレームと同サイズを確保します。
2-2. 生成されたスクリプト(dial.gd)
extends TextureButton
signal digit_changed(digit: int)
const _SPRITESHEET := preload("res://assets/images/dial/spritesheet.png")
const _REGIONS: Array[Rect2] = [
Rect2(1, 1, 146, 369), # 0
Rect2(149, 1, 146, 369), # 1
Rect2(297, 1, 146, 369), # 2
Rect2(445, 1, 146, 369), # 3
Rect2(593, 1, 146, 369), # 4
Rect2(741, 1, 146, 369), # 5
Rect2(1, 372, 146, 369), # 6
Rect2(149, 372, 146, 369), # 7
Rect2(297, 372, 146, 369), # 8
Rect2(445, 372, 146, 369), # 9
]
var _digit: int = 0
var _textures: Array[AtlasTexture] = []
func _ready() -> void:
for region in _REGIONS:
var atlas := AtlasTexture.new()
atlas.atlas = _SPRITESHEET
atlas.region = region
_textures.append(atlas)
texture_normal = _textures[0]
pressed.connect(_on_pressed)
func _on_pressed() -> void:
_digit = (_digit + 1) % 10
texture_normal = _textures[_digit]
digit_changed.emit(_digit)
func get_digit() -> int:
return _digit
func set_digit(value: int) -> void:
_digit = clampi(value, 0, 9)
if not _textures.is_empty():
texture_normal = _textures[_digit]
2-3. ポイント解説
AtlasTexture を _ready() でまとめて生成してキャッシュする
10枚の AtlasTexture を起動時に一括生成して _textures 配列に保持します。タップのたびに AtlasTexture を新規生成するのではなく、配列の参照を差し替えるだけなので効率的です。Axmol のフレームキャッシュと同じ考え方です。
(_digit + 1) % 10 で 0→9 のループ
Axmol 実装と全く同じロジックです。%(剰余)演算子で 9 の次が 0 に戻ります。
set_digit() で外部から初期値を設定可能
ゲームのセーブデータから値を復元したい場合など、外部からダイヤルの値を設定できるようにしています。
3. ダイヤル錠コンポーネントの実装(dial_lock.tscn)
3-1. 生成されたシーン(dial_lock.tscn)
[gd_scene format=3]
[ext_resource type="Texture2D" path="res://assets/images/dial/base.png" id="1_base"]
[ext_resource type="Script" path="res://scripts/components/dial_lock.gd" id="2_dial_lock"]
[ext_resource type="PackedScene" path="res://scenes/components/dial.tscn" id="3_dial"]
[node name="DialLock" type="Control"]
script = ExtResource("2_dial_lock")
[node name="Base" type="TextureRect" parent="."]
layout_mode = 0
texture = ExtResource("1_base")
[node name="Dials" type="HBoxContainer" parent="."]
layout_mode = 0
offset_left = 61.0
offset_top = 62.0
offset_right = 580.0
offset_bottom = 433.0
theme_override_constants/separation = 36
[node name="Dial1" parent="Dials" instance=ExtResource("3_dial")]
layout_mode = 2
[node name="Dial2" parent="Dials" instance=ExtResource("3_dial")]
layout_mode = 2
[node name="Dial3" parent="Dials" instance=ExtResource("3_dial")]
layout_mode = 2
3-2. 生成されたスクリプト(dial_lock.gd)
extends Control
signal combination_changed(digits: Array[int])
func _ready() -> void:
for dial in $Dials.get_children():
dial.digit_changed.connect(_on_digit_changed)
func _on_digit_changed(_digit: int) -> void:
combination_changed.emit(get_combination())
func get_combination() -> Array[int]:
var result: Array[int] = []
for dial in $Dials.get_children():
result.append(dial.get_digit())
return result
3-3. ポイント解説
HBoxContainer の separation でダイヤル間隔を管理する
Dial1〜Dial3 は HBoxContainer(Dials)の子ノードなので、個別に offset で位置を指定することはできません(コンテナが子の位置を管理するため)。ダイヤルの間隔は theme_override_constants/separation で制御します。
ダイヤル幅が 146px でダイヤル間の目標間隔が 182px の場合:
separation = 目標間隔 - ダイヤル幅 = 182 - 146 = 36px
combination_changed シグナルで3桁をまとめて通知する
いずれかのダイヤルが変化するたびに、3桁全体を Array[int] でまとめて通知します。呼び出し側は combination_changed を購読するだけで現在の組み合わせを取得できます。
4. ポップアップへの組み込み
ここまでは実装されたのみでしたので、実際に配置するようにお願いしました。
ポップアップ内、frame_popup.pngのz軸の下に組み込んでください
4-1. z軸の管理(ノードツリーの順序)
Godot ではノードリストの後方にあるものが手前(上)に描画されます。DialLock を Content(frame_popup.png を含む VBoxContainer)より前に置くことで、frame_popup.png の奥にダイヤル錠を配置できます。
Popup (Control)
├── Blocker ← 最背面(半透明黒・タップブロック)
├── DialLock ← frame_popup.png の奥に描画
└── Content (VBoxContainer)
├── Frame ← frame_popup.png(最前面)
└── BtnBack
4-2. 生成された差分(popup.tscn)
[ext_resource type="PackedScene" path="res://scenes/components/dial_lock.tscn" id="4_dial_lock"]
[node name="DialLock" parent="." instance=ExtResource("4_dial_lock")]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -314.0
offset_top = -352.5
grow_horizontal = 2
grow_vertical = 2
4-3. ポイント解説
中央アンカー(anchors_preset = 8)で位置を調整する
DialLock は Popup(plain Control)の子なので、アンカーとオフセットで位置を手動指定できます。anchors_preset = 8(CENTER)を起点に offset_left・offset_top で微調整しています。
5. ダイヤル位置の微調整
実際に表示確認したところ下記のようにずれてました。
godotのエディタで位置調整をしてみました
一旦位置調整の方法をClaude Codeに尋ねてみると、ある程度の位置調整をしてもらえましたがまだずれてましたので細かく指示していきます。
5-1. Claude Codeへの追加指示
Dial1 を x:92, y:64へ
Dial1 は HBoxContainer の子なので直接位置を変更できません。HBoxContainer(Dials)自体の offset を変更することで、ダイヤル全体の表示位置が決まります。
Dial2, 3 は 182pxづつ右へ
ダイヤル間の間隔を 182px にするため separation を調整します。
- theme_override_constants/separation = 0
+ theme_override_constants/separation = 36
separation = 182 - 146(ダイヤル幅)= 36px
各ダイヤルの左端x座標(Dials の offset_left = 61 基準):
| ダイヤル | x座標 |
|---|---|
| Dial1 | 61 |
| Dial2 | 61 + 182 = 243 |
| Dial3 | 61 + 364 = 425 |
補足:
HBoxContainerの子ノードは親がレイアウトを管理するため、子ノード側のoffsetを直接変更しても反映されません。位置の調整はコンテナ(Dials)のoffsetを変更することで行います。
6. 動作確認
F5 でゲームを実行し、以下の流れで動作すれば成功です。
- 郵便受けをタップしてポップアップを開く
-
frame_popup.pngの枠の奥にダイヤル錠が表示されている - 各ダイヤルをタップするたびに数字が 0→1→…→9→0 とループする
- 「戻る」ボタン(
btn_back.png)でポップアップを閉じられる
Claude Codeと共に、gotot上のエディタやプロンプト指示で位置調整を行いまして、ポップアップにダイヤルギミックを表示することができました

うまく動かない場合
| 症状 | 確認ポイント |
|---|---|
| ダイヤルが表示されない |
DialLock が popup.tscn の Content より前に配置されているか |
| 数字がループしない |
dial.gd の pressed.connect(_on_pressed) が _ready() で呼ばれているか |
| ダイヤルがフレームの手前に出てしまう | ノードツリーで DialLock が Content より**前(上)**にあるか確認 |
| ダイヤルの間隔がおかしい |
HBoxContainer の separation が正しく設定されているか |
7. 呼び出し側での使い方
combination_changed シグナルを購読することで、ダイヤルの現在値を取得して正解判定ができます。
# 正解の組み合わせを判定する例
$Popup/DialLock.combination_changed.connect(func(digits: Array[int]):
if digits == [1, 2, 3]:
print("正解!ロック解除")
)
まとめ
今回はClaude Code + godot-mcpを使って以下を実装しました。
| やったこと | ポイント |
|---|---|
| スプライトシートの切り出し |
AtlasTexture.region にフレーム座標を指定 |
| フレームの効率的な切り替え |
_ready() でまとめて生成してキャッシュ、タップ時は参照を差し替えるだけ |
| タップで数字をループ |
(_digit + 1) % 10 でAxmol実装と同じロジック |
| ダイヤル間隔の管理 |
HBoxContainer の separation で宣言的に制御 |
| z軸の管理 | ノードツリーの並び順で制御(後方が手前に描画) |
| 組み合わせの通知 |
combination_changed シグナルで3桁をまとめて通知 |
Axmol からの移植にあたり、Claude CodeにAxmol実装の参考記事URLを渡すことで、フレームキャッシュのロジックや数字ループの考え方をそのままGodotに適用してもらえました。




