Unityを始めたばかりの頃は、つい「とりあえず全部Updateに書いてしまう」実装になりがちですよね。
スキルのクールタイム(再使用時間)も、こんな感じで書いてしまうケースが多いです。

// こういう書き方、心当たりありませんか?
void Update()
{
    // 入力チェック
    if (Input.GetKeyDown(KeyCode.Space))
    {
        if (Time.time >= skillNextUseTime)
        {
            UseSkill();
            skillNextUseTime = Time.time + skillCooldown;
        }
    }

    // スキルUIの更新
    if (Time.time < skillNextUseTime)
    {
        float remain = skillNextUseTime - Time.time;
        skillIconImage.fillAmount = remain / skillCooldown;
        cooldownText.text = remain.ToString("0.0");
        skillIconImage.color = new Color(1, 1, 1, 0.5f);
    }
    else
    {
        skillIconImage.fillAmount = 0f;
        cooldownText.text = "";
        skillIconImage.color = Color.white;
    }

    // さらに他の処理がどんどん追加されていく……
}

入力、スキル発動ロジック、UI更新が全部1つのスクリプトに詰め込まれてしまうと、

  • スキルUIだけ変えたいのに、プレイヤー制御スクリプトまで触る必要がある
  • スキルの数が増えるたびに巨大なGodクラス化していく
  • プレハブの再利用性が低くなる(UIだけ別シーンで使い回したいのにできない)

といった問題が出てきます。

そこで今回は、「スキルのクールタイム表示」だけに責務を絞ったコンポーネント
「SkillCooldownUI」 を作って、UI側をきれいに分離してみましょう。
スキルの再使用可能時刻を教えてあげるだけで、アイコンの暗転や残り秒数の表示を全部やってくれるコンポーネントです。

【Unity】スキルの再使用時間をスマートに可視化!「SkillCooldownUI」コンポーネント

このコンポーネントは「特定スキルのクールタイム状態」を監視し、

  • アイコン画像を暗くする / 元に戻す
  • 残りクールタイム秒数のテキスト表示
  • Image.fillAmount を使ったクールタイムの円形ゲージ

といったUI処理をまとめて担当します。
スキルのロジック側は「次に使える時間」を渡すだけにしておくことで、SRP(単一責任の原則)な構成にできます。


SkillCooldownUI のフルコード

using UnityEngine;
using UnityEngine.UI;
using TMPro;

/// <summary>
/// 特定スキルのクールタイムを監視し、
/// アイコンの暗転・クールタイムゲージ・残り秒数テキストを更新するコンポーネント。
/// 
/// スキル側からは、以下の2つだけ呼べばOK:
/// - SetCooldown(durationSeconds)
/// - NotifySkillUsed()
/// 
/// もしくは、外部で「次に使える時刻」を管理している場合は
/// - SetNextAvailableTime(nextTime, durationSeconds)
/// を呼び出す運用も可能です。
/// </summary>
[DisallowMultipleComponent]
public class SkillCooldownUI : MonoBehaviour
{
    [Header("参照 (UI)")]
    [SerializeField]
    private Image iconImage; // スキルアイコン画像(必須)

    [SerializeField]
    private Image cooldownFillImage; 
    // クールタイム用Image。
    // type = Filled、Fill Method = Radial360 などに設定しておくと円形ゲージになる。

    [SerializeField]
    private TMP_Text cooldownText;
    // 残りクールタイム秒数を表示するテキスト(任意)

    [Header("見た目の設定")]
    [SerializeField, Tooltip("クールタイム中のアイコンのアルファ値(0〜1)")]
    private float cooldownIconAlpha = 0.5f;

    [SerializeField, Tooltip("クールタイム表示の小数点以下桁数")]
    private int decimalDigits = 1;

    [SerializeField, Tooltip("クールタイムが0.1秒未満になったら表示を消すか")]
    private bool hideTextWhenAlmostReady = true;

    [Header("自動開始オプション")]
    [SerializeField, Tooltip("開始時にクールタイムを自動で開始するか")]
    private bool startWithCooldown = false;

    [SerializeField, Tooltip("startWithCooldown が true のときの初期クールタイム秒数")]
    private float initialCooldownSeconds = 3f;

    // --- 内部状態 ---

