Unityを触り始めた頃にやりがちなのが、「とりあえず Update() に全部書いてしまう」スタイルですね。
プレイヤー操作、カメラ、HPバー、ダメージ処理…すべて1つのスクリプトに詰め込むと、次のような問題が出てきます。

  • どこを直せばいいか分からない(バグの原因箇所を特定しづらい)
  • 同じような処理を別オブジェクトで使い回しにくい
  • ちょっとした仕様変更で大量の修正が必要になる

特に「敵の頭上にHPバーを出したい」といった UI と 3D オブジェクトの連携処理は、Update() にベタ書きしがちな典型例です。
そこでこの記事では、「敵の頭上にHPバーを表示し、3D空間座標をUI座標に変換して追従させる」 専用のコンポーネント FloatingHealthBar を作って、責務をきれいに分離していきましょう。

【Unity】3D敵の頭上にピタッと追従するHPバー!「FloatingHealthBar」コンポーネント

ここでは、以下の特徴を持つコンポーネントを作ります。

  • 敵の頭上に HP バーを表示
  • 3D ワールド座標を UI(Screen Space)座標に変換して追従
  • HP の割合に応じてスライダーを更新
  • カメラの参照やオフセットなどをインスペクターから調整可能

フルコード:FloatingHealthBar.cs


using UnityEngine;
using UnityEngine.UI;

namespace Sample.UI
{
    /// <summary>
    /// 敵などの頭上にHPバーを表示し、3Dワールド座標からUI座標へ変換して追従させるコンポーネント。
    /// 
    /// 前提:
    /// - Canvas は Screen Space - Overlay もしくは Screen Space - Camera を想定
    /// - HPバーは Slider を利用
    /// - このスクリプトは「HPバーUIのプレハブ側」にアタッチして使う
    /// </summary>
    [RequireComponent(typeof(RectTransform))]
    public class FloatingHealthBar : MonoBehaviour
    {
        [Header("参照設定")]

        [SerializeField]
        [Tooltip("HPバーを追従させたいワールド空間の対象(敵など)。Transform の位置が基準になります。")]
        private Transform targetWorldTransform;

        [SerializeField]
        [Tooltip("HPバーの値を表示する Slider コンポーネント。")]
        private Slider healthSlider;

        [SerializeField]
        [Tooltip("UI を描画している Canvas。Screen Space の Canvas を想定。")]
        private Canvas targetCanvas;

        [SerializeField]
        [Tooltip("ワールド座標からスクリーン座標への変換に使うカメラ。未指定の場合は mainCamera を使用。")]
        private Camera targetCamera;

        [Header("表示調整")]

        [SerializeField]
        [Tooltip("ワールド空間でのオフセット(例: 頭上に表示したいときは Y を 2.0f などに設定)。")]
        private Vector3 worldOffset = new Vector3(0f, 2f, 0f);

        [SerializeField]
        [Tooltip("スクリーン外に出たときに HPバーを非表示にするかどうか。")]
        private bool hideWhenOffScreen = true;

        [SerializeField]
        [Tooltip("ターゲットが見えないときに HPバーを非表示にするかどうか。")]
        private bool hideWhenBehindCamera = true;

        [Header("HP 値設定")]

        [SerializeField]
        [Tooltip("現在HP。ゲーム側から SetHealthRatio で更新するのを推奨。")]
        [Range(0f, 1f)]
        private float healthRatio = 1.0f;

        // 内部キャッシュ
        private RectTransform _rectTransform;
        private Canvas _canvas;
        private Camera _camera;

