How To Use The Decorator Pattern In Unity

Posted by : on

Category : Unity   Architecture

Introduction

The Decorator Pattern, enables us to dynamically alter the implementation of an object’s behavior at runtime.

This pattern doesn’t modify the available behaviors of a type; rather, it allows us to augment existing behaviors of objects belonging to that type.

Decorators offer a means to inject functionality at runtime, with each decorator serving as a wrapper for our object. Multiple decorators can be stacked on top of each other.

In this post, I will demonstrate an implementation of the decorator pattern tailored for Unity, leveraging scriptable objects.

If you’re unfamiliar with the decorator pattern’s implementation, you can find further information here. This post will provide a brief overview of the pattern and then delve into its implementation within the scriptable object architecture of Unity.

The Decorator Pattern

The advantage of the decorator pattern lies in its ability to augment functionality while maintaining an object of the original type.

For instance, consider a Book class with a string containing its content and a save method. In scenarios where we need to enhance this class, such as adding encryption or compression to the book before saving, we face the challenge of accommodating various combinations of functionality without creating subclasses for each.

Creating subclasses for every possible combination is impractical. Similarly, using extensive if statements to handle all combinations leads to unmaintainable code. Alternatively, exposing private internals of the class to helper classes for behavior extension introduces complexity.

Another approach could involve creating a collection of types derived from the Book class, but this introduces complications. It requires modifying methods throughout the code to accept a collection of books, potentially disrupting existing codebases. Additionally, each class in the collection may need access to the internal state of the preceding element, leading to further complexity.

The decorator pattern offers a solution akin to a linked list. Each decorator is a child of the parent class we seek to extend. After establishing a parent class with the necessary methods for functionality extension, we create decorator classes deriving from the parent. These decorators contain a field referencing types derived from the parent.

By overriding common methods, adding desired functionality, and invoking the same method on the referenced field, we effectively chain functionalities within our objects.

For a practical demonstration, let’s address a hypothetical problem encountered during game development.

A Unity Example

Let’s imagine we have a game where attackers target the player and other entities with various damage types. While we’ve established our attacker class and player class, which implements an IDamageable interface to allow attackers to target anything, we want to enhance functionality to produce different reactions based on the damage types.

Suppose we have physical damage, alongside poison, fire, and electricity damage types, each attack capable of carrying any combination of these. We desire the recipient of damage to react differently based on the types affecting it.

However, we have specific requirements:

1) We anticipate adding and removing damage type reactions, so we need an easily modifiable approach without altering existing code. This should be achieved by simply creating new classes, not modifying existing ones.

2) Because we will be adding and removing damage type reactions, we don’t want either the attacker or the IDamageables and its implementors to depend on the reactions.

3) To ensure adaptability to potential changes in attacker or player implementations, reactions should solely rely on the abstract interface IDamageable and not be influenced by specific implementations.

4) To maintain cohesion as we add or remove reaction types, all components should adhere to a single base type, with behavior varying based on the damage type.

5) The base damage type implementation must remain closed for modification, allowing for extension of functionality through new reactions without altering existing code.

To fulfill these requirements, we’ll employ the decorator pattern.

Initially, we’ll create a reactions manager responsible for assembling a packet containing reactions. This packet, an object housing appropriate reactions based on the damage type, will be transferred from the attacker to the defender.

The defender will utilize this object, passing itself as a parameter. The reactions object, in turn, will leverage this parameter to alter the defender’s state.

Let’s start with an enum with the flags attribute that represents our damage types and the IDamageable interface:

[Flags]
public enum DamageTypes 
{
    Poison = 1 << 0,
    Fire = 1 << 1,
    Electricity = 1 << 2
}

public interface IDamageable
{
   float HitPoints { get; set; }
}

Here, I have established only three damage types, and for the sake of simplicity, the IDamageable interface includes only one property. However, in practice, we intend for our IDamageable interface to encompass all elements susceptible to reactions, such as the sprite or model of the implementors, references to VFX, and sounds associated with the implementors, among others.

Let’s see our Attacker class:

public class Attacker : MonoBehaviour
{
    [SerializeField] private ReactionsManager reactionsManager;
    [SerializeField] private Player defender;
    [SerializeField] private DamageTypes damageTypes;
    
    private void OnMouseDown()
    {
        DamageReactionBase defenderReaction = reactionsManager.CalculateReaction(damageTypes);
        defender.ReactToDamage(defenderReaction);
    }
}

Once more, striving for simplicity, the attacker possesses fields for selecting damage types. However, in a dynamic game environment, these selections could change during runtime. It also holds references to the player being attacked and our reactions manager.

Furthermore, it includes only one method: when the attacker is clicked, it requests a reactions packet from the reactions manager. Subsequently, it passes this packet to the defender, which contains the ReactToDamage method.

Here’s the implementation of the Player class:

public class Player : MonoBehaviour, IDamageable
{
    public float HitPoints { get; set; }
    
    [SerializeField] private int hitPoints = 100;

    private void Start() => HitPoints = hitPoints;

    public void ReactToDamage(DamageReactionBase damageReaction)
    {
       damageReaction.React(this);
    }
}

Once more, maintaining simplicity, we solely implement the IDamageable interface, featuring the ReactToDamage method, which simply invokes the React method of the object.

