Unityを触り始めた頃は、つい1つのスクリプトのUpdateに全部書いてしまうことが多いですよね。プレイヤー操作、敵AI、UI更新、サウンド再生…気づけば1,000行を超える「Godクラス」が誕生して、どこを直せばいいのか分からないカオス状態になりがちです。

特にボス戦のUI、つまり画面下部にドーンと出るボスHPバーは、ゲーム全体のロジックに絡みやすく、「ボスのHP計算」と「UIの見た目更新」がごちゃ混ぜになりやすいポイントです。

そこで今回は、ボスのHPバーUIだけを責務とするコンポーネントとして、BossHealthBar を用意してみましょう。ボス本体のHP管理とは分離し、「HPの現在値・最大値を受け取ってUIを更新するだけ」のシンプルな役割に絞ることで、コードの見通しがかなり良くなります。

【Unity】ボス戦の緊張感を演出!「BossHealthBar」コンポーネント

ここでは、Unity UI(Canvas + Slider + TextMeshPro など)を使って、画面下部に固定表示される巨大なボスHPバーを制御する BossHealthBar コンポーネントを作ります。

  • ボスの現在HP / 最大HP からスライダーを更新
  • ボス名の表示
  • ダメージ時にHPバーが「追いついてくる」ような遅延ダメージ演出
  • HPが0になったらフェードアウトして非表示

といった機能を、UI専用コンポーネントとして完結させます。

フルコード:BossHealthBar.cs


using UnityEngine;
using UnityEngine.UI;
using TMPro;

namespace Game.UI
{
    /// <summary>
    /// 画面下部に固定表示されるボスHPバーUIを制御するコンポーネント。
    /// - ボスの現在HP / 最大HP からスライダーを更新
    /// - ボス名の表示
    /// - 遅延ダメージ演出(front / back の2枚バー)
    /// - ボス撃破時のフェードアウト
    /// 
    /// <remarks>
    /// このコンポーネントは「UI専用」です。
    /// ボス本体のHP計算ロジックとは分離し、
    /// BossHealthBar.Show / UpdateHealth / Hide を外部から呼び出して使います。
    /// </remarks>
    /// </summary>
    public class BossHealthBar : MonoBehaviour
    {
        [Header("UI 参照")]

        [SerializeField]
        private CanvasGroup canvasGroup;
        // HPバーの親全体をフェードさせるために使用

        [SerializeField]
        private Slider frontSlider;
        // 「実際のHP」を素早く反映する手前のバー

        [SerializeField]
        private Slider backSlider;
        // ダメージ時に遅れて追随する奥のバー(白いダメージ演出などに使う)

        [SerializeField]
        private TextMeshProUGUI bossNameText;
        // ボス名の表示用テキスト

        [Header("アニメーション設定")]

        [SerializeField, Tooltip("HP変化時に frontSlider が目標値に追従する速度")]
        private float frontLerpSpeed = 10f;

        [SerializeField, Tooltip("ダメージ演出として backSlider が追従するまでの遅延時間(秒)")]
        private float damageDelay = 0.2f;

        [SerializeField, Tooltip("backSlider が frontSlider に追従する速度")]
        private float backLerpSpeed = 3f;

        [SerializeField, Tooltip("ボス撃破後にHPバーをフェードアウトさせる時間(秒)")]
        private float fadeOutDuration = 0.5f;

        [Header("自動表示設定")]

        [SerializeField, Tooltip("最初は非表示にしておく場合は true")]
        private bool startHidden = true;

        // 内部状態
        private float _currentHp = 1f;
        private float _maxHp = 1f;

        private float _targetNormalized;      // 目標の正規化HP (0〜1)
        private float _frontNormalized;       // frontSlider の現在値
        private float _backNormalized;        // backSlider の現在値

        private float _damageDelayTimer = 0f;
        private bool _isVisible = false;
        private bool _isFadingOut = false;

