Unity Ability Cooldown UI: Radial Fill Icons and Timers

Illustration: a circular ability icon with a radial sweep timer overlay and glowing edge

Players need to know when an ability is ready. The classic answer is an icon that greys out, sweeps back like a clock as it recharges, shows the seconds left, and flashes when it’s ready again. This guide builds that Unity ability cooldown UI with a radial-fill Image, TextMeshPro countdown text and a ready flash, driven by a small reusable cooldown class that keeps gameplay and UI separate.

A Reusable Cooldown Class

Instead of scattering float timer fields through every ability, put the logic in a plain C# class. It’s serializable, so its duration shows in the Inspector of whatever uses it:

using System;
using UnityEngine;

[Serializable]
public class Cooldown
{
    [SerializeField] float duration = 2f;
    float readyAt;

    public float Duration => duration;
    public bool IsReady => Time.time >= readyAt;
    public float Remaining => Mathf.Max(0f, readyAt - Time.time);
    public float Progress01 => duration <= 0f ? 1f : 1f - Remaining / duration; // 0 = just used, 1 = ready

    public bool TryUse()
    {
        if (!IsReady) return false;
        readyAt = Time.time + duration;
        return true;
    }

    public void Reset() => readyAt = 0f;
    public void ReduceBy(float seconds) => readyAt -= seconds;
}

Storing the time when it becomes ready, instead of counting down in Update, means the class needs no Update at all. It also makes cooldown reduction effects (like “kills reduce cooldowns by 1 second”) a one-line change.

It uses Time.time, which stops when Time.timeScale is 0, so cooldowns pause with the game. If you pause another way, see the pause menu guide.

Using It in an Ability

using UnityEngine;
using UnityEngine.InputSystem;

public class FireballAbility : MonoBehaviour
{
    [SerializeField] InputActionReference castAction;
    [SerializeField] Cooldown cooldown = new();
    [SerializeField] Sprite icon;

    public Cooldown Cooldown => cooldown;
    public Sprite Icon => icon;

    void OnEnable()  => castAction.action.Enable();
    void OnDisable() => castAction.action.Disable();

    void Update()
    {
        if (castAction.action.WasPressedThisFrame() && cooldown.TryUse())
            Cast();
    }

    void Cast()
    {
        Debug.Log("Fireball!");
    }
}

The ability exposes its Cooldown and its icon. The UI reads them. The ability doesn’t know the UI exists, which keeps things testable and lets you show the same cooldown in several places.

Setting Up the Icon in the Canvas

Build one icon as a prefab:

  1. Root: an empty RectTransform, around 80 x 80.
  2. Icon: a UI Image with the ability sprite.
  3. Overlay: a UI Image on top, using a plain white sprite (the default UISprite works) with a dark semi-transparent colour like black at 60 percent alpha. Set Image Type to Filled, Fill Method to Radial 360, Fill Origin to Top, and untick Clockwise if you want the dark area to shrink clockwise.
  4. Timer: a TextMeshPro – Text (UI) centred on top, bold, with an outline so it’s readable on any icon.
  5. Flash: an Image the size of the icon, white, alpha 0, used for the ready flash.

Filled images only work when the Image has a Source Image assigned. With no sprite, the fill settings don’t appear.

Use a Canvas Scaler set to Scale With Screen Size so icons stay the same relative size on every screen. The Canvas Scaler guide explains the settings.

The Cooldown Icon Script

using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class CooldownIcon : MonoBehaviour
{
    [SerializeField] Image icon;
    [SerializeField] Image overlay;
    [SerializeField] TMP_Text timer;
    [SerializeField] CanvasGroup flash;
    [SerializeField] float flashTime = 0.25f;

    Cooldown cooldown;
    bool wasReady = true;
    int lastShownSeconds = -1;
    float flashTimer;

    public void Bind(Cooldown cd, Sprite sprite)
    {
        cooldown = cd;
        icon.sprite = sprite;
    }

    void Update()
    {
        if (cooldown == null) return;

        bool ready = cooldown.IsReady;
        overlay.fillAmount = ready ? 0f : 1f - cooldown.Progress01;

        if (ready)
        {
            if (lastShownSeconds != 0) { timer.text = ""; lastShownSeconds = 0; }
        }
        else
        {
            float r = cooldown.Remaining;
            int shown = r < 1f ? -2 : Mathf.CeilToInt(r);   // -2 = tenths mode
            if (shown == -2) timer.SetText("{0:1}", r);
            else if (shown != lastShownSeconds) timer.SetText("{0}", shown);
            lastShownSeconds = shown;
        }

        if (ready && !wasReady) flashTimer = flashTime;
        wasReady = ready;

        if (flashTimer > 0f)
        {
            flashTimer -= Time.unscaledDeltaTime;
            flash.alpha = Mathf.Clamp01(flashTimer / flashTime);
        }
    }
}

