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
{
    public string id;

    [Range(0, 10f)]
    public float 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>.

using Sisus.Init;
using UnityEngine;

[AddComponentMenu("Wrapper/Player")]
public class PlayerComponent : Wrapper<Player> { }

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

When the wrapped class has the [Serializable] attribute its members 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, the Player can be attached to a GameObject using the following syntax:

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

To initialize a Wrapper with arguments as it is being loaded as part of a scene or a prefab, you can use a Wrapper Initializer.

using class PlayerInitializer : WrapperInitializer<PlayerComponent, Player, Guid>
{
    protected override Player CreateWrappedObject(Guid guid)
    {
        return new Player(guid);
    }
}

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 *