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 スライダーを用意する
- Hierarchy で 右クリック > UI > Slider を作成します。
- Canvas の下に Slider ができるので、画面上部など見やすい位置に配置します。
- Slider の Min Value = 0, Max Value = 1 にしておきます(スクリプト側でも初期化しますが、見た目の確認用)。
- Fill 部分の色をお好みで設定しておきます(バフ中に別の色に変えることもできます)。
② プレイヤー(またはバフ対象)にコンポーネントを付ける
- プレイヤー用の GameObject(例:
Player)を選択します。 - Add Component から
SimpleStatusを追加します。 - 同じく Add Component から
BuffTimerを追加します。 BuffTimerのインスペクターで、Buff Slider に先ほど作成した Slider をドラッグ&ドロップしてアサインします。- バフ時間(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();
}
}
}
}
- 空の GameObject(例:
BuffTester)を作成して、上記BuffTestInputをアタッチします。 BuffTestInputの Buff Timer に、プレイヤーのBuffTimerコンポーネントをドラッグ&ドロップします。- 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() にすべてを押し込むのではなく、機能ごとにスクリプトを分割していきましょう。
