unity buff system

In this tutorial, I will show you how to create a flexible buff system for Unity using scriptable objects. We will use scriptable objects as a way to quickly create and manage buffs without having to use external data types such as xml or a text file. This lets us separate our buff data such as duration and stat changes from the game logic. As you will see soon, this will also allow us to bind our scriptable objects as assets, using the CreateAssetMenu attribute.

Update: The buff system has been updated with effect and duration extending flags. The original interface for TimedBuff has been updated to reflect that. Tutorial and github repository has been updated to reflect the changes. 

The premise of our design will revolve around three classes. The player class, a buff class, and a buff scriptable object. The player class serves as our generic player model, it can have multiples buffs at the same time, as well as multiple buffs of the same type (stacking). The buff scriptable object acts as our data and the buff class is our bridge between the two, handling the logic of each individual buff.

Buff System Implementation

First we’ll create an abstract version of our buff scriptable object to build on. Every buff should have a duration, and we need a way to create this buff from our data.

ScriptableBuff.cs

public abstract class ScriptableBuff : ScriptableObject
{

    /**
     * Time duration of the buff in seconds.
     */
    public float Duration;

    /**
     * Duration is increased each time the buff is applied.
     */
    public bool IsDurationStacked;

    /**
     * Effect value is increased each time the buff is applied.
     */
    public bool IsEffectStacked;

    public abstract TimedBuff InitializeBuff(GameObject obj);

}

ScriptableBuff contains buff fields and values that can be used across multiple buffs. These are the duration, and two flags that determine whether the duration and effects re stacked. The TimedBuff is the bridge we need to link the data and played model. We will implement that next.

TimedBuff.cs

public abstract class TimedBuff
{
    protected float Duration;
    protected int EffectStacks;
    public ScriptableBuff Buff { get; }
    protected readonly GameObject Obj;
    public bool IsFinished;

    public TimedBuff(ScriptableBuff buff, GameObject obj)
    {
        Buff = buff;
        Obj = obj;
    }

    public void Tick(float delta)
    {
        Duration -= delta;
        if (Duration <= 0)
        {
            End();
            IsFinished = true;
        }
    }

    /**
     * Activates buff or extends duration if ScriptableBuff has IsDurationStacked or IsEffectStacked set to true.
     */
    public void Activate()
    {
        if (Buff.IsEffectStacked || Duration <= 0)
        {
            ApplyEffect();
            EffectStacks++;
        }
        
        if (Buff.IsDurationStacked || Duration <= 0)
        {
            Duration += Buff.Duration;
        }
    }
    protected abstract void ApplyEffect();
    public abstract void End();
}

Another abstraction! There's a little more code for this class, so I will explain what's going on. For our example, we assume all buffs have a duration, however you can change that pretty easily. We want all TimedBuff objects to store reference to the duration, the buff data, and the game object that is receiving the buff. There's also the following methods:

void Tick(float delta) - call this within the player's update loop. Used to keep a timer on the duration remaining and calls End() when completed.
void Activate() - call this after initialization to activate the buff logic.void End() - called when duration is finished. May also be called early to 'end' the buff.

Now we just need the player class. For this, we will create a MonoBehaviour component called BuffableEntity.

BuffableEntity.cs

public class BuffableEntity: MonoBehaviour
{
    private readonly Dictionary _buffs = new Dictionary();

    void Update()
    {
        //OPTIONAL, return before updating each buff if game is paused
        //if (Game.isPaused)
        //    return;

        foreach (var buff in _buffs.Values.ToList())
        {
            buff.Tick(Time.deltaTime);
            if (buff.IsFinished)
            {
                _buffs.Remove(buff.Buff);
            }
        }
    }

    public void AddBuff(TimedBuff buff)
    {
        if (_buffs.ContainsKey(buff.Buff))
        {
            _buffs[buff.Buff].Activate();
        }
        else
        {
            _buffs.Add(buff.Buff, buff);
            buff.Activate();
        }
    }
}

BuffableEntity keeps a dictionary of all its current buffs. Each update updates existing buffs and removes completed ones. Updating the individual buffs gives more flexibility and control on their lifecycle. For example: buff duration can be frozen when the game is paused or player is phased out.

Speed Buff Example

So how do we use this buff system? Let's make a quick 'speed boost' buff together.

SpeedBuff.cs

[CreateAssetMenu(menuName = "Buffs/SpeedBuff")]
public class SpeedBuff: ScriptableBuff
{
    public float SpeedIncrease;

    public override TimedBuff InitializeBuff(GameObject obj)
    {
       return new TimedSpeedBuff(Duration, this, obj);
    }
}

This extends our ScriptableBuff to hold data on the SpeedIncrease. In addition, by calling the InitializeBuff, we can create a TimedSpeedBuff, which we will implement next. The CreateAssetMenu attribute allows the scriptable object to be created as an asset in the project menu.

TimedSpeedBuff.cs

public class TimedSpeedBuff : TimedBuff
{
    private readonly MovementComponent _movementComponent;

    public TimedSpeedBuff(ScriptableBuff buff, GameObject obj) : base(buff, obj)
    {
        //Getting MovementComponent, replace with your own implementation
        _movementComponent = obj.GetComponent();
    }

