用途
godot-composition 是一套 Godot 游戏架构规范技能,用于以“组合模式(Entity-Component)”构建可扩展的游戏,例如 RPG、平台跳跃和射击游戏。它适用于设计玩家控制器、NPC、敌人、武器或复杂玩法系统,并强制游戏实体采用“Has-A(拥有)”关系,而非继承式的“Is-a”。触发关键词包括 Entity-Component、ECS、Gameplay、Actors、NPCs、Enemies、Weapons、Hitboxes、Game Loop、Level Design。
核心哲学
该技能强制“组合优于继承”。在 Godot 中,节点本身就是组件。一个复杂实体(如玩家)只是一个编排者(Orchestrator),负责管理多个专职的工作节点(组件)。
黄金规则:
- 单一职责:一个脚本只做一件事。
- 封装:组件是“自私的”,只处理自身内部逻辑,不知道谁拥有它。
- 编排者:根脚本(如 player.gd)不做任何逻辑,只管理状态并在组件之间传递数据。
- 解耦:组件之间通过信号(向上)和方法(向下)通信。
决策树:组合 vs Autoload vs 继承
| 情况 | 选择 |
|---|---|
| 玩法实体行为(HP、命中盒、移动、交互) | 组合——子组件 + 编排者(composition_root_init.gd) |
| 跨场景服务(音频总线、存档、网络、经济账本) | Autoload——不是挂在玩家上的组件 |
| 真正的 is-a 引擎特化(带共享生命周期的自定义 Control/Node) | 继承例外——罕见;绝不用于“加一把枪”或“加 HP” |
主要能力(可用脚本)
- health_component.gd:管理任意实体的生命周期、伤害逻辑与死亡信号。
- hit_box_component.gd:基于 Area 的组件,用于拦截伤害并委托给 HealthComponent。
- hurt_box_component.gd:基于 Area 的组件,专门对 HitBoxComponent 造成伤害。
- velocity_component.gd:封装移动与加速度逻辑,可在玩家和敌人之间复用。
- interaction_component.gd:解耦的交互处理器,通过注入 Callable 逻辑实现上下文感知动作。
- follower_component.gd:解耦的追踪逻辑,通过注入 NodePath 实现平滑跟随。
- state_component_vsm.gd:基于组件的状态机模式,用子节点表示各个状态。
- status_effect_component.gd:通过将效果场景作为子节点堆叠,管理临时增益/减益。
- visual_sync_component.gd:将逻辑状态(速度/方向)与视觉表现(精灵翻转)分离。
- composition_root_init.gd:必读的编排者接线脚本,通过带类型的 @export(Inspector / 场景中的 %UniqueNames)完成连接;绝不使用 $ 或 get_node 查找组件。
使用方式
连接策略:带类型的导出
不要依赖树顺序,使用 @export 加静态类型进行显式依赖注入。示例:
# 编排者(如 player.gd)
class_name Player extends CharacterBody3D
# 依赖注入:在背包里定义“插槽”
@export var health_component: HealthComponent
@export var movement_component: MovementComponent
@export var input_component: InputComponent
# 使用场景唯一名(%)在编辑器中自动赋值
# 或在 Inspector 中拖放。
组件思维
组件必须定义 class_name 才能被识别为类型。标准组件模板:
class_name MyComponent extends Node
# 逻辑用 Node,需要位置则用 Node3D/2D
@export var stats: Resource # 组件可以持有自己的数据
signal happened_something(value)
func _ready() -> void:
_validate_dependencies()
func _validate_dependencies() -> void:
# 依赖校验:开发期设置错误时尽早失败
# 注意:assert() 在发布构建中会被剥离。
assert(stats != null, "Stats Resource missing on %s" % name)
func do_logic(delta: float) -> void:
# 执行具体任务
pass
标准组件
内联的输入/移动/生命值配方已移除。强制要求:从 composition_root_init.gd 开始,然后加载匹配脚本:
- 生命/死亡:health_component.gd
- 伤害区域:hit_box_component.gd、hurt_box_component.gd
- 运动:velocity_component.gd
- 交互/跟随/VFX 同步:interaction_component.gd、follower_component.gd、visual_sync_component.gd
- 状态/状态效果:state_component_vsm.gd、status_effect_component.gd
带类型的 @export 接线方式见上文“实现标准”。
专家组合模式
1. 状态-组件模式(FSM)
将复杂行为封装为充当状态的子节点,父 StateComponent 将生命周期调用委托给当前激活的子节点。强制要求阅读 state_component_vsm.gd,不要内联粘贴状态机。更深入的 VSM / 分层 FSM 请打开 godot-state-machine-advanced。
2. 组件注册表(O(1) 查找)
避免为兄弟通信进行缓慢的树遍历,在 ready 时用字典按名称或分组编目子节点:
var _components: Dictionary = {}
func _ready() -> void:
for child in get_children():
_components[child.name] = child
for group in child.get_groups():
_components[group] = child
func get_comp(key: StringName) -> Node:
return _components.get(key)
3. 依赖校验
用 @export 断言快速失败,而不是 get_node_or_null 路径(树重排时路径会失效):
@export var health_component: HealthComponent
@export var input_component: InputComponent
func _ready() -> void:
assert(health_component != null, "Missing HealthComponent export!")
assert(input_component != null, "Missing InputComponent export!")
输入/移动/生命值编排者配方与注册表深度,强制参考 orchestrator-recipes.md。当 composition_root_init.gd 加一个组件脚本已足够时,不要加载该文件。
组合中绝不要做的事
- 绝不使用深层继承链(如 Player > Entity > LivingThing > Node)——会产生难以重构的脆弱“上帝类”。
- 绝不用 get_node() 或 $ 查找组件——场景树重排时会失效,始终用 @export 或 %UniqueNames。
- 绝不让组件直接引用其父脚本——这会使组件无法复用,应使用信号或依赖注入。
- 绝不把输入、物理和游戏逻辑混在一个脚本中——违反单一职责,应拆分为专职组件。
- 绝不创建需要特定 SceneTree 结构的组件——组件应“自私”,只关心自身属性和直接子节点。
- 绝不用继承来“添加功能”——若想让敌人射击,添加 ShootingComponent,而不是让它继承 ShooterEnemy。
- 绝不硬编码组件依赖——若 CombatComponent 需要 HealthComponent,在 _ready() 中查找或由父级注入。
- 绝不把 Godot 节点当作纯数据——节点提供生命周期(_process)和信号;若只需要数据,使用 Resource。
- 绝不忽略组件中的节点生命周期——对无论父级状态如何都必须发生的设置/清理,使用 _enter_tree() 和 _exit_tree()。
- 绝不隐藏组件的访问点——暴露 NodePath 或 Callable 属性,让父级能在 Inspector 中接线。
性能说明
节点是轻量的。不要害怕每个实体增加 10-20 个节点。组合带来的组织收益远超 Node 实例可忽略的内存开销。
适用场景与限制
适用场景:Godot 中需要构建可扩展玩法实体的项目,尤其是玩家控制器、NPC、敌人、武器、命中盒、游戏循环与关卡设计相关的系统。
限制与注意:该技能是一套架构规范与脚本集合,不提供具体游戏成品;assert() 在发布构建中会被剥离,因此依赖校验只在开发期生效;组件不应依赖特定场景树结构,也不应直接引用父脚本。关于收费、价格、平台支持范围等信息,资料中暂未提供。