mcpField
S
scene-organization

scene-organization

github.com/jame581开发889190 次安装4 天前更新
代码仓库

关于 scene-organization

面向 Godot 4.3+ 开发者的场景树组织指南,涵盖场景拆分、组合与继承选择,以及节点通信方式,帮助构建职责清晰且可复用的场景。

主要功能

  1. 01场景拆分准则
  2. 02组合与继承模式
  3. 03父子节点信号与方法流向
  4. 04通过 EventBus 进行同级通信
  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 用于规划场景树,2d-essentials 用于组织 TileMapLayer 和 CanvasLayer。


1. 核心原则

场景是构建模块。每个场景都只封装一个概念——玩家、敌人、生命值条或武器。场景应能独立理解、无需修改即可重用,也能在不破坏相邻场景的情况下替换。

一个场景 = 一项职责。如果你很难用两个词或更少的词为场景命名,它可能承担了过多职责。


2. 组合优于继承

玩家场景——由可重用部分组合而成
Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
├── HealthComponent
├── HitboxComponent
├── StateMachine
└── AnimationPlayer

HealthComponent、HitboxComponent 和 StateMachine 是独立的 .tscn 文件,会作为子场景实例化。任何需要生命值的实体——敌人、可摧毁的箱子、Boss——都可以包含 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 个节点,说明它承担了多项职责
  • 独立性——无需打开父场景即可测试、预览或修改子场景
  • 团队协作——拆分场景可减少多人同时处理同一功能时的合并冲突
何时将节点保留在一起:
  • 节点紧密耦合——拆分后需要大量连接信号,才能实现直接节点引用就能简洁完成的操作
  • 分组规模小且只使用一次——仅存在于单个场景中的双节点辅助对象,不值得单独创建 .tscn 文件
  • 拆分会造成简单操作开销——如果父节点必须连接三个信号,才能告诉子节点“你被击中了”,那么拆分就得不偿失

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 是普通的 Node2D 容器,用于组织分组并简化 get_children() 遍历。


6. 检查清单

  • 每个场景只负责一项职责,名称不超过两个词
  • 可复用组件(HealthComponent、StateMachine 等)应作为独立的 .tscn 文件
  • 除非有记录在案的理由需要合并,否则场景中的节点数不超过约 15 个
  • 子节点向上发出信号;父节点向下调用方法
  • 节点间的对等通信使用 EventBus Autoload,而非 get_parent() 链
  • 代码中不得使用 get_parent().get_parent() 或 get_node("../../SomeNode") 路径
  • 节点按逻辑分组到容器中(Visuals、Components、AI 等),以提高可读性

评论

支持 Markdown