    protected override void ApplyEffect()
    {
        //Add speed increase to MovementComponent
        ScriptableSpeedBuff speedBuff = (ScriptableSpeedBuff) Buff;
        _movementComponent.MovementSpeed += speedBuff.SpeedIncrease;
    }

    public override void End()
    {
        //Revert speed increase
        ScriptableSpeedBuff speedBuff = (ScriptableSpeedBuff) Buff;
        _movementComponent.MovementSpeed -= speedBuff.SpeedIncrease * EffectStacks;
        EffectStacks = 0;
    }
}

This is just a simple buff, but as you can see, in the ApplyEffect() method, the speed of the MovementComponent is increased. End() reverts the buff effect. This is all you need to implement your speed buff.

You should now be able to create an asset labeled Buffs/SpeedBuff. You'll be able to create different versions of the speed buff while using the same implementation! This is one of the benefits of using Scriptable Objects.

Conclusion

So to recap, if you want to implement different types of buffs, the steps you would take are:

  1. Implement a new version of TimedBuff. This is where all the logic goes, if you have any stat changes, effects to generate, they all go here.
  2. Implement a new version of ScriptableBuff. This is where all the logic goes, so the values of stat changes, art resources, sounds, etc should go here.

Hope this tutorial for a buff system in Unity helps, you can find the source on github. Feel free to leave a comment with any suggestions or questions you may have!

Interested in learning more about Unity? Check out my other Unity tutorials!

Change Log

  • 12/30/16 - Added github repository
  • 12/7/19 - Updated buff system with effect and duration stacking flags

Recommended Posts

36
Leave a Reply

avatar
12 Comment threads
24 Thread replies
3 Followers
 
Most reacted comment
Hottest comment thread
15 Comment authors
AndrewJonathanBradleyM Ssubfire Recent comment authors
  Subscribe  
newest oldest most voted
Notify of
Vlad
Guest
Vlad

Thanks man! This is really useful!

Carlos Rubiales Bravo
Guest

Very useful!! Will try it for my game 🙂

Lothar
Guest
Lothar

Hi, very interesting stuff. But I have a problem with understanding something – do you add a buff using those scripts?
I mean, I’d like to be able to add this Speedbuff via code (without assset) and via asset created in Unity (a ScriptableObject). How can I do that?
The problem is, the ScriptableObject is of type SpeedBuff, but buff is of type TimedBuff. So I can’t figure out how to add it.
And I would love to use if both ways – from assets, and via creating and instance of script, just like normal instance class.

Kevin
Guest
Kevin

Great stuff! I have implemented your system to replace my old one with Coroutines. With your approach I can also track every buff currently on the player, which can also help for implementing buff icons in the UI. Thank you so much! There’s one thing that has been troubling me for a couple days, though. Say I want to implement a non-stackable buff, for example a Speed=15 buff. So if the buff ends, I can return the speed value to default (say 10), so if I cast the same buff on the player, he will maintain the buffed values (15)… Read more »

Nico B
Guest
Nico B

Hey man awesome tutorial. I get the gist of it and have the structure working in my code but what I don’t understand is

– public override TimedBuff InitializeBuff(GameObject obj);

We don’t ever call this, yet the code inside seems to run? Do overidden abstract methods automatically get called? Is this some kind of special constructor?

Jack
Guest
Jack

Hello, may I ask if there is a complete demonstration of the case, thank you!

Ryan
Guest
Ryan

Hello i was adding your system today. and cant figure out the 2 overloads for Activate.. private TimedSpeedBuff tsbData; void OnTriggerEnter(Collider other) { //Debug.Log(“speed ” + tsbData.runSpeed “”); tpcmData.Activate(); //Destroy(other.gameObject); } // as is is null private SpeedBuff speedBuff; //Charmotor call private MoveBehaviour movementComponent; public TimedSpeedBuff(float duration, ScriptableBuff buff, GameObject obj) : base(duration, buff, obj) { movementComponent = obj.GetComponent(); speedBuff = (SpeedBuff)buff; } public override void Activate() { SpeedBuff speedBuff = (SpeedBuff)buff; movementComponent.runSpeed += speedBuff.SpeedIncrease; }

Oliver
Guest

Hi, @ first thx for this.
But im totally confused…
Im call BuffableEntity.AddBuff( what here) ; ?
How do i AddBuff ?
and why public List CurrentBuffs = new List(); is public ? its not shown in Inspector because TimedBuff does not inherit from MonoBehaviour

Oliver
Guest

should TimedBuff.cs inherit from Monobehaviour ? because i cant asign TImedSpeedBuff.cs wich inherit from TimedBuff

Cubanbkidd
Guest
Cubanbkidd

so i used the CreateAssetMenu to create a new buff, but i do not know how to add it to a buffableentity via .addbuff(no idea what to put here);
also how would i go about changing variables of the buff at runtime, before applying the buff to an object.

subfire
Guest
subfire

may learn something from your blog, thanks.

Bradley
Guest
Bradley

My game has around 2,000 different buffs, seems like to make that work I’d need 4,000 scripts. Is there a more streamlined approach in your opinion?