- Home /
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!
Answer by Bunny83 · Jul 18, 2021 at 02:17 AM
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
Thanks for the detailed reply! I ended up using a similar solution to what you have proposed! I even tested it and it generated anyhow some floating-point imprecision ( about .0001). But I guess could be because what you said,
"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;"
Thank you!
Answer by DrHerringbone · Jul 18, 2021 at 06:49 AM
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);
}
Thank you for your solution! But I didn't managed to get it working as I wanted :(
Answer by Llama_w_2Ls · Jul 17, 2021 at 10:34 PM
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)!
Hello thanks for the reply!
How would I round the final output? Do you mean Rounding by Nearest? If this is the case, if I want to remove 6.55 of health, won't the round make me miss the .55? I'm sorry I couldn't figure it out
And no I actually display the health on a slider, so the decimal places are used.
If I keep that imprecision (that I saw it reaching the second decimal place (instead of 85 it gave like 85.03593)), what the issues can be? Is it that bad to have that imprecision? Can that imprecision grow to a unit or worse?
Off the top of my head and as a relative noob… you can round numbers using the following method:
private float RoundXDecimals(int numOfDecimals, float numToRound)
{
Int deno$$anonymous$$ator = numOfDecimals * 10;
Int numerator = (int)(numToRound * deno$$anonymous$$ator);
If((float)numerator + 0.5f <= numToRound){
numerator += 1;
}
Return (float)(numerator / deno$$anonymous$$ator);
}