Why use a DI framework?

Hi, I’m pretty new to gamedev and I have a question about Dependency Injection frameworks…

Recently I’ve realised i have a scalability problem with my project and i was looking for solutions, and I’ve realized the solution will be DI.

The thing is.. I’ve seen that the industry standard is using A DI framework like VContainer / Reflex.

I didn’t try VContainer yet, but i did try out Reflex… and I find it a bit… robust?

I mean, I understand how to use it, but it feels like doing the same almost manual injection of dependencies but with extra steps?

what am I missing? Is it worth it?

I’ve used some dependency injection frameworks before.

I like:

  • Not having to worry about order e.g.
builder.Register<ClassThatInjectsLogger>(Lifetime.Singleton);
builder.Register<ILogger, Logger>(Lifetime.Singleton);
  • It’s easier to split installers into multiple classes.
  • Don’t have to update the installer if I inject a new class into the constructor of a service.
  • Lazy initialization

I dislike:

  • Not being able to easily pass in different implementations of the same class e.g. VContainer
builder.Register<IWeapon, Sword>(Lifetime.Singleton)
    .Keyed(WeaponType.Primary);

constructor has different args
public WeaponSystem(
        [Key(WeaponType.Primary)] IWeapon primaryWeapon,
        [Key(WeaponType.Secondary)] IWeapon secondaryWeapon)
    {
}
  • Slow / reflection.
  • Not being able to make more complex structures e.g. behaviour trees.
  • Not being able to easily use async e.g. maybe I want to make sure the localization has async loaded before I inject it into other classes.
  • Circular references don’t appear until runtime.

Personally, I prefer just doing manual injection nowadays. Some frameworks have inbuilt Unity support like prefabs etc. but I don’t really use them. I mainly use it for non-gameplay system I want to unit test. It can end up with a complex installer class and I have to keep passing in new arguments. I also need to make sure the creation order is correct so I keep reordering the instances but it does avoid circular dependencies.

Yeah, the DI frameworks are made to obfuscate what you’re actually doing to make your code more confusing. Unless you’re gunning for job safety due to nobody else being able to modify the mess you have made, avoid them.

Scalability problems are like any other problems - you need to think hard about what the problem is, and how you can solve that problem. I don’t think it’s going to be “move the information about what depends on what away from where things are created and into a config file”.

Explain.

Or is that the explanation? If so, no, that’s not correct. Some developers use DI, but only few have actually good reasons to do so.

A well architected game doesn’t need DI because it really only amounts to saving you from writing a single line of code which you trade for an attribute so that some behind-the-scenes magic happens which you have no control over and may cost you dearly in terms of memory and initialization performance.

The straightforward solution that I reach out to is a reference container (auto-singleton, generic) which I can assign all globally needed references to and then get them by type. And of course the discipline of not calling out to external references in Awake but only doing so in OnEnable or Start.

I’ve created a DI solution called Init(args) which aims to combine the flexibility of Dependency Injection frameworks with the ease-of-use of the Singleton pattern. Perhaps that could be more your cup of tea.

Instead of having to write installers and manage hierarchical contexts, registering a service can be done using a single attribute or by selecting a context menu item in the Inspector window. All clients can also always receive services automatically without you having to remember to attach injector components to all your prefabs. Clients even receive services when you use AddComponent.

Init(args) also visualizes dependencies in Edit Mode in the Inspector, warns you about missing dependencies and lets you ping services by clicking them.

Imo one of the main benefits that dependency injection can offer is providing clarity about dependencies, so if the Inspector for components is totally blank, their dependency configuration is hidden in some Installers, and components attached to prefabs can silently fail to receive their dependencies if you forget to attach an Injector component to them, that’s not really taking full advantage of all the benefits that Dependency Injection can offer.

