Introduction
In the post below I will show a simple spell system that I use in small projects for the Unity engine. I will write the basic code to show the idea of how it is created without Monobehaviours but with plain C# classes and small Scriptable objects, so that the spells can be easily assigned using the editor.
The core logic is all in plain C# classes. With this, the scriptable objects are used only for the serialization of our data, so that the spells can be easily used by the game designers with drag and drop functionality in the editor. In the code, I have omitted the other systems that a spell system might need.
For example I don’t have any code for a timer, as I don’t think that a timer functionality should be baked in the spell system, but it should be a system of its own. Following the same logic, I don’t provide an input system for our spells ( when the spells should be used ) as this will differ from game to game. Also I don’t provide details for the output. The output is the valid targets that a spell may have. I use an Enemy type as the target for our spells, but the targets could be anything depending on the game, from the player character to whole areas.
I chose to write a barebones system and describe the logic of how it is made, so that anyone can expand on it depending on his needs. The whole idea is that the system should have its core logic in C# classes that are not tied to the engine and use small scriptable objects as connections from the engine to the system.
Creating our scriptable object classes
Let’s start from the scriptable objects. They should not contain any logic, just the data that the game designer may need and a way to instantiate the relevant spell.
The above can be implemented with a base scriptable object class, that when we inherit from it, we will only need to change the creation of the relevant spell type.
public abstract class PlayerSpell : ScriptableObject
{
public abstract SpellBase Create { get; }
[SerializeField] protected int coolDownDuration;
[SerializeField] protected int durationInRounds;
}
In the above class, I have two data that the spell may need : the cooldown duration that the spell has so that it can be used again and the duration that the spell may have. Obviously any other common data that the spells may need to have and should be provided from the game designers could be added here.
Any data that our spells may need that are specific to each spell, should go to the child classes. For example some child classes could have a damage field, but that field is only relevant to the spells that do damage.
The only variable that is not a data for our spell, is a get only property called Create
that acts as a way for anything in our game that can cast spells, to be able to create instances of spells, having a reference to those spells and manipulate those instances, by calling spells behaviours.
Each spell will have its own scriptable object that overrides this Create
property and may have data relevant only to that spell. For example the following spell that makes an enemy red doesn’t need any more data:
[CreateAssetMenu(fileName = "ColorToRedSpell", menuName = "Spells/ColorToRed")]
public class ChangeColorToRedSpell : PlayerSpell
{
public override SpellBase Create => new ChangeColorToRed(coolDownDuration, durationInRounds);
}
In contrast the ChangeColorToGreenSpell
also damages the target, so it should have a damage
field serialized:
[CreateAssetMenu(fileName = "ColorToGreenSpell", menuName = "Spells/ColorToGreen")]
public class ChangeColorToGreenSpell : PlayerSpell
{
[SerializeField] private int damage;
public override SpellBase Create => new ChangeColorToGreen(coolDownDuration, durationInRounds, damage);
}
Both of those spells, create a new instance of different spells that both are of type SpellBase
. The SpellBase
class will be our base class that has the common functionality for our spells and its children will be our spell classes that have the specific behaviour of each spell.
Creating our spells
For the creation of our spells, I have chosen to use a base abstract class that shares the common functionality of the spells and is using the template method pattern.
The reason I chose the template method pattern is that it is very helpful for anyone that wants to derive from our SpellBase
class to know what he should implement. If he won’t, he won’t be able to compile the game.
Another benefit of this approach, is that there is no need for someone to remember to call the base method or have to go and read the SpellBase
implementation so that he will know when to call the base method. That is why, even with the template method pattern, I have not included any virtual functions in the SpellBase
class. All the methods in this class are either normal methods or abstract methods.
public abstract class SpellBase
{
protected Enemy Target;
private readonly int _coolDownDuration;
private readonly int _duration;
protected SpellBase(int cooldownDuration, int spellDuration)
{
_coolDownDuration = cooldownDuration;
_duration = spellDuration;
}
public void Initialize(Enemy enemy)
{
Target = enemy;
InitializeSpell();
}
public bool TryCast()
{
if (TryCastEffect())
{
ConfigureTimers();
return true;
}
return false;
}
private void ConfigureTimers()
{
// Cooldown and duration timers code here
}
protected abstract bool TryCastEffect();
protected abstract void InitializeSpell();
}
Here’s what’s happening:
The SpellBase
class takes as arguments in the constructor, the data that is common for all of our spells. This is the same data that is included in the scriptable object PlayerSpell
base abstract class.
The public methods it has, are the methods that anyone might need from a spell. Here I have included two behaviours. The Initialize(Enemy enemy)
method, that is responsible for any common initialization that our spells have and the TryCast()
method that is responsible for the common functionality that exists whenever one of our spells is cast.
For example the Initialize
method here, is responsible for providing a target for our spells and the TryCast
method is responsible for creating/initializing our timers for the cooldown duration and the duration of our spells. Any other common functionality that our spells may need to have, can also be added as public methods in this class.
All our public methods, should call the relevant abstract methods, that will be implemented in each child class. Here is the code for the ChangeColorToRedSpell
and the ChangeColorToGreenSpell
classes:
public class ChangeColorToRed : SpellBase
{
public ChangeColorToRed(int cooldownDuration, int spellDuration) : base(cooldownDuration, spellDuration)
{ }
protected override void InitializeSpell()
{
Debug.Log("Initializing Change color to red spell");
}
protected override bool TryCastEffect()
{
Target.GetComponent<SpriteRenderer>().color = Color.red;
return true;
}
}
public class ChangeColorToGreen : SpellBase
{
private readonly int _damage;
public ChangeColorToGreen(int cooldownDuration, int spellDuration, int damage) : base(
cooldownDuration, spellDuration)
{
_damage = damage;
}
protected override void InitializeSpell()
{
Debug.Log("Initializing Change color to green spell");
}
protected override bool TryCastEffect()
{
Target.GetComponent<SpriteRenderer>().color = Color.green;
Debug.Log($"The enemy: {Target} gets damaged for {_damage} hp.");
return true;
}
}
Here I use the GetComponent
method from Unity for this example. A better way would have been to have any Engine specific code encapsulated in its own class. That would make our system completely engine agnostic, its only connection to the engine would be the class that acts as a “translator” between what we want to do and how it can be done in the engine.
Conclusion
A benefit of this approach, is that any time we want to create a new spell, we only have to create a new scriptable object class that derives from PlayerSpell
and contains only the data we need that is specific to the spell we are making and overrides the Create
property to return the instance we want.
After that, we just need to create our spell by deriving from the SpellBase
class. Our IDE will tell us what methods we need to implement without the need to remember to call any base method, or rewrite any common functionality that all our spells share.
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 always subscribe to my newsletter or the RSS feed.