Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when designing scene tree structure — composition vs inheritance, when to split scenes, node hierarchy patterns
.claude/skills/jame581-scene-organization/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 79% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 96% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 90% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 101% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 108% | 0% |
A guide for structuring Godot 4.3+ scene trees: when to split, when to compose, and how nodes should communicate.
> Related skills: component-system for composition patterns, event-bus for decoupled communication, godot-brainstorming for scene tree planning, 2d-essentials for TileMapLayer and CanvasLayer organization.
Scenes are building blocks. Each scene encapsulates exactly one concept — a player, an enemy, a health bar, a weapon. A scene should be understandable in isolation, reusable without modification, and replaceable without breaking its neighbors.
> One scene = one responsibility. If you struggle to name a scene in two words or fewer, it is probably doing too much.
Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
├── HealthComponent
├── HitboxComponent
├── StateMachine
└── AnimationPlayerHealthComponent, HitboxComponent, and StateMachine are separate .tscn files instantiated as child scenes. Any entity that needs health — enemy, destructible crate, boss — can include HealthComponent without duplicating logic.
GDScript
gdscript# health_component.gd class_name HealthComponent extends Node signal health_changed(old_value: int, new_value: int) signal died @export var max_health: int = 100 var current_health: int func _ready() -> void: current_health = max_health func take_damage(amount: int) -> void: if amount <= 0: return var old_health := current_health current_health = max(0, current_health - amount) health_changed.emit(old_health, current_health) if current_health == 0: died.emit() func heal(amount: int) -> void: if amount <= 0: return var old_health := current_health current_health = min(max_health, current_health + amount) health_changed.emit(old_health, current_health) func is_alive() -> bool: return current_health > 0
C#
csharp// HealthComponent.cs using Godot; [GlobalClass] public partial class HealthComponent : Node { [Signal] public delegate void HealthChangedEventHandler(int oldValue, int newValue); [Signal] public delegate void DiedEventHandler(); [Export] public int MaxHealth { get; set; } = 100; public int CurrentHealth { get; private set; } public override void _Ready() { CurrentHealth = MaxHealth; } public void TakeDamage(int amount) { if (amount <= 0) return; int oldHealth = CurrentHealth; CurrentHealth = Mathf.Max(0, CurrentHealth - amount); EmitSignal(SignalName.HealthChanged, oldHealth, CurrentHealth); if (CurrentHealth == 0) EmitSignal(SignalName.Died); } public void Heal(int amount) { if (amount <= 0) return; int oldHealth = CurrentHealth; CurrentHealth = Mathf.Min(MaxHealth, CurrentHealth + amount); EmitSignal(SignalName.HealthChanged, oldHealth, CurrentHealth); } public bool IsAlive() => CurrentHealth > 0; }
Inheritance suits cases where scenes share structure, not just behavior — when child scenes are variations of the same thing with identical node layout and only a few exported properties differ.
Good candidates:
Enemy → Orc, Goblin — same bones (Sprite2D, CollisionShape2D, HealthComponent, AI), different stats and artWeapon → Sword, Bow — same slot attachment logic, different animations and damage typePickup → HealthPickup, AmmoPickup — same Area2D + CollisionShape2D + animation, different effect on collection| Scenario | Pattern | |---|---| | You would copy-paste the entire scene and change a few exported properties | Inheritance | | You want to mix and match a subset of nodes across different entity types | Composition |
.tscn file [Parent]
/ \
[Child A] [Child B]
\
[Child C]A child node announces that something happened. The parent — or any node that has connected to the signal — decides what to do about it. This keeps children ignorant of their context and fully reusable.
gdscript# Child emits; it does not know who is listening health_component.died.connect(_on_player_died)
A parent drives its children by calling their methods directly. The parent owns the reference; the child exposes a clean API and does not need to know about its parent.
gdscript# Parent calls into child $HealthComponent.take_damage(10) $AnimationPlayer.play("hurt")
For communication between scenes that have no ancestor–descendant relationship — e.g., an enemy notifying the HUD — use an Autoload event bus. Emitting on the bus decouples sender from receiver entirely.
gdscript# Autoload: EventBus.gd signal enemy_killed(enemy: Enemy) # Enemy scene EventBus.enemy_killed.emit(self) # HUD scene EventBus.enemy_killed.connect(_on_enemy_killed)
C#
csharp// Pattern 1: Signals travel up (child → parent) // Child emits; it does not know who is listening. public partial class Player : CharacterBody2D { public override void _Ready() { var health = GetNode<HealthComponent>("HealthComponent"); health.Died += OnPlayerDied; } private void OnPlayerDied() { // Parent reacts — child HealthComponent stays ignorant of context } } // Pattern 2: Method calls travel down (parent → child) // Parent drives children by calling their methods directly. public partial class Level : Node2D { public override void _Ready() { var health = GetNode<HealthComponent>("Player/HealthComponent"); health.TakeDamage(10); var anim = GetNode<AnimationPlayer>("Player/AnimationPlayer"); anim.Play("hurt"); } } // Pattern 3: EventBus travels sideways (peer → peer) // EventBus.cs — registered as an Autoload singleton named "EventBus" public partial class EventBus : Node { [Signal] public delegate void EnemyKilledEventHandler(Enemy enemy); } // Enemy scene — emits on the bus; does not reference HUD public partial class Enemy : CharacterBody2D { private void Die() { var bus = GetNode<EventBus>("/root/EventBus"); bus.EmitSignal(EventBus.SignalName.EnemyKilled, this); QueueFree(); } } // HUD scene — subscribes on the bus; does not reference Enemy public partial class Hud : CanvasLayer { public override void _Ready() { var bus = GetNode<EventBus>("/root/EventBus"); bus.EnemyKilled += OnEnemyKilled; } private void OnEnemyKilled(Enemy enemy) { // Update kill counter, score, etc. } }
Enemy (CharacterBody2D)
├── Visuals
│ ├── Sprite2D
│ └── AnimationPlayer
├── Collision
│ └── CollisionShape2D
├── Components
│ ├── HealthComponent
│ └── HitboxComponent
└── AI
├── NavigationAgent2D
└── StateMachineGroup by concern using plain Node containers (Visuals, Collision, Components, AI). Each sub-group can be collapsed in the editor and worked on independently.
HUD (CanvasLayer)
├── MarginContainer
│ ├── TopBar
│ │ ├── HealthBar
│ │ └── ResourceBar
│ └── BottomBar
│ ├── Hotbar
│ └── MiniMap
└── PauseMenuCanvasLayer ensures HUD elements are always rendered on top. MarginContainer handles safe-area padding. TopBar, BottomBar, and PauseMenu are separate instantiated scenes so each can be edited without opening the root HUD scene.
Level01 (Node2D)
├── TileMapLayer
├── Entities
│ ├── Player (instance)
│ └── Enemies (Node2D)
│ ├── Orc (instance)
│ └── Goblin (instance)
├── Pickups (Node2D)
├── Navigation
│ └── NavigationRegion2D
└── Camera2DThe level scene is a composition root — it owns the layout and spawns instances, but contains no gameplay logic itself. Entities, Pickups, and Navigation are plain Node2D containers used for organizational grouping and to simplify get_children() iteration.
HealthComponent, StateMachine, etc.) are separate .tscn filesget_parent() chainsget_parent().get_parent() or get_node("../../SomeNode") paths in codeVisuals, Components, AI, etc.) for readability| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 18,578 | 15,383 | -17% | 1 | 1 | 0% | 2,938 | 5,267 | +79% | 0 | 0 | — |
case-02 | pass→pass | 17,080 | 17,482 | +2% | 1 | 1 | 0% | 2,851 | 5,719 | +101% | 0 | 0 | — |
case-03 | pass→pass | 11,538 | 9,244 | -20% | 1 | 1 | 0% | 2,053 | 4,266 | +108% | 0 | 0 | — |
case-04 | pass→pass | 7,034 | 4,274 | -39% | 1 | 1 | 0% | 1,222 | 3,262 | +167% | 0 | 0 | — |
case-05 | pass→pass | 15,981 | 14,534 | -9% | 1 | 1 | 0% | 2,532 | 5,175 | +104% | 0 | 0 | — |
case-06 | fail→pass | 14,855 | 11,319 | -24% | 1 | 1 | 0% | 2,182 | 4,281 | +96% | 0 | 0 | — |
case-07 | pass→pass | 15,592 | 9,864 | -37% | 1 | 1 | 0% | 2,301 | 4,133 | +80% | 0 | 0 | — |
case-08 | pass→pass | 11,452 | 9,406 | -18% | 1 | 1 | 0% | 1,733 | 3,954 | +128% | 0 | 0 | — |
case-09 | pass→pass | 14,157 | 11,565 | -18% | 1 | 1 | 0% | 2,536 | 4,593 | +81% | 0 | 0 | — |
case-10 | pass→pass | 15,612 | 15,611 | -0% | 1 | 1 | 0% | 2,566 | 5,196 | +102% | 0 | 0 | — |
case-11 | pass→pass | 9,775 | 6,892 | -29% | 1 | 1 | 0% | 1,868 | 3,940 | +111% | 0 | 0 | — |
case-12 | fail→fail | 13,571 | 10,446 | -23% | 1 | 1 | 0% | 2,315 | 4,580 | +98% | 0 | 0 | — |
case-13 | pass→pass | 16,729 | 11,444 | -32% | 1 | 1 | 0% | 2,486 | 4,480 | +80% | 0 | 0 | — |
case-14 | pass→pass | 14,320 | 13,042 | -9% | 1 | 1 | 0% | 2,246 | 4,945 | +120% | 0 | 0 | — |
case-15 | fail→pass | 15,774 | 11,825 | -25% | 1 | 1 | 0% | 2,456 | 4,666 | +90% | 0 | 0 | — |
case-16 | pass→pass | 8,005 | 3,721 | -54% | 1 | 1 | 0% | 1,183 | 3,116 | +163% | 0 | 0 | — |
case-17 | pass→pass | 6,381 | 4,796 | -25% | 1 | 1 | 0% | 906 | 3,272 | +261% | 0 | 0 | — |
case-18 | pass→pass | 7,147 | 6,716 | -6% | 1 | 1 | 0% | 1,323 | 3,845 | +191% | 0 | 0 | — |
case-19 | pass→pass | 10,657 | 7,344 | -31% | 1 | 1 | 0% | 1,485 | 3,749 | +152% | 0 | 0 | — |
case-20 | pass→pass | 13,839 | 14,875 | +7% | 1 | 1 | 0% | 2,268 | 4,962 | +119% | 0 | 0 | — |
case-21 | pass→pass | 16,406 | 13,126 | -20% | 1 | 1 | 0% | 3,045 | 5,093 | +67% | 0 | 0 | — |
case-22 | pass→pass | 12,239 | 16,427 | +34% | 1 | 1 | 0% | 2,123 | 5,382 | +154% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.