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バープレハブ)を作成する
-
Canvas を用意
– Hierarchy でUI > Canvasを作成
– Render Mode は「Screen Space – Overlay」か「Screen Space – Camera」を推奨 -
HPバー用の Slider を作る
– Canvas の子としてUI > Sliderを作成
– Fill Area の画像が HP の増減を表す部分になります
– 不要な Handle を消したい場合は、Handle Slide Area を削除してもOKです -
HPバーUIをプレハブ化
– Slider を分かりやすくEnemyHealthBarなどにリネーム
–EnemyHealthBarを Project ウィンドウにドラッグしてプレハブ化 -
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);
}
}
手順③:シーンに敵を配置して動作確認する
- 敵プレハブに
SimpleEnemyHealthをアタッチ FloatingHealthBar Prefabに先ほど作成した HPバーのプレハブを指定UI Canvasにシーン上の Canvas をドラッグ&ドロップ- シーン上に敵プレハブを配置して再生
再生すると、敵の頭上に 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 のような再利用しやすい部品として育てていきましょう。
