skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
jame581/godotprompter120 installs

dependency-injection

Use when managing dependencies between systems — autoloads, service locators, @export injection, and scene injection patterns

How do I install this agent skill?

npx skills add https://github.com/jame581/godotprompter --skill dependency-injection
view source ↗

Is this agent skill safe to install?

  • Gen Agent Trust Hubpass

    The skill provides architectural guidance and code examples for implementing dependency injection patterns in Godot 4.3+ (GDScript and C#). It is purely educational and contains no malicious code, data exfiltration patterns, or security vulnerabilities.

  • Socketpass

    No alerts

  • Snykpass

    Risk: LOW · No issues

What does this agent skill do?

Dependency Injection in Godot 4.3+

Patterns for wiring dependencies between systems so nodes stay loosely coupled, swappable, and testable. All examples target Godot 4.3+ with no deprecated APIs.

Related skills: godot-testing for test-friendly architecture, event-bus for signal-based decoupling, godot-project-setup for autoload registration.


1. The Problem

Tight coupling makes code hard to test, extend, and swap. The most common form in Godot is reaching directly into a global autoload from everywhere in the codebase.

# BAD — tight coupling via direct autoload access scattered everywhere

# player.gd
func take_damage(amount: int) -> void:
    health -= amount
    AudioManager.play_sfx("hurt")          # hard dependency on AudioManager
    UIManager.update_health_bar(health)    # hard dependency on UIManager
    if health <= 0:
        GameState.record_death()           # hard dependency on GameState

# enemy.gd
func attack() -> void:
    AudioManager.play_sfx("attack")        # same AudioManager dependency again
// BAD — tight coupling via direct autoload / global access scattered everywhere

// Player.cs
public partial class Player : CharacterBody3D
{
    private int _health = 100;

    public void TakeDamage(int amount)
    {
        _health -= amount;
        GetNode<AudioManager>("/root/AudioManager").PlaySfx("hurt");        // hard dependency
        GetNode<UIManager>("/root/UIManager").UpdateHealthBar(_health);     // hard dependency
        if (_health <= 0)
            GetNode<GameState>("/root/GameState").RecordDeath();            // hard dependency
    }
}

// Enemy.cs
public partial class Enemy : CharacterBody3D
{
    public void Attack()
    {
        GetNode<AudioManager>("/root/AudioManager").PlaySfx("attack");      // same dependency again
    }
}

Problems with this approach:

  • Every node that calls AudioManager directly is coupled to its concrete implementation.
  • Swapping AudioManager for a different implementation requires changing every caller.
  • Unit-testing Player in isolation is impossible — AudioManager, UIManager, and GameState must all exist and be valid.
  • Autoload initialization order bugs silently break behaviour when scenes load.
  • Hidden dependencies make it hard to see what a class actually needs to function.

2. Approach Comparison

PatternComplexityTestabilityBest For
AutoloadsLowLowTruly global singletons: audio, settings, platform services
@export InjectionLowHighMost nodes — wire deps in the editor, no runtime lookup needed
Service LocatorMediumMediumPlugins, optional systems, swappable implementations at runtime
Scene InjectionLowHighParent-to-child wiring: Level sets up Enemy, HUD sets up sub-panels

3. Autoloads as Singletons

Register a script in Project Settings → Autoload for global access (AudioManager.play_sfx(...), GameState.score = 100). Best for cross-cutting concerns: audio, save state, event bus, settings. Resist autoloading domain-specific systems (those should be scene-injected).

See references/autoloads.md for the full AudioManager example (SFX + crossfade music) in GDScript + C#.


4. @export Node Injection

Expose collaborator nodes as @export var health_component: HealthComponent, then wire in the Inspector or via parent scene. Lifecycle: @export properties are assigned BEFORE _ready().

See references/export-injection.md for full @export patterns (GDScript + C#) and lifecycle notes.


5. Service Locator Pattern

A central registry autoload mapping String keys to service instances. Services register themselves at _ready(), deregister at _exit_tree(), consumers call ServiceLocator.get(name). Useful when you want flexible runtime swap of implementations (testing, mods, A/B variants).

See references/service-locator.md for the full Service Locator (GDScript + C#) with typed helper methods.


6. Scene Injection

Parent scene loads its children, then in _ready() walks the tree assigning dependencies (enemy.player = $Player). Children declare @export properties but the parent — not the Inspector — sets them. Best for game-specific dependencies that change per level.

See references/scene-injection.md for the parent-injects-children pattern (GDScript + C#).


7. Testing with Dependency Injection

Injecting fakes / test doubles is what makes nodes testable. For autoloads: mock-replace before the test scene loads. For @export injection: swap the export to a test double. For Service Locator: register a fake under the same key.

See references/testing-with-di.md for GUT-based test patterns showing each injection technique.


8. When to Use What

SituationRecommended Pattern
Service used by nearly every node in every sceneAutoload singleton
Node needs 1–3 deps, scene is editor-authored@export injection
System is optional or swappable at runtimeService Locator
Parent scene constructs children and knows their needsScene injection
Writing tests for a node with external dependencies@export or property injection + stubs
Plugin that must work in any projectService Locator (self-registers, no assumptions)
Two sibling nodes need the same depLet their parent hold it and inject downward

Quick decision guide:

Does every scene in the project need it?
  YES → Autoload singleton
  NO  ↓

Is the dependency known at edit-time and wired in the Inspector?
  YES → @export injection
  NO  ↓

Does the dependency need to be swapped at runtime (plugins, A/B testing)?
  YES → Service Locator
  NO  ↓

Does a parent scene own both the consumer and the dependency?
  YES → Scene injection
  NO  → Reconsider — either promote to autoload or restructure ownership

9. Anti-patterns

Autoload for everything

# BAD — GameManager, EnemySpawner, InventorySystem, DialogueSystem all as autoloads.
# Every node in the game is coupled to every other system at module level.
# Test one component → must initialise all autoloads.

# GOOD — Only AudioManager, Settings, and SceneTransition are autoloads.
# EnemySpawner is a node in the Level scene, injected into enemies that need it.
// BAD — GameManager, EnemySpawner, InventorySystem, DialogueSystem all as autoloads.
// Every node in the game is coupled to every other system at module level.
// Test one component → must initialise all autoloads.

// GOOD — Only AudioManager, Settings, and SceneTransition are autoloads.
// EnemySpawner is a node in the Level scene, injected into enemies that need it.

Deep dependency chains

# BAD — Player needs HealthComponent, which needs AudioManager,
# which needs SoundBank, which needs FileSystem...
# A change deep in the chain breaks everything above it.

# GOOD — flatten: HealthComponent takes only AudioManager (or a narrow interface).
# Each node declares only immediate dependencies.
// BAD — Player needs HealthComponent, which needs AudioManager,
// which needs SoundBank, which needs FileSystem...
// A change deep in the chain breaks everything above it.

// GOOD — flatten: HealthComponent takes only AudioManager (or a narrow interface).
// Each node declares only immediate dependencies.

Circular dependencies

# BAD
# PlayerController._ready() calls ServiceLocator.get_service("inventory")
# InventorySystem._ready() calls ServiceLocator.get_service("player")
# Neither can fully initialise because the other isn't ready yet.

# GOOD — break the cycle with a signal.
# InventorySystem emits item_used; PlayerController connects to it.
# PlayerController never holds a reference to InventorySystem at all.
// BAD
// PlayerController._Ready() calls ServiceLocator.GetService("inventory")
// InventorySystem._Ready() calls ServiceLocator.GetService("player")
// Neither can fully initialise because the other isn't ready yet.

// GOOD — break the cycle with a signal.
// InventorySystem emits ItemUsed; PlayerController connects to it.
// PlayerController never holds a reference to InventorySystem at all.

Service Locator as a god object

# BAD — everything is registered: enemies, UI panels, individual nodes.
# ServiceLocator becomes a second, untyped scene tree.

# GOOD — only register stable, long-lived services (audio, analytics, save system).
# Short-lived nodes are wired by their parent via scene injection.
// BAD — everything is registered: enemies, UI panels, individual nodes.
// ServiceLocator becomes a second, untyped scene tree.

// GOOD — only register stable, long-lived services (audio, analytics, save system).
// Short-lived nodes are wired by their parent via scene injection.

Forgetting null checks after injection

# BAD — crashes if the @export was never set in the editor
func take_damage(amount: int) -> void:
    audio.play_sfx("hurt")   # NullReferenceError if audio was not wired

# GOOD — guard or assert clearly
func take_damage(amount: int) -> void:
    assert(audio != null, "HealthComponent: audio dependency was not injected")
    audio.play_sfx("hurt")

# OR — treat it as optional
func take_damage(amount: int) -> void:
    if audio != null:
        audio.play_sfx("hurt")
// BAD — crashes if the [Export] was never set in the editor
public void TakeDamage(int amount)
{
    _audio.PlaySfx("hurt");   // NullReferenceException if _audio was not wired
}

// GOOD — guard or assert clearly
public void TakeDamage(int amount)
{
    if (_audio == null)
    {
        GD.PushError("HealthComponent: audio dependency was not injected");
        return;
    }
    _audio.PlaySfx("hurt");
}

// OR — treat it as optional
public void TakeDamage(int amount)
{
    _audio?.PlaySfx("hurt");
}

10. Checklist

  • Autoloads are used only for genuinely global services (audio, settings, platform)
  • Nodes declare their dependencies explicitly (@export or a public property) rather than calling get_node on distant relatives
  • @export fields are validated (assert or null check) before use
  • Service Locator services call unregister in _exit_tree() / _ExitTree()
  • Scene injection is done in the parent's _ready(), after children are fully initialised
  • No circular dependencies between services or autoloads
  • Each node depends only on its immediate collaborators — no deep chains
  • Test stubs/mocks are plain nodes that implement the same interface as the real service
  • C# @export ([Export]) dependencies are disconnected / cleared in _ExitTree() if they hold event subscriptions
  • Service Locator is not used to store scene-specific or short-lived nodes

Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.

<a href="https://skillzs.dev/skills/jame581/godotprompter/dependency-injection">View dependency-injection on skillZs</a>