RPG Damage Over Time

Did anyone get the Repeat effect strategy to work? I tried a damage over time but it seems to be no bueno… i had the brilliant idea afterwards of using “repeat()” function… but the question Im having now is… how do I remove the debuff? Where do I keep track of this effect on a target so that it can be dispelled by the correct spell or item??

Thanks all for the help!

Hoping for some help, shameless bump! :slight_smile:

no help eh? Not even from the big gurus? :frowning:

Hi, Aionlasting,
The topic didn’t get tagged with RPG, so it escaped my notice. I’m walking out the door right now, but when I get home, I’ll work up a solution that should fix your needs.

1 Like

Hey brian, thanks for the responce. I’m excited to see how you tackled this particular issue. Please provide insight on how to track debuffs/buffs as well, even if you can’t script the whole system as it may be more complicated than I am thinking but hopefully not!

Also, how do I tag a topic? So next time it does not get missed??

Usually, if you ask a question from the course lecture page, it gets tagged automagically. Not sure what happened here. You can add a tag to a topic when you create it.

There are two ways you can go about DoT… You could make a function on the Health component, or you could have a manager. For now, we’re going to create a dedicated manager to deal with Health. I’ll probably create a topic soon with a more generic approach that can let you do almost -=anything=- over time.

using System.Collections.Generic;
using UnityEngine;

namespace RPG.Attributes
{
    public class HealthDoTManager : MonoBehaviour
    {
        public class DamageOverTimeInstance
        {
            public string name = "Generic effect"; //useful for debugging
            public float amount = 1.0f; //How much damage we want to do each interval
            public int repeats = 10;
            public float interval = 1.0f; //time between each repeat.
            public GameObject instigator = null;
            public float timer = 0.0f;
            public Health health;

            public bool Update()
            {
                timer += Time.deltaTime;
                if (timer > interval)
                {
                    repeats--;
                    Debug.Log($"{health.gameObject.name} taking Damage Over Time from {name} effect inflicted by {instigator}.  {repeats} cycles remaining.");
                    health.TakeDamage(instigator, amount);
                    if (repeats < 0) return true;
                    timer=0.0f; //Edit: Thanks to dramolxe for spotting this, I forgot to reset the timer!
                }
                return false;
            }
        }

        private Health health;
        private List<DamageOverTimeInstance> instances = new List<DamageOverTimeInstance>();
        private List<DamageOverTimeInstance> instancesToRemove = new List<DamageOverTimeInstance>();

        private void Awake()
        {
            health = GetComponent<Health>();
        }

        private void Update()
        {
            if(instances.Count) return;
            foreach (DamageOverTimeInstance instance in instances)
            {
                if(instance.Update(health)) instancesToRemove.Add(instance);
            }
            foreach (DamageOverTimeInstance expiredInstance in instancesToRemove)
            {
                instances.Remove(expiredInstance);
            }
            instancesToRemove.Clear();
        }

        public DamageOverTimeInstance CreateDamageOverTimeInstance(GameObject instigator, float amount, int repeats,
                                                                   float interval)
        {
            DamageOverTimeInstance result = new DamageOverTimeInstance();
            result.instigator = instigator;
            result.amount = amount;
            result.repeats = repeats;
            result.interval = interval;
            result.health = health;
            instances.Add(result);
            return result;
        }

        public void RemoveInstance(DamageOverTimeInstance instance)
        {
            instances.Remove(instance);
        }
        
    }
}

Lots of stuff going on here…
The real work is being done by the DamageOverTimeInstance. This inner class keeps track of a specific Damage Over Time effect (imagine six different characters poisoned you, yep, lots of DoTs overlapping, and this class handles it just fine).

Each instance is responsible for managing itself, through it’s Update method. This is a fairly straightforward approach… increase timers, do something when the timer exceeds the interval. We decrement the number of repeat, when it runs out, the result returns true to our regular update.

In the regular update, we call each of the interval’s Update methods, and if they return true, we add them to a list of instances to remove. We can’t just remove them right away because of the way collecitons work. Once we’ve cycled through the collection of instances, we can remove them in the next loop.

This component should be placed on each character. Whenever you want to perform a damage over time effect, GetComponent the HealthDoTManager on the target and call CreatedDamageOverTimeInstance with the required information.

I made the method return the instance for more advanced functions, perhaps some way that the effect decides to end the DoT prematurely. It’s really not needed for what we’re doing, so you can discard the result (just call CreateDamageOverTimeInstance without caching the result).

