Buff System with Scriptable Objects for Unity
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 {
// How often ApplyTick() is called (in seconds)
protected float TickRate = 0.5f; 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 (_timeSinceLastTick >= TickRate)
{
ApplyTick();
_timeSinceLastTick = 0;
} 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();
// Called every TickRate seconds. Can be used for things such as damage over time or healing over time.
protected abstract void ApplyTick(); Â Â Â 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.
void ApplyTick() – This can be used to implement things such as damage over time. If you don’t have any effects to apply periodically then you can just leave this method blank. It will get called every 0.5s (configured by TickRate).
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; }
protected override void ApplyTick()
{
//Do nothing
} }
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:
- 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.
- 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
- 12/3/23 – Updated buff system to support Tick Effects – this allows for things such as damage over time
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!
Thanks man! This is really useful!
Very useful!! Will try it for my game 🙂
Thanks, Glad you found it helpful!
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.
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.
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.
AddBuff(speedBuff.InitializeBuff(player.gameObject));
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!
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!
That makes sense. I will try it out, thank you!
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….
@Andrew, could you explain what’s the problem you are trying to solve? 🙂
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.
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.
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));
}
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 timeAddBuff
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.
@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 🙂
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!
Oh yes, now it’s working!!! Thank you, man!
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?
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!
Hello, may I ask if there is a complete demonstration of the case, thank you!
Hi Jack, you can find a full example on the github repository for the project: https://github.com/xjjon/unity-flexible-buff-system
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;
}
I’m not sure what you’re asking – what are you having problems with? Everything looks fine
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
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.
should TimedBuff.cs inherit from Monobehaviour ? because i cant asign TImedSpeedBuff.cs wich inherit from TimedBuff
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());
Works great, thx.
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.
may learn something from your blog, thanks.
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?
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)
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.
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 aTimedVisual.cs
object whilebuff1
would be loaded as aTimedAttributeBuff
.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!
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
This tutorial is simply fantastic, thank you so much! You are a life saver
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?
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!
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!
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!
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.
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.
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!
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.
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!
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!
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?
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
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:
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)
{
}
}
}
Hi, what issue are you having?
Is the raycast hitting your target? After hitting the target and calling GetComponent<BuffableEntity>() you can do buffableEntity.AddBuff() to add your desired buff
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.
Hey,
You basically need to pass in TimedBuff when calling AddBuff().
We can get a timed buff from your speed buff by passing the colliding game object to InitializeBuff() which creates an instance of TimedBuff.
var timedBuff = speedBuffScriptableObject.InitializeBuff(raycastHit2D.collider.gameObject);
buffableEntity.AddBuff(timedBuff);
I uploaded a full example using your code here: https://gist.github.com/xjjon/032a5576fd9cc98002d52c66222cc136
Hope this helps!
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);
No problem! I just think it’s easier to read this way.
It could be written in one line too:
buffableEntity.AddBuff(poisonDebuff.InitializeBuff(raycastHit2D.collider.gameObject));
But I think having it on two different lines is easier. The first line creates the buff, the second line of code adds the newly created buff to the
buffableEntity
Good Day Jonathan, I would like to ask with regards to timer. I would like to add a damage that deals every second while the duration of a certain buff is not finished. I’m having a hard time considering the Tick() method since this one is responsible of the buff duration right?
Hey, great question and you’re on the right track. You can adapt Tick() to do something like damage every second!
I’ve made an update to the code (and github project) to implement that. Check out the link to see the parts I changed.
I basically added a method
void ApplyTick()
that is called every 0.5s (configure this by changing TickRate in TimedBuff)In your buff where you want to deal damage every second you can add the damage logic within ApplyTick().
In my example ApplyTick() does nothing but you can replace it with your damage logic.
Thank you once again sensei, your a game life saver! I hhave this fear of modifying the base class in the first place since I lack knowledge of the code itself. But thanks for the immediate and precise response and solution, this serve as a breakthrough for me, A big thanks and Godbless
Glad I was able to help! Good luck with your game!
Nice to visit here again, I have this concern for the past few days my friend. I would like to add a DamageReduction buff to negate the fall damage when a player acquire a powerup and activate it (armorTrigger class) . However Im having a hard time applying the damageReducePercents; variable to HealthSystem class. Initially, the ApplyEffect() computation works however the result of this healthSystem.damageReduction += damageReducBuff.damageReducePercents; applies to HeathSystem class after I re- play(playmode) , it means it doesn’t apply immediately right after I trigger the Click()/ApplyEffect() method.
Thank you so much in advance.
public class DamageReductionBLS : BuffLogicSystem
{
private HealthSystem healthSystem;
public DamageReductionBLS(BaseBuffProp buff, GameObject obj) : base(buff, obj)
{
healthSystem = obj.GetComponent<HealthSystem>();
}
protected override void ApplyEffect()
{
DamageReducBuff damageReducBuff = (DamageReducBuff)BLS_Buff;
Debug.Log($”damageReducBuff.damageReducePercents: {damageReducBuff.damageReducePercents}”);
if (healthSystem != null)
{
healthSystem.damageReduction += damageReducBuff.damageReducePercents;
Debug.Log($”Updated HS damageReduction: {healthSystem.damageReduction}”);
}
else
{
Debug.LogError(“No healthsystem”);
}
}
protected override void ApplyTick() { }
public override void End()
{
DamageReducBuff damageReducBuff = (DamageReducBuff)BLS_Buff;
healthSystem.damageReduction -= damageReducBuff.damageReducePercents;
Debug.Log(“damageReducBuff end”);
}
{
[CreateAssetMenu (menuName = “Buffs/Damage Reduction”)]
public class DamageReducBuff : BaseBuffProp
{
public float damageReducePercents;
public override BuffLogicSystem InitializeBuff(GameObject obj)
{
return new DamageReductionBLS(this, obj);
}
}
public class HealthSystem : SubjectClass
{
public PlayerStatistics playerStats;
public HealthBar healthBar;
private Rigidbody2D rb;
public float currentHealth;
float totalDamage;
public float groundCollisionThreshold = 1000f;
public float groundCollisionDamageMultiplier = .5f;
public float damageReduction;
void Start()
{
rb = GetComponent<Rigidbody2D>();
currentHealth = playerStats.Health;
healthBar.SetMaxHealth(playerStats.Health);
//damageReduction = playerStats.PainTolerance * 0.01f; // percentage
//retrieve the scriptable DmageReducBuff variable damage reduce percentage
}
public void Update()
{
//HP bar following player
Vector3 screenPoint = Camera.main.WorldToScreenPoint(transform.position + Vector3.up * 2);
healthBar.transform.position = screenPoint;
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.CompareTag(“Ground”))
{
float relativeVelocity = collision.relativeVelocity.magnitude;
if (relativeVelocity > groundCollisionThreshold)
{
float damage = relativeVelocity * groundCollisionDamageMultiplier;
damage -= damage * damageReduction;
Debug.Log($”damgereduc{damageReduction}”);
TakeDamage(damage);
}
}
}
public class armorTrigger : MonoBehaviour
{
[SerializeField]
private DamageReducBuff damageReducBuff;
[SerializeField]
private GameObject player;
public void Click()
{
BuffableEntity buffableEntity = player.GetComponent<BuffableEntity>();
if (buffableEntity != null)
{
BuffLogicSystem buffLogicSystem = damageReducBuff.InitializeBuff(buffableEntity.gameObject);
buffableEntity.AddBuff(buffLogicSystem);
gameObject.SetActive(false);
Debug.Log(buffableEntity.name);
}
else
{
throw new Exception();
}
}
Hey,
What do you mean by it doesn’t apply immediately? Is it delayed?
ApplyEffect() will be from the Activate() method from TimedBuff.cs so maybe it would help by having some debug statements there to narrow down what’s going on.
I guess the execution order has conflict the ApplyEffect doesn’t pass the result to health system.damageReduction variable immediately but it was pass after I re-play the playmode, (in short it passes the result at the next runtime)
Hmm that shouldn’t happen. All the buffs only exist during runtime and they don’t save so it shouldn’t be the case.
Maybe it’s the way ApplyEffect() and Activate() are called?
I guess so, the intended outcome should be all the statement inside ApplyEffect() should call when that function is trigger. I try to implement a debug.log inside of Apply Effects() and the value is accurate in the console. however, in the healthsytem’s variable damagereduction didn’t change at all.
Thanks for sharing this helpful resource! I’ve bookmarked it for future reference.