mcpField
S
scene-organization

scene-organization

github.com/jame581開発889190 回インストール4 日前に更新
リポジトリ

scene-organization について

Godot 4.3以降のシーンツリー構成を扱うスキルです。シーンの分割、コンポジションと継承の選択、ノード間通信の設計を支援します。

主な機能

  1. 01シーン分割の指針
  2. 02コンポジションと継承のパターン
  3. 03親子間のシグナルとメソッドの流れ
  4. 04EventBusによる同階層間通信
  5. 05エンティティ、UI、レベルのツリー例

活用シーン

  1. 01シーンを分割するか判断する
  2. 02複数のエンティティでコンポーネントを再利用する
  3. 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など)にグループ化します

コメント