The Proxy Pattern In Unity

Posted by : on

Category : Unity   Architecture

What Is The Proxy Pattern

The proxy pattern is a structural design pattern that involves creating an object capable of serving as a substitute for another object. This proxy can control access to the actual object and perform operations before or after any operation is requested on the original object.

In this post, I will demonstrate how we can utilize a MonoBehaviour script to serve as a proxy, delegating all necessary operations to a plain C# class. If you are unfamiliar with the proxy pattern, there are numerous excellent resources available on the internet, for example the refactoring guru site.

This will not follow the typical use case for the proxy pattern. Typically, a proxy serves as a lightweight stand-in for a costly-to-create class. Our code can interact with the proxy, lazily initializing the class when necessary, or caching some of the expensive class’s results for enhanced performance.

While this may hold true for the example I will present below, in Unity, employing a MonoBehaviour script as a proxy offers another advantage. GameObjects in Unity are resource-intensive to create and destroy. By delegating all logic to classes that share a specific type, a GameObject can dynamically change its subtype at runtime without the need to create and destroy a new GameObject each time.

The Proxy Pattern With Game Objects

The proxy pattern is typically represented by a UML diagram similar to this:

Proxy UML diagram

In this UML diagram, we observe that both the proxy and the original class share a common interface. Any call to the class can be substituted by a call to the proxy. The proxy, in turn, will invoke the appropriate method in the class, performing any necessary extra logic before or after the operation.

The Unity example I will present utilizes a UML diagram like the one below:

Proxy in Unity UML diagram

Here, our MonoBehaviour serves as a proxy to a group of concrete classes. It maintains its functionality as before but now has the capability to represent a different object each time.

With this setup, we can completely alter the behavior of the game object without the need for destruction and recreation.

Description of the example

Suppose we have a game featuring various types of enemies, each belonging to specific subtypes. For instance, we might encounter a goblin that, upon being defeated, can transform into either a zombie goblin or a skeleton goblin under certain conditions.

These conditions aren’t predetermined at compile time. For instance, the goblin might transform into a zombie variant only when vanquished by a specific threshold of damage or a particular damage type. Otherwise, it reverts to a skeleton goblin. Additionally, there might be scenarios where a zombie goblin can be “cured” to revert to its normal goblin state, and so forth.

In essence, we require different enemy types to be dynamically instantiated and removed during runtime, without incurring the overhead of creating and destroying game objects. Pre-creating all enemy types as different GameObjects at the beginning of a level and activating only those required could still be inefficient, especially if only a subset of these types are utilized. For instance, if a game were to potentially include only zombie goblins, creating goblin skeleton objects beforehand would be resource-intensive and wasteful.

While each goblin variant can perform similar actions such as attacking, moving, and dying, they execute these behaviors differently.

Now, let’s examine the code that tackles this challenge by implementing the UML diagram of the proxy pattern for Unity.

The example code

The first thing we need, is the interface that has the common methods our different kinds of goblins have:

public interface IGoblin
{
   void Attack(Unit unit);
   void Move(Vector2 direction);
   void Die();
}

This interface will be inherited by our Monobehaviour proxy and by another interface that will have methods that get called by our Monobehaviour’s event methods. To simplify, in this example, I have only included the Update event method, but obviously, this new interface will contain methods for all required event methods in our game object.

Here’s the interface with the Update event method:

public interface IGoblinEventMethods : IGoblin
{
   void Update();
}

Now, our Monobehaviour will derive from the IGoblin interface and through composition, will have an IGoblinEventMethods type. After that, it will delegate all the calls to its methods, to that IGoblinEventMethods:

public class GoblinProxy : MonoBehaviour, IGoblin
{
    [SerializeField] private Sprite goblinSprite;
    [SerializeField] private Sprite goblinZombieSprite;
    [SerializeField] private Sprite goblinSkeletonSprite;
    
    private IGoblinEventMethods _goblinImplementation;
    private SpriteRenderer _spriteRenderer;

    private void Awake()
    {
        _spriteRenderer = GetComponent<SpriteRenderer>();
        CreateGoblinType(GoblinType.Goblin);
    }

    void Update() => _goblinImplementation.Update();

    public void Attack(GoblinProxy goblinProxy) => _goblinImplementation.Attack(goblinProxy);

    public void Move(Vector2 direction) => _goblinImplementation.Move(direction);

    public void Die() => _goblinImplementation.Die();

