I’ve been working on Godot projects with C#, and I’ve figured out some solutions to code organization problems. Here are my thoughts.
The Problem I Ran Into
I’m making a real-time strategy game, and there’s this super common scenario: players box-select a group of units, right-click on the map, and the units need to pathfind their way there. Sounds simple, but it actually involves several systems—the selection manager needs to know which units are selected, the pathfinding service needs to calculate routes, and the formation controller needs to keep units moving in formation.
At first, I wrote it the most straightforward way:
csharp
public partial class CommandIssuer : Node
{
public override void _Ready()
{
var selectionManager = GetNode<SelectionManager>("/root/Game/SelectionManager");
var pathfinding = GetNode<PathfindingService>("/root/PathfindingService");
// Issue movement commands...
}
}
```
Looks fine at first glance, right? But then when I wanted to refactor the scene structure and moved SelectionManager to a different location, errors popped up everywhere—couldn't find the node. Even more frustrating, when I tried to write unit tests for the pathfinding system, I found it wouldn't run at all because it depends on a bunch of Nodes, and those Nodes depend on the complete scene tree.
So I thought, why not try AutoLoad singletons? I made the pathfinding and fog of war systems global. That did solve the path problem, but new issues cropped up: everything's global, and lifecycle management became a mess. For example, when players switch from level one to level two, I have to manually clean up the old level's pathfinding grid and reset fog data. These global singletons are still alive, and I have to write a bunch of reset code myself. Plus during testing, state bleeds between tests—one test finishes but the state remains, and the next test breaks. Debugging that is a real headache.
## My Idea
That's when it suddenly hit me—isn't Godot's scene tree already a pretty good hierarchical structure? From root node to child nodes, there's naturally a parent-child relationship, and clear lifecycle events. So what if I used this structure directly as the Scope hierarchy for a dependency injection container? Wouldn't that solve these problems I'm facing?
My game's scene structure looks roughly like this:
```
Root
├── MainMenu (Main menu)
└── Campaign (Campaign)
├── Mission (Level container)
│ ├── TerrainManager (Terrain)
│ └── UnitManager (Units)
└── UI (Interface)
├── MiniMap (Mini-map)
└── CommandPanel (Command panel)
Following this idea, I could make the Mission node act as a “level-scoped service container”. It manages all the services this level needs—pathfinding grid, fog system, unit registry, etc. That way, when players complete a level and move to the next one, when the old Mission node is destroyed, all those services get automatically released. The new level creates a new Mission node, everything starts fresh, and the lifecycle perfectly fits the scene tree.
Four Role Design
Based on this idea, I started building GodotSharpDI. The core approach is to divide objects by responsibility into four roles, letting each role do its job.
The first is Singleton Service, which is the pure logic layer. For example, my game’s PathfindingService is dedicated to A* pathfinding calculations. This kind of service doesn’t need to know what the scene tree looks like, doesn’t need to be a Node, it’s just a pure C# class. You give it start point, end point, and obstacle data, it gives you back path points, that’s it. This type of service is completely managed by the container, creation and destruction are automatic, you don’t have to worry about it.
The second is Host, which is the bridge between scene resources and the DI system. Some things have to be Nodes, no way around it. For example, TerrainManager needs to manage terrain meshes, height maps and such, it must inherit from Node3D, must be in the scene tree to function. But the thing is, I also want the pathfinding service to access terrain data, while not wanting it to directly depend on this specific Node implementation. So that’s what Host does: TerrainManager itself is still a Node, but it exposes itself to the DI system in the form of the ITerrainProvider interface, and other services only need to know the interface.
The third is User, which is the dependency consumer. For example, the MiniMap mini-map interface needs to access unit position data and fog state to render. So I add a User marker to it, then use Inject to declare it needs the IUnitRegistry and IFogOfWarQuery interfaces. When this node enters the scene tree, the framework automatically injects these dependencies, you don’t have to do anything.
The fourth is Scope, which is the container itself. In my game, the Mission node plays this role. I declare on it which services this Scope has—PathfindingService, CombatResolver, UnitRegistry, etc. When the Mission node is Ready, these services get created; when the node is destroyed, the services get cleaned up along with it.
See It in Action
After all this theory, we need to see actual code to make it concrete. First, let’s define a pathfinding service:
csharp
public interface IPathfindingService
{
List<Vector3> FindPath(Vector3 from, Vector3 to);
}
[Singleton(typeof(IPathfindingService))]
public partial class PathfindingService : IPathfindingService
{
private readonly ITerrainProvider _terrain;
private AStarGrid3D _grid;
public PathfindingService(ITerrainProvider terrain)
{
_terrain = terrain;
InitializeGrid();
}
public List<Vector3> FindPath(Vector3 from, Vector3 to)
{
return _grid.GetPointPath(from, to).ToList();
}
}
See, this is just a pure logic service that injects ITerrainProvider through the constructor, completely independent of the scene tree. The benefit is you can easily write unit tests—just mock a fake ITerrainProvider and it runs.
Then we need a terrain manager to provide the actual terrain data:
csharp
public interface ITerrainProvider
{
Vector2I GetBounds();
IEnumerable<Vector3I> GetObstacles();
}
[Host]
public partial class TerrainManager : Node3D, ITerrainProvider
{
[Singleton(typeof(ITerrainProvider))]
private TerrainManager Self => this;
private HashSet<Vector3I> _obstacles = new();
public Vector2I GetBounds() => new Vector2I(100, 100);
public IEnumerable<Vector3I> GetObstacles() => _obstacles;
public override partial void _Notification(int what);
}
TerrainManager is a Node that manages actual 3D meshes, colliders and other scene resources. Through the Host role, it bridges itself into the DI system, but only exposes the ITerrainProvider interface, hiding the specific Node implementation details.
Next, define the Scope on the Mission node to wire these services together:
csharp
[Modules(
Services = [typeof(PathfindingService), typeof(CombatResolver)],
Hosts = [typeof(TerrainManager), typeof(UnitManager)]
)]
public partial class MissionScope : Node, IScope
{
public override partial void _Notification(int what);
}
Here I’ve declared which pure logic services this Scope manages, and which Hosts will be in the scene. When the level loads, PathfindingService gets created, it automatically injects the terrain data provided by TerrainManager, then initializes the pathfinding grid.
Finally, let’s see how the command issuer uses these services:
csharp
[User]
public partial class CommandIssuer : Node, IServicesReady
{
[Inject] private IPathfindingService _pathfinding;
[Inject] private ISelectionProvider _selection;
public void OnServicesReady()
{
// All dependencies are injected, ready to handle player input
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event is InputEventMouseButton { ButtonIndex: MouseButton.Right })
{
var target = GetWorldPositionFromMouse();
var units = _selection.GetSelectedUnits();
foreach (var unit in units)
{
var path = _pathfinding.FindPath(unit.Position, target);
unit.SetPath(path);
}
}
}
public override partial void _Notification(int what);
}
Now CommandIssuer doesn’t need to know where these services are in the scene tree, or how they’re created. It just declares which interfaces it needs, and when the node enters the scene tree, the framework automatically injects the dependencies. When the player right-clicks the map, it uses the injected services to calculate paths and issues movement commands to units.
Some Details
During implementation, there are a few points I found pretty interesting, worth mentioning.
First is the deferred injection mechanism. The scene tree’s loading order is actually non-deterministic, which creates a problem: maybe CommandPanel enters the scene tree first, but UnitManager hasn’t entered yet, so the ISelectionProvider service doesn’t even exist. When this happens, the framework puts this injection request in a waiting queue. When UnitManager enters the scene tree and registers ISelectionProvider, the framework notifies all requests in the waiting queue and triggers callbacks to complete injection. This way, you don’t have to worry about which comes first, UI nodes or game logic nodes—the framework handles it for you.
Then there’s Host and User can combine. My UnitManager node is like this—it’s both Host (providing IUnitRegistry for other systems to query units) and User (needing to inject pathfinding service to handle unit movement). At first I worried this would cause circular dependencies, but turns out it doesn’t. The reason is Host registering services happens during the EnterTree phase, and this phase doesn’t trigger any injection actions. User dependency injection is completed asynchronously through callbacks, happening at a later time. So even if the pathfinding service depends on terrain data, and the terrain manager needs to inject other services, the whole chain still won’t form a circle.
There’s also a gotcha—the Notification method must be explicitly declared. I didn’t notice this at first, and Godot just didn’t recognize the code the framework generated. The reason is Godot’s script binding mechanism only scans the source file you attach to the node, it doesn’t look at Source Generator-generated files. So even though the framework generates the _Notification implementation, Godot can’t find it. The fix is simple—just explicitly declare this method as a partial method in your own source file. Fortunately the IDE will show an error when you forget, and it provides an auto-fix feature, one click and it’s added, so it’s not really a hassle.
Trade-offs I Made
While designing this solution, I also made quite a few trade-offs. Here are the main ones.
About why not just use Microsoft.Extensions.DependencyInjection. Actually I did try it at first, but quickly found it’s a completely different system from Godot’s scene tree. ServiceProvider’s Scope lifecycle requires you to manually manage it, you have to remember to CreateScope and Dispose when switching levels. Plus it has no idea about the scene tree structure, can’t leverage parent-child relationships to implement Scope hierarchy. Also, every time you get a service you have to call the GetService method, which is a bit verbose. I tried manually handling Scope during level switches, but there are always places easy to miss, and one miss means memory leak. Since existing solutions don’t really fit Godot well, might as well design one specifically optimized for it from scratch.
About why use compile-time code generation. I chose Source Generator over runtime reflection, mainly considering performance. In RTS games there might be hundreds of units moving and fighting simultaneously, querying many services every frame. If you use reflection to get services, that overhead is pretty noticeable on mobile platforms. Plus compile-time code generation has another benefit—it can do a lot of static analysis. For example, when I’m developing, if I accidentally make the pathfinding service depend on an interface that doesn’t even exist, it’ll error at compile time, don’t have to wait until runtime when opening a level to find out the game crashed.
About the learning curve issue. I admit this solution does have a learning curve, you need to spend some time at first understanding what Service, Host, User, and Scope each do. But I think this complexity is worth it, because it makes each role’s responsibilities super clear: pure logic is Service, managing scene resources is Host, consuming dependencies is User, managing the container is Scope. Once you understand this model, it feels natural to use. And for RTS games where systems are especially complex, a clear architecture can save tons of refactoring hassle down the road.
Finally
I’ve been using this solution in my actual project for a while now, and it feels pretty smooth so far. Especially when doing level transitions and multiplayer modes, lifecycle management became so much easier, no more worrying about state not being cleaned up causing those weird bugs.
But I’m pretty curious—when you’re making Godot C# strategy or simulation games where systems are pretty complex, how do you handle these kinds of problems? How do your various managers communicate with each other? Do you use GetNode, or AutoLoad global singletons, or the signal system, or some other approach? Have you run into similar frustrations with organizing code and managing dependencies?
Also, what do you think about using the scene tree directly as a DI container? Does it feel weird mixing Godot concepts with dependency injection concepts? My own feeling is, since the scene tree is already a hierarchical structure with very clear lifecycle events, using it for dependency management actually feels pretty natural. But I’d also like to hear different opinions and perspectives—maybe there are some use cases where this solution isn’t quite suitable.
If you’re interested in this project, or have any suggestions or ideas, feel free to leave a comment or message me. I’m still continuously refining this solution, many details are still being polished.
Project: GitHub - GodotSharpDI
NuGet Package: GodotSharpDI
License: MIT