Buff System with Scriptable Objects for Unity

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<ScriptableBuff, TimedBuff> _buffs = new Dictionary<ScriptableBuff, TimedBuff>();

    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

51
Leave a Reply

avatar
19 Comment threads
32 Thread replies
55 Followers
 
Most reacted comment
Hottest comment thread
23 Comment authors
JonathanFaeksAjenadroDarrinJohn 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?

kevin n korlin
Guest
kevin n korlin

hey i was wondering if you can help me out ima bit of a noob but ive been following this tutorial on how to make an inventory with scriptable objects, i created an inventory and a small ui on the bottom of screen to hold like 5 items that i want to use to give me health for a certain time then stop. or just give me health.. how would i go about implementing it into my current script

Jorge
Guest
Jorge

This tutorial is simply fantastic, thank you so much! You are a life saver

Omniwrath
Guest
Omniwrath

I realize this is an older post, but I wanted to respond just incase you still check this. Thank you so much. This really helped me to get my buff system up and running. I’m in the process of developing a Turn Based Idle RPG and thus far I’m having great luck implementing a variety of buffs/skills based on your code. I turned the duration into an integer, and I manually call the Update function of BuffableEntity each round. Unfortunately, I hit a snag. I’m trying to implement some Aura type buffs. These will be applied to every party member… Read more »

Omn
Guest
Omn

I’d still be curious about your response, but I found a solution to the problem I was having. Basically, I needed to separate the activation of Auras into a separate foreach party member loop, and then Update buffs in another foreach party member loop. I had them in the same loop initially, which meant some party members were updating auras from other party members that should have been turned off.

Thanks again for your wonderful example!

John
Guest
John

This is really cool, I’m just trying to figure out why in Activate() in TimedBuff.cs we check if the duration is >= 0 in both cases?

Wouldn’t the case be that if the duration was zero and the buff had ended, IsFinished, it would be removed from the BuffableEntity? When would the case be that there would be a buff with duration 0? Just trying to wrap my head around it! Probably missed something.

Cheers!

Darrin
Guest
Darrin

Thank you for making this; it is very useful. I read what I could find about this topic and one of the challenges I had was restoration. A safe implementation may be to have an Enemy method GetMobDataWithBuffsByType(MobDataType mobDataType) that adds the base MobData like Defense, Damage or MoveSpeed to a call UnitBuffManager.GetTotalBuffByType(MobDataType buffType) which calculates the total of all the current Buffs of that stat type via GetBuffByType(buffType). There is no restoration needed. And you can also cleanly clamp the current mobdata stat against negatives and extremes. I’d be happy to share my code if interested. Again great job.

Darrin
Guest
Darrin

Thank you for making this; it is very useful. I read what I could find about this topic and one of the challenges I had was restoration. A safe implementation may be to have an Enemy method GetMobDataWithBuffsByType(MobDataType mobDataType) that adds the base MobData like Defense, Damage or MoveSpeed to a call UnitBuffManager.GetTotalBuffByType(MobDataType buffType) which calculates the total of all the current Buffs of that stat type via GetBuffByType(buffType). There is no restoration needed. And you can also cleanly clamp the current mobdata stat against negatives and extremes. I’d be happy to share my code if interested. Again great job.

Ajenadro
Guest
Ajenadro

Thank you for this post. It helped me a lot to imlement my buff system. I now would like to implement debuffs like ingnited or poisoned. Do you have a hint on how to implement that? My idea was to start a coroutine for the damage but since the TimedBuff class does not inherit from MonoBehaviour it can not do that. Big thanks!

Faeks
Guest
Faeks

Thank you very much, this tutorial was quite useful for a buff system for our game. I still have a problem with removing buffs manually, without timeout. Do you know a simple way to remove a single instance of a buff from an entity only knowing that buff’s name?