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
ParentTypeNamefield, an explicitly assignedParentReference.Object, aFindParent()override, or an override pushed withLifetimeScope.EnqueueParent(...). - Create children in code with
CreateChild<TScope>(...), from aPackedScenewithCreateChildFromPackedScene<TScope>(...), or spin up an ad-hoc scope under the root withLifetimeScope.Create(...). - Set the exported
AutoRunflag tofalsewhen you want to callBuild()at a moment of your own choosing. - A single
RootLifetimeScope— installed as theeContainerautoload — 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
_Readyrather than_EnterTree, which leaves a window where the rest of your project can still contribute registrations to the root container viaLifetimeScope.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
AutoInjectNodesarray injects inspector-assigned nodes the moment the scope builds. Container.InjectNode(node)injects a node and its descendants, stopping at any nestedLifetimeScope— that scope injects its own subtree from its own container.Container.Instantiate<T>(packedScene, parent)instantiates, injects, then attaches, so_EnterTreeand_Readyalready 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 inActionInstaller.
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 targetnet9.0) - ePlugin — install it alongside eContainer and enable it first; eContainer depends on it
- VContainer 1.19.0 is bundled — the
VContainer.StandaloneandVContainer.SourceGeneratorpackages ship inside the archive underaddons/eContainer/.libs, no public feed required
Installation
- Extract
addons/eContainerandaddons/ePlugininto your project'saddonsdirectory. - Open the project in the Godot .NET editor.
- 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.