|

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
Jonathan

Jonathan

Hey there, I’m Jon! I love landscape photography which is why I travel all the time! On my blog I’ll share all the best spots for epic sunsets and sunrises as well as some photography tips and tricks. I also have a interest in game design so you’ll find the occasional post about that too. Thanks for visiting!

Similar Posts

guest

58 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Vlad

Thanks man! This is really useful!

Carlos Rubiales Bravo

Very useful!! Will try it for my game πŸ™‚

Jonathan

Thanks, Glad you found it helpful!

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.

Jonathan

Hey Lothar, thanks for checking out tutorial!

To add the buff to a player/enemy/mob you would give it the BuffableEntity component. From there you can call AddBuff(TimedBuff buff) anytime you want to give it a buff. You can pass any type of timed buff there, so speed buff, etc would be acceptable.

So you can pass buffs that are created as assets, or at runtime.

M S

Can you show an actual example of this, by any chance? Maybe I’m not getting it, but nothing I put into any script in AddBuff(VALUEHERE) seems to work in any context.

Matthew

AddBuff(speedBuff.InitializeBuff(player.gameObject));

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) and not stack them up.

So Activate() would do:
movementComponent.moveSpeed = speedBuff.buffedSpeed;
and End() would do:
movementComponent.moveSpeed = movementComponent.defaultSpeed;

But, if I currently have 2 of the same buffs on the player, when the first one ends, the End() function will return it to default, ignoring the second buff’s speed increase.

For now, I have only implemented a counter on the speed buff, so I can know exactly how many speed buffs the player currently has. So,
Activate() does counter++
End() does counter–, and only reverts to defaultSpeed if counter = 0

This works as intended, but created another issue. If there’s another buff that affects the speed as well, say a Berserk buff with Attack=10 and Speed=20, and I cast speed + berserk on the player. Once the speed buff ends, counter=0 and the speed is returned to default, ignoring the berserk buff.

I have thought about checking for berserk and speed buff counters on the End() functions of both, but I don’t know if this is the best solution.

Also, another problem arises in which I may want the player to keep ‘the best speed buff’ he has. So if player uses Buff1 that gives Speed=20 and Buff2 that gives Speed=10 is casted after it, the player will maintain Speed 20 until Buff1 ends, then the player maintains Speed 10 until that ends.

Any thoughts on how I can solve this problems? Thank you!

Jonathan

Hey Kevin, thanks for checking out the tutorial!

There’s a couple ways to tackle this, I think the first thing would be to add data to the ScriptableBuff, so in addition to the duration, have a bool to designate if the buff is unique (non-stackable). Then when you call AddBuff in the BuffableEntity component, you can check the unique field, and if it is, you can check if the buff already exists. If it’s already existing, you can either extend the duration, override the existing buff, or how ever you wish to implement your system.

In regards to the Berserk + Speed buff, by using a ‘unique’ field it should solve itself. So instead of reverting speed back to the default speed, just +/- the speed as before with buffedSpeed.

So for example

Speedbuff:unique + 20 (speed is now +20)
Berserk:unique + 10 (speed is now +30)
Speedbuff:unique +15 (speed is still 30, since 15 doesn’t override 20)
Berserk:unique + 15 (speed is now +35, since 15 overrides 10)
Speedbuff:expire -20 (speed is now +15)
Berserk:expire -15 (speed is now +0)

Hope this helps!

Kevin

That makes sense. I will try it out, thank you!

Andrew

Hello, Jonathan. Could you please explain how do you actually achieve that? I’ve been trying to solve this problem for 4 hours already and no result….

Jonathan

@Andrew, could you explain what’s the problem you are trying to solve? πŸ™‚

Andrew

I’m trying to make the same buff, let’s say, speed increase to be non-stackable. My default speed is 10. When i pick up the buff, the speed goes to 15. When i pick up the same buff, my speed goes 20 which is not acceptable. I want the speed to remain the same, but at the same time i want to update the duration (duration which is left from the first buff + duration of a new buff). Well, I’m trying to achieve what Kevin said except the Berserk thing.

Jonathan

I see what you mean πŸ™‚

I would add two boolean fields to ScriptableBuff. They would be “IsDurationStacked” and “IsEffectStacked”.

Then for timed buff the following changes can be made:

public void Activate()
{
if (Buff.IsDurationStacked)
{
Duration += Buff.Duration;
}

if (Buff.IsEffectStacked || Duration <= 0)
{
ApplyEffect();
}
}
protected abstract void ApplyEffect();