    // 次にスキルが使用可能になるゲーム内時刻(Time.time ベース)
    private float nextAvailableTime = 0f;

    // クールタイム全体の長さ(秒)
    private float cooldownDuration = 0f;

    // 外部で「nextAvailableTime」を管理しているかどうか
    private bool externalTimingMode = false;

    // 初期カラーを保存しておく(アルファ変更用)
    private Color originalIconColor;

    private void Awake()
    {
        // 参照がInspectorで未設定の場合、同じGameObject上から自動取得を試みる
        if (iconImage == null)
        {
            iconImage = GetComponent<Image>();
        }

        if (cooldownFillImage == null)
        {
            // 子オブジェクトから取得してみる(任意)
            cooldownFillImage = GetComponentInChildren<Image>();
        }

        if (iconImage != null)
        {
            originalIconColor = iconImage.color;
        }
        else
        {
            Debug.LogWarning(
                $"[SkillCooldownUI] iconImage が設定されていません。"{name}" のアイコン表示は動作しません。",
                this
            );
        }

        // 初期状態ではクールタイム表示をオフにしておく
        ResetVisual();
    }

    private void Start()
    {
        // オプション: 開始時に自動でクールタイムを開始する
        if (startWithCooldown && initialCooldownSeconds > 0f)
        {
            SetCooldown(initialCooldownSeconds);
            NotifySkillUsed();
        }
    }

    private void Update()
    {
        UpdateCooldownVisual();
    }

    /// <summary>
    /// クールタイムの長さ(秒)を設定する。
    /// 通常はスキルの設定値をここに渡しておく。
    /// </summary>
    /// <param name="durationSeconds">クールタイム秒数</param>
    public void SetCooldown(float durationSeconds)
    {
        cooldownDuration = Mathf.Max(0f, durationSeconds);
        externalTimingMode = false;
    }

    /// <summary>
    /// スキルが使用されたことをUIに通知する。
    /// ここで nextAvailableTime を計算し、以後はUIが自動で更新される。
    /// 
    /// 事前に SetCooldown を呼んでおく想定。
    /// </summary>
    public void NotifySkillUsed()
    {
        if (cooldownDuration <= 0f)
        {
            // クールタイムが0の場合は特に何もしない
            nextAvailableTime = Time.time;
            return;
        }

        nextAvailableTime = Time.time + cooldownDuration;
        externalTimingMode = false;
    }

    /// <summary>
    /// 外部で「次に使える時刻」を管理している場合に使用する。
    /// 例: サーバーから同期されたクールタイムなど。
    /// </summary>
    /// <param name="nextTime">次に使用可能になる Time.time ベースの時刻</param>
    /// <param name="durationSeconds">クールタイム全体の長さ(UIの割合計算に使用)</param>
    public void SetNextAvailableTime(float nextTime, float durationSeconds)
    {
        nextAvailableTime = nextTime;
        cooldownDuration = Mathf.Max(0.01f, durationSeconds); // 0除算防止
        externalTimingMode = true;
    }

    /// <summary>
    /// 今スキルが使用可能かどうかを返すヘルパー。
    /// UI側の都合でだけ使う想定だが、外部から参照してもよい。
    /// </summary>
    public bool IsReady()
    {
        return Time.time >= nextAvailableTime;
    }

    /// <summary>
    /// 残りクールタイム秒数を返す(マイナスにはならない)。
    /// </summary>
    public float GetRemainingCooldown()
    {
        float remain = nextAvailableTime - Time.time;
        return Mathf.Max(0f, remain);
    }

