Unityを触り始めたころは、プレイヤーの移動も、HP処理も、バフの時間管理も、つい全部を Update() に書いてしまいがちですよね。
「とりあえず動く」ので満足してしまいますが、あとから仕様変更ステータス追加が入ると、一気に地獄化します。

  • どこでバフ時間を減らしているのか分からない
  • UI更新とステータス処理がごちゃ混ぜで修正しづらい
  • 敵やアイテムにも同じロジックをコピペしてバグ祭り

こういった「巨大なUpdate地獄」を避けるには、機能ごとにコンポーネントを分けるのが鉄則です。
この記事では、「バフの残り時間をUIバーで表示し、時間切れでステータスを元に戻す」という機能だけに責任を持つ、シンプルなコンポーネント 「BuffTimer」 を作っていきます。

【Unity】見えるバフで気持ちいい!「BuffTimer」コンポーネント

今回作る BuffTimer は、以下を行うコンポーネントです。

  • 指定したバフ時間をカウントダウン
  • 残り時間を UI のスライダー(バー)に反映
  • バフ開始時にステータスを強化、終了時に元に戻す
  • プレイヤー・敵・トラップなど、どのオブジェクトにも簡単に付けられる

ステータスの管理そのものは別コンポーネントに任せ、
この BuffTimer は「バフ時間の管理」と「UIバーの更新」にだけ責任を持つようにします。


フルコード:BuffTimer とシンプルなステータス例

この記事だけで完結するように、簡易的なステータス管理コンポーネントも含めてコードを載せます。
プレイヤーや敵に合わせて、あとから自由に拡張してください。

using UnityEngine;
using UnityEngine.UI;

namespace Sample.BuffSystem
{
    /// <summary>
    /// 非常にシンプルなステータス管理コンポーネントの例。
    /// 実際のプロジェクトでは、ここにHPや攻撃力などを拡張していく想定です。
    /// </summary>
    public class SimpleStatus : MonoBehaviour
    {
        [Header("基礎ステータス")]
        [SerializeField] private float baseAttackPower = 10f;

        /// <summary>現在の攻撃力(バフ込み)</summary>
        public float CurrentAttackPower { get; private set; }

        // バフ適用前の攻撃力を保存しておくための変数
        private float _attackPowerBeforeBuff;

        private void Awake()
        {
            // 初期状態では基礎攻撃力 = 現在攻撃力
            CurrentAttackPower = baseAttackPower;
        }

        /// <summary>
        /// 攻撃力バフを適用する(例:1.5倍など)
        /// </summary>
        /// <param name="multiplier">攻撃力に掛ける倍率(例:1.5 => 1.5倍)</param>
        public void ApplyAttackBuff(float multiplier)
        {
            // すでにバフがかかっている状態でさらに呼ばれた場合のために、
            // 「バフ適用前の値」を一度だけ保存しておく実装にしても良いです。
            _attackPowerBeforeBuff = CurrentAttackPower;

            CurrentAttackPower = CurrentAttackPower * multiplier;
            Debug.Log($"[SimpleStatus] 攻撃力バフ適用: {_attackPowerBeforeBuff} → {CurrentAttackPower}");
        }

        /// <summary>
        /// 攻撃力バフを解除し、元の値に戻す
        /// </summary>
        public void ResetAttackBuff()
        {
            CurrentAttackPower = _attackPowerBeforeBuff;
            Debug.Log($"[SimpleStatus] 攻撃力バフ解除: 現在攻撃力 = {CurrentAttackPower}");
        }
    }

    /// <summary>
    /// バフ時間を管理し、UIバーに残り時間を表示するコンポーネント。
    /// ・バフ開始時に SimpleStatus にバフを適用
    /// ・時間経過で残り時間を減らし、UI Slider に反映
    /// ・時間切れでステータスを元に戻す
    /// </summary>
    [RequireComponent(typeof(SimpleStatus))]
    public class BuffTimer : MonoBehaviour
    {
        [Header("バフ設定")]
        [SerializeField]
        [Tooltip("バフの効果時間(秒)")]
        private float buffDurationSeconds = 5f;

        [SerializeField]
        [Tooltip("攻撃力に掛ける倍率(例:1.5 = 1.5倍)")]
        private float attackBuffMultiplier = 1.5f;

        [Header("UI 設定")]
        [SerializeField]
        [Tooltip("バフ残り時間を表示する UI Slider")]
        private Slider buffSlider;

        [SerializeField]
        [Tooltip("バフ中に表示する色(任意)")]
        private Color buffBarColor = Color.cyan;

        [SerializeField]
        [Tooltip("バフが切れたときの色(任意)")]
        private Color expiredBarColor = Color.gray;

        // 内部状態
        private SimpleStatus _status;
        private float _remainingTime;
        private bool _isBuffActive;