1 Like

Dear Brian,

Thank you so much for taking the time to help me with this issue! I’m going to go ahead and implement your solution and see if I can understand it and what opportunities it makes available! I’ll report back in a bit with my findings.

Thanks again! I Look forward to a more generic approach in the future :slight_smile: :innocent:

Hey,

Saw this post and wanted to try it out, but I found a strange behavior.
So, after I’ve setup the health dot manager script, I then went and created an ability effect to be able to trigger this.
What I found out was that, if you put in 10 repeats for example, and the interval is 1, When you use the ability, All those repeats will happen really quick, like under 1 second.

If you go and put in an interval of 3seconds, if you use the skill, after 3 seconds, it will do all the 10 repeats under 1 second.
I think that under the bool Update method, the if (timer > interval) check and what comes after it is not working as it should.
So the timer will increase, if your interval is 5s, then the effect won’t fire off until the timer actually gets to 6 seconds, then it will just fire off all the repeats, and not do them as expected.

The way I was doing DoT, was directly from an ability effect script, which basically repeated other effect strategies.
So in my version I have this

private IEnumerator RepeatingEffects(AbilityData data, Action finished)
        {
            if (abortIfCanceled && data.IsCancelled()) yield break;
            for (int i = 0; i <= repeatTime; i++)
            {
                foreach (var effect in repeatEffects)
                {
                    effect.StartEffect(data, finished);
                }
                yield return new WaitForSeconds(timeToRepeatAfter);
            }
        }

But I would like to get your version working Brian, because it’s actually something I wanted to get to do / make work, because this way you can control the DoTs.
I also have a separate Status effect script that works with a list, and keeps track of the statuses and I would actually like to see if I could integrate the DOT manager with my status manager.
But bottom line is, I think there is something wrong with the bool Update method.

LOL, there is something seriously wrong with it. (Understand, I wrote this on the fly here in the editor to provide a solution, so I never actually tested it beyond running the logic).

at the end of the if(timer>interval) block, I forgot to reset the timer! I’m going to edit this to look like I did it right in the first place. :slight_smile:

You misspelled "I think Brian needs to test his code more often."

HAHA, yea believe me, i had to make a few fixes :wink:

The way I went about this was…

public class HealthChangeEffect : EffectStrategy
    {
        public float timeBetweenIntervals;
        public int repeats;
        public float healthChange;

        public GameObject visaulEffect;
        public override void StartEffect(AbilityData data, Action finishedCallback)
        {
            foreach (var h in data.GetTargets())
            {
                h.AddComponent<HealthChangeEffector>()
                    .Apply(data.GetUser(), healthChange, timeBetweenIntervals, repeats, visaulEffect);
            }

            finishedCallback();
        }
    }

Then this is the health changer class that gets applied to each target

public class HealthChangeEffector : MonoBehaviour
    {
        private float healthChange;
        private float timeBetweenIntervals;
        private float repeats;
        private GameObject visualEffect;
        private GameObject applier;
        private bool started = false;

        private Health health;
        private float timeSinceLastApplied = Mathf.Infinity;

        void Awake()
        {
            health = GetComponent<Health>();
        }

        public void Apply(GameObject applier, float healthChange, float timeBetweenIntervals, float repeats,
            GameObject visualEffect)
        {
            this.applier = applier;
            this.healthChange = healthChange;
            this.timeBetweenIntervals = timeBetweenIntervals;
            this.repeats = repeats;
            this.visualEffect = visualEffect;
            started = true;
        }

        void Update()
        {
            if (!started) return;

            if (repeats <= 0)
            {
                Destroy(this);
            }

            timeSinceLastApplied += Time.deltaTime;
            if (timeBetweenIntervals > timeSinceLastApplied) return;


            if (healthChange > 0)
            {
                health.Heal(healthChange);
            }
            else
            {
                health.TakeDamage(healthChange, applier);
            }

            Instantiate(visualEffect, transform);
            timeSinceLastApplied = 0;
            repeats--;
        }

what you guys think about doing it this way? I just need to figure out now how to get a projectile to apply this… such as a fireball with a chance to burn the target… since the effect would have to be triggered AFTER the fireball HITS the target… i could use the delay but i really hate that way of doing things >.>

I don’t see this pattern applied often.

Give the projectile a callback (I’d likely make a new projectile class inheriting from projectile for this, that takes in an AbilityData and a callback).

Privacy & Terms