- 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
- Single Responsibility
-
- One script = One job.
- Encapsulation
-
- Components are "selfish." They handle their internal logic but don't know
- who
- owns them.
- The Orchestrator
-
- The root script (e.g.,
- player.gd
- ) does
- no logic
- . It only manages state and passes data between components.
- 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.
- 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 Master Skill: godot-master