Implementing the Strategy design pattern using ScriptableObject
in Unity
Strategy is a design pattern from the book Design Patterns (GOF)
The purpose of the pattern is to enable changing behaviour at runtime in a clean way (not a bunch of if-statements).
You can read more about the Strategy pattern here as it will not be covered here.
Implementing the pattern in C# is pretty straight forward and can be explained as simple as this:
C# example
Imagine you have a Player
class that can perform different kinds of attacks. This could look something like this.
class Player
{
void Attack(AttackType type)
{
if(type == AttackType.Melee)
{
// TODO: melee logic
}
else if(type == AttackType.Crossbow)
{
// TODO: crossbow logic
}
}
}
Adding a new attack means adding a new if-statement. Not to mention this method will quickly get very long and hard to follow. Commonly, attacks will need different parameters. Both might need some kind of damage, but crossbow will need range and even possibly ammo, while melee won't. Of course we could pass all of those parameters to the Attack
method. But strategy can make this a lot cleaner.
Instead, using Strategy we can make this a bit cleaner:
// Player.cs
class Player
{
void Attack(IAttack attack)
{
attack.Execute(this);
}
}
// IAttack.cs
interface IAttack
{
void Execute(Player player);
}
// MeleeAttack.cs
class MeleeAttack : IAttack
{
public int Damage { get; set; }
void Execute(Player player)
{
// TODO: Melee attack logic
}
}
// CrossbowAttack.cs
class CrossbowAttack : IAttack
{
public int Damage { get; set; }
public float Range { get; set; }
public int Arrows { get; set; }
void Execute(Player player)
{
// TODO: Crossbow attack logic
}
}
Alright, there's a little bit more code. We moved them into different files and separated each attacks' functionality into different classes all implementing IAttack
. Now the player only needs to know about IAttack
and passes itself as a parameter to the attack it is using.
Adding a new attack now is as simple as adding a new attack-class. No changes in the Player
-class are needed.
Unity example using ScriptableObject
So this is all nice and dandy. But what about doing this in Unity? I think there are benefits from doing this with just C# in Unity, but when it really shines is when using Unitys ScriptableObject
for the implementations of each strategy.
Read more about ScriptableObject
The benefit of ScriptableObject
is that they can be created as assets. This means you can drag-drop your strategies in the Unity Editor directly to game objects. (This also comes with a cost, we'll get to that later)
// Player.cs
class Player : MonoBehaviour
{
public void Attack(IAttack attack)
{
attack.Execute(this);
}
}
// IAttack.cs
interface IAttack
{
void Execute(Player player);
}
// Attack.cs
public abstract class Attack : ScriptableObject, IAttack
{
public abstract void Execute(Player player);
}
// MeleeAttack.cs
[CreateAssetMenu(menuName="Custom/Attacks/Melee Attack")]
class MeleeAttack : Attack
{
public int Damage = 1;
public override void Execute(Player player)
{
// TODO: Melee attack logic
}
}
// CrossbowAttack.cs
[CreateAssetMenu(menuName="Custom/Attacks/Crossbow Attack")]
class CrossbowAttack : Attack
{
public int Damage = 3;
public float Range = 20f;
public int Arrows = 5;
public override void Execute(Player player)
{
// TODO: Crossbow attack logic
}
}
A couple of things are different here:
- We added an abstract class
Attack
that inheritsScriptableObject
and implementsIAttack
-
MeleeAttack
andCrossbowAttack
inheritAttack
instead of using the interface directly - An attribute
CreateAssetMenu
was added - this makes it possible to right-click in the Unity Editor and add a new asset for that specific type of attack. - The properties were changed to fields - meaning we can now also edit these values in the Unity Editor for our assets
This also means it is possible to create different assets for the same type of attack. For example we could have a weak melee attack and a strong one, and perhaps add a cost in stamina for using that attack.
The flexibility of this is pretty great. And the separation of the logic from the Player
makes it even easier.
Another thing means that someone who isn't a programmer can easily add new configurations of attacks without touching the code!
The drawbacks
Honestly this isn't a big deal but might save you some time: Remember that these ScriptableObject
s are Assets. This means when you attach a ScriptableObject
to a game object, it will use the same instance of that asset. Even if two different objects are using it.
That is why State should never be stored in the ScriptableObject
. Think of it as configuration and behaviour only.
Another thing is that you can't just instantiate a ScriptableObject
on the fly, Unity doesn't allow it. However you can use this factory method:
ScriptableObject.CreateInstance()
This will give you a new instance of a ScriptableObject
that is not an asset.
This is my first post here so, hello! :)
Top comments (2)
Player.cs
public void Attack(IAttack attack)
{
attack.Execute(this);
}
How do you pass an attack to this function? Do you use a custom serializer to expose IAttack in the inspector??
This is a bit old, but I assume that he just has a parameter of type Attack which is a class that implements ScriptableObject which makes it serializable by Unity.