top of page
Search
yuli0203

Dependency Injection in Unity - How to

Updated: Feb 4, 2022

In my previous post, I have attempted to explain the benefits of using DI. In the previous examples we passed dependencies manually. That is actually not a very practical thing to do in a large project. If we have dedicated classes for dependency creation, which are separated from the classes that use them, we would need to pass them in an efficient way (and not by a constructor-to-constructor chain). For this purpose we have DI frameworks. I choose to demonstrate it using the popular DI framework - Extenject (Zenject) in Unity 3D, which is a free open-source, lightweight, Unity DI framework. It can be added to a Unity project by downloading it from the asset store.


You would need to know the following terms:


Context - The scope of the dependencies.

Installer - A class that is responsible for creating dependencies and their bindings for a certain context.

Container - A "storage" for dependencies, which are distributed by the DI framework, to the appropriate classes, in a certain context.

Binding - The connection between a created dependency, and it's identification (an interface, or an id, or the class itself), which is stored in the Container. The classes that request the identification (using an "Inject" attribute) will receive the dependency.


Getting Started


Once you have set up your project and downloaded Extenject asset, you can start by adding a game object to your first scene and attaching SceneContext to it.



Contract Names, and Parent Contact Names


These are a scene names. The can be used to be able to create hierarchies between scene contexts, to have a child scene to inherit it's parent scene bindings.


Lets imagine that we are beginning to build a platform game. Lets add a player components into the scene context, in order to demonstrate how to use the scene context. The player components would be divided by classic MVVM, module-view-view model-controller.


Scriptable Object Installer


I'm starting with creating the player data (module) using a scriptable object

using System;
using UnityEngine;
using Zenject;

[CreateAssetMenu(fileName = "PlayerSettingsInstaller ", menuName = "Installers/PlayerSettingsInstaller ")]
public class PlayerSettingsInstaller : ScriptableObjectInstaller<PlayerSettingsInstaller >
{
    public PlayerSettings playerSettings;

    [Serializable]
    public class PlayerSettings 
    {
        public int health = 3;
        public int xp = 0;
        public float movementSpeed = 1f;
    }

    public override void InstallBindings()
    {
        Container.BindInstance(playerSettings);
    }
}

Now I can create an instance of the installer using the menu path, and place it in the scene context so that it binds the player settings instance into the Container.


Mono Installer


This installer is responsible for binding the player controller to the scene. It creates a single instance of it (non-lazy) and binds it to the Container.


using Zenject;

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.BindInterfacesAndSelfTo<PlayerController>().AsSingle();
    }
}

The mono was placed on the context game object (it can be on a different game object), and placed it in Mono Installers.


Prefab Installers


We can put the GameInstaller script on a prefab and drag the prefab to Prefab Installers, instead of placing it in the scene and using the mono Installers field.


Player Controller, View and View Model


Now what is left it to implement the player components. In Unity, I usually look at the components as following: Prefabs and GameObject are View, MonoBehaviors are View-Model, and the Controller is non-Unity related logic management code.


Controller:


using Zenject;
using static PlayerSettingsInstaller;

public class PlayerController
{
    private PlayerSettings playerSettings;

    private PlayerViewModel playerViewModel;

    [Inject]
    public PlayerController(PlayerSettings playerSettings, PlayerViewModel playerViewModel)
    {
        this.playerSettings = playerSettings;
        this.playerViewModel= playerViewModel;
    }

    public void Move(Direction direction)
    {
        playerViewModel.Move(direction, playerSettings.movementSpeed);
    }
}

View Model would be something like this:

public class PlayerViewModel : MonoBehaviour
{
    [SerializeField]
    private Transform playerTransform;

    public void Move(Direction direction, float movementSpeed)
    {
        // Implement move on transform
    }
}

When it is already in the Scene (not instantiated during runtime) it can be bound using the Zenject Binding script:


Benefits


When all of our player components are well decoupled, we earn flexibility. For example, if we want to show the player states, we will be able to inject the player data to the relevant classes. We will be able to replace the view easily by binding a different suitable view. We will be able to inject the PlayerController to other classes so that they can move the player, an so on...


Next Steps


If you feel comfortable with the basics, the next recommended thing to learn is how to bind runtime created instantiations, learn about the different types of bindings, factories, and more features. They can be found in the project documentation: Zenject.

Recent Posts

See All

Comments


bottom of page