        private void Awake()
        {
            // 参照チェック(エディタでのミスを早期に検出)
            if (canvasGroup == null)
            {
                Debug.LogWarning("[BossHealthBar] CanvasGroup が設定されていません。フェードアウトが動作しません。", this);
            }
            if (frontSlider == null || backSlider == null)
            {
                Debug.LogError("[BossHealthBar] Slider の参照が設定されていません。", this);
            }
            if (bossNameText == null)
            {
                Debug.LogWarning("[BossHealthBar] BossName Text が設定されていません。ボス名は表示されません。", this);
            }

            // スライダーの初期設定
            if (frontSlider != null)
            {
                frontSlider.minValue = 0f;
                frontSlider.maxValue = 1f;
            }
            if (backSlider != null)
            {
                backSlider.minValue = 0f;
                backSlider.maxValue = 1f;
            }

            _currentHp = 1f;
            _maxHp = 1f;
            _targetNormalized = 1f;
            _frontNormalized = 1f;
            _backNormalized = 1f;

            if (startHidden)
            {
                SetCanvasVisible(false, instant: true);
            }
            else
            {
                SetCanvasVisible(true, instant: true);
            }
        }

        private void Update()
        {
            // 表示されていない、またはスライダー参照が無い場合は何もしない
            if (!_isVisible || frontSlider == null || backSlider == null)
            {
                return;
            }

            // frontSlider は常に目標値に向かって素早く追従
            _frontNormalized = Mathf.MoveTowards(
                _frontNormalized,
                _targetNormalized,
                frontLerpSpeed * Time.deltaTime
            );

            // backSlider はダメージ時のみ遅れて追従する演出
            // (回復時は front と同時に動かしてもOKだが、ここではダメージ演出に特化)
            if (_backNormalized > _targetNormalized)
            {
                // ダメージを受けた直後は少し待ってから追従
                if (_damageDelayTimer > 0f)
                {
                    _damageDelayTimer -= Time.deltaTime;
                }
                else
                {
                    _backNormalized = Mathf.MoveTowards(
                        _backNormalized,
                        _targetNormalized,
                        backLerpSpeed * Time.deltaTime
                    );
                }
            }
            else
            {
                // 回復時など、back が target 以下の場合は即座に合わせる
                _backNormalized = _targetNormalized;
            }

            // 実際のスライダー値に反映
            frontSlider.value = _frontNormalized;
            backSlider.value = _backNormalized;
        }

        /// <summary>
        /// ボスHPバーを表示し、ボス名と最大HPを設定します。
        /// ボス出現時などに一度だけ呼ぶ想定です。
        /// </summary>
        /// <param name="bossName">ボス名(UIに表示)</param>
        /// <param name="maxHp">ボスの最大HP(0以下は無効)</param>
        /// <param name="currentHp">ボスの現在HP(省略時は maxHp と同じ)</param>
        public void Show(string bossName, float maxHp, float currentHp = -1f)
        {
            if (maxHp <= 0f)
            {
                Debug.LogError("[BossHealthBar] maxHp は 0 より大きい必要があります。", this);
                maxHp = 1f;
            }

            _maxHp = maxHp;

            if (currentHp < 0f)
            {
                currentHp = maxHp;
            }

            _currentHp = Mathf.Clamp(currentHp, 0f, _maxHp);

            // 正規化して内部状態を初期化
            _targetNormalized = _currentHp / _maxHp;
            _frontNormalized = _targetNormalized;
            _backNormalized = _targetNormalized;
            _damageDelayTimer = 0f;
            _isFadingOut = false;

            if (bossNameText != null)
            {
                bossNameText.text = bossName;
            }

            // スライダー即時反映
            if (frontSlider != null) frontSlider.value = _frontNormalized;
            if (backSlider != null) backSlider.value = _backNormalized;

            SetCanvasVisible(true, instant: false);
        }

        /// <summary>
        /// ボスのHPを更新します。
        /// ボスのダメージ処理や回復処理から呼び出してください。
        /// </summary>
        /// <param name="currentHp">現在HP(0〜maxHp)</param>
        public void UpdateHealth(float currentHp)
        {
            if (!_isVisible)
            {
                // 非表示なら何もしない(必要なら自動表示するように変更してもOK)
                return;
            }

            _currentHp = Mathf.Clamp(currentHp, 0f, _maxHp);
            _targetNormalized = _maxHp > 0f ? _currentHp / _maxHp : 0f;

            // ダメージを受けたとき(現在値が下がったとき)は遅延演出を開始
            if (_backNormalized > _targetNormalized)
            {
                _damageDelayTimer = damageDelay;
            }

            // HPが0になったらフェードアウト
            if (_currentHp <= 0f && !_isFadingOut)
            {
                StartFadeOut();
            }
        }

        /// <summary>
        /// 即座にHPバーを非表示にします。
        /// (フェード演出なしで消したいときに使用)
        /// </summary>
        public void HideInstant()
        {
            SetCanvasVisible(false, instant: true);
        }

        /// <summary>
        /// 外部から明示的にフェードアウトさせたい場合に呼び出します。
        /// 通常は HP が 0 になったときに自動で呼ばれます。
        /// </summary>
        public void StartFadeOut()
        {
            if (_isFadingOut || canvasGroup == null)
            {
                return;
            }

            _isFadingOut = true;
            // コルーチンでフェードアウト
            StartCoroutine(FadeOutRoutine());
        }

        /// <summary>
        /// CanvasGroup の表示・非表示を切り替える内部メソッド。
        /// </summary>
        private void SetCanvasVisible(bool visible, bool instant)
        {
            _isVisible = visible;

            if (canvasGroup == null)
            {
                // CanvasGroup が無い場合は GameObject の active を切り替えるだけ
                gameObject.SetActive(visible);
                return;
            }

            if (instant)
            {
                canvasGroup.alpha = visible ? 1f : 0f;
                canvasGroup.interactable = visible;
                canvasGroup.blocksRaycasts = visible;
                gameObject.SetActive(visible);
            }
            else
            {
                // 非表示→表示のときは即座にアクティブ化&アルファ1にしておく
                if (visible)
                {
                    gameObject.SetActive(true);
                    canvasGroup.alpha = 1f;
                    canvasGroup.interactable = true;
                    canvasGroup.blocksRaycasts = true;
                }
                else
                {
                    // 表示→非表示はフェードアウトで行う想定だが、
                    // ここでは一応即時非表示もサポート
                    canvasGroup.alpha = 0f;
                    canvasGroup.interactable = false;
                    canvasGroup.blocksRaycasts = false;
                    gameObject.SetActive(false);
                }
            }
        }

        /// <summary>
        /// HPバーをフェードアウトさせるコルーチン。
        /// </summary>
        private System.Collections.IEnumerator FadeOutRoutine()
        {
            if (canvasGroup == null)
            {
                // CanvasGroup が無い場合は即非表示
                gameObject.SetActive(false);
                yield break;
            }

            float elapsed = 0f;
            float startAlpha = canvasGroup.alpha;

            while (elapsed < fadeOutDuration)
            {
                elapsed += Time.deltaTime;
                float t = Mathf.Clamp01(elapsed / fadeOutDuration);
                canvasGroup.alpha = Mathf.Lerp(startAlpha, 0f, t);
                yield return null;
            }

            canvasGroup.alpha = 0f;
            canvasGroup.interactable = false;
            canvasGroup.blocksRaycasts = false;
            gameObject.SetActive(false);
            _isVisible = false;
        }

        /// <summary>
        /// 現在のHPと最大HPを取得したい場合用のヘルパー。
        /// (デバッグや他のUIとの連携用)
        /// </summary>
        public void GetHealth(out float currentHp, out float maxHp)
        {
            currentHp = _currentHp;
            maxHp = _maxHp;
        }
    }
}

