2. Creating Your First 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 manually retrieving their dependencies using various methods like GameObject.Find, FindObjectOfType, GetComponent, and Singleton accessors, they just receive all their dependencies through their Init function without having to do anything for it. 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 on its main responsibility and much more flexible.

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
{
    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
{
    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]
    InputManager inputManager;

    [SerializeField]
    Camera camera;

    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?

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>

To make this component as flexible as possible, let’s also change the dependency from the concrete InputManager class, into a dependency to an 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.

public class Player : MonoBehaviour<IInputManager, Camera>

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.

After this you’ll need to do two things to get the code to compile:

  1. Import types from the Sisus.Init namespace with a using directive.
  2. Implement the Init method to receive the IInputManager and Camera references and assign them into fields.

You can automatically do both by selecting the Generate Init method context action in your IDE.

Generate Init method

If your IDE cannot resolve the Sisus namespace, you might need first add InitArgs to the Assembly Definition References list in your Assembly Definition Asset.

After this you can get rid of the [SerializeField] attributes above our fields – the dependencies will now be getting provided through the Init method at runtime, not stored in these fields.

using Sisus.Init;
using UnityEngine;

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

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

    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.

If both IInputManager and Camera are turned into services, then the component will receive them automatically.

If you want to configure them using the Inspector with cross-scene reference support, that’s also possible using an Initializer.

And if you want to pass the arguments in code, e.g. during a unit test, that can be done using GameObject.AddComponent<TComponent, T…> and prefab.Instantiate<T…> as well.

Leave a Reply

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