1. MonoBehaviour<T…>

  4. Clients No Comments

Receiving Dependencies

Init(args) extends the MonoBehaviour class with the ability to receive up to twelve arguments during its initialization, before any of its lifetime events are executed.

To enable your component to receive objects passed to it from the outside, make the class derive from MonoBehaviour<>, and list the types of all the objects needed as generic type arguments of the base class. Next implement the Init method, where all the required objects will be received.

In the example below, the Player object can receive three objects during its initialization: IInputManager, Camera and Collider.

using Sisus.Init;
using UnityEngine;

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

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

The Init function gets executed when the object is being initialized, before the Awake or OnEnable events.

OnAwake

The MonoBehaviour<T…> base class automatically receives Init arguments that have been injected to the object in its Awake method. It then first passes the received arguments to the Init method, and after that executes the OnAwake method.

If you want to execute code during the Awake lifetime event in a component that derives from MonoBehaviour<T..>, you should override the OnAwake method, instead of defining a new Awake method:

class Player : MonoBehaviour<IInputManager>
{
   IInputManager inputManager;

   protected override void Init(IInputManager inputManager) => this.inputManager = inputManager;

   protected override void OnAwake() => Debug.Log($"I can use {inputManager} in OnAwake!");
   void OnEnable() => Debug.Log($"I can use {inputManager} in OnEnable!");
   void Start() => Debug.Log($"I can use {inputManager} in Start!");
}

OnReset

Similarly, the MonoBehaviour<T…> base class already defines a Reset method, to support the InitOnReset feature.

If you want to execute code during the Reset lifetime event in a component that derives from MonoBehaviour<T..>, you should override the OnReset method, instead of defining a new Reset method:

class Player : MonoBehaviour<IInputManager>
{
   IInputManager inputManager;
   [SerializeField] Collider collider;

   protected override void Init(IInputManager inputManager) => this.inputManager = inputManager;

   protected override void OnReset() => collider = GetComponent<Collider>();
}

AddComponent with Arguments

Init(args) extends the GameObject type with new AddComponent methods that allow passing Init arguments to the created component.

To do so, list the types of the Init parameters after the type of the added component, and pass in the objects you want as arguments of the method:

gameObject.AddComponent<Player, IInputManager, Camera>(inputManager, camera);

Alternatively, you can use an AddComponent variant which assigns the created component into an out parameter, instead of return it.

gameObject.AddComponent(out Player player, inputManager, camera);

This is functionally identical, but it can make it possible to omit all the generic type arguments, which can help improve readability. This is possible because the C# compiler can determine the generic type arguments implicitly from the types of the variables that are passed in to the method as arguments.

Note that in order for the C# compiler to be able to do this, the types of the provided variables must match the types of the Init method parameters exactly. So if, for example, the Player’s Init method has a parameter of type IInputManager, and you pass in a variable of type InputManager, the C# compiler will not be able to figure out the generic type arguments for the method call automatically. You can however help the compiler to get past this hurdle by casting the variable to the exact type of the Init method parameter:

var inputManager = Service.GetFor<InputManager>(gameObject);
gameObject.AddComponent(out Player player, (IInputManager)inputManager, camera);

The AddComponent extension methods can be used automatically with any components that derive from MonoBehaviour<T…>.

They can also be used with any components that implement IArgs<T…> and manually receive the injected arguments using InitArgs.TryGet in their Awake or OnEnable method.

It can also be used automatically with any components that implement IInitializable<T…>, even if they don’t manually receive the injected arguments via InitArgs.TryGet. However, if they don’t receive the arguments manually, then they will only receive them in their Init method after the Awake and OnEnable events. This means that it is only possible to make use of those services during and after the Start event.

Instantiate with Arguments

Init(args) also extends all components and scriptable objects with new Instantiate methods that allow passing arguments to the created instance.

To clone an Object while passing Init arguments to the clone, call Instantiate on the Object you want cloned, and pass in the Init arguments to the method:

prefab.Instantiate(inputManager, camera);

New GameObject with Arguments

Init(args) also lets you create a new GameObject from scratch, attach a component to it, and pass Init arguments to the component, all in one go.

To do so, use new GameObject like you normally would, but now also include the type of the component you want attached to the GameObject as generic type argument. Then, call the Init method on the object to provide Init arguments to the attached component and finalize the whole process:

var player = new GameObject<Player>().Init(inputManager, camera);

If you don’t need to pass any Init arguments to the attached component, you can finalize the process either by calling Init() with no arguments, or by assigning the object into a variable of type GameObject, or of a type matching the attached component:

var player = new GameObject<Player>().Init();

Player player = new GameObject<Player>();

GameObject gameObject = new GameObject<Player>();

You can also attach and initialize more than one component (up to three) at the same time, by adding generic type arguments for all the components you want attached.

Then instead of calling Init on the result, you can use Init1, Init2 and Init3 to initialize the first, second and third attached components respectively:

new GameObject<Player, Collider>().Init1(inputManager, camera, Second.Component);

Once you have provided Init arguments for all the components that need them, you can finalize the whole process by assigning the object into a variable of type GameObject, of a type matching one of the added component, or of a tuple type matching several of the added components:

Player player = new GameObject<Player, Collider>();

GameObject gameObject = new GameObject<Player, Collider>();

(Player player, Collider collider) = new GameObject<Player, Collider>();

