Skip to content

Healthbar

I (Marwan) made a gauge class that supports animation when increasing or decreasing its value. Animation helps draw the attention of the player to the gauge, and also improves game feel.

Basic gauge

Before making an animated gauge, I made an gauge without animation. It breaks the problem into smaller steps and is easier to use if animation is not needed.

Creating the gauge object

Creating the gauge is done by calling the static create function, since monobehaviour cannot have constructors.

  public static Gauge Create(string name, RectTransform parent)
  {
    Gauge gauge = new GameObject(name).AddComponent<Gauge>();
    gauge.AddComponent<RectTransform>();
    gauge.transform.SetParent(parent, true);

    return gauge;
  }

Adding the gauge rects

I used two functions to facilitate the creation of rectangles for the gauges. The function at (1) makes sure the foreground image has the same dimensions as the actual gauge. In (2), I set the sprite to a simple square which is cached in the Rectangle class.

public class Gauge : MonoBehaviour {
  public Image Background { get; private set; }
  public Image Foreground { get; private set; }

  public void AddBackground(Color color)
  {
    Background = gameObject.AddComponent<Image>();
    Background.sprite = Rectangle.SquareSprite;
    Background.color = color;
  }
  public void AddForeground(Color color)
  {
    RectTransform rt = new GameObject("Fill").AddComponent<RectTransform>();
    rt.SetParent(transform)
    rt.SetOffsetAllCorners(0f); // 1

    Foreground = rt.AddComponent<Image>();
    Foreground.sprite = Rectangle.SquareSprite; // 2
    Foreground.color = color;
    Foreground.type = Image.Type.Filled;
    Foreground.fillMethod = Image.FillMethod.Horizontal;
  }
}

Using the gauge

Using the gauge is as simple as changing the following properties.

private float _max;
public float Max
{
  get => _max;
  set
  {
    _max = value;
    Value = Value;
  }
}
private float _min;
public float Min
{
  get => _min;
  set
  {
    _min = value;
    Value = Value;
  }
}
private float _value;
public float Value
{
  get => _value;
  set
  {
    value = Mathf.Clamp(value, Min, Max);
    _value = value;
    Foreground.fillAmount = _value / (Max - Min);
  }
}

Animated gauge

The animated gauge uses two instances of the gauge class, and is used in the same way (just change the Value property).

Creating the animated gauge

The animated gauge handles creating the sub-gauges directly in the create function.

public class DoubleGauge : MonoBehaviour {

  public static DoubleGauge Create(string name, RectTransform parent)
  {
    DoubleGauge gauge = new GameObject(name).AddComponent<DoubleGauge>();
    gauge.AddComponent<RectTransform>();
    gauge.transform.SetParent(parent);

    gauge.Back = Gauge.Create("Back", gauge.transform);
    gauge.Back.AddForeground(new());
    gauge.Back.AddBackground(new());
    gauge.Back.transform.SetOffsetAllCorners(0f);

    gauge.Front = Gauge.Create("Front", gauge.transform);
    gauge.Front.AddForeground(new());
    gauge.Front.transform.SetOffsetAllCorners(0f);

    return gauge;
  }
}

Setting the parameters

Some colors can just be directly set in the image, but some other are simply stored to be used dynamically.

public Color MainColor
{
  get => Front.Foreground.color;
  set => Front.Foreground.color = value;
}
public Color BackgroundColor
{
  get => Back.Background.color;
  set => Back.Background.color = value;
}
public Color SubIncreaseColor { get; set; }
public Color SubDecreaseColor { get; set; }
public float AnimationTime { get; set; }

public float Min
{
  get => _min;
  set
  {
    _min = value;
    Front.Min = value;
    Back.Min = value;
  }
}
public float Max
{
  get => _max;
  set
  {
    _max = value;
    Front.Max = value;
    Back.Max = value;
  }
}

Animation

When changing the value, the sub gauge (the animated one) is determined depending on whether the value just increased or decreased. The color of the back gauge is also updated accordingly. Then a coroutine starts which slowly updates the size of the sub gauge every frame. The ForceUpdate function stops the animation and updates the sub gauge to reach the main one.

public Gauge Front { get; private set; }
public Gauge Back { get; private set; }

private Gauge _main;
private Gauge _sub;

private bool _isAnimating;
private bool _isIncreasing;

private Coroutine _coroutine;

private bool Increasing
{
  get => _isIncreasing;
  set
  {
    _isIncreasing = value;
    if (value)
    {
      _main = Back;
      _sub = Front;
      Back.Foreground.color = SubIncreaseColor;
    }
    else
    {
      _main = Front;
      _sub = Back;
      Back.Foreground.color = SubDecreaseColor;
    }
  }
}

public float Value
{
  get => _value;
  set
  {
    float newValue = Mathf.Clamp(value, _min, _max);
    if (newValue == _value) return;
    _value = newValue;

    bool increasing = _value > _subValue;
    if (_isAnimating)
    {
      if (Increasing != increasing)
        ForceUpdate();
    }
    Increasing = increasing;
    _main.Value = newValue;

    _isAnimating = true;
    _coroutine = StartCoroutine(AnimateRoutine());
  }
}
public float SubValue
{
  get => _subValue;
  set
  {
    _subValue = value;
    _sub.Value = value;
  }
}

public void ForceUpdate()
{
  _isAnimating = false;

  StopCoroutine(_coroutine);
  _subValue = _value;
  Front.Value = _value;
  Back.Value = _value;
}

private IEnumerator AnimateRoutine()
{
  float delta = Value - SubValue;
  delta /= AnimationTime;
  while (SubValue != Value)
  {
      SubValue += delta * Time.deltaTime;
      if (Increasing)
      {
          if (SubValue > Value)
          {
              ForceUpdate();
              break;
          }
      }
      else if (SubValue < Value)
      {
          ForceUpdate();
          break;
      }

      yield return null;
  }
  _isAnimating = false;
}

Result

Note: The bullets should not damage the player but I am using that bug to demonstrate the animation.

Health bar demo

Use example

private static void CreateHealthBar()
{
  HealthBar = DoubleGauge.Create("Health bar", Canvas.transform as RectTransform);

  HealthBar.transform.pivot = Vector2.up;
  HealthBar.transform.anchorMin = HealthBar.transform.anchorMax = Vector2.up;
  HealthBar.transform.anchoredPosition = new Vector2(5, -5);

  HealthBar.transform.sizeDelta = new Vector2(300f, 10f);

  HealthBar.MainColor = Color.red;
  HealthBar.SubIncreaseColor = Color.green;
  HealthBar.SubDecreaseColor = new(80, 20, 20);
  HealthBar.BackgroundColor = Color.gray;
  HealthBar.AnimationTime = 1f;
}

Last update: May 18, 2023