        // スライダーの元の色を覚えておく
        private Color _originalBarColor;

        private void Awake()
        {
            _status = GetComponent<SimpleStatus>();

            if (buffSlider != null)
            {
                // Slider の Fill 部分の Image を取得して色を変える想定
                Image fillImage = buffSlider.fillRect != null
                    ? buffSlider.fillRect.GetComponent<Image>()
                    : null;

                if (fillImage != null)
                {
                    _originalBarColor = fillImage.color;
                }
            }
        }

        private void Start()
        {
            // ゲーム開始時はバフなし状態にしておく
            InitializeSliderInactive();
        }

        private void Update()
        {
            // バフがアクティブでなければ何もしない
            if (!_isBuffActive)
            {
                return;
            }

            // 経過時間分だけ残り時間を減らす
            _remainingTime -= Time.deltaTime;

            // 0秒未満にはならないように Clamp
            if (_remainingTime < 0f)
            {
                _remainingTime = 0f;
            }

            UpdateSliderUI();

            // 時間切れ判定
            if (_remainingTime <= 0f)
            {
                EndBuff();
            }
        }

        /// <summary>
        /// バフを開始する。外部のスクリプトから呼び出してもOK。
        /// すでにバフ中の場合は、時間をリセットして延長する挙動にしています。
        /// </summary>
        public void StartBuff()
        {
            // バフ時間をリセット
            _remainingTime = buffDurationSeconds;
            _isBuffActive = true;

            // ステータスにバフを適用(すでにバフ中なら一度リセットしてから、などの設計もあり)
            _status.ApplyAttackBuff(attackBuffMultiplier);

            // UI を有効化して更新
            ActivateSliderUI();
            UpdateSliderUI();

            Debug.Log($"[BuffTimer] バフ開始: {buffDurationSeconds} 秒 / 倍率 {attackBuffMultiplier}");
        }

        /// <summary>
        /// バフを強制終了する。時間切れ時にも内部から呼ばれる。
        /// </summary>
        public void EndBuff()
        {
            if (!_isBuffActive)
            {
                return;
            }

            _isBuffActive = false;

            // ステータスを元に戻す
            _status.ResetAttackBuff();

            // UI をバフ切れ状態にする
            DeactivateSliderUI();

            Debug.Log("[BuffTimer] バフ終了");
        }

        /// <summary>
        /// バフが有効かどうか
        /// </summary>
        public bool IsBuffActive => _isBuffActive;

        /// <summary>
        /// 残り時間(秒)を取得する
        /// </summary>
        public float RemainingTime => _remainingTime;

        /// <summary>
        /// UI スライダーをバフなし状態として初期化
        /// </summary>
        private void InitializeSliderInactive()
        {
            if (buffSlider == null)
            {
                return;
            }

            buffSlider.gameObject.SetActive(false);
            buffSlider.minValue = 0f;
            buffSlider.maxValue = 1f;
            buffSlider.value = 0f;

            // 色を元に戻す
            Image fillImage = buffSlider.fillRect != null
                ? buffSlider.fillRect.GetComponent<Image>()
                : null;

            if (fillImage != null)
            {
                fillImage.color = _originalBarColor;
            }
        }

        /// <summary>
        /// UI スライダーをバフ中として有効化
        /// </summary>
        private void ActivateSliderUI()
        {
            if (buffSlider == null)
            {
                return;
            }

            buffSlider.gameObject.SetActive(true);

            Image fillImage = buffSlider.fillRect != null
                ? buffSlider.fillRect.GetComponent<Image>()
                : null;

            if (fillImage != null)
            {
                fillImage.color = buffBarColor;
            }
        }

        /// <summary>
        /// UI スライダーをバフ切れ状態として無効化(色変更のみなども可)
        /// </summary>
        private void DeactivateSliderUI()
        {
            if (buffSlider == null)
            {
                return;
            }

            // 表示は残したい場合は SetActive(true) のままにして、色だけ変えるなどもあり
            buffSlider.gameObject.SetActive(true);
            buffSlider.value = 0f;

            Image fillImage = buffSlider.fillRect != null
                ? buffSlider.fillRect.GetComponent<Image>()
                : null;

            if (fillImage != null)
            {
                fillImage.color = expiredBarColor;
            }
        }

        /// <summary>
        /// 残り時間に応じてスライダーの値を更新する
        /// </summary>
        private void UpdateSliderUI()
        {
            if (buffSlider == null)
            {
                return;
            }

            if (buffDurationSeconds <= 0f)
            {
                buffSlider.value = 0f;
                return;
            }

            // 0〜1 に正規化してスライダーに反映
            float normalized = Mathf.Clamp01(_remainingTime / buffDurationSeconds);
            buffSlider.value = normalized;
        }
    }
}