Service Arguments

If all Init arguments of a component that derives from MonoBehaviour<T…> are global or local services, then the component will receive all of them automatically during its initialization.

In this case you do not need to need to attach an Initializer to the component when it exists as part of a scene or a prefab.

In this case you can also use the built-in Instantiate and AddComponent methods to create instances of the components, without needing to pass in their dependencies manually.

Note that you can still attach an Initializer or use Instantiate or AddComponent with arguments to customize the services being provided to specific client instance.

For example, you could make all components use the DefaultLogger class for logging text to the Console by default, but in unit tests pass in a TestLogger using AddComponent with arguments. Or to debug the behaviour of a particular component exhibiting strange behaviour, you could temporarily attach an Initializer to it and configure it to use the more verbose DebugLogger.

[Service(typeof(ILogger))]
class DefaultLogger : ILogger
{
    public void Debug(string message) { }
    public void Info(string message) => Debug.Log(message);
    public void Error(string message) => Debug.LogError(message);
}

class DebugLogger : ILogger
{
    public void Debug(string message) => Debug.Log(message);
    public void Info(string message) => Debug.Log(message);
    public void Error(string message) => Debug.LogError(message);
}

class TestLogger : ILogger
{
    public void Debug(string message) { }
    public void Info(string message) { }
    public void Error(string message) => Debug.LogError(message);
}

Arguments via the Inspector

You can also use the Inspector to configure the Init arguments for instances of components that derive from MonoBehaviour<T…>.

Refer to the Initializer documentation for more information on how to do this.

Manually Passing Arguments In Code

It addition to all the methods listed above for passing Init arguments to components that derive from MonoBehaviour<T…> during their initialization, it is also possible pass arguments to the Init method manually.

One example of a scenario where this might be useful is when using the object pool pattern, where you might want to initialize reused instances with new Init arguments every time that they are returned from the pool.

To manually pass in arguments to the Init method, simply cast the component to IInitializable<T…> and execute the Init method on it:

var initializable = (IInitializable<IInputManager, Camera>)player;
initializable.Init(inputManager, camera);

Init Section

An Init section is shown by default in the Inspector for all components that derive from MonoBehaviour<T…>.

You can mouseover the Init label to see information about the objects that the component can receive during its initialization.

If you want to hide the Init section for all components of a particular type, you can do so by disabling Show Init Section in the context menu of any component of said type.

This can be useful in situations where Edit Mode configuration is handled via serialized fields, and providing arguments via the Init method is supposed to be optional, and only done when creating instances at runtime using AddComponent (perhaps to facilitate easy unit testing).

Null Argument Guard Icon

All components that derive from MonoBehaviour<T…> can validate that they have received all objects that they depend on via the Init method before the OnAwake event. If the Null Argument Guard is active, and a MonoBehaviour<T…> component is loaded without it having been provided it with its Init arguments, then a MissingInitArgumentsException will be thrown.

You can see information about the current state of the Null Argument Guard by mouseovering the Null Argument Guard icon in the Inspector.

The Null Argument Guard is enabled by default. You can disable it for all components of a particular type by clicking the Null Argument Guard icon in the Init section of any component of that particular type, and selecting None.

Note that you can also adjust Null Argument Guard settings on a per-instance basis, if you attach an Initializer.

Add Initializer Icon

You can use the plus icon to generate or attach an Initializer for the component.

Runtime State

By default the Unity editor only visualizes serialized fields in the Inspector. This can make it difficult to debug components at runtime, and gain information about whether or not they’ve acquired all the objects that they depend on.

To account for this, Init(args) augments the Inspector of all components that derive from MonoBehaviour<T…> with the ability to also see the state of non-serialized members in the Inspector in Play Mode.

Init Method Best Practices

It is generally speaking recommended to not use the Init method for anything else besides assigning the received arguments into member variables.

Lifetime event methods like OnAwake, OnEnable and Start can then be used to execute additional logic that can make use of those services.

There are a couple of reasons for this recommendation:

  • The Init method can be executed in Edit Mode if the class has the [InitOnReset] or [InitInEditMode] attribute.
  • The Init method can be executed by InactiveInitializer while the GameObject is still inactive, if this behaviour has been selected using the Inspector.
  • The Init method can be executed by an Initializer while the component is still disabled when there’s a need to wait for an async service or value provider.
  • The Init method can be executed by new GameObject<T> while the GameObject is still inactive, in order to defer component lifetime events from executing until initialization arguments have been passed to all the attached components.
  • InitArgs.TryGet is thread-safe, and can theoretically be used to receive injected Init arguments even in the constructor or during the OnAfterDeserialize event. If this is done, then the Init method could get executed from a background thread, and before deserialization has finished for the whole GameObject.

All these things mean that there can be particular situations where doing things beyond just assigning arguments to members in an Init method might result in undesired behaviour such as:

  • Methods getting executed on components in Edit Mode.
  • Methods getting executed on components before the OnAwake, OnEnable, Start methods have been executed.
  • StartCoroutine call failing with an error due to the GameObject still being inactive.
  • Calls to Unity method failing with an error due to being called during the deserialization process.

Of course, this is just a general recommendation, and there can be situations it makes sense to not follow it. For example, enabling a component at the end of its Init method can sometimes be a useful pattern with components that need to be disabled until they’ve received an asynchronously loaded service or value provider value.

Leave a Reply

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