1. Initializer

  6. Initialization No Comments

A major benefit of injecting dependencies to your classes through the Init method is that it makes it easy to decouple your components from specific implementations when you use interfaces instead of specific classes as your argument types. The list of arguments that the Init method accepts also makes it very clear what other objects the client objects depends on.

On the other hand, the ability to assign values using Unity’s inspector is also a very convenient and powerful way to hook up dependencies; you can completely change object behaviour without having to write a single line of code!

The Initializer system is a solution that aims to marry the best of both worlds.

Benefits Of Using Initializers

  • Clearly Defined Dependencies – You can see all dependencies of a component easily on both the code side and the Inspector side, just by looking at its Init arguments; no need to open the script and read through the whole thing to find all potential singleton references, GetComponent calls and serialized fields.
  • Interface Support – With Initializers interfaces become first class citizens: you can drag and drop to assign interface type arguments, and they are serialized automatically as well (even if they derive from UnityEngine.Object).
  • Automatic Service Injection – All Service dependencies are resolved automatically, so you don’t need to drag-and-drop the same manager object to dozens of fields. This also means that you can easily change the service later on to use a different implementation, and all references to the service across the project update automatically!
  • Manual Service Overriding – When the need arises, you can also change a single component to use something else besides the shared Service, simply by dragging-and-dropping another Object into the Init argument field. This gives you the perfect combination of making it easy to change a service project wide, while also giving you the flexibility to change the service on just a single client.
  • Easily Locate Services – You can click the Service Tag on any service arguments in the Initializer’s inspector to locate the service in question wherever it’s located in the scene hierarchy or defined in a script asset.
  • Cross-Scene References – Simply drag-and-drop a reference from another scene into an Init argument field, and it will persist even after the scenes are unloaded. Multi-scene workflows have never been easier.
  • Default Object Field Values – You can add the InitOnReset attribute on the Initializer to automate the component setup process without having to clutter the client component with these implementation details.
  • Edit Mode Null Guard – Initializers can automatically warn you about any missing references in edit mode.
  • Runtime Null Guard – Initializers can automatically throw an exception when a missing reference is detected at runtime as well.
  • Unit Testable By Default – When you use initializers, it also means that your components will have an Init function that the initializers can use to pass the arguments. This means that creating unity tests for your components becomes automatically so much easier, because you can initialize all your components using a single line of code, and substituting dependencies with mocks becomes trivial.
  • Separated Serialization Logic – Since the Init arguments are serialized separately from the main component, you can freely use auto-implemented properties and any types you want, without having to worry about whether or not Unity can serialize them. You can finally work with dictionaries, tuples etc. without having to clutter your main component with serialization related boilerplate code. It also makes it possible to make your collections and other fields read-only, so you can know that it’s impossible for them to ever cause NullReferenceExceptions.
  • Single Responsibility Principle – When the responsibility of resolving all dependencies is off-loaded to Initializers, it simplifies your main components and lets them focus solely on their main responsibilities. This makes it possible to better follow the single-responsibility principle and make your code more readable.

Creating Initializers

The Init section

The easiest and recommended way to create an Initializer for a component is to have Init(args) generate it for you automatically.

When you have a component that derives from MonoBehaviour<T…> or implements IInitializable<T…>, an Init section will automatically appear at the top of the components of that type in the Inspector.

To generate an Initializer for the the component class, click on the + button in the Init section and select “Generate Initializer”.

This will cause a new Initializer class to get automatically generated for your component class. It will be saved at the same location where your component script is located and named the same as your component class but with the “Initializer” suffix added.

When your component class already has an Initializer generated, you can use this same + button to attach an instance of the Initializer class to your component instead.

Script Asset Context Menu

In some case you might want to generate an Initializer for a class but it either is not a component type, or doesn’t have an Init section in the Inspector yet.
For example you might want to generate an Initializer for a plain old class object which you will be attaching to a GameObject using a Wrappers.

In such cases you can select the script asset that defines the class in the Project view and select “Generate Initializer” from the context menu.

Creating Manually With Code

Initializer<T…>

It is also easy to create new Initializers in code for your components by deriving from the Initializer<T…> base classes with the type of the component class as the first generic argument, followed by the types of its Init parameters.

For example, to define an Initializer for component Player, which derives from MonoBehaviour<IInputManager, Camera>, you would write the following:

public class PlayerInitializer : Initializer<Player, IInputManager, Camera> { }

InitializerBase<T…>