        private void Awake()
        {
            _rectTransform = GetComponent<RectTransform>();

            // Canvas が未指定なら親階層から探す
            if (targetCanvas == null)
            {
                _canvas = GetComponentInParent<Canvas>();
            }
            else
            {
                _canvas = targetCanvas;
            }

            if (_canvas == null)
            {
                Debug.LogError(
                    $"[{nameof(FloatingHealthBar)}] Canvas が見つかりません。" +
                    "インスペクターで Canvas を指定するか、HPバーUIを Canvas の子に配置してください。",
                    this
                );
            }

            // カメラが未指定なら mainCamera を利用
            if (targetCamera != null)
            {
                _camera = targetCamera;
            }
            else
            {
                _camera = Camera.main;
            }

            if (_camera == null && _canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceCamera)
            {
                // Screen Space - Camera の場合は Canvas の worldCamera を利用
                _camera = _canvas.worldCamera;
            }

            if (healthSlider == null)
            {
                // 自動で探してみる(子階層から)
                healthSlider = GetComponentInChildren<Slider>();
            }

            if (healthSlider == null)
            {
                Debug.LogError(
                    $"[{nameof(FloatingHealthBar)}] Slider コンポーネントが見つかりません。" +
                    "HPバー用の Slider をインスペクターで設定してください。",
                    this
                );
            }

            // 初期値を反映
            ApplyHealthRatioToSlider();
        }

        private void LateUpdate()
        {
            if (targetWorldTransform == null || _canvas == null)
            {
                // ターゲットや Canvas が無ければ何もしない
                return;
            }

            if (_camera == null && _canvas.renderMode != RenderMode.ScreenSpaceOverlay)
            {
                // Camera が必要なモードなのに見つからない場合は警告を出して終了
                Debug.LogWarning(
                    $"[{nameof(FloatingHealthBar)}] ワールド座標変換に使用する Camera が見つかりません。",
                    this
                );
                return;
            }

            // 1. ターゲットのワールド座標 + オフセットを取得
            Vector3 worldPosition = targetWorldTransform.position + worldOffset;

            // 2. ワールド座標 → スクリーン座標へ変換
            Vector3 screenPosition;
            bool isBehindCamera = false;

            if (_canvas.renderMode == RenderMode.ScreenSpaceOverlay)
            {
                // Overlay の場合は Camera を使わずに WorldToScreenPoint する
                var cam = _camera != null ? _camera : Camera.main;
                if (cam != null)
                {
                    screenPosition = cam.WorldToScreenPoint(worldPosition);
                    isBehindCamera = screenPosition.z < 0f;
                }
                else
                {
                    // 最悪のフォールバックとして簡易的に投影(Zは無視)
                    screenPosition = worldPosition;
                }
            }
            else
            {
                // ScreenSpace-Camera / WorldSpace の場合
                screenPosition = _camera.WorldToScreenPoint(worldPosition);
                isBehindCamera = screenPosition.z < 0f;
            }

            // 3. 見える・見えないの判定と表示制御
            bool isOffScreen =
                screenPosition.x < 0f ||
                screenPosition.x > Screen.width ||
                screenPosition.y < 0f ||
                screenPosition.y > Screen.height;

            bool shouldHide =
                (hideWhenOffScreen && isOffScreen) ||
                (hideWhenBehindCamera && isBehindCamera);

            if (shouldHide)
            {
                if (healthSlider != null && healthSlider.gameObject.activeSelf)
                {
                    healthSlider.gameObject.SetActive(false);
                }
                return;
            }
            else
            {
                if (healthSlider != null && !healthSlider.gameObject.activeSelf)
                {
                    healthSlider.gameObject.SetActive(true);
                }
            }

            // 4. スクリーン座標 → Canvas のローカル座標へ変換
            Vector2 localPos;
            RectTransform canvasRect = _canvas.transform as RectTransform;

            if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
                canvasRect,
                screenPosition,
                _canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : _camera,
                out localPos))
            {
                _rectTransform.anchoredPosition = localPos;
            }
        }

        /// <summary>
        /// HP の割合を 0.0 ~ 1.0 の範囲で設定します。
        /// 例: currentHP / maxHP を渡す想定。
        /// </summary>
        /// <param name="ratio">0~1 の範囲の値</param>
        public void SetHealthRatio(float ratio)
        {
            healthRatio = Mathf.Clamp01(ratio);
            ApplyHealthRatioToSlider();
        }

        /// <summary>
        /// 追従対象の Transform を動的に差し替えたい場合に使用します。
        /// 例: 敵の生成時にインスタンスごとに設定する。
        /// </summary>
        public void SetTarget(Transform newTarget)
        {
            targetWorldTransform = newTarget;
        }

        /// <summary>
        /// HPバーの表示・非表示を外部から制御したい場合に使用します。
        /// </summary>
        public void SetVisible(bool visible)
        {
            if (healthSlider != null)
            {
                healthSlider.gameObject.SetActive(visible);
            }
        }

        /// <summary>
        /// 内部の Slider に healthRatio を反映するヘルパー。
        /// </summary>
        private void ApplyHealthRatioToSlider()
        {
            if (healthSlider == null) return;

            // Slider の値は 0~1 を想定
            healthSlider.normalizedValue = healthRatio;
        }
    }
}