    public void CreateGoblinType(GoblinType goblinType)
    {
        _goblinImplementation = goblinType switch
        {
            GoblinType.Goblin => CreateGoblin(),
            GoblinType.GoblinZombie => CreateGoblinZombie(),
            GoblinType.GoblinSkeleton => CreateGoblinSkeleton(),
            _ => throw new ArgumentOutOfRangeException(nameof(goblinType), goblinType, null)
        };
        
        return;

        Goblin CreateGoblin()
        {
            _spriteRenderer.sprite = goblinSprite;
            return new Goblin(_spriteRenderer);
        }
        
        GoblinSkeleton CreateGoblinSkeleton()
        {
            _spriteRenderer.sprite = goblinSkeletonSprite;
            return new GoblinSkeleton(_spriteRenderer);
        }

        GoblinZombie CreateGoblinZombie()
        {
            _spriteRenderer.sprite = goblinZombieSprite;
            return new GoblinZombie(_spriteRenderer);
        }
    }
}

Here, every method calls the appropriate _goblinImplementation method.

There are some things to take notice though:

1) The class contains serialized fields for any assets that are needed by our different types of goblins. In our case these are the sprites of the goblins. We could avoid that, if necessary, by having our plain C# classes load resources dynamically, but that would be a waste of performance.

2) There is a CreateGoblinType method responsible for creating the objects from our types as needed. Every time we create a new goblin, we are responsible for any initialization, such as changing the sprite in the renderer as shown in the example.

3) The GoblinType here is an enum that acts as an easy way in the example to differentiate between goblin types, it is defined like this:

public enum GoblinType
{
   Goblin,
   GoblinZombie,
   GoblinSkeleton
}

4) An extension of the second point, is that any initialization can that doesn’t require new assets, can become in the constructor of our concrete classes. This is why, in the example, the _spriteRenderer is passed as a parameter, because in each class, I change the color of the sprite.

Speaking of concrete classes and their constructors, here is the code for the three concrete classes. In their methods, I have Debug.Log statements, but obviously in a real project, each method would have its own implementation.

public class Goblin : IGoblinEventMethods
{
   private readonly SpriteRenderer _spriteRenderer;

   public Goblin(SpriteRenderer spriteRenderer)
   {
      _spriteRenderer = spriteRenderer;
      _spriteRenderer.color = Color.green;
      Debug.Log("A new Goblin Has Been Created");
   }
   
   public void Attack(GoblinProxy goblinProxy) => Debug.Log("Inside Goblin Attack");

   public void Move(Vector2 direction) => Debug.Log("Inside Goblin Move");

   public void Die() => Debug.Log("Goblin is dying.");

   public void Update() => Debug.Log("Inside Goblin Update");
}

public class GoblinZombie : IGoblinEventMethods
{
   private readonly SpriteRenderer _spriteRenderer;
   
   public GoblinZombie(SpriteRenderer spriteRenderer)
   {
      _spriteRenderer = spriteRenderer;
      _spriteRenderer.color = Color.red;
      Debug.Log("A new GoblinZombie Has Been Created");
   }
   
   public void Attack(GoblinProxy goblinProxy) => Debug.Log("Inside GoblinZombie Attack");

   public void Move(Vector2 direction) => Debug.Log("Inside GoblinZombie Move");

   public void Die() => Debug.Log("GoblinZombie is dying.");

   public void Update() => Debug.Log("Inside GoblinZombie Update");
}

public class GoblinSkeleton : IGoblinEventMethods
{
   private readonly SpriteRenderer _spriteRenderer;
   
   public GoblinSkeleton(SpriteRenderer spriteRenderer)
   {
      _spriteRenderer = spriteRenderer;
      _spriteRenderer.color = Color.gray;
      Debug.Log("A new GoblinSkeleton Has Been Created");
   }
   
   public void Attack(GoblinProxy goblinProxy) => Debug.Log("Inside GoblinSkeleton Attack");

   public void Move(Vector2 direction) => Debug.Log("Inside GoblinSkeleton Move");

   public void Die() => Debug.Log("GoblinSkeleton is dying.");

   public void Update() => Debug.Log("Inside GoblinSkeleton Update");
}

As you can see, the constructor of each class, acts as the awake method, for anything that doesn’t need to load a new asset.

You can test the above scripts, by creating the following simple Monobehaviour that changes the goblin type depending on the key pressed:

public class InputManager : MonoBehaviour
{
   [SerializeField] private GoblinProxy goblin;
   
