Coming from a game development background as a game play programmer and transitioning into more of a traditional software developer path, certain things are the same. You use many of the same languages, programming paradigms and even the same IDE’s. But there are, of course, also plenty of things that are different. These can depend on many factors, such as project size, what type of project you’re developing and so on.
One of these things is the Dependency Injection (DI) technique and Inversion of Control (IoC) containers.
Through my, for now, somewhat short experience as a traditional software developer, I’ve noticed that this is probably the most prevalent set of design patterns and handy techniques I’ve gotten the most use out of.
So, naturally, that made me wonder. How come this isn't more widespread in the game development scene?
Well, it should come as no surprise that there are of course also DI frameworks for popular game engines and frameworks.
In this post we’ll be taking a look at the dependency injection framework called Extenject for the Unity game engine.
Many of you may already know what DI and IoC containers are, as well as what kind of benefits they can bring to the project, but in case you don't, I’ll briefly give you an overview of the topics and how they can improve your architecture.
DI is the act of moving the responsibility of fulfilling the class's dependencies and construction of these dependencies to somewhere else in the application.
And this is where the IoC container comes in. Its job is to automatically provide these dependencies where needed which is done by setting up all the correct bindings in the container.
The biggest benefit Dependency Injection and IoC containers is the loosely coupled architecture you achieve by using it. Classes no longer tightly depend on other class implementations and are not in control of how they obtain that dependency, or of how they should populate that dependency’s dependencies.
Moreover, the architecture allows you to automatically lean towards the Single Responsibility Principle which ensures that each class only focuses on its own responsibilities rather than worrying about other things, such as achieving the correct dependencies.
Finally, the architecture helps you create a bigger picture of the dependency of your applications as well as of how they are all wired up in the IoC container. And with that, it also makes it easier to exchange concrete implementations of such dependencies.
So, to give a proper introduction to Extenject, I thought we should start out with a really simple example containing what many of you are probably most familiar with. In this case, I’ll use a simple .NET Core Web API.
In this example, we see the IoC container in a .NET Core project where we're telling it to hook up the ISomeService interface as a SomeServiceImplementation class wherever it’s needed. The SomeServiceImplementation is then injected as a singleton. There are other ways to inject your dependencies, but that is beyond the scope of this post.
And now the great thing is that we can simply inject this ISomeService interface into our classes where needed, and we don’t need to worry about where to get that dependency from or about what type of concrete implementation is needed. The IoC container takes care of all that for us.
That’s neat! So let’s now take a look at how Extenject does this and how the process is a little bit different with Unity.
In order to get started with Extenject, you first have to download the latest package. I recommend downloading it from the official github repository since the one on the Unity Asset Store isn't updated as frequently.
In Extenject, you set up your bindings through what is called Installers which basically constitutes your IoC container. These installers are linked up with a Context; a context defines the scope of where your bindings are set up. For instance, it's possible to have bindings that are only defined for certain scenes with a Scene Context, but there’s also a Project Context which sets up your bindings for the whole lifecycle of your project.
There are also different types of installers based on the types of bindings you want to set up. In our example, we will be using a Mono Installer on a Scene Context.
You can create a Scene Context by right-clicking in your hierarchy and navigating to Zenject -> Scene Context.
Here, we take a look at our Installer which basically does the same as our .NET example, i.e. creating the ISomeService with the concrete class SomeServiceImplementation as a singleton.
Extenject has a ton of different ways to set up your bindings, and I recommend you to check out their documentation for more info on that. They support many Unity specific bindings which will definitely come in handy for your projects.
Extenject also supports multiple different ways of injecting your dependencies into your classes. From the official documentation, they are the following:
- Constructor injection
- Method injection
- Property injection
- Field injection
Those of you familiar with Unity might be wondering “Constructor injection with Monobehaviours? How?” - and you would be right to think that.
What the documentation is referring to is constructor injection through normal C# classes.
Since MonoBehaviours can’t really have constructors, we have to use other ways of injecting our dependencies. They can still be injected through either field, property or method injection, where method injection is the preferred way.
Here, we see a basic example of MonoBehaviour that injects their dependency through a method. My personal preference is to have a method at the top called Construct and use that with the [Inject] attribute to simulate a constructor. It also gives a fast and easy overview of which kind of dependencies this object needs.
DI and IoC containers are great! And if you’re like me, you might have had the thought “I’ll need to start using this in every Unity project from now on!”. However, I’m not sure that would be the right way to look at it. Different tools, for different problems.
Extenject is a really good and powerful tool. It comes with a lot more than what I have shown in this post (it even has built-in support for memory pools). But, this also means it comes with some overhead, and it can also get a bit hairy to set up all your bindings correctly.
It also depends on whether you’re working alone or with a team. Some considerations might be whether the entire team understands the ins and outs DI frameworks, and whether you have time/money to invest into onboarding people in the way your architecture for the project is set up.
All these things need to be considered before you just start throwing Extenject into every one of your projects.
But I hope this post has shown you yet another tool that you can add to your toolbelt.
Thank you for reading!