This checks if Duration should be stacked and also if the Effect should be stacked before applying them.

I will push an update to the github repository as well. Hope this helps.

Feel free to let me know if you would like to see any more updates to this. These are all great features to have in a buff system.

Andrew

public void Activate()
{
if (Buff.IsDurationStacked)
{
Duration += Buff.Duration;
}
Above stuff increases the duration by 2 even though it was the 1st item that was picked up.
Below stuff won’t trigger ApplyEffect(); when Buff.IsEffectStacked is set to false
if (Buff.IsEffectStacked || Duration <= 0)
{
ApplyEffect();
}
Maybe I'm calling the AddBuff() wrong?
This is how i do it now:
public ScriptableSpeedBuff buff;

protected override void Pick()
{
_pickingCollider.GetComponent().AddBuff(buff.InitializeBuff(_pickingCollider.gameObject));
}

Jonathan

Sorry Andrew, small bug in the previous update. Here is a fix: https://github.com/xjjon/unity-flexible-buff-system/commit/2b7a30a27cc36ec7769ed6b4e589689399dea85a

Basically for the two new flags to work, BuffableEntity should call ApplyEffect on the same instance of TimedBuff. Previously it was adding a new instance to the list each time AddBuff was called.

Updated to use a Dictionary so that only one instance of each buff is active at a time. This way the duration and effect stacking should work as expected.

Thanks for pointing this out!

Also, the way you AddBuff looks correct to me.

Andrew

@Jonathan,
foreach loop in BuffableEntity throws an error when the buff has ended:
foreach (var buff in _buffs.Values)
{
buff.Tick(Time.deltaTime);
if (buff.IsFinished)
{
//Here it throws an error
_buffs.Remove(buff.Buff);
}
}
I’ve changed the first line to this: foreach (var buff in _buffs.Values.ToList()) and it seems to work, but I don’t think this is a good solution.
Also, extending the duration works properly now, but the effect still doesn’t apply. It’s either extend the duration or apply the effect. Is it possible to apply the effect and extend the duration at the same time? I’ve tried to put ApplyEffect() in TimedBuff:
if (Buff.IsDurationStacked)
{
Duration += Buff.Duration;
//RIGHT HERE
ApplyEffect()
}
Effect applies, but stacks when i pick the second buff. Even though IsEffectStacked is set to false. Sorry for disturbing you that much, but I really want to solve this problem and I’m pretty sure all other guys will be happy when they find a working system πŸ™‚

Jonathan

Oops, sorry! I should have caught that in my tests.

.Values().ToList() is the correct way to do it as the collection can’t be mutated if we are using it in the loop.

Fixed the issue with the effect not being applied:

https://github.com/xjjon/unity-flexible-buff-system/commit/37ebec0473baf4b6231004194b89b55ae50963b9

The issue was the ordering of the two conditions. Since the effect checks for duration or the flag. Applying the effect before duration will fix the issue.

Thanks again for pointing these out!

Andrew

Oh yes, now it’s working!!! Thank you, man!

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?

Jonathan

Thanks for reading, Nico

-public override TimedBuff InitializeBuff(GameObject obj)

This is contained inside a Scriptable Object which is primarily used to store data. This is where we store the values of the buff. So when we want to apply the buff to a player, we need an instance of TimedBuff, which is when we would call InitializeBuff with the GameObject, and in return we get the TimedBuff object.

The reason we do this is because Scriptable Objects are used to store data, hence each only has one instance of it. We need to keep track of values that change constantly, such as the duration and time remaining, which is why need to create a ‘TimedBuff’ object to use.

This TimedBuff is then applied to the player, which activates the buff, and then gets the effects the buff.

Hope this helps, let me know if you have any more questions!

Jack

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

Jonathan

Hi Jack, you can find a full example on the github repository for the project: https://github.com/xjjon/unity-flexible-buff-system

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;
}

Jonathan

I’m not sure what you’re asking – what are you having problems with? Everything looks fine

Oliver

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

Jonathan

Hey Oliver,

To add the buff, you should call AddBuff with whatever TimedBuff you wish to add.

Usually that would be something like from a OnTriggerEnter when the player collides with a powerup or through some other method.

Oliver

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

Jonathan

No, it shouldn’t inherit MonoBehavior.

You can assign SpeedBuff, which is a ScriptableBuff, which can be assigned via inspector to some GameObject.

Then you can call something like: buffableEntity.addBuff(speedBuff.InitializeBuff());