    /// <summary>
    /// 毎フレーム、クールタイムの状態に応じてUIを更新する。
    /// </summary>
    private void UpdateCooldownVisual()
    {
        if (iconImage == null && cooldownFillImage == null && cooldownText == null)
        {
            // 何も参照がなければ何もしない
            return;
        }

        float remain = GetRemainingCooldown();

        if (remain > 0f && cooldownDuration > 0f)
        {
            // --- クールタイム中 ---

            // アイコンを暗くする
            if (iconImage != null)
            {
                Color c = originalIconColor;
                c.a *= Mathf.Clamp01(cooldownIconAlpha);
                iconImage.color = c;
            }

            // 円形ゲージなどの fillAmount を更新
            if (cooldownFillImage != null)
            {
                float ratio = Mathf.Clamp01(remain / cooldownDuration);
                cooldownFillImage.fillAmount = ratio;
                cooldownFillImage.enabled = true;
            }

            // 残り秒数テキストを更新
            if (cooldownText != null)
            {
                // 表示するかどうかの判定
                if (hideTextWhenAlmostReady && remain < 0.1f)
                {
                    cooldownText.text = string.Empty;
                }
                else
                {
                    // 小数点以下桁数に応じてフォーマット
                    string format = "0";
                    if (decimalDigits > 0)
                    {
                        format += "." + new string('0', decimalDigits);
                    }
                    cooldownText.text = remain.ToString(format);
                }
            }
        }
        else
        {
            // --- クールタイム終了(または未設定) ---
            ResetVisual();
        }
    }

    /// <summary>
    /// クールタイムがない状態の見た目に戻す。
    /// </summary>
    private void ResetVisual()
    {
        // アイコンカラーを元に戻す
        if (iconImage != null)
        {
            iconImage.color = originalIconColor;
        }

        // fillAmountを0にして非表示にする
        if (cooldownFillImage != null)
        {
            cooldownFillImage.fillAmount = 0f;
            cooldownFillImage.enabled = false;
        }

        // テキストを消す
        if (cooldownText != null)
        {
            cooldownText.text = string.Empty;
        }
    }
}

使い方の手順

ここでは「プレイヤーのスキルボタンUI」にクールタイム表示をつける例で説明します。
Unity UI (uGUI) + TextMeshPro を前提にしています。

手順①:UIプレハブの準備

  1. Canvas 配下に「スキルボタン」の GameObject を1つ作成します。
    • 例: SkillButton_Fireball
  2. その中に以下の構造を用意します(例):
    • SkillButton_Fireball(親)
      • Icon … Image(スキルのアイコン)
      • CooldownMask … Image(type=Filled, Fill Method=Radial360, Fill Origin=Top)
      • CooldownText … TextMeshPro – Text (UI)
  3. CooldownMask の Image は、黒やグレーの半透明画像を設定し、
    Type = Filled にし、Fill Method = Radial360 にしておきましょう。
    これで円形に縮んでいくクールタイムゲージになります。

手順②:SkillCooldownUI コンポーネントをアタッチ

  1. SkillButton_Fireball の GameObject に SkillCooldownUI をアタッチします。
  2. Inspector で以下を設定します。
    • Icon ImageIcon をドラッグ&ドロップ
    • Cooldown Fill ImageCooldownMask をドラッグ&ドロップ
    • Cooldown TextCooldownText をドラッグ&ドロップ
    • Cooldown Icon Alpha … 0.4〜0.6 くらいが見やすいです
    • Decimal Digits … 1 なら「3.2」のような表示になります
    • Start With Cooldown … ゲーム開始時にCD状態にしたいならオン
    • Initial Cooldown Seconds … そのときの秒数

手順③:スキルロジックから通知を送る

プレイヤーのスキル発動ロジックは、UIを直接いじらずに、
「スキルが使われた」ことだけを SkillCooldownUI に伝えるようにします。

例えば、プレイヤーのスキルスクリプトを以下のようにします。

using UnityEngine;
using UnityEngine.InputSystem;

/// <summary>
/// ごくシンプルなプレイヤースキル例。
/// スペースキー(または Input System の "Fire" アクション)でスキルを発動し、
/// UI には SkillCooldownUI を使ってクールタイムを表示する。
/// </summary>
public class PlayerSkillExample : MonoBehaviour
{
    [Header("スキル設定")]
    [SerializeField]
    private float cooldownSeconds = 5f;

    [SerializeField]
    private SkillCooldownUI cooldownUI;

    // 次に使用可能な時刻(Time.time ベース)
    private float nextAvailableTime;

    private void Start()
    {
        // UI側にクールタイム秒数を教えておく
        if (cooldownUI != null)
        {
            cooldownUI.SetCooldown(cooldownSeconds);
        }
    }

    private void Update()
    {
        // 入力チェック(ここでは簡単にスペースキーで)
        if (Keyboard.current != null && Keyboard.current.spaceKey.wasPressedThisFrame)
        {
            TryUseSkill();
        }
    }

