Health Bar UI Pro
Advanced health bar with delayed damage indicator, shield overlay, boss mode with phase markers, and segmented health display.
How to Use
Create UI Canvas with background Image + fill Image (set to Filled type)
Attach HealthBarUIPro to the bar root
Assign fill image and optional delayed damage image (behind fill)
Connect HealthSystem.OnHealthChanged to SetHealth()
For shields: add separate fill image, call SetShield()
For boss bars: enable Boss Mode, set name text and phase count
Call ShowBossBar() when boss encounter starts
Enable segments for segmented health display (like Hollow Knight)
Billboard option auto-faces camera for world-space bars
Features
- Smooth health fill with gradient color based on health percentage
- Delayed damage indicator bar (white trail effect) with configurable delay
- Shield overlay bar displayed on top of health
- Boss mode with name text, phase markers, and fade-in/out transitions
- Damage flash effect on the background image
- Segmented health display with auto-generated segment lines
When to Use This
Use in RPGs, FPS games, and boss fights that need polished health bar feedback beyond a simple fill bar. Ideal for games with shield mechanics, boss encounters with phase transitions, or world-space enemy health bars that need to billboard toward the camera.
Common Mistakes
The fill Image must have its Image Type set to Filled in the Inspector — leaving it as Simple means fillAmount has no effect. The delayed damage image must be placed behind the main fill in the hierarchy or it will render on top. For world-space bars, make sure Billboard To Camera is enabled and the Canvas render mode is set to World Space.
Source Code
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class HealthBarUIPro : MonoBehaviour
{
[Header("Health Bar")]
[SerializeField] private Image fillImage;
[SerializeField] private Gradient healthGradient;
[SerializeField] private float smoothSpeed = 5f;
[Header("Delayed Damage")]
[SerializeField] private Image delayedFillImage;
[SerializeField] private Color delayedColor = new Color(1f, 1f, 1f, 0.7f);
[SerializeField] private float delayedDelay = 0.6f;
[SerializeField] private float delayedSpeed = 2f;
[Header("Shield")]
[SerializeField] private Image shieldFillImage;
[SerializeField] private Color shieldColor = new Color(0.3f, 0.7f, 1f, 0.8f);
[SerializeField] private GameObject shieldContainer;
[Header("Boss Mode")]
[SerializeField] private bool isBossBar;
[SerializeField] private TextMeshProUGUI bossNameText;
[SerializeField] private RectTransform phaseMarkerContainer;
[SerializeField] private GameObject phaseMarkerPrefab;
[SerializeField] private CanvasGroup bossGroup;
[SerializeField] private float bossFadeSpeed = 3f;
[Header("Damage Flash")]
[SerializeField] private Image backgroundImage;
[SerializeField] private Color flashColor = new Color(1f, 0.2f, 0.2f, 0.5f);
[SerializeField] private float flashDuration = 0.15f;
[Header("Segments")]
[SerializeField] private bool showSegments;
[SerializeField] private int segmentCount = 5;
[SerializeField] private RectTransform segmentContainer;
[SerializeField] private GameObject segmentLinePrefab;
[Header("Portrait")]
[SerializeField] private Image portraitImage;
[SerializeField] private Sprite portraitSprite;
[Header("Billboard")]
[SerializeField] private bool billboardToCamera = true;
private float targetFill = 1f;
private float currentFill = 1f;
private float delayedFill = 1f;
private float delayedTimer;
private float shieldFill;
private float flashTimer;
private Color bgOriginalColor;
private bool bossVisible;
private Camera mainCam;
private void Awake()
{
if (backgroundImage != null)
bgOriginalColor = backgroundImage.color;
if (portraitImage != null && portraitSprite != null)
portraitImage.sprite = portraitSprite;
if (delayedFillImage != null)
delayedFillImage.color = delayedColor;
if (shieldFillImage != null)
shieldFillImage.color = shieldColor;
if (shieldContainer != null)
shieldContainer.SetActive(false);
if (bossGroup != null && isBossBar)
{
bossGroup.alpha = 0f;
bossVisible = false;
}
mainCam = Camera.main;
GenerateSegments();
}
private void Update()
{
// Smooth health fill
currentFill = Mathf.Lerp(currentFill, targetFill, smoothSpeed * Time.deltaTime);
if (fillImage != null)
{
fillImage.fillAmount = currentFill;
fillImage.color = healthGradient.Evaluate(currentFill);
}
// Delayed damage bar
if (delayedFillImage != null)
{
if (delayedFill > targetFill)
{
delayedTimer -= Time.deltaTime;
if (delayedTimer <= 0f)
delayedFill = Mathf.Lerp(delayedFill, targetFill, delayedSpeed * Time.deltaTime);
}
else
{
delayedFill = targetFill;
}
delayedFillImage.fillAmount = delayedFill;
}
// Damage flash
if (flashTimer > 0f)
{
flashTimer -= Time.deltaTime;
if (flashTimer <= 0f && backgroundImage != null)
backgroundImage.color = bgOriginalColor;
}
// Boss fade
if (isBossBar && bossGroup != null)
{
float targetAlpha = bossVisible ? 1f : 0f;
bossGroup.alpha = Mathf.Lerp(bossGroup.alpha, targetAlpha, bossFadeSpeed * Time.deltaTime);
}
}
private void LateUpdate()
{
if (billboardToCamera)
{
Camera cam = Camera.main;
if (cam != null)
transform.forward = cam.transform.forward;
}
}
/// <summary>
/// Update health fill. Connect to HealthSystem.OnHealthChanged.
/// </summary>
public void SetHealth(float current, float max)
{
if (max <= 0f) return;
float previous = targetFill;
targetFill = Mathf.Clamp01(current / max);
if (targetFill < previous)
{
// Start delayed damage
delayedTimer = delayedDelay;
// Flash
if (backgroundImage != null)
{
backgroundImage.color = flashColor;
flashTimer = flashDuration;
}
}
else if (targetFill > previous)
{
delayedFill = targetFill;
}
}
/// <summary>
/// Set health instantly without smooth transition.
/// </summary>
public void SetHealthImmediate(float current, float max)
{
if (max <= 0f) return;
targetFill = Mathf.Clamp01(current / max);
currentFill = targetFill;
delayedFill = targetFill;
if (fillImage != null)
{
fillImage.fillAmount = currentFill;
fillImage.color = healthGradient.Evaluate(currentFill);
}
if (delayedFillImage != null)
delayedFillImage.fillAmount = delayedFill;
}
/// <summary>
/// Update shield amount. Shield is displayed on top of health.
/// </summary>
public void SetShield(float current, float max)
{
if (shieldContainer != null)
shieldContainer.SetActive(current > 0f);
if (shieldFillImage != null && max > 0f)
{
shieldFill = Mathf.Clamp01(current / max);
shieldFillImage.fillAmount = shieldFill;
}
}
/// <summary>
/// Configure boss mode: set name and number of phases.
/// </summary>
public void SetBossMode(string bossName, int phases)
{
isBossBar = true;
if (bossNameText != null)
bossNameText.text = bossName;
// Create phase markers
if (phaseMarkerContainer != null && phaseMarkerPrefab != null)
{
foreach (Transform child in phaseMarkerContainer)
Destroy(child.gameObject);
for (int i = 1; i < phases; i++)
{
GameObject marker = Instantiate(phaseMarkerPrefab, phaseMarkerContainer);
RectTransform rt = marker.GetComponent<RectTransform>();
if (rt != null)
{
float xPos = (float)i / phases;
rt.anchorMin = new Vector2(xPos, 0f);
rt.anchorMax = new Vector2(xPos, 1f);
rt.anchoredPosition = Vector2.zero;
rt.sizeDelta = new Vector2(2f, 0f);
}
}
}
}
/// <summary>Show the boss health bar with fade-in.</summary>
public void ShowBossBar()
{
bossVisible = true;
}
/// <summary>Hide the boss health bar with fade-out.</summary>
public void HideBossBar()
{
bossVisible = false;
}
/// <summary>Set portrait sprite at runtime.</summary>
public void SetPortrait(Sprite sprite)
{
if (portraitImage != null)
{
portraitImage.sprite = sprite;
portraitImage.gameObject.SetActive(sprite != null);
}
}
private void GenerateSegments()
{
if (!showSegments || segmentContainer == null || segmentLinePrefab == null) return;
if (segmentCount <= 1) return;
foreach (Transform child in segmentContainer)
Destroy(child.gameObject);
for (int i = 1; i < segmentCount; i++)
{
GameObject line = Instantiate(segmentLinePrefab, segmentContainer);
RectTransform rt = line.GetComponent<RectTransform>();
if (rt != null)
{
float xPos = (float)i / segmentCount;
rt.anchorMin = new Vector2(xPos, 0f);
rt.anchorMax = new Vector2(xPos, 1f);
rt.anchoredPosition = Vector2.zero;
rt.sizeDelta = new Vector2(1f, 0f);
}
}
}
}