godot-composition

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Godot Composition Architecture

Godot组合架构

Core Philosophy

核心理念

This skill enforces Composition over Inheritance ("Has-a" vs "Is-a"). In Godot, Nodes are components. A complex entity (Player) is simply an Orchestrator managing specialized Worker Nodes (Components).
本技能遵循组合优于继承(“拥有” vs “是”)原则。 在Godot中,Node就是组件。一个复杂实体(如玩家)只是一个协调器(Orchestrator),负责管理专门的工作节点(Worker Nodes,即组件)。

The Golden Rules

黄金准则

  1. Single Responsibility: One script = One job.
  2. Encapsulation: Components are "selfish." They handle their internal logic but don't know who owns them.
  3. The Orchestrator: The root script (e.g.,
    player.gd
    ) does no logic. It only manages state and passes data between components.
  4. Decoupling: Components communicate via Signals (up) and Methods (down).

  1. 单一职责:一个脚本只负责一项工作。
  2. 封装性:组件是“独立”的。它们处理自身内部逻辑,但无需知晓自己的所属对象。
  3. 协调器原则:根脚本(如
    player.gd
    不处理任何逻辑,仅负责管理状态以及在组件间传递数据。
  4. 解耦原则:组件通过**信号(Signals,向上传递)方法(Methods,向下调用)**进行通信。

Anti-Patterns (NEVER Do This)

反模式(绝对不要这样做)

  • NEVER use deep inheritance chains (e.g.,
    Player > Entity > LivingThing > Node
    ). This creates brittle "God Classes."
  • NEVER use
    get_node("Path/To/Thing")
    or
    $
    syntax for components. This breaks if the scene tree changes.
  • NEVER let components reference the Parent directly (unless absolutely necessary via typed injection).
  • NEVER mix Input, Physics, and Game Logic in a single script.

  • 绝对不要使用深度继承链(如
    Player > Entity > LivingThing > Node
    )。这会导致脆弱的“上帝类”(God Classes)。
  • 绝对不要对组件使用
    get_node("Path/To/Thing")
    $
    语法。如果场景树结构变化,这种写法会失效。
  • 绝对不要让组件直接引用父节点(除非通过类型注入的绝对必要场景)。
  • 绝对不要在单个脚本中混合输入、物理和游戏逻辑。

Implementation Standards

实现标准

1. Connection Strategy: Typed Exports

1. 连接策略:类型化导出

Do not rely on tree order. Use explicit dependency injection via
@export
with static typing.
The "Godot Way" for strict godot-composition:
gdscript
undefined
不要依赖节点树顺序。使用带静态类型的
@export
实现显式依赖注入。
严格遵循Godot组合模式的“Godot方式”:
gdscript
undefined

The Orchestrator (e.g., player.gd)

The Orchestrator (e.g., player.gd)

class_name Player extends CharacterBody3D
class_name Player extends CharacterBody3D

Dependency Injection: Define the "slots" in the backpack

Dependency Injection: Define the "slots" in the backpack

@export var health_component: HealthComponent @export var movement_component: MovementComponent @export var input_component: InputComponent
@export var health_component: HealthComponent @export var movement_component: MovementComponent @export var input_component: InputComponent

Use Scene Unique Names (%) for auto-assignment in Editor

Use Scene Unique Names (%) for auto-assignment in Editor

or drag-and-drop in the Inspector.

or drag-and-drop in the Inspector.

undefined
undefined

2. Component Mindset

组件设计思路

Components must define
class_name
to be recognized as types.
Standard Component Boilerplate:
gdscript
class_name MyComponent extends Node
组件必须定义
class_name
才能被识别为类型。
标准组件模板:
gdscript
class_name MyComponent extends Node

Use Node for logic, Node3D/2D if it needs position

Use Node for logic, Node3D/2D if it needs position

@export var stats: Resource # Components can hold their own data signal happened_something(value)
func do_logic(delta: float) -> void: # Perform specific task pass

---
@export var stats: Resource # Components can hold their own data signal happened_something(value)
func do_logic(delta: float) -> void: # Perform specific task pass

---

Standard Components

标准组件

The Input Component (The Senses)

输入组件(感知模块)

Responsibility: Read hardware state. Store it. Do NOT act on it. State:
move_dir
,
jump_pressed
,
attack_just_pressed
.
gdscript
class_name InputComponent extends Node

var move_dir: Vector2
var jump_pressed: bool

func update() -> void:
    # Called by Orchestrator every frame
    move_dir = Input.get_vector("left", "right", "up", "down")
    jump_pressed = Input.is_action_just_pressed("jump")
职责:读取硬件状态并存储,不执行任何动作。 状态:
move_dir
(移动方向)、
jump_pressed
(跳跃触发)、
attack_just_pressed
(攻击触发)。
gdscript
class_name InputComponent extends Node

var move_dir: Vector2
var jump_pressed: bool

func update() -> void:
    # Called by Orchestrator every frame
    move_dir = Input.get_vector("left", "right", "up", "down")
    jump_pressed = Input.is_action_just_pressed("jump")

The Movement Component (The Legs)

移动组件(行动模块)

Responsibility: Manipulate physics body. Handle velocity/gravity. Constraint: Requires a reference to the physics body it moves.
gdscript
class_name MovementComponent extends Node

@export var body: CharacterBody3D # The thing we move
@export var speed: float = 8.0
@export var jump_velocity: float = 12.0

func tick(delta: float, direction: Vector2, wants_jump: bool) -> void:
    if not body: return
    
    # Handle Gravity
    if not body.is_on_floor():
        body.velocity.y -= 9.8 * delta
        
    # Handle Movement
    if direction:
        body.velocity.x = direction.x * speed
        body.velocity.z = direction.y * speed # 3D conversion
    else:
        body.velocity.x = move_toward(body.velocity.x, 0, speed)
        body.velocity.z = move_toward(body.velocity.z, 0, speed)
        
    # Handle Jump
    if wants_jump and body.is_on_floor():
        body.velocity.y = jump_velocity
        
    body.move_and_slide()
职责:操控物理体,处理速度与重力。 约束:需要持有被移动物理体的引用。
gdscript
class_name MovementComponent extends Node

@export var body: CharacterBody3D # The thing we move
@export var speed: float = 8.0
@export var jump_velocity: float = 12.0

func tick(delta: float, direction: Vector2, wants_jump: bool) -> void:
    if not body: return
    
    # Handle Gravity
    if not body.is_on_floor():
        body.velocity.y -= 9.8 * delta
        
    # Handle Movement
    if direction:
        body.velocity.x = direction.x * speed
        body.velocity.z = direction.y * speed # 3D conversion
    else:
        body.velocity.x = move_toward(body.velocity.x, 0, speed)
        body.velocity.z = move_toward(body.velocity.z, 0, speed)
        
    # Handle Jump
    if wants_jump and body.is_on_floor():
        body.velocity.y = jump_velocity
        
    body.move_and_slide()

The Health Component (The Life)

生命值组件(生命模块)

Responsibility: Manage HP, Clamp values, Signal changes. Context Agnostic: Can be put on a Player, Enemy, or a Wooden Crate.
gdscript
class_name HealthComponent extends Node

signal died
signal health_changed(current, max)

@export var max_health: float = 100.0
var current_health: float

func _ready():
    current_health = max_health

func damage(amount: float):
    current_health = clamp(current_health - amount, 0, max_health)
    health_changed.emit(current_health, max_health)
    if current_health == 0:
        died.emit()

职责:管理生命值(HP)、限制数值范围、状态变化时发送信号。 无上下文依赖性:可应用于玩家、敌人甚至木箱等任何实体。
gdscript
class_name HealthComponent extends Node

signal died
signal health_changed(current, max)

@export var max_health: float = 100.0
var current_health: float

func _ready():
    current_health = max_health

func damage(amount: float):
    current_health = clamp(current_health - amount, 0, max_health)
    health_changed.emit(current_health, max_health)
    if current_health == 0:
        died.emit()

The Orchestrator (Putting it Together)

协调器(整合所有组件)

The Orchestrator (
player.gd
) binds the components in the
_physics_process
. It acts as the bridge.
gdscript
class_name Player extends CharacterBody3D

@onready var input: InputComponent = %InputComponent
@onready var move: MovementComponent = %MovementComponent
@onready var health: HealthComponent = %HealthComponent

func _ready():
    # Connect signals (The ears)
    health.died.connect(_on_death)

func _physics_process(delta):
    # 1. Update Senses
    input.update()
    
    # 2. Pass Data to Workers (State Management)
    # The Player script decides that "Input Direction" maps to "Movement Direction"
    move.tick(delta, input.move_dir, input.jump_pressed)

func _on_death():
    queue_free()
协调器(如
player.gd
)在
_physics_process
方法中绑定所有组件,充当组件间的桥梁。
gdscript
class_name Player extends CharacterBody3D

@onready var input: InputComponent = %InputComponent
@onready var move: MovementComponent = %MovementComponent
@onready var health: HealthComponent = %HealthComponent

func _ready():
    # Connect signals (The ears)
    health.died.connect(_on_death)

func _physics_process(delta):
    # 1. Update Senses
    input.update()
    
    # 2. Pass Data to Workers (State Management)
    # The Player script decides that "Input Direction" maps to "Movement Direction"
    move.tick(delta, input.move_dir, input.jump_pressed)

func _on_death():
    queue_free()

Performance Note

性能说明

Nodes are lightweight. Do not fear adding 10-20 nodes per entity. The organizational benefit of Composition vastly outweighs the negligible memory cost of
Node
instances.
Node是轻量级的,无需担心每个实体添加10-20个节点。组合模式带来的架构组织优势,远超过
Node
实例带来的可忽略的内存开销。

Reference

参考资料

  • Master Skill: godot-master
  • 核心技能:godot-master