Some reasons why I like to use a Dependency Injection framework in Unity are:

  1. Serialized fields don’t support references across scene and prefab asset boundaries.
  2. Serialized fields don’t really support interfaces. Being able to use interfaces like ICommand can allow you to make extremely reusable simple components and avoid a lot of unnecessary duplication of code.
  3. Relying on Singletons or Service Locators instead of dependency injection means that dependencies are hidden inside the implementation details of your methods. You will have no idea what dependencies a Component has by looking at its Inspector, or what dependencies a method has by looking at its definition. With dependency injection if your code compiles and you assign to values to every slot in the Inspector, you can feel pretty confident that everything has been configured properly even before you enter Play Mode and test things manually.
  4. Unit-testability. When unit testing your components is trivial, you tend to end up with much better test coverage, which can result in drastically fewer bugs in the game, and greatly improve maintainability in longer projects.
  5. Easy swapping of default services at any point. You can swap any global service to a different one just by changing a few lines of code, even if it’s used by dozens of components. With serialized fields you might need to update dozens of prefabs manually, and with Singletons you might need to modify dozens of classes manually to do the same.
  6. Dynamic and per-client services. With the Singleton pattern all clients are forever stuck using the one and same service instance. With dependency injection you can always inject different services in different context with total flexibility.

That’s the thing, I think my architecture is pretty solid ATM, and while it has some dependencies, I would not call it sphagetti code..

Most of the actions are event based, so I do have a few event buses, but… they are are pretty much each located in a prefab in the acene in which they are needed…

So it feels like while DI would help me indeed to save a few lines here and there..
It will in the same time add more unnecessary complexity which is much more to handle than just having the prebuf with the eventbus hanging in the scene…

Imo architecture should at the end of the day be focused on minimizing concrete workflow pain points and empowering you to create the best possible end product. So it always makes sense to use what works best for you in practice, rather than adapting “best practices” from authoritative sources only because they say so. The majority of Unity developers still do just stick with the Singleton pattern, so one could make the case that the Singleton pattern is the industry standard.

If it feels like the cons of an architectural patterns out-weight its pros in practice, then it could mean that you’re indeed overusing the pattern, it could be you’re not using it effectively (e.g. just because you use Dependency Injection doesn’t mean you have to use Dependency Inversion and stick interfaces everywhere) or it could be that additional investments need to be made into tooling to get rid of some pain points and really make the pattern shine (like how a simple visual editor window can alone transform the workflow of working with branching dialogues from awkward to great).

So it feels like while DI would help me indeed to save a few lines here and there..

One thing worth noting is that in the context of Unity, a DI framework is not just useful for saving you the time of having to manually configure the services for every component, but it actually makes it possible to even use dependency injection in the first place.

While Unity components already have built-in DI support via serialized fields, they don’t support things like cross-scene references, interface types and runtime DI out-of-the-gate. At least for me, these are the main reasons why I use a DI Framework, not so much because it saves me from having to drag-and-drop references manually using the Inspector, or to pass dependencies manually in code.

Cross-scene is a good point, interfaces less so for me as I prefer not to rely on Inspector assignments for references generally. But I’m most curious about “runtime DI out-of-the-gate” - what do you mean by that?

I immediately thought that .. perhaps DI is actually relieving me from null-ing references that go out of scope (“missing”)? If so then that would be the first truly beneficial thing I would consider DI for. But something tells me this isn’t actually going to happen since it would likely require the reference to be wrapped in a proxy object.

I just mean that with serialized fields the dependencies can by default only be configured in Edit Mode using the Inspector, and can’t be passed at runtime / in code when you use AddComponent or Instantiate.

This is in contrast to plain old C# objects with which you can easily provide dependencies to be passed to the constructor when using the new operator.

Do you mean a system that would automatically convert references to destroyed unity Objects into actual null references?

What would be the use case for this? The ability to use null-conditional operators and such safely? Avoiding preventing the garbage collector from releasing the referenced objects from memory?

This is not something that I’ve found myself needing to do often, but yeah, it would be possible to achieve this with a DI container. You’d just need to automatically subscribe to service changed events and reinject services whenever any of them change, including when they’re changed to none (aka removed).

What DI containers are more often used for is automatically handling calling IDisposable.Dispose, releasing addressable asset references etc. once services are no longer needed.