This post will be the first in what will likely be a long series. Dependency Injection and Unit Testing are generally considered staples in modern software development. Game development for a variety of reasons is one of the few areas where this isn’t true. While it’s starting to become more popular, there are quite a few things holding back game programmers from embracing these valuable paradigms.
I’d like to open by giving a quick description of the benefits you’ll gain by embracing dependency injection. Before you jump away thinking “I don’t need this” or “this will be too complicated”, just take a look at what you have to gain. And let me try to quell any fears, this is something you can definitely take advantage of, it won’t be hard, it’ll save you time, and your code will be improved.
Benefits – What do I have to gain?
Loose Coupling
Dependency Injection by it’s nature encourages very loose coupling. This means your objects and classes don’t have tight dependencies on other classes. Loose coupling leads to having code that is much less rigid and brittle.
Re-usbility Across Projects
Loose coupling also makes your classes much easier to use across projects. When your code has dependencies on many other classes or static global variables, it’s much harder to re-use that code in other projects. Once you get into the habit of good separation and dependency injection, you’ll find that reuse becomes a near trivial task.
Encourages Coding to Interfaces
While you can certainly code to interfaces without Dependency Injection, using it will naturally encourage this behavior. The benefits of this will become a bit more obvious in the example below, but it essentially allows you to swap behavior and systems in a clean way.
Cleaner Code
When you start injecting your dependencies, you quickly end up with less “spaghetti-code”. It becomes much clearer what a classes job is, and how the class interacts with the rest of your project. You’ll find that you no-longer have to worry about a minor change in one piece of code having an unexpected consequence in something that you thought would be completely unrelated.
As an example, once while working on a major AAA MMO game, I saw a bug fix to a specific class ability completely break the entire crafting system. This exactly the kind of thing we want to avoid.
Unit Testing
This is one of the most commonly stated benefits to Dependency Injection. While it’s a huge benefit, you can see above that it’s not the only one. Even if you don’t plan to unit test initially (though you should), don’t rule out dependency injection.
If you haven’t experienced a well unit tested project before, let me just say that it’s career changing. A well tested project is less likely to slip on deadlines, ship with bugs, or fail completely. When your project is under test, there’s no fear when you want to make a change, because you know immediately when something is broken. You no-longer need to run through your entire game loop to verify that your changes work, and more importantly that you haven’t broken other functionality.
If it’s so good, why isn’t this common place?
Now, I’d like to cover a few of the reasons the game industry has been slow to adopt Dependency Injection & Unit Testing.
C++
While this doesn’t apply to Unity specifically, the game industry as a whole has primarily relied on C++. There were of course studios that developed in other languages, but to this date, outside of Unity, the major engines are all C++ based. C++ has not had nearly the same movement towards Dependency Injection or Unit Testing as other common enterprise languages (Java, C#, JS, Ruby, etc). This is changing though, and with the proliferation of unit testing and dependency injection in C#, it’s the perfect time to jump in with your games.
Performance
Dependency Injection adds overhead to your game. In the past, that overhead could be too much for the hardware to handle. Given the option between 60fps and dependency injection, 60fps is almost always the correct answer. Now though, hardware is really fast, and 99% of games can easily support Injection without giving up any performance.
Mindset
While there are countless other “reasons” you could come across from game programmers, the key one is just an issue of mindset. Too many people have been programming without Injection and Unit testing and just haven’t been exposed to the benefits. They think “that’s for enterprise software”, “that’s something web developers do”, or “that doesn’t work for games”. My goal here is to convince you that it’s worth trying. I promise if you dig in and try dependency injection and unit testing, you’ll quickly start to see the benefits, and you’ll want to spread the word as well.
Dependency Injection Frameworks
When you’re searching, you may also see the DI frameworks referred to as IOC containers.
You may be wondering how you get started with Dependency Injection in Unity. It’s not something built into the Unity engine, but there are a variety of options to choose from on GitHub and in the Asset Store.
Personally, I’ve been using Zenject, and my samples will be done using it. But that doesn’t mean you shouldn’t look into the other options available.
Zenject
I think the description provided on the Zenject asset page does a better job describing it than I could, so here it is:
Zenject is a lightweight dependency injection framework built specifically to target Unity. It can be used to turn the code base of your Unity application into a collection of loosely-coupled parts with highly segmented responsibilities. Zenject can then glue the parts together in many different configurations to allow you to easily write, re-use, refactor and test your code in a scalable and extremely flexible way.
While I hope that after reading this series you have a good idea why you should use a dependency injection framework, and how to get started, I must highly recommend you take a look at the documentation provided on the Zenject GitHub page.
Constructor Injection
Most Dependency Injection is done via what’s called Constructor Injection. This means that anything your class relies on outside itself is passed in via the constructor.
Example
I want to give an example of how you’d use Constructor Injection in a more general sense before diving too deep into the differences with Unity. What I’m presenting here is a simplified version of a game state system I’m using in a Unity project currently.
In my project, I have a variety of game state classes. The different “GameStates” handle how the game functions at different stages throughout the game cycle. There are game states for things like generating terrain, lost/gameover, building, attacking, and in this example, ready to start.
In the game state “ready to start“, all we want to do is wait for the user to start the game. The game state doesn’t care how the user starts the game, only that they do. The simplest way to implement this would be to check on every update and see if the user pressed the “Fire1” button.
It may look something like this:
using UnityEngine; public class GameStateReadyToStart : MonoBehaviour { void Update() { if (Input.GetButton("Fire1")) SwitchToNextGameState(); } private void SwitchToNextGameState() { // Logic to go to next gamestate here } }
This will work, it’s simple, and quick to implement, but there are some issues with it.
Problems
- Our gamestate is a monobehaviour so we can read the input during the update.
- The Input logic is inside a class who’s job isn’t Input. The gamestate should handle game state/flow, not input.
- Changing our input logic requires us to touch the gamestate class.
- We’ll have to add input logic to every other gamestate.
- Input can’t be easily varied across different devices. If we want a touch button on iPad and the A button on an xbox, we have to make bigger changes to our gamestate class.
- We can’t write unit tests against our gamestate because we can’t trigger input button presses.
You may be thinking that’s a long list, but I guarantee there are more problems than just those.
Why not just use a Singleton?
The first answer you may come across to some of these problems is the Singleton pattern.
While it’s very popular, simple, and resolves half of our issues, it doesn’t fix the rest.
Because of that, outside the game development world, the singleton pattern is generally considered bad practice and is often referred to as an anti-pattern.
Let’s try some separation
Now, let me show you an easy way to resolve all of the problems above.
public class GameStateReadyToStart { public GameStateReadyToStart(IHandleInput inputHandler) { inputHandler.OnContinue += () => { SwitchToNextGameState(); }; } private void SwitchToNextGameState() { // Logic to go to next gamestate here } }
Here, you can see we’ve moved input handling out of the “gamestate” object into it’s own “inputHandler” class. Instead of reading input in an update loop, we simply wait for the InputHandler to tell us when the user is ready to continue. The gamestate doesn’t care how the user tells us to continue. All the gamestate cares about is that the user told it to switch to the next state. It’s now properly separated and doing only what it should do, nothing more.
The “IHandleInput” interface for this example is very simple:
using System; public interface IHandleInput { Action OnContinue { get; set; } }
Now, if we want to switch input types across devices, we simply write different implementations of the “IHandleInput“ interface.
We could for example have implementations like:
- TouchInputHandler – Continues when the user presses anything bound to “Fire1“
- GUIBasedInputHandler – Continues when the user clicks a GUI button
- VoiceInputHandler – Continues when the user says a phrase
- NetworkInputHandler – Continues when the user presses something on another device (think controlling a PC game with a phone)
- TestInputHandler – Continues via a unit test designed to verify state switching doesn’t break
Time to Inject!
Now without going too much deeper into my example, you may be thinking “that’s nice, but now I have to pass in an input handler and manage that”.
This is where dependency injection comes into play. Instead of creating your handler and passing it into the constructor, what we’ll do is Register the handler(s) we want with our Dependency Injection Container.
To do that, we need to create a new class that derives from the Zenject class “MonoInstaller”
using System; using UnityEngine; using Zenject; using Zenject.Commands; public class TouchGameInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<IHandleInput>().ToTransient<TouchInputHandler>(); Container.Bind<GameStateReadyToStart>().ToTransient<GameStateReadyToStart>(); Container.Resolve<GameStateReadyToStart>(); } }
In the TouchGameInstaller class, we override InstallBindings and register our 2 classes.
Line 13 simply asks the container for an instance of the game state.
This is a very simplified version of the Installer with a single game state, later parts of this series will show how we handle multiple game states.
What we’ve done here though is avoid having to manage the life and dependencies of our “gamestate” class.
The Dependency Injection Container will inspect our classes and realize that the “GameStateReadyToStart” class has a dependency on an “IHandleInput“, because the constructor has it as a parameter.
It will then look at it’s bindings and find that “IHandleInput” is bound to “TouchInputHandler“, so it will instantiate a “TouchInputHandler” and pass it into our “gamestate” automatically.
Now, if we want to switch our implementations on different devices, we simply switch our our “TouchGameInstaller” with a new installer for the device and make no changes to our GameState classes or any existing InputHandler classes. We no longer risk breaking anything existing when we want to add a new platform. And we can now hook up our GameState to unit tests by using an Installer that registers a TestInputHandler.
You may realize that I haven’t injected any gameobjects yet, and could be wondering how this works with monobehaviors that can’t constructors.
In the next part of this series, I’ll explain how to hook up your gameobjects and monobehaviors with the dependency injection framework and continue the example showing how the entire thing interacts.