Description
Changelog
Reviews (0)

eContainer — VContainer DI for Godot

Godot ships no dependency-injection container, and hand-wiring scoped dependencies across a growing scene tree turns into a web of GetNode() calls, singletons and autoloads that know too much about each other. eContainer solves that the way Unity developers already know: it is a port of VContainer, hadashiA's fast DI container, adapted to Godot's Node and scene-tree model.

You keep VContainer's IContainerBuilder / IObjectResolver API and its compile-time source generator — resolution is generated at build time instead of discovered through reflection — and you get a Godot-native lifecycle wrapped around it.

Scopes are nodes

LifetimeScope is a Godot Node. Add it to a scene, register your services in Configure, and the container's lifetime becomes the node's lifetime — built on _EnterTree, disposed on _ExitTree.

public partial class GameLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterEntryPoint<PlayerService>(Lifetime.Singleton);
        builder.Register<EnemySpawner>(Lifetime.Scoped);
    }
}

Scopes nest, and a child resolves anything its parents registered:

  • A scope finds its parent automatically — from the exported ParentTypeName field, an explicitly assigned ParentReference.Object, a FindParent() override, or an override pushed with LifetimeScope.EnqueueParent(...).
  • Create children in code with CreateChild<TScope>(...), from a PackedScene with CreateChildFromPackedScene<TScope>(...), or spin up an ad-hoc scope under the root with LifetimeScope.Create(...).
  • Set the exported AutoRun flag to false when you want to call Build() at a moment of your own choosing.
  • A single RootLifetimeScope — installed as the eContainer autoload — sits at the top of the hierarchy. Scopes whose declared parent has not entered the tree yet are queued and flushed as parents appear or the scene changes, so load order stops being your problem.
  • The root deliberately builds in _Ready rather than _EnterTree, which leaves a window where the rest of your project can still contribute registrations to the root container via LifetimeScope.Enqueue(...) — handy for your own bootstrap autoloads, regardless of autoload order.

Injection that understands the scene tree

Nodes already exist, so they are injected rather than constructed:

public partial class Hud : Node
{
    PlayerService player = null!;

    [Inject]
    public void Construct(PlayerService player) => this.player = player;
}
  • The exported AutoInjectNodes array injects inspector-assigned nodes the moment the scope builds.
  • Container.InjectNode(node) injects a node and its descendants, stopping at any nested LifetimeScope — that scope injects its own subtree from its own container.
  • Container.Instantiate<T>(packedScene, parent) instantiates, injects, then attaches, so _EnterTree and _Ready already see injected members. No more half-initialized nodes.
  • builder.RegisterNode<IHud>(hud) registers an existing node as a service and injects it when the container builds.
  • Reuse registrations across scopes by implementing IInstaller, or wrap a callback in ActionInstaller.

Entry points instead of more nodes

Plain C# classes can take part in the frame loop without becoming nodes. Register them with RegisterEntryPoint(...) or UseEntryPoints(...) and implement the ones you need:

public class PlayerService : IInitializable, ITickable
{
    public void Initialize() { /* runs once the scope resolves */ }
    public void Tick(long frameCount) { /* runs every _Process */ }
}

IInitializable → IPostInitializable run at build time; ITickable and IPhysicsTickable are dispatched from _Process and _PhysicsProcess. Failures are contained per scope through RegisterEntryPointExceptionHandler(...) or e.OnException(...); without a handler they are reported via GD.PrintErr.

Also included: GodotTimeProvider.Process and GodotTimeProvider.PhysicsProcess — System.TimeProvider implementations whose clocks and timers are driven by Godot's process and physics loops rather than wall-clock time, so delays and timeouts respect pausing and time scaling and stay testable.

Installation is declarative

Installation is handled by the ePlugin framework: enabling the plugin adds the bundled VContainer packages to your project, registers their local package source in nuget.config, adds the eContainer autoload, and un-hides the plugin's runtime sources. There is no manual dotnet add package step, and a downloaded-but-disabled plugin is not compiled into your project at all.

Requirements

  • Godot 4.4+ .NET (the C#/Mono build — eContainer is C#-only and is not usable from GDScript)
  • .NET 8 SDK (net8.0; Android builds target net9.0)
  • ePlugin — install it alongside eContainer and enable it first; eContainer depends on it
  • VContainer 1.19.0 is bundled — the VContainer.Standalone and VContainer.SourceGenerator packages ship inside the archive under addons/eContainer/.libs, no public feed required

Installation

  1. Extract addons/eContainer and addons/ePlugin into your project's addons directory.
  2. Open the project in the Godot .NET editor.
  3. Enable ePlugin under Project → Project Settings → Plugins, then enable eContainer.

Status and license

eContainer is experimental and a work in progress — the API may still change between releases. Every pull request and release is built and run against a gdUnit4 test suite in CI.

MIT licensed, and built on hadashiA's MIT-licensed VContainer.

Changelog for version v0.2.1

2026-09-26 19:56:00

No changelog provided for this version.

Reviews

eContainer has no reviews yet.

Login to write a review.

Consider supporting the creators!

If you enjoyed this asset consider supporting its creator. Follow the link below.