
Godot Composition
- 274 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-composition for development tasks
About
godot-composition: A skill for development. This provides functionality for development workflows.
- godot-composition
Godot Composition by the numbers
- 274 all-time installs (skills.sh)
- +17 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,406 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-compositionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 274 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-composition for development tasks
Files
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).
---
Available Scripts
health_component.gd
Specialized Node for managing lifespan, damage logic, and death signals across any entity.
hit_box_component.gd
Area-based component for intercepting damage and delegating it to a HealthComponent.
hurt_box_component.gd
Area-based component for dealing damage specifically to HitBoxComponents.
velocity_component.gd
Encapsulated movement and acceleration logic for reuse across Players and Enemies.
interaction_component.gd
Decoupled interaction handler using injecting Callable logic for context-aware actions.
follower_component.gd
Decoupled tracking logic using NodePath injection for smooth entity following.
state_component_vsm.gd
Component-based state machine pattern using child nodes as individual states.
status_effect_component.gd
Managing temporary modifiers (buffs/debuffs) by stacking effect scenes as children.
visual_sync_component.gd
Separating logical state (velocity/direction) from visual representation (sprite flipping).
composition_root_init.gd
The "Orchestrator" pattern for wiring and connecting components in a parent node.
NEVER Do in Composition
- NEVER use deep inheritance chains (e.g.,
Player > Entity > LivingThing > Node) — Creates brittle "God Classes" that are hard to refactor [21]. - NEVER use `get_node()` or `$` for components — This breaks if the scene tree is rearranged. Always use
@exportor%UniqueNames[22]. - NEVER let a component reference its parent script directly — This makes the component impossible to reuse. Use signals or dependency injection [23].
- NEVER mix Input, Physics, and Game Logic in one script — This violates Single Responsibility. Split them into specialized components [24, 13].
- NEVER create components that require a specific SceneTree structure — A component should be "selfish" and only care about its own properties and direct children.
- NEVER use inheritance to "add a feature" — If you want an enemy to shoot, add a
ShootingComponent, don't make it inherit fromShooterEnemy. - NEVER hardcode component dependencies — If
CombatComponentneedsHealthComponent, look it up in_ready()or inject it via the parent [11]. - NEVER treat Godot nodes as pure data — Nodes provide lifecycle (
_process) and signals. If you only need data, use aResource. - NEVER ignore the Node lifecycle in components — Use
_enter_tree()and_exit_tree()for setup/cleanup that must happen regardless of the parent's state. - NEVER hide component points of access — Expose
NodePathorCallableproperties so the parent can wire the component in the Inspector [13].
---
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 _ready() -> void:
_validate_dependencies()
func _validate_dependencies() -> void:
# 2. Dependency-Validation: Fail early during development if setup is wrong [2]
# NOTE: assert() is stripped in release builds [10].
assert(stats != null, "Stats Resource missing on %s" % name)
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()Expert Composition Patterns
1. State-Component Pattern (FSM)
Encapsulate complex behaviors into child nodes that act as states. The parent StateMachine component delegates lifecycle calls to the active child [4, 6].
class_name StateMachine extends Node
@export var initial_state: Node
@onready var _state: Node = initial_state
func _ready() -> void:
if _state.has_method("enter"): _state.enter()
func _physics_process(delta: float) -> void:
if _state.has_method("physics_process"):
_state.physics_process(delta)
func transition_to(target_state_path: NodePath) -> void:
if _state.has_method("exit"): _state.exit()
_state = get_node(target_state_path)
if _state.has_method("enter"): _state.enter()2. Component-Registry (O(1) Lookup)
Avoid slow tree traversal (get_node) for sibling communication. The Orchestrator catalogs children in a Dictionary for instant access [3, 13].
# Inside the Orchestrator (e.g. Entity.gd)
var _components: Dictionary = {}
func _ready() -> void:
for child in get_children():
_components[child.name] = child
# Or register by group for interface-like access
for group in child.get_groups():
_components[group] = child
### 3. Dependency-Validation
Ensure critical components are present before execution. Use `assert()` in `_ready()` to fail fast during development if a required component is missing [7, 8].
Inside Orchestrator
func _ready() -> void: assert(get_node_or_null("HealthComponent") != null, "Missing HealthComponent!") assert(get_node_or_null("InputComponent") != null, "Missing InputComponent!")
func get_comp(key: StringName) -> Node:
return _components.get(key)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
- Master Skill: godot-master
Skill Evaluation Report: godot-composition
Summary
- Total Score: 112/120 (93%)
- Grade: A
- Pattern: Mindset
- Knowledge Ratio: E:A:R = 80:15:5
- Verdict: Excellent expert guidance on game architecture, effectively preventing common inheritance pitfalls.
Dimension Scores
| Dimension | Score | Max | Notes |
|---|---|---|---|
| D1: Knowledge Delta | 19 | 20 | "Rock Litmus Test" and "Backpack Model" are high-value conceptual tools. |
| D2: Mindset vs Mechanics | 14 | 15 | Strong emphasis on thinking in components. |
| D3: Anti-Pattern Quality | 14 | 15 | "The Monolith" and "The Chain" are classic, well-refuted anti-patterns. |
| D4: Specification Compliance | 15 | 15 | Perfect description with clear WHAT, WHEN, and KEYWORDS (Game specific). |
| D5: Progressive Disclosure | 13 | 15 | Good use of resources (health/hitbox examples effectively demonstrate pattern). |
| D6: Freedom Calibration | 13 | 15 | Appropriate strictness on architecture, freedom on implementation details. |
| D7: Pattern Recognition | 9 | 10 | Follows Mindset/Philosophy pattern closely. |
| D8: Practical Usability | 15 | 15 | Clear rules for communication (Up/Down) make it immediately usable. |
Critical Issues
None.
Top 3 Improvements
1. Add more complex examples of inter-component communication in the resources. 2. Elaborate on "State Management" specifics in a game loop context. 3. Add a specific "Level Design" workflow section.
class_name HealthComponent extends Node
signal health_changed(new_health: int, max_health: int)
signal died
@export var max_health: int = 100
var current_health: int
func _ready() -> void:
current_health = max_health
health_changed.emit(current_health, max_health)
func damage(amount: int) -> void:
current_health = max(0, current_health - amount)
health_changed.emit(current_health, max_health)
if current_health == 0:
died.emit()
func heal(amount: int) -> void:
current_health = min(max_health, current_health + amount)
health_changed.emit(current_health, max_health)
class_name HitboxComponent extends Area2D
@export var health_component: HealthComponent
func _ready() -> void:
if not health_component:
push_error("HitboxComponent requires a HealthComponent")
func damage(amount: int) -> void:
if health_component:
health_component.damage(amount)
# composition_root_init.gd
# The "Smart Parent" pattern for wiring components
extends CharacterBody2D
# EXPERT NOTE: The parent node acts as the 'orchestrator' or
# 'composition root' that connects disparate components.
@onready var health = $HealthComponent
@onready var hitbox = $HitBoxComponent
@onready var velocity_comp = $VelocityComponent
func _ready():
# Wire components together
hitbox.health_component = health
# Respond to component signals
health.health_depleted.connect(_on_death)
func _physics_process(delta):
# Delegation
velocity_comp.accelerate_in_direction(Input.get_vector("left", "right", "up", "down"), delta)
velocity_comp.apply_velocity(self)
func _on_death():
queue_free()
# follower_component.gd
# Decoupled tracking logic using NodePath injection
class_name FollowerComponent extends Node
# EXPERT NOTE: Using NodePath allows the component to be wired in
# the inspector by the parent, keeping it context-aware but decoupled.
@export var target_path: NodePath
var target: Node2D
func _ready() -> void:
if not target_path.is_empty():
target = get_node(target_path)
func _process(delta: float) -> void:
if is_instance_valid(target):
owner.global_position = owner.global_position.lerp(target.global_position, 5.0 * delta)
# health_component.gd
# Specialized Node for managing lifespan and damage logic
class_name HealthComponent extends Node
# EXPERT NOTE: Components should be "ignorant" of their parent.
# They emit signals up and the parent (or other components) respond.
signal health_changed(current: float, max: float)
signal health_depleted
@export var max_health: float = 100.0
@onready var current_health: float = max_health
func take_damage(amount: float) -> void:
current_health = clamp(current_health - amount, 0, max_health)
health_changed.emit(current_health, max_health)
if current_health <= 0:
health_depleted.emit()
func heal(amount: float) -> void:
current_health = clamp(current_health + amount, 0, max_health)
health_changed.emit(current_health, max_health)
# hit_box_component.gd
# Area-based component for intercepting damage
class_name HitBoxComponent extends Area2D
# EXPERT NOTE: Hitboxes delegate damage to a HealthComponent.
# This decouples the collision shape from the health logic.
@export var health_component: HealthComponent
func handle_hit(damage: float) -> void:
if health_component:
health_component.take_damage(damage)
# hurt_box_component.gd
# Area-based component for dealing damage to HitBoxes
class_name HurtBoxComponent extends Area2D
# EXPERT NOTE: Hurtboxes look for HitBoxComponents specifically,
# rather than generic physics bodies, ensuring type safety.
@export var damage: float = 10.0
func _on_area_entered(area: Area2D) -> void:
var hitbox = area as HitBoxComponent
if hitbox:
hitbox.handle_hit(damage)
# interaction_component.gd
# Handling contextual logic via Callable injection
class_name InteractionComponent extends Area2D
# EXPERT NOTE: Instead of a massive 'match' statement, the parent
# injects the specific interaction logic into this component.
var interaction_logic: Callable
func interact() -> void:
if interaction_logic.is_valid():
interaction_logic.call()
else:
push_warning("InteractionComponent on %s has no logic defined!" % owner.name)
# state_component_vsm.gd
# Component-based state machine pattern
class_name StateComponent extends Node
# EXPERT NOTE: Each state is a child node. The parent component
# manages transitions between them.
signal state_changed(new_state: String)
var current_state: Node = null
func transition_to(state_name: String) -> void:
var next_state = get_node_or_null(state_name)
if next_state:
if current_state: current_state.exit()
current_state = next_state
current_state.enter()
state_changed.emit(state_name)
# status_effect_component.gd
# Managing temporary modifiers via composition
class_name StatusEffectComponent extends Node
# EXPERT NOTE: Stacking status effects as children allows for
# easy management of durations and overlapping logic.
func apply_effect(effect_scene: PackedScene):
var effect = effect_scene.instantiate()
add_child(effect)
# Effect script handles its own timer and removal
# velocity_component.gd
# Encapsulating movement logic for reuse across entities
class_name VelocityComponent extends Node
# EXPERT NOTE: Use a velocity component to share movement code
# between Player and Enemies without deep inheritance.
@export var max_speed: float = 300.0
@export var acceleration: float = 1000.0
var velocity: Vector2 = Vector2.ZERO
func accelerate_in_direction(direction: Vector2, delta: float) -> void:
var target_velocity = direction * max_speed
velocity = velocity.move_toward(target_velocity, acceleration * delta)
func apply_velocity(character: CharacterBody2D) -> void:
character.velocity = velocity
character.move_and_slide()
# Update local velocity based on actual movement (e.g., collisions)
velocity = character.velocity
# visual_sync_component.gd
# Separating logic from visuals
class_name VisualSyncComponent extends Node
# EXPERT NOTE: This component syncs a Sprite or Mesh to the
# parent's logical state (e.g., flipping based on velocity).
@export var sprite: Sprite2D
@export var velocity_component: VelocityComponent
func _process(_delta: float) -> void:
if sprite and velocity_component:
if velocity_component.velocity.x != 0:
sprite.flip_h = velocity_component.velocity.x < 0