Implementation of the Decorator Pattern

Having set the scene, let’s delve into the primary focus of this post: the implementation of the decorator pattern in Unity using scriptable objects.

Our initial step involves creating the base class from which all others will derive. As previously discussed, this class is the DamageReactionBase class, which is a scriptable object:

public abstract class DamageReactionBase : ScriptableObject
{
    public abstract void React(IDamageable damageable);
}

Next, we require two subclasses of this class. The first is a concrete implementation of our fundamental reaction. In our example, this entails the reaction to any type of damage, which reduces the hit points of the IDamageable:

[CreateAssetMenu(fileName = "BaseDamageReaction", menuName = "DecoratorPattern/BaseDamageReaction")]
public class DamageReaction : DamageReactionBase
{
   [SerializeField] protected float damage;
   
   public override void React(IDamageable damageable)
   {
      damageable.HitPoints -= damage;
      
      Debug.Log($"Got hit for {damage} hp, hp dropped to {damageable.HitPoints}");
   }
}

This scriptable object contains the amount of damage inflicted on the defender. However, in different game scenarios, a reaction such as an animation or a change in color might be more appropriate. I opted for hit points here to demonstrate how we can access the defender’s fields.

The second child of the DamageReactionBase class, is an abstract class DamageReactionDecorator:

public abstract class DamageReactionDecorator : DamageReactionBase
{
   private DamageReactionBase _damageReaction;

   public void ChainReaction(DamageReactionBase damageReaction) => _damageReaction = damageReaction;

   public override void React(IDamageable damageable)
   {
      if (_damageReaction != null)
         _damageReaction.React(damageable);
   }
}

This class is responsible for chaining the reactions of the different decorators and the damage reaction class.

By calling the ChainReaction method, we add using composition, a new reaction object to our object and the React method, is responsible for calling the React method of that object.

Now we can easily create scriptable objects that derive from this class and add functionality depending on the damage type:

[CreateAssetMenu(fileName = "PoisonDamageReaction", menuName = "DecoratorPattern/PoisonDamageReaction")]
public class PoisonDamageReactionReaction : DamageReactionDecorator
{
    public override void React(IDamageable damageable)
    {
        base.React(damageable);
        
        // Behavior for damageable
        Debug.Log("Poison Damage Reaction");
    }
}

[CreateAssetMenu(fileName = "FireDamageReaction", menuName = "DecoratorPattern/FireDamageReaction")]
public class FireDamageReactionReaction : DamageReactionDecorator
{
   public override void React(IDamageable damageable)
   {
      base.React(damageable);
        
      // Behavior for damageable
      Debug.Log("Fire Damage Reaction");
   }
}

[CreateAssetMenu(fileName = "ElectricityDamageReaction", menuName = "DecoratorPattern/ElectricityDamageReaction")]
public class ElectricityDamageReactionReaction : DamageReactionDecorator
{
   public override void React(IDamageable damageable)
   {
      base.React(damageable);
        
      // Behavior for damageable
      Debug.Log("Electricity Damage Reaction");
   }
}

Here everything looks the same, but obviously we can substitute the ‘Behavior for damageable’ comment with the reactions we need. Any info about the object that will have the reaction can be taken from the IDamageable interface.

The only thing left, is the creation of the ReactionsManager being used by our attacker, that creates the packets using our decorators:

public class ReactionsManager : MonoBehaviour
{
    [SerializeField] private DamageReaction damageReaction;
    [SerializeField] private PoisonDamageReactionReaction poisonDamageReactionReaction;
    [SerializeField] private FireDamageReactionReaction fireDamageReactionReaction;
    [SerializeField] private ElectricityDamageReactionReaction electricityDamageReactionReaction;

    private readonly Dictionary<DamageTypes, DamageReactionDecorator> _reactions = new();

    private void Awake()
    {
        _reactions.Add(DamageTypes.Poison, poisonDamageReactionReaction);
        _reactions.Add(DamageTypes.Fire, fireDamageReactionReaction);
        _reactions.Add(DamageTypes.Electricity, electricityDamageReactionReaction);
    }

    public DamageReactionBase CalculateReaction(DamageTypes damageTypes)
    {
        DamageReactionBase tempReaction = damageReaction;

        foreach (DamageTypes damageType in Enum.GetValues(typeof(DamageTypes)))
        {
            if (!damageTypes.HasFlag(damageType)) continue;

            _reactions[damageType].ChainReaction(tempReaction);
            tempReaction = _reactions[damageType];
        }
        
        return tempReaction;
    }
}

For every new damage type we have, we need to create a scriptable object with the appropriate reaction and the plug the damage type with the reaction inside the Awake method of the ReactionsManager.

Conclusion

In this example, the decorator pattern may appear excessive since we only utilize a Debug.Log. However, in real-world scenarios, each reaction would likely entail multiple lines of code and require access to internal object states, which here is represented simply by a HitPoints property.

The decorator pattern enables the creation of an object serving as a packet of reactions. This object can be passed around and utilized by any entity without necessitating dependencies between reaction types and their users.

The addition or removal of reaction types will not impact the rest of our code, except for the dictionary initialization in the ReactionsManager class. No alterations to the Attacker or Player classes are necessary.

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: