Dynamic health bar

health.gif

For my Spacecraft project, I wanted to build a health system that was intuitive to understand and provided feedback for damage effects. Because this game was designed around the core engagement type of player expression, I wanted to give the player a variety of build options to experiment with. To support this goal, I split health up into two elements: health and shields. This works like many similar systems in other games, where the player has a small pool of shields that regenerate after not taking damage, and a large pool of health that doesn’t regenerate. The player can prioritize either of these stats through their choices of ship parts, allowing them to lean into the system they enjoy most. In order to make the health UI visualize how much damage the player took recently, I decided to implement a simple visual I’ve seen in many games where the damage the player takes lingers for a little while and then lerps down to the new health value.

When I approached programming this system, I chose to split it up into two scripts: “PlayerHealth” and “BarController“. PlayerHealth controls all of the data about the player’s health, including maximum values and shield regeneration rate, as well as all functionality for healing and taking damage. This script sends the data to the BarController script, which visualizes this information and animates it. There is a separate BarController for the health bar and the shield bar.

Below this is the entire PlayerHealth script. This script handles all of the gameplay functionality on its own, but in order to visualize the health it sends the data to the BarController.

using UnityEngine;
using UnityEngine.Events; // To assign delegate function through inspector

public class PlayerHealth : MonoBehaviour
{
    [Header("Health")]
    private int maxHP = 100;               // Maximum health
    int currentHP;                         // Current health
    public BarController HPBar;            // Reference to health UI bar

    [Header("Shields")]
    private int maxShields = 50;           // Maximum shields
    int currentShields;                    // Current shields
    public float regenRate = 5f;           // Shield regeneration rate (higher numbers are faster)
    float currentRegenTimer = 0f;          // Timer to count regeneration intervals
    public float regenDelay = 3f;          // Delay to healing after being hit
    bool regenCountdownActive = false;     // Tracks whether you have been hit recently
    float regenCountdown = 0f;             // Timer to track shield regeneration delay
    bool regening = false;                 // Tracks if you are currently regenerating shields
    public BarController shieldBar;        // Reference to shield UI bar

    [Header("Damage")]
    public GameObject hitParticle;            // Reference to prefab for particles when you get hit
    public int collisionDamage = 75;          // How much damage you take when colliding with something
    public float collisionIFrames = 0.3f;     // Invincibility frames (in seconds) after you collide
    float currentColIFrames = 0f;             // Timer to count how long you have been invinicible for
    bool canBeCollided = true;                // Tracks whether you can currently collide with something

    [Header("Death")]
    public GameObject deathParticle;       // Reference to prefab for particles when you die
    public UnityEvent onDeath;             // Event delegate to call when you die (assign through inspector)

    public int MaxHP       // This constructor allows access to the maxHP variable
    {
        get => maxHP;      // You can always get the maxHP
        set                       // The important element of this constructor is that when you set maxHP,
        {                         // you also set the currentHP to the new value. I did this so that changing
            maxHP = value;        // to a spaceship part with higher health did not leave you with the current
            currentHP = maxHP;    // health of the previous part.
        }
    }
    public int MaxShields  // This constructor allows access to the maxShields variable
    {
        get => maxShields; // This constructor works the same as the MaxHP constructor
        set
        {
            maxShields = value;
            currentShields = maxShields;
        }
    }

    /******************************************************************************
     Function: Start
    
        Description: Unity method that is called when this object is initialized
    
        Inputs: None
    
        Outputs: None
    ******************************************************************************/
    void Start()
    {
        currentHP = MaxHP;              // Set the starting health to the max health
        currentShields = MaxShields;    // Set the starting shields to the max shields
    }

    /******************************************************************************
     Function: Update
    
        Description: Unity method that is called every frame.
    
        Inputs: None
    
        Outputs: None
    ******************************************************************************/
    void Update()
    {
        if (Input.GetKey(KeyCode.G))    // Cheat key for healing to full health
        {
            HealToFull();
        }
        if (Input.GetKey(KeyCode.H))    // Cheat key for instantly dying
        {
            Death();
        }

        if (regenCountdownActive)                // If you have been hit recently:
        {
            regenCountdown += Time.deltaTime;    // Increment the timer

            if (regenCountdown >= regenDelay)    // If the timer is finished:
            {
                regening = true;                 // Start regenerating
            }
        }

        if (regening)                                      // If regeneration is active:
        {
            currentRegenTimer += Time.deltaTime;           // Increment the timer for regen intervals

            if (currentRegenTimer >= (1f / regenRate))     // If the timer is done:
            {
                currentShields++;                          // Heal shields by 1
                currentRegenTimer = 0f;                    // Reset the timer
            }
            if (currentShields >= MaxShields)              // If player is at or above max shields:
            {
                currentShields = MaxShields;               // Set current shields to the max (prevents overshields)
                regening = false;                          // Stop regenerating
            }
        }
        if (canBeCollided)                                 // If you are currently invincible:
        {
            currentColIFrames += Time.deltaTime;           // Increment invincibility timer
            if (currentColIFrames >= collisionIFrames)     // If the timer is done:
            {
                canBeCollided = true;                      // You can now collide
            }
        }


        // After all health calculations have been made this frame, update the UI
        HPBar.SetFill((float)currentHP / (float)MaxHP);                // Set the fill of the health bar to the current health
        HPBar.SetText(currentHP / 10);                                 // Set the text of the health bar to the current health
        shieldBar.SetFill((float)currentShields / (float)MaxShields);  // Set the fill of the shield bar to the current shields
        shieldBar.SetText(currentShields / 10);                        // Set the text of the shield bar to the current shields
    }

