1. Wrapper

  7. Wrappers 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>.

[AddComponentMenu("Wrapper/Player")]
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.

If the wrapped class has the [Serializable] attribute, then an instance of it will be automatically created for any wrappers that exist as part of scene or prefab assets, and the objects serializable fields will be visible in the Inspector.

Initializing a Wrapper

System.SerializableAttribute

As stated before, if the wrapped class has the [Serializable] attribute, then an instance will be automatically created for all wrappers that exist as part of scene or prefab assets.

Provide Instance Via Constructor

If the wrapped object is not serializable, and doesn’t depend any external Object references or services, then you can initialize the instance right in the wrapper’s parameterless constructor, and pass it to the base constructor which accepts the instance as an argument.

[AddComponentMenu("Wrapper/Player")]
sealed class PlayerComponent : Wrapper<Player>
{
    public PlayerComponent() : base(new Player(Id.NewId(), 0f)) { }
}

Pass Instance During Instantiation

As wrappers implement IInitializable<TWrapped>, it means that you can also pass an instance to the component when creating an instance using Instantiate or AddComponent.

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

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

Wrapper Initializer

If the wrapped object depends on services, or you want to assign some arguments using the Inspector, you can generate an Initializer for the wrapper component.

The initializer should derive from WrapperInitializer<T…>, with its generic arguments of being the types of the wrapper component, the wrapped object, and all the objects the wrapped object needs to be provided to it.

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 necessarily 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();
}

Wrapped Objects vs MonoBehaviour<T…>

Wrapping plain old C# objects using wrapper components is an alternative to deriving from the MonoBehaviour<T…> base classes. Both approaches make it easy to receive objects they depend on from the outside, and make it easy to create unit tests for the objects. You’ll probably mostly want to use one or the other in your project consistently, rather than mixing and matching them at random.

Wrapped Object Pros

Unit testing wrapped objects can be even simpler and efficient, because you usually don’t even need to initialize any GameObjects or components when testing them, and can instead just test the plain old C# objects directly.

Your object won’t have any private unity event methods, that can’t be executed during Edit Mode unit tests without resorting to reflection. You can always easily execute any of their their lifetime events via the interfaces that they implement.

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 nice little 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.

If you care about keeping your code as decoupled from the Unity framework as possible, so that it could be easier to port it to other platforms, then wrapped objects could also be able to help you with this.

Wrapped Object Cons

Using wrapped objects instead of MonoBehaviour<T…> can introduce some additional boilerplate and complexity. In addition to creating your plain old C# object, you’ll always also need to generate a wrapper for it, resulting in one additional class (albeit a really simple one).

There’s also more of a learning cure compared to just using MonoBehaviour<T…>, because you’ll need to learn to use interfaces like IUpdate and ICoroutines when you need to make use of functionality that is already built into components.

Also, because constructor injection by its very nature doesn’t support circular references (A depends on B, and B depends on A), you can encounter runtime exceptions if you ever try to make two wrapped objects receive references to each other during their initialization.

Leave a Reply

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