Gradually reduce a float over time with a Coroutine that can be run multiple times simultaneously.

Hello guys! I could use some help!

I have this coroutine

float currentHP = 100;

IEnumerator GraduallyReduceHP(float damage, float rate)
{
    // The value that currentHP should have after it has been gradually decreased
    float finalHP = currentHP - damage;

    while (currentHP > finalHP)
    {
        currentHP -= rate * Time.deltaTime;
        yield return null;
    }
    currentHP = finalHP; // to make sure that currentHP reaches the correct value and isn't wrong even by 0.0001 because of rate * Time.deltaTime

    yield break;
}

This code works perfectly for me, the problem is that I want to run multiple “instances” of this coroutine.

Let’s suppose that the health of the character is = 100.
In my game the character can be poisoned, let’s say he gets poisioned - so I start this coroutine GraduallyReduceHP(15, .1f) // “Poision” coroutine.

Which reduces the HP very slowly as I want, and it goes until currentHP reaches 85.

Now let’s suppose an enemy attacks me while I am poisioned - I start the coroutine again with those parameters: GraduallyReduceHP(40, 10f) - Which rapidly makes my HP go down.

Everything goes perfectly until the “Poision” coroutine ends.
When it does, my health becomes 85 (the value of finalHP inside the coroutine - and I can see why).

But I can’t get a way to prevent this from happening.
If I remove the line “currentHP = finalHP;” from the coroutine everything goes well, but finalHP isn’t accurate (instead of being 85 when the Poision coroutine ends it becomes like 84.96849).

How can I get around this? Thanks!

Your approach does not work if you want to have multiple coroutines affecting the CurrentHP at the same time. The main issue is your finalHP which is calculated once at the start of the coroutine. Just think through your code logically. Imagine you want to apply 10 HP damage over the time of 10 seconds (so 1 HP per second). Imagine the starting hp is 100. So When the coroutine is started, its finalHP is calculated as 100 - 10 == 90. If after just one second you start another coroutine, also 10 hp with a rate of 1HP / s, the second coroutine would calculate a finalHP of 99-10 == 89. Since now two coroutines run at the same time you will effectively subtract 2HP/s as expected. However the first coroutine will stop once currentHP reaches 90HP and the second one will stop when it reaches 89HP. So the second coroutine essentially just subtacted 1HP instead of 10 due to the time overlap.

What you have to do is just “count down” the damage you want to deal and that should be your condition.

IEnumerator GraduallyReduceHP(float damage, float rate)
{
    while (damage > 0)
    {
        float delta = rate * Time.deltaTime;
        if (delta > damage)
        {
            currentHP -= damage;
            break;
        }
        currentHP -= delta;
        damage -= delta;
        yield return null;
    }
}

Note that your final line in your original code currentHP = finalHP; does not prevent floating point inaccuracies because they can already happen here:

float finalHP = currentHP - damage;

In my version we just calculate a “delta” value that should be subtracted this frame. The if statement makes sure when we reach the end to not overshoot the amount we wanted to subtract. In essence as the damage value is counting down to 0 we count down currentHP in parallel by the same amount. Once delta is larger than the total remaining damage, we just subtract the remaining damage directly and quit. This coroutine can run in parallel with any other code that is changing currentHP.

In an actual system you would probably replace the currentHP -= delta; line with something like DealDamage(delta); That’s because when you deal damage you usually want to check bounds so currentHP does not go below 0 or above maxHP. Likewise that is also the point where you want to check for the death of the player / object

I threw in a comment on how to round numbers but I think what you might want to do is use the Mathf.lerp Function. Not only does it make linearly interpolating (fancy words for gradually increase or decrease) but it does so at a constant rate that you define and it does so at a rate that can be reactive to your frame rate.

float health;
float healthBeforeDamage;
bool doFixedDamageOverTime;
bool doConstantDamageOverFixedTime;
bool doConstantDamageIndefinitely;

void update()
{
   If(!doFixedDamageOverTime && !doConstantDamageOverFixedTime
       && !doConstantDamageIndefinitely)
    {
        healthBeforeDamage = health;
    }

    If(doFixedDamageOverTime)
    {
        DoFixedDamageOverTime(/some amount of damage/, /how fast you want to get there/);
    }

    If(doConstantDamageOverFixedTime)
    {
        DoConstantDamageOverFixedTime(/some amount of time/, /how fast you want the player to lose health/);
    }

    If(doConstantDamageIndefinitely)
    {
        DoConstantDamageIndefinitely(/rate of damage/);
    }

}

void DoFixedDamageOverTime(float amountOfDamage, float rate)
{
    health = Mathf.lerp(health, healthBeforeDamage - amountOfDamage, rate * Time.deltaTime);
}


void DoConstantDamageOverFixedTime(float timeInSeconds, float damageRate)
{
    doConstantDamageOverFixedTime = false;
    float totalDeltaTime = 0
    For(totalDeltaTime + Time.deltaTime < timeInSeconds; totalDeltaTime += Time.deltaTime)
    {
        health = Mathf.lerp(health, 0, rateOfDamage * Time.deltaTime);
    }
}

Void DoConstantDamageIndefinitely(float rateOfDamage)
{
    health = Mathf.lerp(health, 0, rateOfDamage * Time.deltaTime);
}

You should do one last check to see if your final hp is close to the desired final hp. If it is very close (they are the same when rounded to nearest whole number), then set it to that hp. Else, do nothing.


Or, just round the final output instead of setting it to the finalHP amount. This would, in theory, produce the same effect. Minor floating point errors won’t be visible to the player. The health display would only read the health as an int (i hope)!