Dependency Injection with Unity - Part 2

Table of Contents

In part 1 I discussed the approaches that can solve the problem of wiring dependencies within your game project. In this article I will show how you can actually use dependency injection in your Unity based game. I will also show how to work with objects that are not under your direct control, like MonoBehaviours.

Use a framework or roll your own?

There are a few options on dependency injection frameworks that work with Unity like Zenject or Ninject. If you are really short on time or just want something that works without having to know how it does its magic frameworks then by all means go with a framework.

If you really want to understand what is going on (and you will need to understand what is going on, when inevitably problems are showing up later in the project) rolling your own solution may be the better option. This also gives you more flexibility to tailor the dependency injection to the needs of your project.

For this article I will go with a “roll your own” solution, because this shows how to solve certain problems. You can apply this knowledge later to your own code or the framework of your choice.

As an example project I would like to create a button which spawns player objects in the scene. The player object will be spawned at a random location. The prefab to be used and the player’s initial health and strength will be read from a configuration file when the game starts. It’s not a great game but it should suffice as a demonstration of the principles.

The components

In the project there are a few components. First there is a ConfigurationService which provides access to some configuration settings:

public class ConfigurationService
{
  // Returns anything required to spawn a player.
  public PlayerInfo GetPlayerSpawnConfiguration()
  {
     //...
  }
}

ConfigurationService needs to get the configuration from somewhere. It could read it from a file or get it from a server or just randomly create it. In this example, I will read the configuration from a file using Unity’s built-in JsonUtility into a custom Configuration object and give this to the ConfigurationService.

public class Configuration
{
  public int initialPlayerHealth;
  public int initialPlayerStrength;
  public string playerPrefab;
}

Then there is a PlayerService that is responsible for spawning the players:

public class PlayerService
{
  public void SpawnPlayerAt(Vector3 position)
  {
      // ...
  }
}

Then there is a Button which should be clicked to spawn a new player. This is just a simple Unity-GUI button. For the button to actually work another component is needed which listens on click events of the button and triggers the spawning of the player. I called it Spawner:

public class Spawner
{
  private void Spawn()
  {
    // call the player service here
  }
}

These are the main components. Let’s have a quick look at how they depend on each other:

The Spawner needs the Button and the PlayerService. The PlayerService needs the ConfigurationService and finally the ConfigurationService needs the Configuration. Now that you know the dependencies of each component you can write the appropriate constructors - I show the Spawner class here as an example, you can see the others in the example project:

public class Spawner
{
  private readonly PlayerService _playerService;

  public Spawner(Button button, PlayerService playerService)
  {
    // save the PlayerService to an instance variable 
    _playerService = playerService;
    
    // wire up the button click event
    button.onClick.AddListener(Spawn);
  }
  
  private void Spawn()
  {
    // calculate a random spawn location
    var position = Random.insideUnitCircle;
    var spawnPosition = 5 * new Vector3(position.x, 0, position.y);
    
    // call the player service to actually spawn the player.
    _playerService.SpawnPlayerAt(spawnPosition);    
  }
}

Bootstrapping the project

Now that all the components are there, they need to be put together somehow. As a first step they can be simply initialized by hand:

public class ManualBootstrapper : MonoBehaviour
{
  void Start() 
  {
    // read config file
    var configText = File.ReadAllText(
        Application.dataPath + "/config.json");

    // build up dependencies
    var config = JsonUtility.FromJson<Configuration>(configText);
    var configService = new ConfigurationService(config);
    var playerService = new PlayerService(configService);
    var button = (Button) FindObjectOfType(typeof(Button));
    var _ = new Spawner(button, playerService);
  }
}

Just by thinking about the dependencies you get a nicely designed set of dependencies that can be easily initialized. At this point you don’t even need a framework as only a few lines of code will make it all work.

The benefit of doing things manually is that you have full control over what is happening. Note how easy it is to wire up MonoBehaviours like the Button with non-MonoBehaviour components. In addition the compiler will actually prevent many mistakes at compile time instead of later at runtime. You cannot forget to give a dependency to an object as the compiler will remind you that you are lacking a constructor parameter. You cannot accidentally create circular dependencies as there is no way to express this in the language:

public class ComponentA {
    public ComponentA(ComponentB b) {
    }
}

public class ComponentB {
    public ComponentB(ComponentA a) {
    }
}

// this will not compile
var componentA = new ComponentA( /* no B to give */ );
var componentB = new ComponentB( /* no A to give */ );