Oliver

Works great, thx.

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

may learn something from your blog, thanks.

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?

Jonathan

Hi Bradley, thanks for visiting.

What type of buffs are you looking to make? You should only need 1 buff script per type of buff. (i.e. if the buff has different effects. For buffs with different stats or values, you can share the same script.

So if you had

SpeedBuff
DamageBuff

those would both use different scripts – but if you had multiple speed buffs (at varying speeds) then you could just share that script and configure the values in the scriptable object.

—-

Another improvement you can make: Are you using anything to define your Attributes? For example, Speed, Strength, Health, etc?

If you had an enum for it AttributeType then you can modify the scriptable object to be something like:

[CreateAssetMenu(menuName = "Buffs/AttributeBuff")]
public class AttributeBuff: ScriptableBuff {
public float AttributeIncrease;
public AttributeType Attribute;
public override TimedBuff InitializeBuff(GameObject obj) {
return new TimedSpeedBuff(Duration, this, obj);
}
}

Then you can make a TimedAttributeBuff (instead of just speed) that modifies the objects attributes based on the AttributeType (assume you have a map or something to hold these values)

Bradley

you know actually that could work. If I showed you what I’m doing maybe it would help

I’m remaking the Final Fantasy XIV battle system inside Unity. So none of the MMO stuff (gear, items etc) just the jobs of the game and enemies to fight in scaling difficulty in a battle arena. The game uses MANY buffs to work. Ranging from stat increases to complicated effects and combo actions that are technically buffs till the next action. From what you just said I can already see that making a combo buff abstract class and then generating object from there would work but that still leaves sooooo many buffs.

https://eu.finalfantasyxiv.com/jobguide/monk/

the effect column on the right shows that every action leads to a series of “buffs” that tend to be quite different. All having icons etc. Then theres 16 other jobs and on top of that bosses who basically do all their damage with unique buffs on themselves and the characters. Its definitely in the quadruple figures. Since the game gives the vast majority of these effects the “buff” label I need them to all be able to work with the UI canvas in multiple places. I just want to start with best practices, rather than change the system 300 buffs down the line.

Jonathan

Yeah that’s a lot of buffs – I think for something of that scale you are better off defining your data in a database or json type of file. Scriptable object is nice for prototyping or games with less data, but for larger scale stuff I find it’s not as easy to manage and it’s not easy to update.

The scriptable object in this tutorial is basically used as the datastore, so if you replace that with a json/yaml object then you can just programatically load the data for the buffs.

buffs.yaml

- buff1:
type: timedAttributeBuff
icon: speedBoost.png
data:
attributeType: speed
value: 10
- buff2:
type: timedVisual
data:
sprite: effectSprite.anim

If you had something like this, then you can build your buffs from code with similar setup as in the tutorial. Main difference being that instead of scriptable objects, you create ScriptableBuff from the data source instead.

You would need to do something with the type field to map the type of buff to the underlying code that controls the logic. So for example, buff2 needs to be loaded as a TimedVisual.cs object while buff1 would be loaded as a TimedAttributeBuff.

Bradley

I had a prototype xml that was working well enough for the buffs before. I think I should probably go back to that and add functionality. There doesn’t seem to be a super great catchall solution to the fact that the buffs all do such vastly different things. You’ve been a great help!

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

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

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 if 1 of the heroes takes an appropriate skill, and lost if if that party member dies. It’s easy enough to apply and remove a single copy of the buff, but I’m having trouble when 2 or more party members both take the same Aura skill. The buff does not stack by default, even when cast by different party members. Fantastic! But I’m having some trouble with the buff starting to stack strangely when there are two DEAD heroes with the Aura skill.

Without getting too much into my own code (though I’m certainly happy to share it). How would you go about implementing an Aura system like this?

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!

Jonathan

I think that would be a good solution. Basically each `entity` that can be buffed should have it’s own update/loop to manage it’s own buffs. It has the added benefit that when the `entity` is killed/destroyed, the buffs are cleaned up automatically with it.

Thanks for checking out this tutorial and I’m glad it could help!

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

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

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

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!

Jonathan

Hi, simple way would be to implement another timer in the buff to do some effect each ‘tick’ (such as damage from poison and bleed).

Here’s a small example:

https://gist.github.com/xjjon/6bbdeaaf2c6d31dc8284b087ed8c88f1

You’ll notice I added “TickTime” to ScriptableBuff to customize how often the tick effect should be applied.
Then in the TimedBuff class there is a TickTime that updates and when it hits 0 it will ApplyTickEffect()

The underlying buffs (such as poison and bleed) can implement the ApplyTickEffect() method to deal damage with each tick.

Ajenadro

First of all big thanks for enlightening me. I still have a lot to learn. One quick note: when the enemy keeps standing in the fire and the debuff is constantly reapplied and therefore the debuff is readded and reactivated ApplyTickEffect is never called because TickTime is resetted. How would you avoid that? For now i just check TickTime is 0 in the Activate method and then set TickTime to Buff.TickTime.

Again thank you very much!

Jonathan

Hi, sorry for the late reply. I think you can just keep the existing TickTime if the buff is still active.

See: https://gist.github.com/xjjon/f7f51598d535728de68af840c603e648

hope this helps!

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?

Jonathan

Hi, you could add a string Name field to the ScriptableBuff object if you want to remove by name. Then create a RemoveBuff(string name) method that checks each entry in the dictionary for the buff name and remove it if it matches.

Otherwise I would suggest just removing it using the scriptable object reference. See example:

https://gist.github.com/xjjon/33def9a56c3a319fd22f685db906a284

Valentyn

Very good breakdown of universal system (unlike a lot of tutorials that focus on one particular effect, but do not cover actual system architecture). I was trying to achieve something similar, perhaps with universal buff metadata (so buff itself does not apply effect but tracks duration/stacks), to avoid creating separate class for each buff, but it gets out of hand very quickly, so having separate buff for each occasion seems the simplest solution.

Few other things that others would want to consider while implementing this:

  • Consider implementing most of properties of TimedBuff as abstract. For example leave out duration and stacks, leaving just abstract IfFinished, and implement them when you need them. This way, you could more freely control when buff expires (for example, buff that powers up your next attack, and must not expire until you attack).
  • Be very careful when removing effect – if value you remove differs from value you’ve added, you might cause permanent change. This even complicates when you start working with multipliers.
Jeythrew

Hi Jonathan I hope this article is still up, I am having trouble implementing the concept of your code to my Firepoint script.
public class Firepoint : MonoBehaviour
{
  public float rayCastDistance = 10f;
  public LayerMask layerMask; // Assign the desired layer(s) in the Unity Inspector.
  private EnemyHealth enemyHealth;
  float damageOvertime = 5f;
  SpeedBuff speedBuffScriptableObject;
  private void Start()
  {
    enemyHealth = FindObjectOfType<EnemyHealth>();
  }
  private void OnDrawGizmos()
  {
    // Draw a line to represent the raycast direction and distance in the scene view.
    Gizmos.color = Color.red; // You can choose any color you like
    Vector2 rayDirection = new Vector2(Mathf.Cos(transform.eulerAngles.z * Mathf.Deg2Rad), Mathf.Sin(transform.eulerAngles.z * Mathf.Deg2Rad));
    Gizmos.DrawRay(transform.position, rayDirection * rayCastDistance);
  }
  private void Update()
  {
    float angle = transform.eulerAngles.z;
    Vector2 rayDirection = new Vector2(Mathf.Cos(angle * Mathf.Deg2Rad), Mathf.Sin(angle * Mathf.Deg2Rad));  // DYNAMIC ROTATION 2d SPACE

    RaycastHit2D raycastHit2D = Physics2D.Raycast(transform.position, rayDirection, rayCastDistance, layerMask); //specific mask

    if (raycastHit2D.collider != null)
    {
      /*Debug.Log(“Hit ” + raycastHit2D.collider.gameObject.name);

      BuffableEntity buffableEntity = raycastHit2D.collider.gameObject.GetComponent<BuffableEntity>();
      if (buffableEntity != null)
      {
    
      }
    }
  }

Jeythrew

Thanks for your response sorry as well for the late reply, I did the buffable.AddBuff() and other workarounds that I knew however I got confused with the specific arguments to pass inside the Add(here). I don’t have any solution in my mind, I admit that I have lack of knowledge and easily get confused especially in OOP.

Jeythrew

Thank you sensei, your surely one call away I really appreciate this. Can I ask about the breakdown or the reasoning on how you declare this way, since It looks totally new to me, its okay if not hehe just a little bit curious since I myself aspire as well to be proficient in Unity Game Development.
        var buffLogicSystem = poisonDebuff.InitializeBuff(raycastHit2D.collider.gameObject);
        buffableEntity.AddBuff(buffLogicSystem);