godot-composition

📁 thedivergentai/gd-agentic-skills 📅 3 days ago
29
总安装量
5
周安装量
#12617
全站排名
安装命令
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-composition

Agent 安装分布

gemini-cli 5
codex 5
opencode 5
kimi-cli 4
amp 4

Skill 文档

Godot Composition Architecture

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).

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).

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.

Implementation Standards

1. Connection Strategy: Typed Exports

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.

2. Component Mindset

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

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.

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.

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.

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.

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.

Reference