本文へ移動
cccskills
無料GitHub で公開

dependency-injection

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

インストール方法を見る

含まれるファイル(6)

  • SKILL.md11.1 KB
  • references/autoloads.md2.6 KB
  • references/export-injection.md2.5 KB
  • references/scene-injection.md2.3 KB
  • references/service-locator.md4.8 KB
  • references/testing-with-di.md3.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

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 → Globals → 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

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Use when working with 2D-specific systems — TileMaps, parallax scrolling, 2D lights and shadows, canvas layers, particles 2D, custom drawing, and 2D meshes in Godot 4.3+

日本語の概要は準備中です。原文の説明を表示しています。

jame581/GodotPrompter8122026年10月12日 更新

Use when working with 3D-specific systems — materials, lighting, shadows, environment, global illumination, fog, LOD, occlusion culling, and decals in Godot 4.3+

日本語の概要は準備中です。原文の説明を表示しています。

jame581/GodotPrompter8122026年10月12日 更新

Use when building character abilities — Resource-based abilities with cost/cooldown/cast, buffs/debuffs, stat modifiers, gameplay tags, and HUD binding

日本語の概要は準備中です。原文の説明を表示しています。

jame581/GodotPrompter8122026年10月12日 更新

Use when creating Godot editor plugins — EditorPlugin, @tool scripts, custom inspectors, and dock panels

日本語の概要は準備中です。原文の説明を表示しています。

jame581/GodotPrompter8122026年10月12日 更新

Use when implementing AI movement — NavigationAgent2D/3D, steering behaviors, behavior trees, and patrol patterns

日本語の概要は準備中です。原文の説明を表示しています。

jame581/GodotPrompter8122026年10月12日 更新

Use when implementing animations — AnimationPlayer, AnimationTree, blend trees, state machines, sprite animation, and code-driven animation

日本語の概要は準備中です。原文の説明を表示しています。

jame581/GodotPrompter8122026年10月12日 更新

jame581 のスキルをすべて見る

このスキルの問題を報告する