# Dependency Injection

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

- **Type:** Skill
- **Install:** `agentstack add skill-jame581-godotprompter-dependency-injection`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [jame581](https://agentstack.voostack.com/s/jame581)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [jame581](https://github.com/jame581)
- **Source:** https://github.com/jame581/GodotPrompter/tree/master/skills/dependency-injection

## Install

```sh
agentstack add skill-jame581-godotprompter-dependency-injection
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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.

```gdscript
# 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  void:
    AudioManager.play_sfx("attack")        # same AudioManager dependency again
```

```csharp
// 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("/root/AudioManager").PlaySfx("hurt");        // hard dependency
        GetNode("/root/UIManager").UpdateHealthBar(_health);     // hard dependency
        if (_health ("/root/GameState").RecordDeath();            // hard dependency
    }
}

// Enemy.cs
public partial class Enemy : CharacterBody3D
{
    public void Attack()
    {
        GetNode("/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

| Pattern | Complexity | Testability | Best For |
|---|---|---|---|
| **Autoloads** | Low | Low | Truly global singletons: audio, settings, platform services |
| **@export Injection** | Low | High | Most nodes — wire deps in the editor, no runtime lookup needed |
| **Service Locator** | Medium | Medium | Plugins, optional systems, swappable implementations at runtime |
| **Scene Injection** | Low | High | Parent-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](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](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](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](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](references/testing-with-di.md) for GUT-based test patterns showing each injection technique.

---

## 8. When to Use What

| Situation | Recommended Pattern |
|---|---|
| Service used by nearly every node in every scene | Autoload singleton |
| Node needs 1–3 deps, scene is editor-authored | `@export` injection |
| System is optional or swappable at runtime | Service Locator |
| Parent scene constructs children and knows their needs | Scene injection |
| Writing tests for a node with external dependencies | `@export` or property injection + stubs |
| Plugin that must work in any project | Service Locator (self-registers, no assumptions) |
| Two sibling nodes need the same dep | Let 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

```gdscript
# 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.
```

```csharp
// 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

```gdscript
# 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.
```

```csharp
// 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

```gdscript
# 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.
```

```csharp
// 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

```gdscript
# 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.
```

```csharp
// 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

```gdscript
# 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")
```

```csharp
// 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

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [jame581](https://github.com/jame581)
- **Source:** [jame581/GodotPrompter](https://github.com/jame581/GodotPrompter)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-jame581-godotprompter-dependency-injection
- Seller: https://agentstack.voostack.com/s/jame581
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