   private void Update()
   {
      if (Keyboard.current.qKey.wasPressedThisFrame)
      {
         goblin.Die();
         goblin.CreateGoblinType(GoblinType.Goblin);
      }
      
      if (Keyboard.current.wKey.wasPressedThisFrame)
      {
         goblin.Die();
         goblin.CreateGoblinType(GoblinType.GoblinZombie);
      }
      
      if (Keyboard.current.eKey.wasPressedThisFrame)
      {
         goblin.Die();
         goblin.CreateGoblinType(GoblinType.GoblinSkeleton);
      }
      
      if (Keyboard.current.aKey.wasPressedThisFrame)
      {
         goblin.Attack(null);
      }
      
      if (Keyboard.current.sKey.wasPressedThisFrame)
      {
         goblin.Move(Vector2.zero);
      }
   }
}

Benefits Of The Proxy Pattern In Unity

1) Performance gains
The first benefit of a MonoBehaviour acting as a proxy for a subset of types is the performance gain of not having to create and destroy game objects during runtime. But there are some other benefits as well.

2) Lazy initialization
We can have lazy initialization of our C# classes as needed, when we have some expensive objects to create. We can delay the creation for when it is needed, or pre-create the objects we might need when performance is not that important.

3) Conditional execution
Our proxy Monobehaviour can act as a gate for certain methods. We can have a condition in our Monobehaviours methods, that will call the goblin types’ methods only when it is true.

4) Composition over Inheritance
Because our Monobehaviour doesn’t act as a proxy for a concrete class, but as a proxy for classes that implement an interface, we can have common code in the Monobehaviour’s methods, that gets executed before or after calling the appropriate method of each class. In essence, we substitute inheritance with composition.

5) Separation of concerns
Because each goblin type is its own class, it has only the specific logic that is needed for that type. What we are doing by using a Monobehaviour as a proxy is that we have a bunch of strategies called from a class that doesn’t contain any logic but delegates everything to other implementations.

6) Testability
It is easier to create tests for our logic, as it is contained in methods that are part of a normal C# class and not part of a Unity’s Monobehaviour class. Any normal traditional test will be enough, as we don’t have to create any play mode tests for most of the logic.

Downsides Of The Proxy Pattern In Unity

The most obvious downside of this pattern in Unity is class explosion. In the case of only one goblin type, instead of having one MonoBehaviour class, we have four: two interfaces, one MonoBehaviour, and one plain C# class.

The pattern can demonstrate its advantages with the increasing number of subtypes of a type. For a couple of goblin subtypes, it might not be worth it, and having pre-created MonoBehaviours with those types that get enabled when appropriate may be a better solution. However, with seven, ten, or more subtypes, not all of which will be needed, and the decision having to be made at runtime, this pattern can demonstrate its power.

Another drawback is that we have to be careful with the initialization of our types. Our MonoBehaviour proxy has its Awake and Start methods called only once. For any logic that needs to run after every subtype change, we have to be careful not only to initialize our class correctly but also to reset any state that is not appropriate for our GameObject. In the example above, that was the sprite image in the sprite renderer, but in a real project, it can be any number of things. For this reason, it is preferable to keep the MonoBehaviour’s states to a minimum by having fields that contain only serialized assets, like the sprite above, and any other state, like the color of the sprite, to be handled by the constructor of each individual plain C# class.

Finally, there is the lesser problem of having a mechanism to differentiate between our subtypes. In the example above, if we ever needed to know if our object represents a skeleton or a zombie goblin, we would need a property that returns a GoblinType. Usually this is not a problem, as we don’t necessarily need to know the concrete implementation. Knowing that our object is of type IGoblin is enough and any differences in behavior between our subtypes, should be handled through polymorphism. In any case, a property that returns the GoblinType will suffice most of the time, even though it may make our program harder to change by depending on an enum for any new addition of a subtype.

Conclusion

This was an implementation of the proxy pattern, specifically designed for Unity’s game objects. In contrast with the traditional implementation, where the proxy class acts as a substitute for a concrete implementation, here, the proxy acts as a substitute for any number of subtypes that can change at runtime.

Traditionally, the proxy is cheaper to create and less resource-intensive than the class we have. However, in Unity, because the proxy is a MonoBehaviour that has a C++ representation, it is more costly to create and destroy. For this reason, it acts as a proxy for a number of classes, allowing us to avoid the creation and destruction of multiple game objects during runtime when performance is critical.

Other than that, we can benefit from increased testability and composition over inheritance in common code between our subtypes, albeit at the risk of a more complicated architecture due to the class explosion that can occur.

Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.


Follow me: