09. Wrapper

  04. Features No Comments

The Wrapper class is a component that acts as a simple wrapper for a plain old C# object.

It makes it easy to take a plain old C# object and attach it to a GameObject and have it receive callbacks during any Unity events you care about such as Update or OnDestroy as well as to start coroutines running on the wrapper.

Creating a Wrapper

Let’s say you had a plain old C# class called Player:

using System;
using UnityEngine;

[Serializable]
public class Player
{
    [SerializeField]
    private Id id;

    [SerializeField, Range(0f, 10f)]
    private float speed;

    public Player(Id id, float speed)
    {
        this.id = id;
        this.speed = speed;
    }
}

The easiest way to create a Wrapper component for the class is to select the script, open its context menu in the Inspector and select Generate Wrapper.

Or you could also create it manually by defining a class that inherits from Wrapper<Player>.

Player player = new Player();
gameObject.AddComponent<PlayerComponent, Player>(player);

The AddComponentMenu attribute is optional; it just makes the PlayerComponent appear as simply “Player” in the Inspector and the Add Component menu.

If the wrapped class has the [Serializable] attribute its serializable fields can be seen in the Inspector, just as if it was a normal component.

Initializing a Wrapper

This wrapper implements IInitializable<Player>, which means that a new instance can be initialized with the Player object passed as an argument using any of the various methods listed in the Creating Instances section.

For example, an instance of Player could be attached to a game object like this:

Player player = new Player();
gameObject.AddComponent<PlayerComponent, Player>(player);

To initialize a Wrapper that exists in scene or a prefab with Inspector-assigned arguments at runtime, you can use a Wrapper Initializer.

public class PlayerInitializer : WrapperInitializer<PlayerComponent, Player, Id, float>
{
    private class Init
    {
        public Id Id;

        [Range(0f, 10f)] public float Speed;
    }

    protected override Player CreateWrappedObject(Id id, float speed)
    {
        Debug.Assert(id != Id.Empty);
        Debug.Assert(speed > 0f);

        return new Player(id, speed);
    }
}
You can optionally also define a nested class named “Init” inside the Initializer  class, containing fields whose types match those of the arguments that the client accepts. If you do this, you can then attach property attributes to its fields, and their property drawers will be used when drawing the matching Init arguments in the Inspector.
When you use a Wrapper Initializer, it will take care of serializing all the arguments that will be passed to the client. This means that the client class itself does not need to be serializable.
public class Player
{
    public Id Id { get; }
    public float Speed { get; }

    public Player(Id id, float speed)
    {
        Id = id;
        Speed = speed;
    }
}

Unity Events

Wrapped objects can receive callbacks during select Unity events from their wrapper by implementing one of the following interfaces:

  1. IAwake – Receive callback during the MonoBehaviour.Awake event.
  2. IOnEnable – Receive callback during the MonoBehaviour.OnEnable event.
  3. IStart – Receive callback during the MonoBehaviour.Start event.
  4. IUpdate – Receive callback during the MonoBehaviour.Update event.
  5. IFixedUpdate – Receive callback during the MonoBehaviour.FixedUpdate event.
  6. ILateUpdate – Receive callback during the MonoBehaviour.LateUpdate event.
  7. IOnDisable – Receive callback during the MonoBehaviour.OnDisable event.
  8. IOnDestroy – Receive callback during the MonoBehaviour.OnDestroy event.

For example, to receive callbacks during the Update event the Player class would need to be modified like this:

public class Player : IUpdate
{
        public void Update(float deltaTime)
        {
                // Do something every frame
        }
}

Coroutines

Wrapped objects can also start and stop coroutines in their wrapper.

To gain this ability the wrapped object has to implement the ICoroutines interface.

public class Player : ICoroutines
{
        public ICoroutineRunner CoroutineRunner { get; set; }
}

The wrapper component gets automatically assigned to the CoroutineRunner property during its initialization phase.

You can start a coroutine from the wrapped object using CoroutineRunner.StartCoroutine.

public class Player : ICoroutines
{
        public ICoroutineRunner CoroutineRunner { get; set; }

        public void SayDelayed(string message)
        {
                CoroutineRunner.StartCoroutine(SayDelayedCoroutine(message));
        }

        IEnumerator SayDelayedCoroutine(string message)
        {
                yield return new WaitForSeconds(1f);

                Debug.Log(message);
        }
}

The started coroutine is tied to the lifetime of the GameObject just like it would be if you started the coroutine directly within a MonoBehaviour.

You can stop a coroutine that is running on the wrapper using CoroutineRunner.StopCoroutine or stop all coroutines that are running on it using CoroutineRunner.StopAllCoroutines.

public void OnDisable()
{
        CoroutineRunner.StopAllCoroutines();
}

Why Wrapped Objects?

The main benefit with using wrapped objects instead of a MonoBehaviour directly is that it can make unit testing the class easier.

You no longer subscribe to unity events via private magic functions hidden inside the body of the class, but have to explicitly implement an interface and expose a method for this. This makes it easier to invoke these functions in unit tests.

You also no longer need to think about scene management or creating GameObjects during unit testing which helps make it faster and easier to write reliable unit testing code.

Additionally the pattern that wrapped objects use to handle coroutines makes it easy to swap the coroutine runner class during unit tests, making it possible to even unit tests coroutines, for example with the help of the EditorCoroutineRunner class.

An additional benefit with using wrapped objects is that you can assign dependencies to read-only fields and properties in the constructor, which makes it possible to make your classes immutable. Having your wrapped objects be stateless can make your code less error-prone and makes it completely thread safe as well.

Leave a Reply

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