    /******************************************************************************
     Function: Damage
    
        Description: This function is called when you take damage.
                     It handles dealing damage to the player and triggering death.
    
        Inputs: Damage - The amount of damage you took.
    
        Outputs: None
    ******************************************************************************/
    public void Damage(int damage)
    {
        regening = false;                 // Stop regenerating
        regenCountdownActive = true;      // Mark that you have been hit recently
        regenCountdown = 0f;              // Reset the regeneration countdown

        int remainder = 0;                // The damage that will be dealt to the health bar after substracting from shields

        if (currentShields > 0)                             // If you have any shields:
        {
            if (currentShields - damage >= 0)               // If the damage you take wouldn't go past shields:
            {
                currentShields -= damage;                   // Deal the damage only to the shields
            }
            else                                            // If the damage would have gone past shields:
            {
                remainder = -(currentShields - damage);     // Calculate the remaining damage after substracting from shields
                currentShields = 0;                         // Shields are reduced to zero
            }
        }
        else remainder = damage;          // If you didn't have any shields, deal all damage to health

        currentHP -= remainder;           // Reduce your current health by the remaining damage

        if (currentHP <= 0)               // If your health is less than zero after the damage:
        {
            Death();                      // The player dies
        }
    }

    /******************************************************************************
     Function: Heal
    
        Description: This function is called when the player is healed.
    
        Inputs: amount - The amount of healing you recieved.
    
        Outputs: None
    ******************************************************************************/
    public void Heal(int amount)
    {
        if (currentHP < MaxHP)                           // If you are not at full health:
        {
            currentHP += amount;                         // Heal by the amount
            if (currentHP > MaxHP) currentHP = MaxHP;    // If you overhealed, return back to max health
        }
    }

    /******************************************************************************
     Function: Collide

        Description: This function is called when you collide with an asteroid or enemy.
                     It handles dealing damage to the player and starting inviniciblity
                     frames.

        Inputs: None

        Outputs: None
    ******************************************************************************/
    public void Collide()
    {
        if (canBeCollided)              // If you can collide:
        {
            Damage(collisionDamage);    // Damage the player
            currentColIFrames = 0f;     // Start the invincibility timer from 0
            canBeCollided = false;      // You can't collide until the time is done
        }
    }

    /******************************************************************************
     Function: OnCollisionEnter
    
        Description: Unity method that is called when this object collides with another collider
    
        Inputs: collision - Stores data about collision
    
        Outputs: None
    ******************************************************************************/
    private void OnCollisionEnter(Collision collision)
    {
        GameObject other = collision.gameObject;
        if (other.tag == "Asteroid")                                                     // If you collided with an asteroid:
        {
            Collide();                                                                   // Take collision damage
            other.GetComponent<Asteroid>().Damage(GetComponent<Parts>().crashDamage);    // Deal damage to asteroid
        }
        if (other.tag == "Enemy")                                                        // If you collided with an enemy:
        {
            Collide();                                                                   // Take collision damage
            other.GetComponent<EnemyHealth>().Damage(GetComponent<Parts>().crashDamage); // Deal damage to enemy
        }
    }

    /******************************************************************************
     Function: Death
    
        Description: This function is called when you die.
                      It spawns particles and starts the timer to death screen.
    
        Inputs: None
    
        Outputs: None
    ******************************************************************************/
    void Death()
    {
        Instantiate(deathParticle, transform.position, Quaternion.identity);  // Spawn death particles
        onDeath.Invoke();                                                     // Start timer to death screen
        Destroy(gameObject);                                                  // Destroy the player object
    }

    /******************************************************************************
     Function: HealToFull
    
        Description: Heals the player to full health (accessed through cheat key).
    
        Inputs: None
    
        Outputs: None
    ******************************************************************************/
    void HealToFull()
    {
        currentHP = MaxHP;                  // Set health to max health
        currentShields = MaxShields;        // Set shields to max shields
    }
}

