01Node Composition Architecture
Godot's scene tree is the composition framework. There's no ECS to bolt on, no component registry to maintain. Nodes are components, scenes are prefabs, and the tree is the entity hierarchy. The engine does composition at a higher level than traditional ECS — you get inspector editing, signals, and scene inheritance for free.
Two Approaches to Reusable Components
Register a custom type with class_name. It appears in Add Node like a built-in.
class_name HealthComponent
extends Node
## Reusable health logic — attach to any entity.
## Wire via @export in siblings or use groups.
@export var max_health: float = 100.0
@export var invincibility_time: float = 0.2
signal health_changed(new_hp: float, max_hp: float)
signal died
var _current: float
var _invincible: bool = false
func _ready() -> void:
_current = max_health
func take_damage(amount: float) -> void:
if _invincible or _current <= 0.0:
return
_current = maxf(_current - amount, 0.0)
health_changed.emit(_current, max_health)
if _current <= 0.0:
died.emit()
else:
_start_invincibility()
func heal(amount: float) -> void:
_current = minf(_current + amount, max_health)
health_changed.emit(_current, max_health)
func _start_invincibility() -> void:
_invincible = true
await get_tree().create_timer(invincibility_time).timeout
_invincible = false
Components own data + signals. The parent entity wires them. The component never hard-references siblings — it's fully reusable across any entity.
Save a node subtree as .tscn. Instantiate it via Instantiate Child Scene. Base scene changes propagate to all instances.
class_name Hitbox
extends Area3D
@export var damage: float = 25.0
@export var knockback_force: float = 10.0
@export var damage_type: DamageData.Type = DamageData.Type.PHYSICAL
## Call from AnimationPlayer to activate during attack frames
func activate() -> void:
$Shape.disabled = false
func deactivate() -> void:
$Shape.disabled = true
Scene components are best when the component has visual structure (multiple child nodes) or requires inspector layout that scripts alone can't express.
Single node, behavior-only. Stats, AI decisions, input mapping, movement logic. No child nodes needed.
Multi-node structure. Hitboxes with shapes, UI widgets with layout, VFX with particles + lights. Visual editing matters.
Inheritance = "is-a" (EnemyBase → MeleeEnemy, RangedEnemy). Composition = "has-a" (Player has HealthComponent, InputComponent, MovementComponent). Default to composition; only inherit when entities share identical node structure.
Canonical Entity Structure
02Wiring Components Together
The "call down, signal up" rule is the backbone. Parents call methods on children directly. Children emit signals upward. Siblings never reference each other directly — they go through @export wiring or through a shared parent.
Wiring Priority (Best → Fallback)
extends CharacterBody3D
## Inspector-wired references — explicit, type-safe, testable
@export var health: HealthComponent
@export var hurtbox: Area3D
@export var weapon_hitbox: Hitbox
func _ready() -> void:
# Connect child signals in the parent
health.died.connect(_on_died)
hurtbox.area_entered.connect(_on_hurtbox_hit)
func _on_hurtbox_hit(hitbox: Area3D) -> void:
if hitbox is Hitbox:
health.take_damage(hitbox.damage)
func _on_died() -> void:
queue_free()
Type-checked at editor time. Visible in Inspector. Swappable for testing. No fragile get_node() paths. In Godot 4.6, unique node IDs mean even renamed nodes keep their wiring.
Signals decouple emitter from listener. The emitter doesn't know or care who's listening.
# HealthComponent emits — doesn't know who listens
signal health_changed(current: float, maximum: float)
# HUD connects — doesn't know health internals
func _ready() -> void:
player.health.health_changed.connect(_update_bar)
func _update_bar(current: float, maximum: float) -> void:
bar.value = current / maximum
Groups broadcast to all members. Ideal for "damage all enemies in explosion radius."
# Every enemy adds itself to "enemies" group in _ready()
add_to_group("enemies")
# Explosion broadcasts damage to all enemies
get_tree().call_group("enemies", "take_damage", 50.0)
# Or query: find all HealthComponents under owner
var comps: Array[Node] = owner.find_children("*", "HealthComponent")
An autoload with global signals for truly cross-tree communication. Use sparingly — 1-3 focused autoloads max.
extends Node
## Global event bus — only for cross-tree events
signal enemy_killed(enemy_type: StringName, position: Vector3)
signal player_died
signal checkpoint_reached(checkpoint_id: int)
signal score_changed(new_score: int)
# Emitter (in any scene):
# GameEvents.enemy_killed.emit(&"goblin", global_position)
# Listener (in HUD, audio manager, achievement system, etc.):
# GameEvents.enemy_killed.connect(_on_enemy_killed)
Event bus makes debugging hard — you can't see connections in the Inspector. Prefer direct signals when emitter and listener are in the same or nearby scenes. Only use the bus for HUD updates, achievement tracking, audio triggers, and analytics.
03Custom Resources as Data Backbone
Custom Resources (extends Resource) are Godot's answer to Unity ScriptableObjects. They provide type-safe, Inspector-editable, serializable data containers that can be shared across scenes. They're the tool for data-driven design.
class_name DamageData
extends Resource
enum Type { PHYSICAL, FIRE, ICE, LIGHTNING, POISON }
@export var amount: float = 10.0
@export var type: Type = Type.PHYSICAL
@export var knockback: float = 5.0
@export var stagger_duration: float = 0.3
@export var status_effect: StatusEffect # nested Resource!
class_name WeaponStats
extends Resource
@export var name: StringName
@export var icon: Texture2D
@export var attack_speed: float = 1.0
@export var light_attack: DamageData
@export var heavy_attack: DamageData
@export var combo_chain: Array[DamageData] = []
## Use as: @export var weapon: WeaponStats
## Save as: res://data/weapons/greatsword.tres
Resources are shared by default. If two enemies export the same .tres file and one modifies it at runtime, both see the change. Fix: set resource_local_to_scene = true in the Inspector, or call resource.duplicate() in _ready().
Weapon definitions, enemy stat tables, loot tables, ability configurations, dialogue data, AI behavior parameters, level metadata. Anything that's data not behavior.
Runtime state that needs _process(). Anything requiring scene tree access. Use nodes for behavior, resources for data.
04GDScript 4.x Production Patterns
Static Typing — Non-Negotiable
Static typing gives ~2× performance for numeric/vector ops via the VM's typed instruction paths. Enforce project-wide: Project Settings → Debug → GDScript → Untyped Declaration = Error.
## Every variable, parameter, and return fully typed.
## The GDScript VM generates optimized "typed instructions."
func calculate_damage(
base: float,
multiplier: float,
resistances: Dictionary[StringName, float], # typed dict (4.4+)
damage_type: DamageData.Type,
) -> float:
var type_name: StringName = DamageData.Type.keys()[damage_type]
var resist: float = resistances.get(type_name, 0.0)
var final: float = base * multiplier * (1.0 - resist)
return maxf(final, 0.0)
@export Power Variants
@export_group("Movement")
@export var speed: float = 5.0
@export_range(0, 100, 0.5, "or_greater", "suffix:m/s") var max_speed: float
@export_exp_easing var accel_curve: float = 2.0
@export_group("Combat")
@export_enum("Slash", "Thrust", "Slam") var attack_type: int
@export_flags("Fire", "Ice", "Lightning") var elements: int
@export var weapon_data: WeaponStats # inline Resource editor!
@export_subgroup("Advanced")
@export_file("*.tres") var loot_table_path: String
## 4.4+ Inspector button:
@export_tool_button("Regenerate", "Play") var _regen = _regenerate
Performance Patterns
05World Streaming for Large Maps
Godot has no built-in world streaming. You build it or use addons. The core pattern: divide the world into chunks saved as separate .tscn files, load/unload based on player proximity using background threads.
extends Node3D
@export var chunk_size: float = 256.0
@export var load_radius: int = 3 # chunks in each direction
@export var unload_buffer: int = 1 # extra ring before unload
var _loaded_chunks: Dictionary[Vector2i, Node3D] = {}
var _loading: Dictionary[Vector2i, bool] = {}
func _physics_process(_delta: float) -> void:
var player_chunk: Vector2i = _world_to_chunk(player.global_position)
_load_nearby(player_chunk)
_unload_distant(player_chunk)
func _world_to_chunk(pos: Vector3) -> Vector2i:
return Vector2i(
floori(pos.x / chunk_size),
floori(pos.z / chunk_size)
)
func _load_nearby(center: Vector2i) -> void:
for x: int in range(-load_radius, load_radius + 1):
for z: int in range(-load_radius, load_radius + 1):
var coord: Vector2i = center + Vector2i(x, z)
if coord not in _loaded_chunks and coord not in _loading:
_request_load(coord)
func _request_load(coord: Vector2i) -> void:
var path: String = "res://world/chunk_%d_%d.tscn" % [coord.x, coord.y]
if not ResourceLoader.exists(path):
return
_loading[coord] = true
# Background thread load — non-blocking
ResourceLoader.load_threaded_request(path)
# Poll in _physics_process or use a timer
Chunx — partitions Node3D children by position, handles load/unload around a target. Open World Database (OWDB) — batch processing with time budgets and persistent state. Both on the Asset Library.
Large World Coordinates
Standard float32 precision is accurate within ~8192m from origin. Beyond that, physics jitter and rendering artifacts appear. Two solutions:
Teleport the entire world to keep the player near Vector3.ZERO. Works with standard builds. Needs careful handling of persistent positions.
Compile with precision=double. All vectors become float64. No code changes needed. ~10-15% perf overhead. Required for planet-scale.
06LOD, HLOD & Occlusion Culling
Layer all three systems — they're complementary, not competing.
Visibility Ranges (HLOD Pattern)
# Individual house meshes:
visibility_range_begin = 0 # visible from 0m
visibility_range_end = 150 # disappear at 150m
visibility_range_fade = Dependencies # smooth fade
visibility_parent = BatchOfHouses # HLOD parent
# BatchOfHouses (merged low-poly mesh):
visibility_range_begin = 100 # appear from 100m (overlap for fade)
visibility_range_end = 0 # 0 = visible to infinity
Occlusion Culling Setup
073D Performance at Scale
Draw Call Reduction
MultiMeshInstance3D renders thousands of identical objects in a single draw call. Essential for vegetation, debris, rocks, crowd extras.
var mm: MultiMesh = MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_3D
mm.instance_count = 10000
mm.mesh = rock_mesh
for i: int in range(10000):
var t: Transform3D = Transform3D()
t.origin = _random_position()
t = t.rotated(Vector3.UP, randf() * TAU)
mm.set_instance_transform(i, t)
$MultiMeshInstance3D.multimesh = mm
Collision Shape Performance Ranking
| Shape | Speed | Use For |
|---|---|---|
| Sphere | Fastest | Detection areas, broad-phase, projectiles |
| Box | Very Fast | Buildings, crates, triggers |
| Capsule | Fast | Characters, humanoid hitboxes |
| ConvexPolygon | Moderate | Irregular static props |
| ConcavePolygon | Slow — static only! | Level geometry only. Never on moving bodies. |
Never toggle CollisionShape3D.disabled directly inside physics callbacks. Always use set_deferred("disabled", true) or the shape will crash.
Physics Layer Organization
| Layer | Name | Used By |
|---|---|---|
| 1 | World | Static geometry, terrain, walls |
| 2 | Player | Player CharacterBody3D |
| 3 | Enemies | Enemy CharacterBody3D |
| 4 | Hitboxes | Weapon hitbox Area3Ds |
| 5 | Hurtboxes | Damageable area Area3Ds |
| 6 | Projectiles | Bullets, arrows, spells |
| 7 | Interactables | Doors, chests, NPCs |
| 8 | Detection | Aggro range, trigger volumes |
08Hitbox / Hurtbox System
The canonical pattern for melee and projectile damage in action games. Hitboxes belong to weapons, hurtboxes belong to damageable entities. The hurtbox detects the hitbox — this lets each entity manage its own damage response.
Area3D • monitoring=OFF
layer=4 (Hitboxes) ← detected by → Entity Hurtbox
Area3D • monitoring=ON
mask=4 (Hitboxes)
## In the entity script (enemy.gd, player.gd, etc.)
@export var health: HealthComponent
@export var hurtbox: Area3D
func _ready() -> void:
hurtbox.area_entered.connect(_on_hit)
func _on_hit(area: Area3D) -> void:
if area is Hitbox:
var hb: Hitbox = area as Hitbox
health.take_damage(hb.damage)
# Knockback direction: from hitbox toward us
var dir: Vector3 = (global_position - hb.global_position).normalized()
velocity += dir * hb.knockback_force
Keep CollisionShape3D.disabled = true by default. Use AnimationPlayer property tracks to set disabled = false only during active attack frames. This gives frame-precise hit windows with zero code.
monitoring property should be set to:monitoring = false. The hurtbox (on the damageable entity) has monitoring = true and detects incoming hitboxes. This way each entity controls its own damage handling.09Node-Based State Machines
Each state is a child Node. Clean separation, easy debugging (active state visible in Remote tree), and plays well with Godot's architecture.
class_name State
extends Node
## Override these in concrete states.
## Return a sibling State node to transition, or null to stay.
var entity: CharacterBody3D # set by StateMachine
func enter() -> void:
pass
func exit() -> void:
pass
func process_input(event: InputEvent) -> State:
return null
func process_frame(delta: float) -> State:
return null
func process_physics(delta: float) -> State:
return null
class_name StateMachine
extends Node
@export var initial_state: State
var current_state: State
func _ready() -> void:
# Wire entity reference to all child states
for child: Node in get_children():
if child is State:
child.entity = owner as CharacterBody3D
_transition(initial_state)
func _unhandled_input(event: InputEvent) -> void:
var next: State = current_state.process_input(event)
if next: _transition(next)
func _process(delta: float) -> void:
var next: State = current_state.process_frame(delta)
if next: _transition(next)
func _physics_process(delta: float) -> void:
var next: State = current_state.process_physics(delta)
if next: _transition(next)
func _transition(new_state: State) -> void:
if current_state:
current_state.exit()
current_state = new_state
current_state.enter()
extends State
@export var attack_anim: StringName = &"attack_1"
var _combo_queued: bool = false
func enter() -> void:
_combo_queued = false
entity.anim_tree["parameters/attack/request"] = \
AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE
func process_input(event: InputEvent) -> State:
if event.is_action_pressed("attack"):
_combo_queued = true
return null
func process_physics(delta: float) -> State:
# Check if animation finished
if not entity.anim_tree["parameters/attack/active"]:
if _combo_queued:
return $"../ComboState" # chain to next attack
return $"../IdleState"
return null
10AnimationTree for Combat
AnimationTree with AnimationNodeStateMachine root handles blending, transitions, and combo chains. It's separate from (but coordinates with) your code state machine.
Key Transition Modes
| Mode | Behavior | Use For |
|---|---|---|
| Immediate | Switch now, blend over xfade time | Hurt reaction, dodge cancel |
| Sync | Switch at same normalized time | Walk↔run blending |
| At End | Wait for current to finish, then switch | Combo chains (attack1→attack2) |
Root Motion Pattern
func _physics_process(delta: float) -> void:
# Get animation-driven displacement
var root_motion: Vector3 = anim_tree.get_root_motion_position()
# Convert to velocity (accounts for delta internally)
velocity = root_motion / delta
# Apply gravity on top
velocity.y -= gravity * delta
move_and_slide()
Your code state machine handles game logic (can I attack? am I stunned?). The AnimationTree state machine handles animation blending. They coordinate but aren't the same thing. The code SM drives the anim SM, not the other way around.
11Hidden Gem Nodes
Nodes that solve common problems without custom code. Stop reinventing these.
VisibleOnScreenEnabler3D
Automatically enables/disables processing when the node enters or leaves the camera frustum. Zero code — just add the node and set which processing to toggle.
# Add VisibleOnScreenEnabler3D as child of any entity
# Set enable_mode = ENABLE_MODE_PROCESS
# That's it. Off-screen enemies stop processing.
# For manual control, use VisibleOnScreenNotifier3D instead:
# Emits screen_entered / screen_exited signals
# Use for: LOD switching, AI activation, sound culling
RemoteTransform3D
Pushes this node's transform to a remote node without parent-child relationship. Camera follow, UI elements tracking 3D positions, weapon attachment points.
# Player scene:
# Player (CharacterBody3D)
# └─ CameraTarget (RemoteTransform3D)
# remote_path = ../../Camera3D
# update_rotation = false # camera handles own rotation
# Camera stays in world root, receives position from player
# No reparenting, no signal wiring, works with scene changes
More Underused Nodes
| Node | Replaces |
|---|---|
| SubViewport | Custom render-to-texture. Minimaps, portals, picture-in-picture, split-screen. |
| Area3D (gravity override) | Custom gravity code. Set gravity_space_override for zero-G zones, underwater, gravity wells. |
| AnimationPlayer (non-character) | Custom tween code. Animate ANY property on ANY node — UI transitions, material changes, camera moves, cutscenes. |
| NavigationLink3D | Custom jump-point logic. Connects disconnected navmesh regions for elevators, jump points, teleporters. |
| NavigationObstacle3D | Custom avoidance. Dynamic obstacles that agents avoid without rebaking. |
12What's New in Godot 4.6
Released January 2026. The theme is production polish and workflow improvements.
Biggest Hits for Action Games
Jolt is now the default 3D physics engine for all new projects. Same API as Godot Physics — zero code changes. Better collision stability, performance, and determinism. Existing projects keep their current engine.
IKModifier3D with 5 solver types: TwoBoneIK3D (foot/hand placement), SplineIK3D (tails/tentacles), FABRIK3D, CCDIK3D, JacobianIK3D. Supports twist constraints, angular limits, and targets set to Node3D objects.
Every node gets a persistent internal ID. Renaming or moving nodes no longer breaks references in inherited/instantiated scenes. Massive for large projects. Run Project → Tools → Upgrade Project Files to assign IDs.
Rendering & Performance
| Feature | Impact |
|---|---|
| Rewritten SSR | Dramatically improved quality. Full-res and half-res modes. |
| GLES3 SSAO | SSAO on mobile/GLES3 renderer (previously Forward+ only) |
| D3D12 default (Windows) | Better perf on Windows. Vulkan still available. |
| 2D GPU batching | 1.1–7× improvement in GPU-bound 2D scenarios |
| 2× faster 3D texture imports | Halved iteration time for large asset pipelines |
| Delta patch PCKs | ~75% smaller game updates |
Editor Quality of Life
| Feature | What It Does |
|---|---|
| ObjectDB Snapshots | Capture live object lists, diff between snapshots to find memory leaks |
| Step Out (Debugger) | Step out of current function — finally |
| Tracy/Perfetto for GDScript | Profile GDScript functions in Tracy profiler |
| Floating docks | Detach and float any dock panel — multi-monitor setups |
| Drag-drop @export gen | Drag a node to a script to auto-generate @export var |