03. ConstructorBehaviour<T…>

  03. Features No Comments

A base class for MonoBehaviours that depend on receiving upto six objects in their constructor during initialization.

For example the following Player class depends on an object that implements the IInputManager interface and an object of type Camera.

public class Player : ConstructorBehaviour<IInputManager, Camera>

When you create a component that inherits from one of the generic ConstructorBehaviour base classes, you’ll always also need to add a parameterless constructor for receiving the arguments. This constructor should call the constructor in the base class to receive the initialization arguments that were provided to it when it was created.

Since the arguments are received inside the constructor it is possible to assign them to read-only fields and properties.

public class Player : ConstructorBehaviour<IInputManager, Camera>
{
   private readonly IInputManager inputManager;
   public Camera InputManager { get; }

   public Player() : base(out IInputManager inputManager, out Camera camera)
   {
      this.inputManager = inputManager;
      Camera = camera;
   }
}

Supporting Init Injection

While ConstructurBehaviours can oftentimes receive their arguments in the constructor, in some cases this might not be possible for one reason and another; for this reason the base classes also implement one of the IInitializable<T…> interfaces, allowing Initializers and other classes to inject arguments to them later on during the initialization phase after the constructor has already finished executing. This process utilizes reflection to assign the arguments into the same fields and properties that the constructor would normally assign them to.

For this reflection based injection to be possible, the base class needs to know the names of the fields and properties that should receive the arguments. In most cases the base class can automatically figure these out by going through all the fields and auto-implemented properties of the class and finding the ones that match the type of the argument. However, if no match is found for any argument, or if more than one option is found for any argument, this process will fail and throw an exception.

If this happens you can resolve the situation by overriding the GetInitArgumentClassMemberNames method in your derived class to inform the system the correct field and property names.

protected override (string, string) GetInitArgumentClassMemberNames()
{
   return (nameof(inputManager), nameof(Camera));
}

Creating Instances

While with MonoBehaviour<T…> providing arguments during initialization is optional, with ConstructorBehaviour<T…> this is mandatory; an exception will be thrown if an instance is instantiated without the arguments it requires!
The idea behind this is to mimic the way constructors work with plain old C# objects, allowing you to restrict object creation to only be allowed when all the required dependencies are provided.
With MonoBehaviour<T…> it was decided to make receiving arguments only optional, so that injecting values through the Inspector could be used as an alternative way to set up components in scenes and prefabs.

Below are listed different ways you can create instances of classes that derive from a ConstructorBehaviour<T…> base class:

AddComponent(args)

If you have a component of type TComponent that inherits from MonoBehaviour<TArgument>, you can add the component to a GameObject and initialize it with an argument using the following syntax:

gameObject.AddComponent<TComponent, TArgument>(argument);

Instantiate(args)

You can create a clone of a prefab that has the component attached to it using the following syntax:

prefab.Instantiate(argument);

Note: The constructor gets called before Unity’s deserialization process takes place. This means that if you instantiate a ConstructorBehaviour from a prefab and assign any arguments into serialized fields in the constructor, those values will get overridden when the deserialization takes place. Because of this it is recommended to only assign arguments into non-serialized fields or properties in the constructor.
That notwithstanding, assigning arguments into serialized fields in ConstructorBehaviours does still work; the ConstructorBehaviour base class will detect when this happens and automatically handle re-injecting the arguments into the fields once the deserialization process has finished. However this injection utilizes reflection, so it’s still better to avoid it when you can for most optimal performance.

new GameObject

You can create a new GameObject and attach the component to it using the following syntax:

new GameObject<TComponent>().Init(argument);

Initializer

You can add the component to a scene or a prefab and use Unity’s inspector to specify the argument used to initialize the component by defining an Initializer for the component and adding one to the same GameObject.

public class TComponentInitializer : Initializer<TComponent, TArgument> { }

Note: When using an Initializer to provide the arguments for a ConstructorBehaviour the arguments can not be assigned through the constructor and have to instead be passed in through the Init method and assigned to the target fields using reflection. This means that performance isn’t as optimal as it would be when no reflection is involved. However this whole process happens automatically behind the scenes, and unless you have a very large number of ConstructorBehaviours in your scenes this probably isn’t a big deal.

Manual

In rare instances you might want to manually initialize an existing instance of the component without doing it through one of the pre-existing methods listed above.

In order to manually call the Init function you must first cast the component instance to IInitializable<TArgument>.

var initializable = (IInitializable<TArgument>)component;
initializable.Init(argument);

Note: Calling the Init method causes the provided arguments to be assigned to the target fields using reflection. To avoid this you can use the MonoBehaviour<T…> base class instead.

Initialization Best Practices

It is recommended to only use the constructor for assigning the received arguments into member fields and properties and nothing else.
Use OnAwake, OnEnable or Start for other initialization logic, such as calling other methods or starting coroutines.

There are a couple of different reasons for this recommendation:

  • The constructor can get called in edit mode for any component that are part of a scene or a prefab. As such calling other methods from the Init function could result in unwanted modifications to being done to your scenes or prefabs in edit mode.
  • The constructor is often executed in a background thread. Since most of Unity’s internal methods and properties are not thread safe, calling any of them from an Init function can be risky and result in errors.

If you wish to perform validation on the received arguments, such as checking them for null, you can override the ValidateArguments method and do it here.
It is better to perform validation here instead of doing it directly inside the constructor so that the arguments also get validated when they are provided through the Init method instead.
You can use the ThrowIfNull method to check that a received argument is not null and throw an exception if it is.
You can use the AssertNotNull method to check that a received argument is not null and log an assertion message to the console if it is.

ConstructorBehaviour combines perfectly with using AddComponent(args) or new GameObject<T> for initialization. Because in these cases there is no deserialization step, it means that the ConstructorBehaviour base class doesn’t ever need to check whether any values were assigned to serialized fields or inject values using reflection.

Using Instantiate(args) for initializing ConstructorBehaviours also works great when care is taken to only assign values into non-serialized fields and properties. In this case the ConstructorBehaviour base class will detect that no values will get overridden during deserialization and will not use reflection to inject the arguments after the deserialization process has finished. In this case reflection only needs to be used once per class (not instance) to examine whether or not the fields into which arguments are assigned are serializable by Unity or not.

More Than Six Dependencies

Only a maximum of six arguments can be passed to ConstructorBehaviour<T…> objects. In cases where you need to pass more than six dependencies to your objects you can do so by wrapping multiple objects inside one container object.

This can be done either using custom classes (recommended for better serialization support), or using tuples:

public class SevenNumbers : ConstructorBehaviour<(int first, int second, int third, int fourth, int fifth, int sixth, int seventh)>
{
	private int first;
	private int second;
	private int third;
	private int fourth;
	private int fifth;
	private int sixth;
	private int seventh;

	public SevenNumbers() : base(out int first, out int second, out int third, out int fourth, out int fifth, out int sixth, out int seventh)
	{
		first = args.first;
		second = args.second;
		third = args.third;
		fourth = args.fourth;
		fifth = args.fifth;
		sixth = args.sixth;
		seventh = args.seventh;
	}
}

Then when initializing your object, instead of passing all the dependencies as separate arguments, you just pass the single container object.

var numbers = (1, 2, 3, 4, 5, 6, 7);
new GameObject<SevenNumbers>().Init(numbers);

 

Leave a Reply

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