使い方の手順

ここでは「ボス戦シーンで、画面下部に巨大なHPバーを出す」具体的な例として、ボスプレハブ + BossHealthBar UI の連携方法を手順で説明します。

手順①:UIのセットアップ(Canvas 上にボスHPバーを作る)

  1. Hierarchy で Canvas を作成(既にある場合はそれを使用)。
    Canvas の Render Mode は「Screen Space – Overlay」がおすすめです。
  2. Canvas の子に Panel(または空の GameObject) を作成し、
    RectTransform を画面下部いっぱいに広げて「ボスHPバーの土台」とします。
    • Anchor Presets を「下中央ストレッチ」に設定(Shift + Alt を押しながらクリック)
    • Height を 80〜120 くらいにするとそれっぽくなります。
  3. 土台オブジェクトの子として:
    • Slider(Front) … 手前のメインHPバー
    • Slider(Back) … 奥に置くダメージ演出用バー(色を白や暗い赤に)
    • TextMeshPro – Text … ボス名表示用
  4. 土台オブジェクトに CanvasGroup コンポーネントを追加します(フェード用)。
  5. 土台オブジェクトに BossHealthBar.cs をアタッチし、インスペクタで以下を紐付けます。
    • Canvas Group … 土台の CanvasGroup
    • Front Slider … 手前の Slider
    • Back Slider … 奥の Slider
    • Boss Name Text … ボス名の TextMeshProUGUI
  6. Start HiddenチェックON にしておくと、シーン開始時は非表示になります。

手順②:ボス側のHP管理コンポーネントを用意する

ボス本体には、純粋にHPだけを管理するコンポーネントを別途用意し、そこから BossHealthBar を呼び出します。例として、シンプルな BossHealth を用意します。


using UnityEngine;
using Game.UI; // BossHealthBar の namespace

/// <summary>
/// ボス本体のHPを管理するコンポーネント。
/// ダメージ処理と、BossHealthBar への通知だけを担当します。
/// </summary>
public class BossHealth : MonoBehaviour
{
    [SerializeField]
    private string bossName = "Ancient Dragon";

    [SerializeField]
    private float maxHp = 1000f;

    [SerializeField]
    private BossHealthBar bossHealthBar;
    // シーン上の BossHealthBar をインスペクタからアサイン

    private float _currentHp;

    private void Start()
    {
        _currentHp = maxHp;

        if (bossHealthBar != null)
        {
            // ボス出現時にHPバーを表示
            bossHealthBar.Show(bossName, maxHp, _currentHp);
        }
        else
        {
            Debug.LogWarning("[BossHealth] BossHealthBar が設定されていません。", this);
        }
    }

    /// <summary>
    /// 外部(プレイヤーの攻撃など)からダメージを受ける。
    /// </summary>
    public void TakeDamage(float amount)
    {
        if (_currentHp <= 0f) return;

        _currentHp = Mathf.Max(0f, _currentHp - amount);

        if (bossHealthBar != null)
        {
            bossHealthBar.UpdateHealth(_currentHp);
        }

        if (_currentHp <= 0f)
        {
            OnDead();
        }
    }

    private void OnDead()
    {
        Debug.Log("Boss defeated!");

        // 必要に応じてここでボスのアニメーションやドロップ処理などを行う
        // HPバーは BossHealthBar 側で自動フェードアウトされます
    }
}

手順③:プレイヤーやデバッグキーからダメージを与える

実際のゲームではプレイヤーの攻撃判定から BossHealth.TakeDamage を呼びますが、まずは簡単に動作確認するため、キーボード入力でダメージを与えるデバッグ用スクリプトを作ってもOKです。


using UnityEngine;

