Trying to have damage flashing for a short time every time player takes damage

The code below has the player flashing when touching a hazard but continues flashing indefinitely. If I stand on the spike for a few seconds, the flashing speeds up faster than I want.

public class Player : MonoBehaviour {

   public int maxHealth = 10;
   public int currentHealth;
   public HealthBar healthBar;

   private float currentDamageInterval;
   [SerializeField] private float damageInterval = 2f; //in seconds

   [SerializeField] private Transform groundCheck;
   [SerializeField] private LayerMask hazardLayer;

   private SpriteRenderer sr;
   private Color[] colors = { Color.red, Color.white };

   void Start() {
       sr = gameObject.GetComponent<SpriteRenderer>();
       currentHealth = maxHealth;
       healthBar.SetMaxHealth(maxHealth);
   }

   void Update() {
       //subtract 1 every real life second
       if (currentDamageInterval > 0) {
           //sr.color = Color.white;
           currentDamageInterval -= Time.deltaTime;
       }
       if(currentDamageInterval <= 0 ) {
           if (Physics2D.OverlapCircle(groundCheck.position, 0.5f, hazardLayer)) {
               TakeDamage(1);
           }
       }
   }

   public bool CanTakeDamage() {
       return !(currentDamageInterval > 0f);
   }

   public void TakeDamage(int damage) {
       //if the current number in the currentDamageInterval is <= 0 or in other words if
       //2 seconds delay has passed, subtract 1 from the healthbar
       if (currentDamageInterval <= 0) {
           currentHealth -= damage;
           healthBar.SetHealth(currentHealth);
           StartCoroutine(DamageFlashing(1f, .3f));
           //set currentDamageInterval to start a new cooldown/delay period (2 seconds)
           currentDamageInterval = damageInterval;
       }
       sr.color = Color.white;
   }

   IEnumerator DamageFlashing(float time, float intervalTime) {
       float elapsedTime = 0f;
       int index = 0;

       while (elapsedTime < time) {
           sr.color = colors[index % 2];

           elapsedTime += Time.deltaTime;
           index++;
           yield return new WaitForSeconds(intervalTime);
       }
   }
}

How can I have the flashing speed/intervals stay stable and stop flashing after 1 second (parameter I set in the DamageFlashing Coroutine)?

Time.deltaTime doesn’t give you the time since the last coroutine call, it gives you the last time Update was called, which is going to be pretty small. (If you’re in FixedUpdate, it’ll return the last Physics update call). Instead of adding Time.deltaTime you should add the intervalTime.

2 Likes

Sticking a print (elapsedTime) under your elapsedTime += will show you exactly what it’s doing and demonstrate what RadRedPanda said.

1 Like

This is brilliant. Thank you, both. Also thanks to DMGregory on StackOverflow, this code works. I realized the faster flashing over time was multiple coroutines layering over eachother:

using System.Collections;
using UnityEngine;

public class Player : MonoBehaviour {

    public int maxHealth = 10;
    public int currentHealth;
    public HealthBar healthBar;

    private float currentDamageInterval;
    [SerializeField] private float damageInterval = 2f; //in seconds

    [SerializeField] private Transform groundCheck;
    [SerializeField] private LayerMask hazardLayer;

    private SpriteRenderer sr;
    private Color[] colors = { Color.red, Color.white };
    private Coroutine damageFlash;

    void Start() {
        sr = gameObject.GetComponent<SpriteRenderer>();
        currentHealth = maxHealth;
        healthBar.SetMaxHealth(maxHealth);
    }

    void Update() {
        //subtract 1 every real life second
        if (currentDamageInterval > 0) {
            //sr.color = Color.white;
            currentDamageInterval -= Time.deltaTime;
        }
        //if the current number in the currentDamageInterval is <= 0 or in other words if
        //2 seconds delay has passed, take damage
        if(currentDamageInterval <= 0 ) {
            if (Physics2D.OverlapCircle(groundCheck.position, 0.5f, hazardLayer)) {
                TakeDamage(1);
            }
        }
    }

    public bool CanTakeDamage() {
        return !(currentDamageInterval > 0f);
    }

    public void TakeDamage(int damage) {
        currentHealth -= damage;
        healthBar.SetHealth(currentHealth);

        //guarantee at most, one coroutine runs at a time
        if (damageFlash != null)
            StopCoroutine(damageFlash);

        damageFlash = StartCoroutine(DamageFlashing(1f, .1f));
        //set currentDamageInterval to start a new cooldown/delay period (2 seconds)
        currentDamageInterval = damageInterval;
        sr.color = Color.white;
    }

    IEnumerator DamageFlashing(float duration, float intervalTime) {
        int index = 0;
        var wait = new WaitForSeconds(intervalTime);

        for (float elapsedTime = 0; elapsedTime < duration; elapsedTime += intervalTime) {
            sr.color = colors[index % 2];
            index++;
            yield return wait;
        }
        damageFlash = null;
    }
}