godot-composition by thedivergentai/gd-agentic-skills
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-composition此技能倡导组合优于继承("拥有" 与 "是" 的关系)。在 Godot 中,节点就是组件。一个复杂的实体(如玩家)只是一个管理着专门工作节点(组件)的协调器。
player.gd)不执行逻辑。它只管理状态并在组件间传递数据。用于管理任何实体的生命周期、伤害逻辑和死亡信号的专用节点。
基于区域的组件,用于拦截伤害并将其委托给 HealthComponent。
基于区域的组件,专门用于对 HitBoxComponents 造成伤害。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
封装的移动和加速逻辑,可在玩家和敌人之间复用。
解耦的交互处理器,通过注入 Callable 逻辑来实现上下文感知操作。
解耦的追踪逻辑,使用 NodePath 注入来实现平滑的实体跟随。
基于组件的状态机模式,使用子节点作为独立状态。
通过将效果场景作为子节点堆叠来管理临时修饰符(增益/减益)。
将逻辑状态(速度/方向)与视觉表现(精灵翻转)分离。
用于在父节点中连接和组合组件的"协调器"模式。
Player > Entity > LivingThing > Node)—— 这会创建脆弱且难以重构的"上帝类" [21]。get_node() 或 $ 来获取组件 —— 如果场景树被重新排列,这会失效。始终使用 @export 或 %UniqueNames [22]。ShootingComponent,而不是让它继承自 ShooterEnemy。CombatComponent 需要 HealthComponent,在 _ready() 中查找它或通过父节点注入 [11]。_process)和信号。如果你只需要数据,请使用 Resource。_enter_tree() 和 _exit_tree() 来进行无论父节点状态如何都必须执行的设置/清理工作。NodePath 或 Callable 属性,以便父节点可以在检查器中连接组件 [13]。不要依赖树顺序。通过带有静态类型的 @export 使用显式的依赖注入。
严格的 godot-composition 的"Godot 方式":
# 协调器 (例如,player.gd)
class_name Player extends CharacterBody3D
# 依赖注入:定义背包中的"插槽"
@export var health_component: HealthComponent
@export var movement_component: MovementComponent
@export var input_component: InputComponent
# 使用场景唯一名称 (%) 以便在编辑器中自动分配,
# 或在检查器中拖放。
组件必须定义 class_name 才能被识别为类型。
标准组件样板:
class_name MyComponent extends Node
# 逻辑使用 Node,如果需要位置则使用 Node3D/2D
@export var stats: Resource # 组件可以持有自己的数据
signal happened_something(value)
func do_logic(delta: float) -> void:
# 执行特定任务
pass
职责:读取硬件状态。存储它。不要对其采取行动。状态:move_dir、jump_pressed、attack_just_pressed。
class_name InputComponent extends Node
var move_dir: Vector2
var jump_pressed: bool
func update() -> void:
# 由协调器每帧调用
move_dir = Input.get_vector("left", "right", "up", "down")
jump_pressed = Input.is_action_just_pressed("jump")
职责:操纵物理体。处理速度/重力。约束:需要一个对其移动的物理体的引用。
class_name MovementComponent extends Node
@export var body: CharacterBody3D # 我们要移动的东西
@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
# 处理重力
if not body.is_on_floor():
body.velocity.y -= 9.8 * delta
# 处理移动
if direction:
body.velocity.x = direction.x * speed
body.velocity.z = direction.y * speed # 3D 转换
else:
body.velocity.x = move_toward(body.velocity.x, 0, speed)
body.velocity.z = move_toward(body.velocity.z, 0, speed)
# 处理跳跃
if wants_jump and body.is_on_floor():
body.velocity.y = jump_velocity
body.move_and_slide()
职责:管理生命值,限制数值,发出变更信号。上下文无关:可以放在玩家、敌人或木箱上。
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()
协调器 (player.gd) 在 _physics_process 中绑定组件。它充当桥梁。
class_name Player extends CharacterBody3D
@onready var input: InputComponent = %InputComponent
@onready var move: MovementComponent = %MovementComponent
@onready var health: HealthComponent = %HealthComponent
func _ready():
# 连接信号(耳朵)
health.died.connect(_on_death)
func _physics_process(delta):
# 1. 更新感官
input.update()
# 2. 将数据传递给工作者(状态管理)
# Player 脚本决定"输入方向"映射到"移动方向"
move.tick(delta, input.move_dir, input.jump_pressed)
func _on_death():
queue_free()
节点是轻量级的。不要害怕为每个实体添加 10-20 个节点。组合式架构的组织优势远远超过 Node 实例可忽略不计的内存成本。
每周安装数
91
代码仓库
GitHub 星标数
59
首次出现
2026年2月10日
安全审计
安装于
gemini-cli88
opencode87
codex87
kimi-cli84
github-copilot84
amp84
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).
player.gd) does no logic. It only manages state and passes data between components.Specialized Node for managing lifespan, damage logic, and death signals across any entity.
Area-based component for intercepting damage and delegating it to a HealthComponent.
Area-based component for dealing damage specifically to HitBoxComponents.
Encapsulated movement and acceleration logic for reuse across Players and Enemies.
Decoupled interaction handler using injecting Callable logic for context-aware actions.
Decoupled tracking logic using NodePath injection for smooth entity following.
Component-based state machine pattern using child nodes as individual states.
Managing temporary modifiers (buffs/debuffs) by stacking effect scenes as children.
Separating logical state (velocity/direction) from visual representation (sprite flipping).
The "Orchestrator" pattern for wiring and connecting components in a parent node.
Player > Entity > LivingThing > Node) — Creates brittle "God Classes" that are hard to refactor [21].get_node() or $ for components — This breaks if the scene tree is rearranged. Always use @export or %UniqueNames [22].ShootingComponent, don't make it inherit from ShooterEnemy.Do not rely on tree order. Use explicit dependency injection via @export with static typing.
The "Godot Way" for strict godot-composition:
# The Orchestrator (e.g., player.gd)
class_name Player extends CharacterBody3D
# Dependency Injection: Define the "slots" in the backpack
@export var health_component: HealthComponent
@export var movement_component: MovementComponent
@export var input_component: InputComponent
# Use Scene Unique Names (%) for auto-assignment in Editor
# or drag-and-drop in the Inspector.
Components must define class_name to be recognized as types.
Standard Component Boilerplate:
class_name MyComponent extends Node
# 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
Responsibility : Read hardware state. Store it. Do NOT act on it. State : move_dir, jump_pressed, attack_just_pressed.
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")
Responsibility : Manipulate physics body. Handle velocity/gravity. Constraint : Requires a reference to the physics body it moves.
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()
Responsibility : Manage HP, Clamp values, Signal changes. Context Agnostic : Can be put on a Player, Enemy, or a Wooden Crate.
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 (player.gd) binds the components in the _physics_process. It acts as the bridge.
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()
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.
Weekly Installs
91
Repository
GitHub Stars
59
First Seen
Feb 10, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli88
opencode87
codex87
kimi-cli84
github-copilot84
amp84
Android 整洁架构指南:模块化设计、依赖注入与数据层实现
1,300 周安装
CombatComponent needs HealthComponent, look it up in _ready() or inject it via the parent [11]._process) and signals. If you only need data, use a Resource._enter_tree() and _exit_tree() for setup/cleanup that must happen regardless of the parent's state.NodePath or Callable properties so the parent can wire the component in the Inspector [13].