使い方の手順

ここからは、具体的なセットアップ手順を見ていきましょう。例として「敵キャラクターの頭上に HPバーを表示する」ケースを想定します。

手順①:UI側(HPバープレハブ)を作成する

  1. Canvas を用意
    – Hierarchy で UI > Canvas を作成
    – Render Mode は「Screen Space – Overlay」か「Screen Space – Camera」を推奨
  2. HPバー用の Slider を作る
    – Canvas の子として UI > Slider を作成
    – Fill Area の画像が HP の増減を表す部分になります
    – 不要な Handle を消したい場合は、Handle Slide Area を削除してもOKです
  3. HPバーUIをプレハブ化
    – Slider を分かりやすく EnemyHealthBar などにリネーム
    EnemyHealthBar を Project ウィンドウにドラッグしてプレハブ化
  4. FloatingHealthBar コンポーネントをアタッチ
    – プレハブ(EnemyHealthBar)を選択
    FloatingHealthBar を Add Component で追加
    Health Slider に自分自身の Slider をドラッグ&ドロップ
    – Canvas フィールドは空でもOK(親から自動取得されます)
    – World Offset の Y を 2.0 〜 3.0 くらいにすると「頭上」っぽくなります

手順②:敵キャラクターにHPと生成処理を用意する

敵プレハブに簡単な HP 管理スクリプトを用意して、HPバーを生成・更新してみましょう。
(あくまで例なので、実際のゲームではもっと細かく責務分離してもOKです)


using UnityEngine;
using Sample.UI; // FloatingHealthBar の namespace

/// <summary>
/// 非常にシンプルな敵HP管理と、FloatingHealthBar の生成・更新例。
/// </summary>
public class SimpleEnemyHealth : MonoBehaviour
{
    [SerializeField]
    private int maxHealth = 100;

    [SerializeField]
    private int currentHealth = 100;

    [Header("HPバー設定")]
    [SerializeField]
    [Tooltip("FloatingHealthBar を含む HPバーUI のプレハブ。")]
    private FloatingHealthBar floatingHealthBarPrefab;

    [SerializeField]
    [Tooltip("HPバーをぶら下げる Canvas。Scene 上の Canvas を指定。")]
    private Canvas uiCanvas;

    private FloatingHealthBar _healthBarInstance;

    private void Start()
    {
        currentHealth = maxHealth;

        // HPバーのインスタンスを生成
        if (floatingHealthBarPrefab != null && uiCanvas != null)
        {
            _healthBarInstance = Instantiate(floatingHealthBarPrefab, uiCanvas.transform);

            // 追従対象をこの敵に設定
            _healthBarInstance.SetTarget(transform);

            // 初期HPを反映
            _healthBarInstance.SetHealthRatio(1.0f);
        }
        else
        {
            Debug.LogWarning(
                $"[{nameof(SimpleEnemyHealth)}] HPバーのプレハブまたは Canvas が設定されていません。",
                this
            );
        }
    }

    /// <summary>
    /// 外部からダメージを与えるためのメソッド例。
    /// </summary>
    public void TakeDamage(int damage)
    {
        currentHealth = Mathf.Max(currentHealth - damage, 0);

        float ratio = (float)currentHealth / maxHealth;

        if (_healthBarInstance != null)
        {
            _healthBarInstance.SetHealthRatio(ratio);
        }

        if (currentHealth <= 0)
        {
            Die();
        }
    }

