02. Creating a Component

  03. Getting Started No Comments

What Is Dependency Injection

Creating new components using the base classes in Init(args) is very similar to how it would work normally in Unity. The key difference is how the component obtains references to other objects that it depends on (its dependencies).

Instead of components retrieving their dependencies using GameObject.Find, FindObjectOfType, GetComponent, static singleton properties or serialized fields, they just receive all their dependencies through their Init function. This design pattern is called dependency injection. With the component no longer being responsible for figuring out how all it dependencies should be resolved, it can become more focused, as well as more flexible (more on this later).

Tightly Coupled Components

Let’s say you wanted to create a Player component. You want the GameObject to which the Player component is attached to move when movement input is given, relative to the facing of the camera.

using UnityEngine;

public class Player : MonoBehaviour
{
    private void Update()
    {
        Vector2 moveInput = inputManager.MoveInput;

        if(moveInput != Vector2.zero)
        {
            const float speed = 0.2f;
            float time = Time.deltaTime;
            float distance = time * speed;

            Vector3 moveDirection = camera.transform.rotation * moveInput;

            transform.Translate(moveDirection * distance);
        }
    }
}

In order for this Player class to function, it requires instances of the InputManager and Camera classes.

Now one common and straight-forward strategy for resolving a dependency is by having a static property that holds a shared instance of a class, which the dependent class can use to retrieve the instance. This is often implemented using the singleton pattern.

using UnityEngine;

public class Player : MonoBehaviour
{
    private void Update()
    {
        // Gets the singleton instance via the static property.
        Vector2 moveInput = InputManager.Instance.MoveInput;

        if(moveInput != Vector2.zero)
        {
            const float speed = 0.2f;
            float time = Time.deltaTime;
            float distance = time * speed;

            // Gets the first active Camera with the tag "MainCamera" from the loaded scenes.
            Vector3 moveDirection = Camera.main.transform.rotation * moveInput;

            transform.Translate(moveDirection * distance);
        }
    }
}

This approach has some issues though. One of them is that the dependencies of the component are hidden, scattered across the body of the class. This means you need to read through the whole script to understand what other objects you need to add to your scene for this component to work.

Another issue is that it’s very rigid: the class only works with a specific InputManager singleton object, and only with a specific Camera in the scene that has been assigned the “MainCamera” tag.

You could also take a different approach, and use serialized fields instead.

using UnityEngine;

public class Player : MonoBehaviour
{
    [SerializeField]
    private InputManager inputManager;

    [SerializeField]
    private Camera camera;

    private void Update()
    {
        Vector2 moveInput = inputManager.MoveInput;

        if(moveInput != Vector2.zero)
        {
            const float speed = 0.2f;
            float time = Time.deltaTime;
            float distance = time * speed;

            Vector3 moveDirection = camera.transform.rotation * moveInput;

            transform.Translate(moveDirection * distance);
        }
    }
}

This solves some of the issues when it comes to flexibility. Now it’s possible to drag-and-drop any Camera or InputManager object in and the Player class will use them! This is in fact a form of dependency injection.

But this approach isn’t without its problems either. If you need to change your InputManager to a different instance a couple months later, you will need to go change references in all components across the whole project by hand to point to the new instance. Also when working with prefabs, multiple scenes, or objects that are only created at runtime, it could be impossible to drag-and-drop references in. Not to mention creating unit tests for this component would be very difficult.

So how can we get rid of all these limitations and downsides, and create loosely coupled components that are as flexible as possible, as well as easily unit testable by default? This is where Init(args) comes in.

MonoBehaviour<T…>

The first thing we need to change is explicitly define all the other types that the Player class depends on, as generic arguments of the MonoBehaviour base class.

public class Player : MonoBehaviour<InputManager, Camera>

Since we want to make this component as flexible as possible, let’s also at this point change the dependency to the concrete InputManager class, into a dependency to an abstract IInputManager interface instead. This way it’s possible to very easily swap the component to use a different implementation at any point. This can be especially useful when writing unit tests.

Normally using interfaces in Unity is problematic, firstly because Unity’s serializer doesn’t support Object references in interface type fields, and secondly because there’s no Inspector support for interfaces that would allow any other type of data into these fields either. With Init(args) these are no longer an issue, so we can use interfaces as much as we want.

public class Player : MonoBehaviour<IInputManager, Camera>

After these changes, the code won’t compile any longer. This is because the built-in UnityEngine.MonoBehaviour class cannot be used with type arguments.

The Sisus.Init namespace contains new generic MonoBehaviour bases classes, which we want to switch to using here instead. To allow us to reference it here, we need to add a using directive for Sisus.Init to the script.

This can easily be done using Quick Actions in Visual Studio.

If you’re unable to perform this step, you might need first add InitArgs to the Assembly Definition References list in your Assembly Definition Asset.

After these steps we’re faced with a second compile error, this time about the Init function not being implemented.

This can also be fixed with a few clicks using the Quick Actions menu.

Now for the finishing touches: we’ll implement the body of the Init function, rename its parameters to something a bit more descriptive, and get rid of the [SerializeField] attributes above our fields (the instances will be provided through the Init function at runtime, not stored in these fields).

using Sisus.Init;
using UnityEngine;

public class Player : MonoBehaviour<IInputManager, Camera>
{
    private IInputManager inputManager;
    private Camera camera;

    protected override void Init(IInputManager inputManager, Camera camera)    
    {
        this.inputManager = inputManager;
        this.camera = camera;
    }

    private void Update()
    {
        Vector2 moveInput = inputManager.MoveInput;
        if(moveInput != Vector2.zero)
        {
            const float speed = 0.2f;
            float time = Time.deltaTime;
            float distance = time * speed;
            Vector3 moveDirection = camera.transform.rotation * moveInput;
            transform.Translate(moveDirection * distance);
        }
    }
}

And that’s it, we are now all done creating our extra flexible Player component that receives all its dependencies via dependency injection.

The component can now be initialized with arguments using GameObject.AddComponent<TComponent, T…>, prefab.Instantiate<T…> or using the Inspector by generating an Initializer for the component.

If both IInputManager and Camera are made into services, then these will be injected to the component automatically, even if the component is a scene object with no initializer or if GameObject.AddComponent<T> or Object.Instantiate<T> is used.

Leave a Reply

Your email address will not be published. Required fields are marked *