/// <summary>
/// デバッグ用:キー入力でボスにダメージを与える。
/// </summary>
public class BossDamageTester : MonoBehaviour
{
    [SerializeField]
    private BossHealth bossHealth;

    [SerializeField]
    private float damageAmount = 100f;

    private void Update()
    {
        // スペースキーでダメージ
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (bossHealth != null)
            {
                bossHealth.TakeDamage(damageAmount);
            }
        }
    }
}
  • 適当な空の GameObject に BossDamageTester をアタッチし、BossHealth をアサイン。
  • Play して、スペースキーを押すとボスHPバーが減っていくのを確認できます。

手順④:動く床や別シーンでも使い回す

この BossHealthBarボスのロジックに依存していないため、

  • 別シーンの別ボス
  • レイド戦の巨大敵
  • 動く要塞や戦艦の耐久ゲージ

などにもそのまま使い回せます。シーンごとにボス名と最大HPだけ変えて Show を呼べばOKです。

メリットと応用

BossHealthBar を UI専用コンポーネントとして切り出すことで、いくつか大きなメリットがあります。

  • 責務が明確
    「HPバーをどう描画するか」という見た目の責務だけを持ち、
    「HPをいつ減らすか」「ボスがいつ死ぬか」といったゲームロジックから切り離されています。
    その結果、ボスのロジックをいじってもUIが壊れにくくなります。
  • プレハブ管理が楽
    ボスHPバーを1つのプレハブとして作っておけば、
    新しいボス戦シーンを作るときは「Canvasにプレハブを置いて、BossHealth から参照する」だけで済みます。
    UIのテーマ変更(色やフォント、アニメーション)もこのプレハブだけ直せば全ボス戦に反映されます。
  • レベルデザインの自由度アップ
    レベルデザイナーは「このシーンではHPバーを常に表示」「このシーンではHP50%以下から表示」など、
    表示タイミングのルールだけを考えればよく、HPバーの実装詳細を気にする必要がありません。
  • 演出の追加が簡単
    ダメージ時の遅延バー、フェードアウト、点滅などの演出は BossHealthBar 内で完結しているため、
    演出をどんどん盛ってもボス本体のスクリプトを汚さずに済みます。

改造案:HPバーの色を残りHPに応じて変える

応用として、「HPが少なくなるとバーの色を緑 → 黄 → 赤に変える」機能を追加してみましょう。
BossHealthBar 内に以下のメソッドを追加し、Update() の最後で呼び出すだけでも演出がグッと良くなります。


        /// <summary>
        /// 残りHP割合に応じて frontSlider の色を変える簡易演出。
        /// 0.5以上: 緑, 0.2〜0.5: 黄, 0.2未満: 赤
        /// (Slider の Fill Area に Image を設定しておく必要があります)
        /// </summary>
        private void UpdateBarColor()
        {
            if (frontSlider == null) return;

            // Slider の Fill 部分の Image を取得
            var fill = frontSlider.fillRect != null
                ? frontSlider.fillRect.GetComponent<UnityEngine.UI.Image>()
                : null;

            if (fill == null) return;

            float ratio = _frontNormalized;

            if (ratio >= 0.5f)
            {
                fill.color = Color.Lerp(Color.yellow, Color.green, (ratio - 0.5f) / 0.5f);
            }
            else if (ratio >= 0.2f)
            {
                fill.color = Color.yellow;
            }
            else
            {
                fill.color = Color.Lerp(Color.yellow, Color.red, (0.2f - ratio) / 0.2f);
            }
        }

Update() の最後に UpdateBarColor(); を1行追加するだけで、
HPが減るにつれて色が変わる、分かりやすいボスHPバーになります。

このように、UIの責務を1つのコンポーネントに閉じ込めておくと、
「こんな演出を足したい!」と思ったときに、迷わず BossHealthBar だけを開けば良い状態になり、
結果として、Godクラスを避けながら気持ちよく開発を進められます。