使い方の手順

ここからは、実際にシーンに組み込んで動かす手順を見ていきましょう。

① UI スライダーを用意する

  1. Hierarchy で 右クリック > UI > Slider を作成します。
  2. Canvas の下に Slider ができるので、画面上部など見やすい位置に配置します。
  3. Slider の Min Value = 0, Max Value = 1 にしておきます(スクリプト側でも初期化しますが、見た目の確認用)。
  4. Fill 部分の色をお好みで設定しておきます(バフ中に別の色に変えることもできます)。

② プレイヤー(またはバフ対象)にコンポーネントを付ける

  1. プレイヤー用の GameObject(例:Player)を選択します。
  2. Add Component から SimpleStatus を追加します。
  3. 同じく Add Component から BuffTimer を追加します。
  4. BuffTimer のインスペクターで、Buff Slider に先ほど作成した Slider をドラッグ&ドロップしてアサインします。
  5. バフ時間(Buff Duration Seconds)や倍率(Attack Buff Multiplier)を好みの値に調整します。
    例:buffDurationSeconds = 8, attackBuffMultiplier = 2.0 など。

③ バフを発動させるトリガーを作る

バフの開始は、外部スクリプトから BuffTimer.StartBuff() を呼ぶだけでOKです。
例として、スペースキーを押したらバフを発動するだけの簡単なスクリプトを示します。

using UnityEngine;
using Sample.BuffSystem;

public class BuffTestInput : MonoBehaviour
{
    [SerializeField] private BuffTimer buffTimer;

    private void Update()
    {
        // スペースキーでバフ発動
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (buffTimer != null)
            {
                buffTimer.StartBuff();
            }
        }
    }
}
  1. 空の GameObject(例:BuffTester)を作成して、上記 BuffTestInput をアタッチします。
  2. BuffTestInputBuff Timer に、プレイヤーの BuffTimer コンポーネントをドラッグ&ドロップします。
  3. Play してスペースキーを押すと、バフが開始し、UIバーが減っていくのが確認できます。

④ 具体的な使用例

  • プレイヤーの攻撃バフ
    攻撃アップアイテムを拾ったときに StartBuff() を呼ぶだけで、
    攻撃力アップ+残り時間UI表示+時間切れで元に戻す、が一括で処理できます。
  • 敵の狂暴化モード
    敵の HP が一定以下になったら StartBuff() を呼び、
    一定時間だけ攻撃力を上げる「狂暴化モード」を作ることができます。
    敵ごとに BuffTimer を持たせれば、個別に残り時間を制御できます。
  • 動く床の加速バフ
    動く床に乗ったプレイヤーの移動速度を一時的に上げる、といったギミックも、
    「速度ステータス+BuffTimer」の組み合わせでスッキリ書けます(速度用のステータスに書き換えればOK)。

メリットと応用

BuffTimer を独立したコンポーネントとして切り出すことで、以下のメリットがあります。

  • プレハブ化しやすい
    「バフ付きプレイヤー」「バフ付き敵」などをプレハブにしておけば、
    シーンにポンポン配置するだけで同じ挙動を再利用できます。
  • レベルデザインが楽になる
    バフ時間や倍率はインスペクターから調整可能なので、
    レベルデザイナーがコードを書かずに「この敵は短時間だけ超強い」などを設定できます。
  • 責務が分離されていて保守しやすい
    ステータスの中身(攻撃力、移動速度など)は SimpleStatus 側で自由に拡張しつつ、
    バフ時間とUI表示は BuffTimer に任せておけばOKです。
    どこを直せばいいかが明確なので、後からの仕様変更にも強くなります。

改造案:バフ終了時にエフェクトを出す

バフが切れた瞬間に、パーティクルやサウンドを再生したい場合は、
BuffTimer にちょっとしたフックメソッドを追加すると便利です。

private void PlayBuffEndEffect()
{
    // ここにパーティクルやサウンドの再生処理を書く
    // 例:
    // if (buffEndParticle != null)
    // {
    //     Instantiate(buffEndParticle, transform.position, Quaternion.identity);
    // }
    Debug.Log("[BuffTimer] バフ終了エフェクト再生");
}

そして EndBuff() の最後で PlayBuffEndEffect() を呼び出せば、
演出変更もこのメソッド内だけを触ればよくなります。

バフごとに演出を変えたい場合は、
バフの種類ごとに別コンポーネントを作る、あるいは ScriptableObject で設定を分ける、なども発展案としておすすめです。


BuffTimer のように、「バフ時間の管理+UI表示」だけに責任を絞ったコンポーネントを作っておくと、
プロジェクトが大きくなっても見通しの良いコードベースを維持しやすくなります。
巨大な Update() にすべてを押し込むのではなく、機能ごとにスクリプトを分割していきましょう。