Dependency Injection is a very important concept in general, not just for games, but also for in general coding structure. When B is dependent on A for a component, then A is called a dependency of B. As a game developer we see this coming very often while programming. You build a player, you need to communicate with GameManager, AudioManager, EnemyController, UIManager and what not. It necessarily means player is dependent on these. Or these are dependencies of the player.
To resolve these dependencies, i.e. to get player what he needs, we usually go for the quick solution and create a static instance of the components, or we do a FindObjectOfType call on Awake() or even a serialized public field. What we are doing is letting the player get his dependencies by himself. In other words, player is resolving it’s own dependencies. Dependency Injection is the concept where player won’t resolve it’s own dependencies rather the control for this would lie on someone else (Inversion of Control).
I have used a plugin named Zenject Dependency Injection on a project, and I’m still very much a beginner user at this. I have completed 2 projects with this and I am very much pleased at what it provides. I am starting to use Zenject but I can see how much it can provide. I have to admit, the solution it provides takes a bit of engineering. Learning Zenject is a steep curve. And for small projects its not worth it. Some might call it “over engineering”. But when learning Zenject, small projects are good. Because in small projects you have less things to worry about and you can learn more and more on the go.
Zenject contains an IoC container which takes care of all the dependencies of a certain group or the whole scene. Most common usage is to create a scene wide IoC container (SceneContext) and resolve the dependencies of the scene objects there. Let’s revisit the previous example, in Player component you need to access GameManager. The Zenject way of doing it would be
[Inject] private GameManager m_gameManager;
By [Inject] you are indicating the IoC Container to take care of the dependency. Now, the player is not responsible to get its own dependency
In the IoC Container, such as SceneContext, you write
What this doing is the container is Binding GameManager, taken from the one found in the hierarchy (like FindObjectOfType) and declaring it as a single (like Singleton). We are essentially doing the same thing in a different way. What’s the difference?
Here’s the difference, now lets say you want multiple games running at the same time. Means there will be multiple GameManager in the scene which would break the whole system. But with system you can do this
Now each player will only get the GameManager under which it is situated and each time a new instance will be served. Meaning each Player will get different GameManager from it’s parent.
There are certain scripts which we write and attach with a GameObject for it to run. For example, StorageManager. It only deals with saving and loading. Why does it have to attach itself with a gameobject introducing a new transform in the scene? With Zenject it doesn’t have to be. You can instantiate the script from IoC. No need to attach it with a gameobject
It makes the scene a whole lot cleaner.
If you want to access Unity events within a script like this, you can. You can add 3 interfaces and call their methods to access respective Unity Events. For example, in a Non-Monobehaviour class if you need to access Start(), just implement IInitializable, for Update implement ITickable(), for Destroy implement IDisposable()
To check if everything in the scene has their dependencies resolved, press Shift+Alt+V, and Zenject will validate the Dependencies for all components and if we have some component with unresolved dependency/dependencies an error will pop up. Which is good, we don’t want runtime errors such as NullReference error suddenly popping up in game.
I have barely scratched the surface of Zenject in this article. It has it’s own Object Pooling system, and the best part is it’s own Event system named ‘Signals’. I love it. But most importantly for me, it enforces clean code. When you use Zenject you will follow principles and the code will start to get organized. The use of interfaces with Zenject is amazing. Along with ‘Scriptable Object Installer’ which till now has been great for me.
If you are a clean code enthusiast make sure to check this out.