Dynamic health bar
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 } } }