Initializers that derive from Initializer<T…> can hold and serialize Init arguments of UnityEngine.Object, types, as well as any types that Unity can serialize with the SerializeReference attribute. This includes interface types, and even in cases where the Init parameter is an interface type and the assigned value is a reference to a UnityEngine.Object type object.

This means that Initializer<T…> classes have all the same limitations that fields with the SerializeReference attribute have when it comes to serialization: most notably the lack of support for serializing generic types (with the exception of List<T>).

In cases where you need an Initializer to serialize an object that isn’t supported by SerializeReference, you can derive from InitializerBase<T…>, implement the properties for all the arguments, and handle serializing them however you want.

using System;
using System.Collections.Generic;
using System.Linq;
using Sisus.Init;
using UnityEngine;
using Object = UnityEngine.Object;

public class DatabaseInitializer : InitializerBase<Database, Dictionary<string, Object>>, ISerializationCallbackReceiver
{
    protected override Dictionary<string, Object> Argument { get; set; }

    [SerializeField]
    private SerializedElement[] serializedElements;

    void ISerializationCallbackReceiver.OnBeforeSerialize()
    {
        serializedElements = Argument.Select(element => new SerializedElement(element.Key, element.Value)).ToArray();
    }

    void ISerializationCallbackReceiver.OnAfterDeserialize()
    {
        Argument = new Dictionary<string, Object>();
        Array.ForEach(serializedElements, (element) => Argument.Add(element.key, element.value));
    }

    [Serializable]
    private sealed class SerializedElement
    {
        public string key;
        public Object value;

        public SerializedElement(string key, Object value)
        {
            this.key = key;
            this.value = value;
        }
    }
}

WrapperInitializer<T…>

To define an Initializer for a Wrapper, derive from the WrapperInitializer<T…> base class, with the type of the wrapped C# class as the first generic argument, followed by the types of its constructor parameters (assuming that a constructor is being used to pass in the object’s dependencies).

Secondly you need to override the CreateWrappedObject function with parameters matching the constructor parameters of the wrapped class, and implement the logic for creating the wrapped object.

public class PlayerInitializer : WrapperInitializer<PlayerComponent, Player, IInputManager, Camera>
{
    protected override Player CreateWrappedObject(IInputManager inputManager, Camera camera)
    {
        return new Player(inputManager, camera);
    }
}
Circular Dependencies

Note that when using constructors to initialize your wrapped objects, it’s possible to run into an issue with circular dependencies. For example, if Player’s constructor requires an InputManager argument, and InputManager’s constructor requires a Player argument, then it’s not possible to create either object.

This issue can be resolved by splitting creation of the wrapped object into two phases: first getting/creating the instance, and secondly passing to it its Init arguments.

Get Or Create Instance Step

To enable the first step to happen, you need to also override the GetOrCreateUnitializedWrappedObject function.

public class PlayerInitializer : WrapperInitializer<PlayerComponent, Player, IInputManager, Camera>
{
    protected override Player GetOrCreateUnitializedWrappedObject()
    {
        return new Player();
    }

    protected override Player CreateWrappedObject(IInputManager inputManager, Camera camera)
    {
        return new Player(inputManager, camera);
    }
}

Alternatively if you add the [Serializable] attribute to the wrapped class, then the Wrapper will hold an uninitialized instance of it by default, from which the default implementation of GetOrCreateUnitializedWrappedObject can automatically retrieve it.

Init Step

To enable the second step to happen you need to have your wrapped class implement IInitializable<T…> for receiving the initialization arguments in a deferred manner.

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

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

With this two changes Init(args) can get past the circular reference by first creating the Player instance without initializing it with its Init arguments, then creating the InputManager instance with the Player instance passed to it, and then finally initializing the Player instance by passing in its Init arguments.

PropertyAttributes And Initializers

Sometimes you may want to customize how the Initializer arguments appear in the inspector by adding PropertyAttributes to them.

To do this, you will need to add a private nested Init class inside the Initializer, and then define a field for each Init argument accepted by the Initializer’s client with their order and types matching those of the arguments in the client’s Init method.

Any property attributes you attach to these fields will then get used when the corresponding initialization arguments are drawn in the Inspector.

public class PlayerInitializer : Initializer<Player, IInputManager, float>
{
   #if UNITY_EDITOR
   private class Init
   {
      public IInputManager inputManager;

      [Range(0f, 100)]
      public float speed;
   }
   #endif
}

Alternatively you can derive from InitializerBase<T…> which gives you the ability to manually define serialized fields to hold the data for all the Init arguments.

Leave a Reply

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