However when you have a few hundred components, initializing stuff manually is quickly turning into a boilerplate fest. Writing hundreds of similar constructor calls in a pages-long initialization function and manually sorting a large dependency tree isn’t great either. So how can you automate this to help streamline the code and reduce cognitive overhead?

A simple dependency injection solution

Let’s think for a moment what the dependency injection solution should do for you:

  • It should be able to know components in your project.
  • It should find out the dependencies between these components.
  • It should find out the correct initialization order of the components.
  • It should initialize and wire all components together.
  • It should work with objects where you have control over the lifecycle as well as with objects where you don’t have this control (like MonoBehaviours).

That is quite a list and you did all of this in your head until now! As a starting point you could create class named DependencyContext. This class will need a few methods to be useful. Going by the list, you will need a method to make a component known:

public class DependencyContext {
  // Allows to declare a component
  public void Declare<T>() {}
}

For now even though the method is currently empty, let’s assume it would work. I will cover implementation of this later on. This new method will allow you to declare components you write, so the the DependencyContext knows they exist and can create them:

public class Bootstrapper : MonoBehaviour
{
  void Start() 
  {
    var context = new DependencyContext();
    
    context.Declare<ConfigurationService>();
    context.Declare<PlayerService>();
    context.Declare<Spawner>();
  }
}

This takes care of components where the lifecycle is under your control. Now you will need a way of declaring components that Unity (or some other code outside of your control) creates. In the example this would be the Button which is created by Unity and needs to be read from the scene and the Configuration which is created by JsonUtility. So let’s add a second Declare method where instead of a type parameter a fully initialized object can be given:

public class DependencyContext {
  ...
  
  // Allows to declare an already initialized component
  public void Declare(object component) {}
}

With this in place you can declare the remaining components:

public class Bootstrapper : MonoBehaviour
{
  void Start() 
  {
    var configText = File.ReadAllText(
        Application.dataPath + "/config.json");
    
    var context = new DependencyContext();
    
    context.Declare<ConfigurationService>();
    context.Declare<PlayerService>();
    context.Declare<Spawner>();

    var button = (Button) FindObjectOfType(typeof(Button));
    var config = JsonUtility.FromJson<Configuration>(configText);
    context.Declare(button);
    context.Declare(config);
  }
}

Now that all the components are made known to the dependency context, it is time that everything is initialized. For this you will need a third method:

public class DependencyContext {
  ...
  
  // Constructs and wires up all known components
  public void Resolve() {}
}

So finally you will have initialization code that looks like this:

public class Bootstrapper : MonoBehaviour
{
  void Start() 
  {
    var configText = File.ReadAllText(
        Application.dataPath + "/config.json");

    var context = new DependencyContext();
    
    context.Declare<ConfigurationService>();
    context.Declare<PlayerService>();
    context.Declare<Spawner>();

    var button = (Button) FindObjectOfType(typeof(Button));
    var config = JsonUtility.FromJson<Configuration>(configText);
    context.Declare(button);
    context.Declare(config);
    
    context.Resolve();
  }
}

This is actually more code than the manual approach before. So how is this better? It is better in three ways:

  1. You don’t need to sort dependencies yourself. The DependencyContext will figure out the correct order of instantiation.
  2. There is a lot less noise in the code because you don’t have to invoke all the constructors and shuffle variables around.
  3. You now have the ability to dynamically add components (e.g. using reflection). This is useful if you plan to have mod support in your game, because it allows you to load code from mods and easily integrate it with your existing components. This wouldn’t be possible if all the bootstrapping was hard-coded.

But - as always in life - there is a trade-off here. You could build cyclic dependencies or forget to Declare a component and the compiler will not warn you about this anymore.

Implementing the missing methods

So now that the bootstrap code is in place, you still need to implement the Declare and Resolve methods you left empty earlier on. How could the Resolve method do its magic? First, it would need to know what work still needs to be done, so it would need some kind of collection that stores unresolved types:

public class DependencyContext {
  private HashSet<Type> _unresolved = new HashSet<Type>();
}

It would also need to know which dependencies it already has resolved. Because the DependencyContext matches dependencies by type (e.g. the constructor of the Spawner takes a Button and a PlayerService, so a component of type Button and one of type PlayerService is required) having resolved dependencies as a dictionary will help look up missing constructor arguments:

public class DependencyContext {
  private List<Type> _unresolved = new List<Type>();
  private Dictionary<Type,object> _resolved = 
      new Dictionary<Type,object>();
}

Now that you have these in place, it is very easy to implement the two Declare methods:

public class DependencyContext {
  private HashSet<Type> _unresolved = new HashSet<Type>();
  private Dictionary<Type,object> _resolved = 
      new Dictionary<Type,object>();
      
  public void Declare<T>() {
    _unresolved.Add(typeof(T));
  }
  
  public void Declare(object component) {
    _resolved[component.GetType()] = component;
  }
}

The second Declare method takes fully initialized components. Because of this, the DependencyContext does not need to resolve these components anymore and you can put them directly into the _resolved dictionary. Now all that is remaining is to implement the Resolve method. The algorithm for this could be:

  • While there are still unresolved dependencies:
    • Take a type from _unresolved and look at its constructor.
    • Check if every parameter of this constructor is already in the _resolved dictionary. If so, create an instance using the arguments from the _resolved dictionary and remove the type from the _unresolved dictionary.
public class DependencyContext {
  private HashSet<Type> _unresolved = new HashSet<Type>();
  private Dictionary<Type,object> _resolved = 
      new Dictionary<Type,object>();

  ...
  
  public void Resolve() {

    // while there are still unresolved dependencies
    while(_unresolved.Count > 0) {

      var unresolvedTypes = new List<Type>(_unresolved);
      var hasProgress = false;
    
      // walk over all remaining unresolved dependencies
      foreach(var unresolvedType in unresolvedTypes) {

        // check if the constructor can be invoked 
        // using the known dependencies
        if (TryConstruct(unresolvedType, out var result)) {
          _unresolved.Remove(unresolvedType);
          _resolved[unresolvedType] = result;
          hasProgress = true; 
        }
      }

      // The hasProgress flag helps to avoid infinite loops
      // in case you forgot to declare a component
      // or in case some components form a cyclic dependency      
      if (!hasProgress) {
        throw new InvalidOperationException(
            "Context contains unresolvable components");
      }
    }
  }
}

So the Resolve method isn’t too complicated after all. It uses a private method TryConstruct to do the actual construction of the dependency:

private bool TryConstruct(Type type, out object result) {
  var constructors = type.GetConstructors();

  foreach (var constructor in constructors) {
    if (!ResolveConstructorDependencies(constructor,
        out var resolvedDependencies)) {
      continue;
    }

    // all dependencies of this constructor have been 
    // resolved, invoke the constructor
    result = constructor.Invoke(resolvedDependencies);
    return true;
  }

  // no constructor could be resolved, so give up.
  result = default;
  return false;
}

TryConstruct uses reflection to find all the constructors in the given type. Then it walks over all the constructors and checks if it can find one for which all the parameters are already known in the _resolved dictionary. If one is found, the constructor is invoked and the component is created. TryConstruct again uses some private method to do the actual constructor argument resolution:

private bool ResolveConstructorDependencies(
    Constructor constructor, 
    out object[] dependencies)
{
  var parameters = constructor.GetParameters();
  var resolvedDependencies = new object[parameters.Length];
  
  // walk over all parameters of the constructor
  for (var index = 0; index < parameters.Length; index++)
  {
    var parameter = parameters[index];
    var parameterType = parameter.ParameterType;
    
    if (_resolved.TryGetValue(parameterType, out var value)) {
      resolvedDependencies[index] = value;
      continue;
    }
    // if there is no matching dependency, give up.
    dependencies = default;
    return false;
  }
  
  // all parameters could be resolved
  dependencies = resolvedDependencies;
  return true;
}

ResolveConstructorDependencies is where the actual dependency resolution takes place. The method again is rather simple. It walks over all parameters of the given constructor and checks if all parameters are already contained in the _resolved dictionary. If so it returns true along with an array of constructor arguments that can be used to actually invoke the constructor in the TryConstruct method above.

You don’t necessarily need to split this code over three methods, but I found that a little divide and conquer really helps to keep the code short, clean and easy to read.

If you made it this far, congratulations! It surely has been a long post.

Summary

In order to use dependency injection you actually don’t need a framework at all. You can bootstrap a project with just a few calls to constructors and FindObjectOfType. However in non-trivial projects there are many components that have dependencies and wiring them up all manually is a tedious process. I showed that with only a small amount of code you can automate this. You also saw that it is important to be able to work with objects that have their own lifecycle outside of your control (like MonoBehaviours) because they are an important part of Unity’s way of doing things. Any dependency injection solution you use will need to be able to handle them.

In the third part I will show you how you can inject dependencies into MonoBehaviours (so far we only injected MonoBehaviours into non-MonoBehaviour classes). Also I will show how you can use assembly scanning to limit the amount of boilerplate code even more. Finally I will go over how a dependency injection setup will ease unit testing.