    private void TryUseSkill()
    {
        if (Time.time < nextAvailableTime)
        {
            // まだクールタイム中
            return;
        }

        // スキル発動処理(ここではダミー)
        Debug.Log("Fireball!");

        // 次に使える時刻を更新
        nextAvailableTime = Time.time + cooldownSeconds;

        // UI に「使ったよ」と通知
        if (cooldownUI != null)
        {
            cooldownUI.NotifySkillUsed();
        }
    }
}

このように、プレイヤー側は「クールタイムのロジック」だけを持ち、
UIの見た目は SkillCooldownUI に丸投げできます。

手順④:敵や別スキルにも簡単に流用

  • 敵の強力な攻撃スキルのクールタイム表示
  • 動く床が「何秒後に動き出すか」をアイコンで表示する
  • ボス戦で「次のフェーズまでの時間」をスキル風アイコンで見せる

といった用途でも、同じ SkillCooldownUI をプレハブ化してポンポン置くだけで再利用できます。
ロジック側からは SetCooldownNotifySkillUsed さえ呼んでおけばOKです。


メリットと応用

SkillCooldownUI を使うことで、以下のようなメリットがあります。

  • プレハブ単位で完結したスキルボタンUI
    • アイコン画像・クールタイムマスク・テキスト・ロジックが1つのプレハブにまとまる
    • 別シーン・別プロジェクトへの持ち出しも楽になります
  • プレイヤー制御クラスがスリムになる
    • 「いつスキルを使えるか」だけを管理し、見た目の更新はUIコンポーネントに任せられる
    • Godクラス化を防ぎやすくなります
  • レベルデザインの柔軟性アップ
    • スキルごとにクールタイム秒数や表示フォーマットを変えたいときも、Inspectorから個別調整できる
    • テキストを非表示にして「ゲージだけのシンプルUI」にするなど、見た目だけ差し替えが簡単

さらに、サーバー同期型のゲームでは、サーバーから届いた「スキル再使用可能時刻」をそのまま UI に渡すだけ、という運用もできます。

// 例: サーバーから nextUseUnixTime と cooldownSeconds を受け取った場合
private void OnReceiveSkillCooldownFromServer(double nextUseUnixTime, float cooldownSeconds)
{
    // ここでは簡単に「サーバー時間とクライアント時間が一致している」と仮定
    // 現実にはオフセット補正などが必要になります。
    float nextTime = (float)nextUseUnixTime; // サーバー時刻を Time.time 相当の値に変換したとする

    if (cooldownUI != null)
    {
        cooldownUI.SetNextAvailableTime(nextTime, cooldownSeconds);
    }
}

このように、「時間の管理」と「UIの表示」をきれいに分離しておくと、
ネットワーク同期や難しいロジックが絡んできたときでも、UI側はほとんど変更せずに済みます。

改造案:クールタイム完了時にエフェクトを出す

クールタイムが終わった瞬間に、ちょっとしたエフェクトやSEを鳴らしたい場合は、
SkillCooldownUI に「前フレームの状態」を覚えておき、
「クールタイム中 → 完了」に変わった瞬間を検出するメソッドを追加すると便利です。

/// <summary>
/// クールタイム完了時に一度だけ呼び出されるコールバックを設定する例。
/// </summary>
[SerializeField]
private ParticleSystem readyEffect;

private bool wasOnCooldownLastFrame = false;

private void LateUpdate()
{
    bool onCooldownNow = !IsReady();

    // 「クールタイム中だった」->「今は準備完了」の瞬間を検出
    if (wasOnCooldownLastFrame && !onCooldownNow)
    {
        // エフェクトを再生
        if (readyEffect != null)
        {
            readyEffect.Play();
        }

        // ここでSEを鳴らしたり、ボタンをポヨンとスケールさせたりもできる
    }

    wasOnCooldownLastFrame = onCooldownNow;
}

このように、小さな責務ごとにコンポーネントを分けていくと、
「クールタイムUI」「完了時の演出」「スキルロジック」がそれぞれ独立して進化させやすくなります。
ぜひ自分のプロジェクトでも、Godクラスを避けたコンポーネント指向の設計を意識してみてください。