Hook it up from the ability or a HUD script: cooldownIcon.Bind(fireball.Cooldown, fireball.Icon);

Details worth copying:

  • fillAmount goes from 1 (fully dark, just used) down to 0 (ready), so the dark wedge shrinks around the clock.
  • Whole seconds above 1, tenths below. “3, 2, 1, 0.9, 0.8…” matches what players expect from most games.
  • SetText with a format avoids creating a new string every frame, which matters with many icons. Whole-second text only updates when the number changes.
  • The flash uses unscaled time so it still fades if the game is slowed.

Ready Flash and Feedback

A short flash is the minimum. Other cheap touches:

  • Scale pop. Scale the icon to 1.15 and back over 0.15 seconds when it becomes ready. A tween library makes this one line; the DOTween vs PrimeTween guide compares the options.
  • Desaturate while cooling. Tint the icon grey while not ready, or swap to a greyscale material.
  • Sound. A soft chime for long cooldowns (ultimates), nothing for short ones, or it gets noisy.
  • Failed press feedback. When TryUse returns false, shake the icon slightly so players know the press registered but the ability isn’t ready.

A Hotbar of Abilities

For several abilities, give them a shared interface or base class that exposes Cooldown and Icon, then let a hotbar build icons at startup:

using UnityEngine;

public interface IAbility
{
    Cooldown Cooldown { get; }
    Sprite Icon { get; }
}

public class Hotbar : MonoBehaviour
{
    [SerializeField] CooldownIcon iconPrefab;
    [SerializeField] Transform slots;
    [SerializeField] MonoBehaviour[] abilities; // components that implement IAbility

    void Start()
    {
        foreach (var mb in abilities)
        {
            if (mb is not IAbility a) continue;
            var ui = Instantiate(iconPrefab, slots);
            ui.Bind(a.Cooldown, a.Icon);
        }
    }
}

Add IAbility to FireballAbility‘s class declaration. Put a Horizontal Layout Group on slots so icons line up automatically. To show the key binding under each icon, pass the action and use GetBindingDisplayString().

Performance Notes

Changing fillAmount marks the Image dirty and the Canvas rebuilds that batch. For a handful of icons this is nothing. If you have dozens updating each frame, put the hotbar on its own nested Canvas so its rebuilds don’t touch the rest of the HUD. Avoid string concatenation in Update for the text. These same rules apply to the health bar tutorial.

🔧 Want polished UI faster?

UI kits and tween tools from the Asset Store can save days of fiddling with icons and animations. See the Asset Store tools we recommend.

Troubleshooting

Fill Amount option doesn’t show on the Image

Assign a Source Image. Image Type options only appear when the Image has a sprite.

The radial fill goes the wrong way

Toggle Clockwise, or invert the value (use Progress01 instead of 1 - Progress01), depending on whether you want the dark part to shrink or the bright part to grow.

Cooldown keeps running while paused

You’re using unscaled time somewhere, or pausing without setting timeScale. The Cooldown class uses Time.time, which stops at timeScale 0.

Timer text shows lots of decimals

Use a format like SetText("{0:1}", value) for one decimal, or Mathf.CeilToInt for whole seconds.

Icons are blurry or tiny on phones

Set the Canvas Scaler to Scale With Screen Size with a sensible reference resolution, and import icons at a size close to how they’re displayed.

FAQ

Should cooldowns use Time.time or a countdown in Update?

Storing the ready time with Time.time is simpler and cheaper. Use a countdown only if cooldowns must tick at different rates.

Can I save cooldowns between sessions?

Save the remaining seconds, not Time.time, since Time.time resets when the game starts. On load, set readyAt to Time.time plus the saved remaining time.

Does this work with UI Toolkit?

The Cooldown class does. For the visual, UI Toolkit doesn’t have a radial fill Image built in, so you’d draw it with a custom element or a shader.

How do I show charges, like 2 dashes?

Track a charge count and refill one charge each time the cooldown completes, then start the cooldown again if charges are below max.

Why does the overlay need a sprite?

Filled image types work on a sprite’s geometry. Any sprite works, including Unity’s built-in UISprite.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *