scene-organization について
Godot 4.3以降のシーンツリー構成を扱うスキルです。シーンの分割、コンポジションと継承の選択、ノード間通信の設計を支援します。
主な機能
- 01シーン分割の指針
- 02コンポジションと継承のパターン
- 03親子間のシグナルとメソッドの流れ
- 04EventBusによる同階層間通信
- 05エンティティ、UI、レベルのツリー例
活用シーン
- 01シーンを分割するか判断する
- 02複数のエンティティでコンポーネントを再利用する
- 03ノード間通信を設計する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill scene-organizationインストール後、Claude Code で/scene-organizationを入力して読み込まれたか確認します。
詳細
シーン構成
Godot 4.3+ のシーンツリーを構成するためのガイドです。シーンを分割するタイミングや、コンポジションを使うタイミング、ノード間の通信方法について説明します。
関連スキル: コンポジションパターンには component-system、疎結合な通信には event-bus、シーンツリーの計画には godot-brainstorming、TileMapLayer と CanvasLayer の構成には 2d-essentials を参照してください。
1. 基本原則
シーンは構成要素です。それぞれのシーンは、プレイヤー、敵、ヘルスバー、武器など、1つの概念だけをカプセル化します。シーン単体で理解でき、変更せずに再利用でき、周囲のシーンを壊さずに置き換えられるようにします。
1つのシーンにつき責務は1つです。シーンの名前を2語以内で付けるのが難しい場合は、おそらく機能を詰め込みすぎています。
2. 継承よりコンポジション
Player シーン — 再利用可能なパーツで構成
Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
├── HealthComponent
├── HitboxComponent
├── StateMachine
└── AnimationPlayer
HealthComponent、HitboxComponent、StateMachine は、それぞれ独立した .tscn ファイルとして、子シーンにインスタンス化されています。敵、破壊可能な木箱、ボスなど、ヘルス機能が必要なエンティティには、ロジックを重複させずに HealthComponent を含められます。
HealthComponent — 完全な例
GDScript
# health_component.gd
class_name HealthComponent
extends Node
signal health_changed(old_value: int, new_value: int)
signal died
@export var max_health: int = 100
var current_health: int
func _ready() -> void:
current_health = max_health
func take_damage(amount: int) -> void:
if amount <= 0:
return
var old_health := current_health
current_health = max(0, current_health - amount)
health_changed.emit(old_health, current_health)
if current_health == 0:
died.emit()
func heal(amount: int) -> void:
if amount <= 0:
return
var old_health := current_health
current_health = min(max_health, current_health + amount)
health_changed.emit(old_health, current_health)
func is_alive() -> bool:
return current_health > 0
C#
// HealthComponent.cs
using Godot;
[GlobalClass]
public partial class HealthComponent : Node
{
[Signal]
public delegate void HealthChangedEventHandler(int oldValue, int newValue);
[Signal]
public delegate void DiedEventHandler();
[Export]
public int MaxHealth { get; set; } = 100;
public int CurrentHealth { get; private set; }
public override void _Ready()
{
CurrentHealth = MaxHealth;
}
public void TakeDamage(int amount)
{
if (amount <= 0)
return;
int oldHealth = CurrentHealth;
CurrentHealth = Mathf.Max(0, CurrentHealth - amount);
EmitSignal(SignalName.HealthChanged, oldHealth, CurrentHealth);
if (CurrentHealth == 0)
EmitSignal(SignalName.Died);
}
public void Heal(int amount)
{
if (amount <= 0)
return;
int oldHealth = CurrentHealth;
CurrentHealth = Mathf.Min(MaxHealth, CurrentHealth + amount);
EmitSignal(SignalName.HealthChanged, oldHealth, CurrentHealth);
}
public bool IsAlive() => CurrentHealth > 0;
}
継承を使う場合
シーンが共有するのが振る舞いだけではなく、構造である場合に継承が適しています。つまり、子シーンが同じノード構成を持つ同種のバリエーションで、異なるのはエクスポートされたプロパティの一部だけである場合です。
適した例:
Enemy→Orc、Goblin— 同じ骨格(Sprite2D、CollisionShape2D、HealthComponent、AI)で、ステータスとアートが異なるWeapon→Sword、Bow— 同じスロットへのアタッチ処理で、アニメーションとダメージタイプが異なるPickup→HealthPickup、AmmoPickup— 同じ Area2D + CollisionShape2D + アニメーションで、取得時の効果が異なる
判断の目安
| シナリオ | パターン |
|---|---|
| シーン全体をコピーして、エクスポートされたプロパティをいくつか変更することになる | 継承 |
| 異なる種類のエンティティ間でノードの一部を組み合わせたい | コンポジション |
3. シーンを分割するルール
次の場合はシーンを分割します:
- 再利用 — サブシーンが複数の親シーンで必要になる
- 複雑さ — シーンのノード数が約15を超え、複数の責務を担っている
- 独立性 — 親シーンを開かずにサブシーンをテスト、プレビュー、変更できる
- チーム — 複数人で同じ機能を作業する場合、シーンを分けるとマージコンフリクトを減らせる
次の場合はノードをまとめたままにします:
- ノードが密接に結合している — 分割すると、ノード参照で簡潔に処理できることを再現するためだけに、過剰なシグナル接続が必要になる
- グループが小さく、一度しか使われない — 1つのシーン内にしかない2ノードのヘルパーに、独自の
.tscnファイルを作る必要はない - 分割によって単純な処理のオーバーヘッドが生じる — 親が子に「攻撃を受けた」と伝えるだけで3つのシグナルを接続する必要があるなら、分割するメリットがありません
4. ノード間の通信パターン
[Parent]
/ \
[Child A] [Child B]
\
[Child C]
シグナルは上方向に伝わる(子 → 親)
子ノードが何かが起きたことを通知します。親、またはそのシグナルに接続したノードが、どう対応するかを決めます。これにより、子ノードは自身のコンテキストに依存せず、完全に再利用可能になります。
# Child emits; it does not know who is listening
health_component.died.connect(_on_player_died)
メソッド呼び出しは下方向に伝わる(親 → 子)
親が子のメソッドを直接呼び出して、子を制御します。親が参照を保持し、子は明確な API を公開するため、親について知る必要はありません。
# Parent calls into child
$HealthComponent.take_damage(10)
$AnimationPlayer.play("hurt")
EventBus は横方向に伝わる(ピア → ピア)
敵が HUD に通知する場合など、祖先・子孫関係にないシーン間の通信には、Autoload のイベントバスを使います。バスを介して送信することで、送信元と受信側を完全に疎結合にできます。
# Autoload: EventBus.gd
signal enemy_killed(enemy: Enemy)
# Enemy scene
EventBus.enemy_killed.emit(self)
# HUD scene
EventBus.enemy_killed.connect(_on_enemy_killed)
C#
// Pattern 1: Signals travel up (child → parent)
// Child emits; it does not know who is listening.
public partial class Player : CharacterBody2D
{
public override void _Ready()
{
var health = GetNode<HealthComponent>("HealthComponent");
health.Died += OnPlayerDied;
}
private void OnPlayerDied()
{
// Parent reacts — child HealthComponent stays ignorant of context
}
}
// Pattern 2: Method calls travel down (parent → child)
// Parent drives children by calling their methods directly.
public partial class Level : Node2D
{
public override void _Ready()
{
var health = GetNode<HealthComponent>("Player/HealthComponent");
health.TakeDamage(10);
var anim = GetNode<AnimationPlayer>("Player/AnimationPlayer");
anim.Play("hurt");
}
}
// Pattern 3: EventBus travels sideways (peer → peer)
// EventBus.cs — registered as an Autoload singleton named "EventBus"
public partial class EventBus : Node
{
[Signal] public delegate void EnemyKilledEventHandler(Enemy enemy);
}
// Enemy scene — emits on the bus; does not reference HUD
public partial class Enemy : CharacterBody2D
{
private void Die()
{
var bus = GetNode<EventBus>("/root/EventBus");
bus.EmitSignal(EventBus.SignalName.EnemyKilled, this);
QueueFree();
}
}
// HUD scene — subscribes on the bus; does not reference Enemy
public partial class Hud : CanvasLayer
{
public override void _Ready()
{
var bus = GetNode<EventBus>("/root/EventBus");
bus.EnemyKilled += OnEnemyKilled;
}
private void OnEnemyKilled(Enemy enemy)
{
// Update kill counter, score, etc.
}
}
5. シーンツリーのパターン
エンティティ・コンポーネントパターン
Enemy (CharacterBody2D)
├── CollisionShape2D # stays a direct child of the body
├── Visuals (Node2D)
│ ├── Sprite2D
│ └── AnimationPlayer
├── Components (Node2D)
│ ├── HealthComponent
│ └── HitboxComponent
└── AI (Node)
├── NavigationAgent2D
└── StateMachine
関心ごと(Visuals、Components、AI)ごとにグループ化します。各サブグループはエディターで折りたたみ、個別に作業できます。位置を持つものにはNode2Dコンテナを使用します。通常のNodeではトランスフォームが継承されないため、その下に置いたSprite2DやArea2Dの当たり判定は本体に追従しません。CollisionShape2Dは直接の親にしか形状を適用しないため、コンテナには配置できません。
UIシーンパターン
HUD (CanvasLayer)
├── MarginContainer
│ ├── TopBar
│ │ ├── HealthBar
│ │ └── ResourceBar
│ └── BottomBar
│ ├── Hotbar
│ └── MiniMap
└── PauseMenu
CanvasLayerはHUDをデフォルトのキャンバスレイヤーの上に描画します(レイヤー1は0の上)。MarginContainerは端に固定のマージンを設けますが、デバイスのセーフエリアは読み取りません(responsive-uiを参照)。TopBar、BottomBar、PauseMenuはそれぞれ独立したインスタンス化シーンなので、ルートHUDシーンを開かずに編集できます。
レベルシーンパターン
Level01 (Node2D)
├── TileMapLayer
├── Entities
│ ├── Player (instance)
│ └── Enemies (Node2D)
│ ├── Orc (instance)
│ └── Goblin (instance)
├── Pickups (Node2D)
├── Navigation
│ └── NavigationRegion2D
└── Camera2D
レベルシーンはコンポジションルートです。レイアウトを管理してインスタンスを生成しますが、それ自体にゲームプレイのロジックは含みません。Entities、Pickups、Navigationは、整理のためのグループ化やget_children()の反復処理を簡単にするために使う、通常のNode2Dコンテナです。
6. チェックリスト
- 各シーンの責務は1つだけで、名前は2語以内です
- 再利用可能なコンポーネント(
HealthComponent、StateMachineなど)は、個別の.tscnファイルにします - 一体化しておく理由が文書化されていない限り、シーンのノード数は約15個を超えません
- 子は親に向けてシグナルを発し、親は子のメソッドを呼び出します
- ピア間の通信には
get_parent()の連鎖ではなく、EventBus Autoloadを使用します - コード内で
get_parent().get_parent()やget_node("../../SomeNode")のパスを使用しません - 読みやすさのため、ノードを論理的なコンテナ(
Visuals、Components、AIなど)にグループ化します