    private void Die()
    {
        // 死亡時の処理(ここでは単純に破壊する)
        if (_healthBarInstance != null)
        {
            Destroy(_healthBarInstance.gameObject);
        }

        Destroy(gameObject);
    }
}

手順③:シーンに敵を配置して動作確認する

  1. 敵プレハブに SimpleEnemyHealth をアタッチ
  2. FloatingHealthBar Prefab に先ほど作成した HPバーのプレハブを指定
  3. UI Canvas にシーン上の Canvas をドラッグ&ドロップ
  4. シーン上に敵プレハブを配置して再生

再生すると、敵の頭上に HPバーが表示され、敵が動いても常に頭上に追従するようになります。
ダメージを与える処理(例:テスト用にキー入力で TakeDamage() を呼ぶ)を追加すると、HPバーが減っていくのも確認できます。

手順④:別の用途への応用例

  • プレイヤーキャラクターの名前プレート
    HPバーの代わりに Text を使って名前を表示すれば、MMO風のネームプレートにも流用できます。
  • NPC の会話アイコン
    会話可能な NPC の頭上に「!」アイコンを出すなど、インタラクションのヒントにも使えます。
  • 動く床やギミックの状態表示
    「あと何秒で動くか」「現在の耐久度」などを頭上に出しておくと、プレイヤーにとって分かりやすいUIになります。

メリットと応用

FloatingHealthBar のように、「ワールド座標 → UI座標変換&追従」 という責務だけに絞ったコンポーネントを用意しておくと、次のようなメリットがあります。

  • プレハブ管理が楽になる
    – HPバーの見た目(色、フォント、アニメーション)は UI プレハブ側だけをいじればOK
    – 敵ロジック側は「HPが減ったら ratio を渡す」だけなので、見た目とロジックが分離されます
  • レベルデザイン時の再利用性が高い
    – 新しい敵を追加するときは、HP管理スクリプトと HPバーの参照を設定するだけ
    – ボス、雑魚、NPCなど、どのオブジェクトにも同じ方式で頭上UIを付けられます
  • 責務が明確でテストしやすい
    – 「追従がズレる」「画面外で消えない」といった問題も、このコンポーネントだけ見ればよい
    – HPの計算やダメージ処理とは切り離されているので、デバッグしやすくなります

さらに、ちょっとした改造で「距離が遠い敵の HPバーは薄く表示する」といった演出も簡単に追加できます。

改造案:距離に応じて HPバーの透明度を変える

FloatingHealthBar に次のようなメソッドを追加し、LateUpdate() の最後で呼び出すと、カメラからの距離に応じて HPバーの透明度を変えられます。


/// <summary>
/// カメラとの距離に応じて HPバーの透明度を変化させる例。
/// 近いときは不透明、遠いときは半透明にする。
/// </summary>
private void UpdateAlphaByDistance()
{
    if (_camera == null || targetWorldTransform == null || healthSlider == null)
    {
        return;
    }

    float distance = Vector3.Distance(_camera.transform.position, targetWorldTransform.position);

    // 0 ~ 1 の範囲に正規化(例: 5m 以内なら1、20m以上なら0.3)
    float alpha = Mathf.InverseLerp(20f, 5f, distance); // 20m:0, 5m:1
    alpha = Mathf.Lerp(0.3f, 1.0f, alpha);              // 0.3 ~ 1.0 にマッピング

    // Slider 全体の CanvasGroup で制御するのが簡単
    var canvasGroup = healthSlider.GetComponent<CanvasGroup>();
    if (canvasGroup == null)
    {
        canvasGroup = healthSlider.gameObject.AddComponent<CanvasGroup>();
    }

    canvasGroup.alpha = alpha;
}

このように、小さなコンポーネントごとに責務を分けておくと、必要なときにだけ拡張しやすくなります。
「頭上HPバー」も 1 つの大きな God クラスに押し込まず、FloatingHealthBar のような再利用しやすい部品として育てていきましょう。