The PlayerHealth script sends all of the health data to this BarController script, which is located on two UI bar objects:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro; // TextMeshPro (Unity supported text plugin)

public class BarController : MonoBehaviour
{
    /* 
     This script controls the UI bars for the player's health and shields. When the player takes damage,
     the health bar is instantly set to the new value, but there is another bar that is left behind showing
     how much damage the player took. This damage bar waits for some time before lerping down to the new
     health value. This is controlled through two timers, one that counts the time the damage bar stays there
     and another to count the time it takes for the damage bar to lerp down to the new value.
    */

    public RectTransform barFill;      // Reference to UI object displaying current health
    public RectTransform damageFill;   // Reference to UI object that lerps behind to display damage taken
    public TextMeshProUGUI healthText; // Reference to text that displays the health value

    public float damageDelayTime = 0.5f;          // Time until the damage visual lerps down to new value
    public float damageLerpTime = 0.5f;           // How long it takes for the lerp to happen
    bool damageDelay, damageLerping = false;      // Bools to store whether lerp is happening/will happen
    float goalValue, currentValue, oldValue = 1f; // Float values used to lerp the damage bar
    float currentDelayTime, currentLerpTime = 0f; // Current values of the timers

    /******************************************************************************
     Function: Update
    
        Description: Unity method that is called every frame.
    
        Inputs: None
    
        Outputs: None
    ******************************************************************************/
    private void Update()
    {
        // This if block controls the timer for how long the damage bar lingers
        if (damageDelay)                                  // If you have been hit recently:
        {
            currentDelayTime += Time.deltaTime;           // Increment the delay timer
            if (currentDelayTime >= damageDelayTime)      // If the timer is done:
            {
                damageLerping = true;                     // Start lerping the bar down
                damageDelay = false;                      // The bar is no longer lingering
            }
        }

        // This if block controls the timer for when the bar is lerping down
        if (damageLerping)                                // If the bar is currently lerping:
        {
            currentLerpTime += Time.deltaTime;            // Increment the timer

            float t = currentLerpTime / damageLerpTime;   // This value stores the progress of the lerp (from 0 to 1)

            if (currentLerpTime >= damageLerpTime)        // If the timer is done:
            {
                damageLerping = false;                    // Stop lerping

                // Set the scale of the damage bar to the final health value after damage was applied (goalValue)
                damageFill.localScale = new Vector3(goalValue, barFill.localScale.y, barFill.localScale.z);

                // Then I set the lerp values to the new health value
                currentValue = goalValue;
                oldValue = goalValue;
            }
            else                                          // If the timer is still going:
            {
                // Calculate current lerp value between the old health (oldValue) and the new health (goalValue)
                currentValue = Mathf.Lerp(oldValue, goalValue, t);    

                // Set the scale of the damage bar to the lerped value
                damageFill.localScale = new Vector3(currentValue, barFill.localScale.y, barFill.localScale.z);
            }
        }
    }

    /******************************************************************************
     Function: SetText
    
        Description: This function sets the text display value.
                     It is meant to be called from other scripts.
    
        Inputs: value - The new health value (converted to a string in the script)
    
        Outputs: None
    ******************************************************************************/
    public void SetText(int value)
    {
        healthText.text = value.ToString(); // Set the text to the new value after converting it to a string
    }

    /******************************************************************************
     Function: SetFill
    
        Description: This function updates the fill of the health bar and starts up the damage bar timer
                     It is meant to be called from other scripts.
    
        Inputs: fill - The new health progress (from 0 to 1)
    
        Outputs: None
    ******************************************************************************/
    public void SetFill(float fill)
    {
        if (fill > goalValue)     // If this would increase your health (heal):
        {
            // Set the health bar to the new value
            barFill.localScale = new Vector3(fill, barFill.localScale.y, barFill.localScale.z);
            // Set the damage bar to the new value without lerping
            damageFill.localScale = new Vector3(fill, barFill.localScale.y, barFill.localScale.z);

            currentValue = fill;  // Set the lerp values to the new health value
            oldValue = fill;      // ^
            goalValue = fill;     // ^
        }

        if(fill < goalValue)          // If this would reduce your health (damage):
        {
            // Set the health bar to the new value
            barFill.localScale = new Vector3(fill, barFill.localScale.y, barFill.localScale.z);

            goalValue = fill;         // Set value that the damage bar will lerp to as the new health
            oldValue = currentValue;  // Set value that the damage bar will lerp from as the old health
            damageDelay = true;       // Start the delay until the lerp starts
            damageLerping = false;    // Stop any ongoing lerp
            currentLerpTime = 0f;     // Reset the lerp timer
            currentDelayTime = 0f;    // Reset the delay timer
        }
    }
}