【Cocos Creator 3.8】FloatingHealthBar の実装:アタッチするだけで「3Dキャラの頭上に追従するHPバーUI」を実現する汎用スクリプト
このガイドでは、3D空間上の任意のノード(敵・プレイヤーなど)の頭上に HP バーを表示し、カメラの動きに合わせて UI 上で追従させるための汎用コンポーネント FloatingHealthBar を実装します。
このコンポーネントを UI の HP バー用ノードにアタッチし、ターゲットとなる 3D ノードとカメラをインスペクタで指定するだけで、3D → 2D 座標変換〜追従〜HP 表示までを自動で行います。外部の GameManager やシングルトンには一切依存しない、完全独立型の設計です。
コンポーネントの設計方針
1. 機能要件の整理
- 3D 空間上のターゲットノード(敵など)の頭上位置を基準に HP バーを表示する。
- カメラの位置・向きの変化に応じて、HP バーの UI ノードをスクリーン座標上で追従させる。
- ターゲットがカメラの背面に回った場合など、画面外にいるときは HP バーを非表示にできる。
- HP 値(現在値・最大値)を持ち、メソッド呼び出しだけで HP を変更できる。
- HP バーの見た目(背景・前景の Sprite / ProgressBar / UITransform など)は、インスペクタから自由に差し替え可能にする。
- 外部スクリプトに依存せず、このコンポーネント単体で完結する。
2. 3D → UI 座標変換の方針
- 前提: HP バーは
Canvas配下の 2D UI ノードとして存在する。 - ターゲットの 3D ワールド座標(頭上オフセットを加算)を、
Cameraでスクリーン座標に変換。 - スクリーン座標を
UITransform/viewを使って Canvas ローカル座標に変換し、HP バーノードの位置に反映。 - ターゲットがカメラの前面にあるかどうかは、
camera.camera.worldToScreenの結果や、ターゲットまでのベクトルとカメラの forward ベクトルの内積で判定。
3. インスペクタで設定可能なプロパティ
以下のような @property を用意し、すべてインスペクタから設定可能にします。
target(Node)
HP バーが追従する 3D ターゲットノード。敵やプレイヤーなど。
役割: このノードのワールド座標 + オフセットを HP バーの表示位置とする。camera(Camera)
3D シーンを映しているカメラ。
役割: ターゲットのワールド座標をスクリーン座標に変換するために使用。canvas(Node)
HP バーが属するCanvasノード。
役割: スクリーン座標を Canvas ローカル座標に変換する基準。worldOffset(Vec3)
ターゲットのワールド座標に加算するオフセット。
役割: 頭上に表示したい場合は Y を 1.5〜2.0 などに設定。useBillboard(boolean)
ターゲットの向きに関わらず、常にカメラ正面基準のオフセットにするかどうか。
(今回は 単純なワールド固定オフセット用途が主なので、true/false で切り替えられるようにしておく。)hideWhenOffscreen(boolean)
ターゲットが画面外 or カメラ背面にいるときに HP バーを自動的に非表示にするかどうか。maxHealth(number)
最大 HP 値。初期値としても使用。currentHealth(number)
現在 HP 値。インスペクタからテスト調整可能にしつつ、コードからも更新できるようにする。healthBarFill(UITransform | Sprite | ProgressBar など)
HP の割合を反映するための UI 要素。
実装では汎用性の高い UITransform の width を変える方式と、Sprite の fillRange の両方に対応できるよう設計します。fullWidth(number)
HP 100% 時のバーの幅(UITransform.width)。
役割: currentHealth / maxHealth に応じて、この幅を掛けた値を実際の width に設定する。minVisibleRatio(number)
HP がこの割合(0〜1)未満になったときに、HP バー自体を非表示にする閾値。
例: 0 の場合は常に表示、0.01 なら 1% 未満で非表示。
また、防御的実装として以下を行います。
onLoadでcanvasにUITransformがあるか確認し、なければエラーログ。- HP バー自身の
UITransformがない場合もエラーログ。 - ターゲット・カメラ・Canvas が未設定の場合は update 中に警告ログを出し、処理をスキップ。
TypeScriptコードの実装
import {
_decorator,
Component,
Node,
Camera,
Vec3,
Vec2,
UITransform,
view,
Sprite,
ProgressBar,
math,
} from 'cc';
const { ccclass, property } = _decorator;
@ccclass('FloatingHealthBar')
export class FloatingHealthBar extends Component {
@property({
type: Node,
tooltip: 'HPバーが追従する3Dターゲットノード(敵やプレイヤーなど)',
})
public target: Node | null = null;
@property({
type: Camera,
tooltip: '3Dシーンを描画しているカメラ。ターゲット座標をスクリーン座標に変換するために使用します。',
})
public camera: Camera | null = null;
@property({
type: Node,
tooltip: 'このHPバーが属するCanvasノード(必ずUITransformを持つ必要があります)',
})
public canvas: Node | null = null;
@property({
tooltip: 'ターゲットのワールド座標に加算するオフセット。頭上に表示したい場合はYを1.5~2.0程度に設定します。',
})
public worldOffset: Vec3 = new Vec3(0, 2, 0);
@property({
tooltip: 'trueの場合、ターゲットの向きに関係なく、ワールド空間で固定オフセットを使用します(今回の用途では通常true推奨)',
})
public useBillboard: boolean = true;
@property({
tooltip: 'ターゲットが画面外またはカメラ背面にいるときにHPバーを自動で非表示にするかどうか',
})
public hideWhenOffscreen: boolean = true;
@property({
tooltip: '最大HP値(初期値としても使用されます)',
min: 1,
})
public maxHealth: number = 100;
@property({
tooltip: '現在のHP値。ゲーム中はスクリプトから更新してください。',
min: 0,
})
public currentHealth: number = 100;
@property({
type: UITransform,
tooltip: 'HPバーの塗り部分(前景)に対応するUITransform。幅を変化させる方式でHP割合を表現します。',
})
public fillTransform: UITransform | null = null;
@property({
type: Sprite,
tooltip: 'SpriteのfillRangeでHP割合を表現する場合に指定します(未指定でも動作します)。',
})
public fillSprite: Sprite | null = null;
@property({
type: ProgressBar,
tooltip: 'ProgressBarのprogressでHP割合を表現する場合に指定します(未指定でも動作します)。',
})
public progressBar: ProgressBar | null = null;
@property({
tooltip: 'HP100%時のバーの幅(fillTransform.width)。0以下の場合、起動時にfillTransformの現在の幅を使用します。',
min: 0,
})
public fullWidth: number = 0;
@property({
tooltip: 'この割合(0~1)未満のHPになったときにHPバーを非表示にします。0なら常に表示します。',
min: 0,
max: 1,
})
public minVisibleRatio: number = 0;
// 内部で使い回す一時ベクトル(GC削減のため毎フレームnewしない)
private _worldPos: Vec3 = new Vec3();
private _screenPos: Vec3 = new Vec3();
private _canvasPos: Vec3 = new Vec3();
private _tmp2D: Vec2 = new Vec2();
private _canvasTransform: UITransform | null = null;
private _selfTransform: UITransform | null = null;
onLoad() {
// 自身のUITransformを取得
this._selfTransform = this.getComponent(UITransform);
if (!this._selfTransform) {
console.error('[FloatingHealthBar] このノードにはUITransformコンポーネントが必要です。Canvas配下のUIノードにアタッチしてください。', this.node);
}
// CanvasのUITransformを取得
if (this.canvas) {
this._canvasTransform = this.canvas.getComponent(UITransform);
if (!this._canvasTransform) {
console.error('[FloatingHealthBar] 指定されたCanvasノードにUITransformが存在しません。Canvasノードを正しく指定してください。', this.canvas);
}
} else {
console.warn('[FloatingHealthBar] Canvasノードが未設定です。インスペクタからCanvasを設定してください。');
}
// fillTransformが指定されていてfullWidthが0以下なら、現在の幅を基準とする
if (this.fillTransform) {
if (this.fullWidth <= 0) {
this.fullWidth = this.fillTransform.width;
}
}
// currentHealthがmaxHealthを超えていたらクランプ
if (this.currentHealth > this.maxHealth) {
this.currentHealth = this.maxHealth;
}
// 初期表示を反映
this.updateHealthVisual();
}
start() {
// ここでは特に追加処理はないが、将来的な拡張用に分離
}
update(deltaTime: number) {
this.updatePosition();
}
/**
* ターゲットのワールド座標をスクリーン座標→Canvasローカル座標に変換し、HPバーの位置を更新する。
*/
private updatePosition() {
if (!this.target || !this.camera || !this.canvas || !this._canvasTransform || !this._selfTransform) {
// 必須参照が揃っていない場合は処理しない
return;
}
// ターゲットのワールド座標を取得
this.target.getWorldPosition(this._worldPos);
// オフセットを加算(頭上に表示)
Vec3.add(this._worldPos, this._worldPos, this.worldOffset);
// ワールド座標をスクリーン座標に変換
this.camera.worldToScreen(this._worldPos, this._screenPos);
// カメラの前面にいるかどうかを簡易チェック(zが0以下 = 背面とみなす)
const isBehindCamera = this._screenPos.z <= 0;
// スクリーン座標をCanvasローカル座標に変換
// view.getDesignResolutionSize() を基準に (0,0)→左下, (w,h)→右上 として扱う
const designSize = view.getDesignResolutionSize();
this._tmp2D.set(this._screenPos.x, this._screenPos.y);
// スクリーン座標系(左下原点)→Canvasローカル座標系(中心原点)への変換
// UITransformのconvertToNodeSpaceARを使う
this._canvasTransform.convertToNodeSpaceAR(new Vec3(this._tmp2D.x, this._tmp2D.y, 0), this._canvasPos);
// HPバーの位置を更新
this.node.setPosition(this._canvasPos);
// 画面外 or 背面のときの表示制御
let visibleByScreen = true;
if (this.hideWhenOffscreen) {
// 画面外判定(少し余白を持たせてもよいが、ここでは厳密に)
const x = this._screenPos.x;
const y = this._screenPos.y;
if (isBehindCamera ||
x < 0 || x > designSize.width ||
y < 0 || y > designSize.height) {
visibleByScreen = false;
}
}
this.node.active = visibleByScreen && this.isVisibleByHealth();
}
/**
* HP割合に応じてバーの見た目を更新する。
*/
private updateHealthVisual() {
// HP値をクランプ
if (this.currentHealth < 0) {
this.currentHealth = 0;
}
if (this.currentHealth > this.maxHealth) {
this.currentHealth = this.maxHealth;
}
const ratio = this.maxHealth > 0 ? (this.currentHealth / this.maxHealth) : 0;
// UITransformの幅で表現
if (this.fillTransform && this.fullWidth > 0) {
this.fillTransform.width = this.fullWidth * math.clamp01(ratio);
}
// Sprite.fillRangeで表現
if (this.fillSprite) {
this.fillSprite.fillRange = math.clamp01(ratio);
}
// ProgressBar.progressで表現
if (this.progressBar) {
this.progressBar.progress = math.clamp01(ratio);
}
// HP割合による可視/不可視
if (this.minVisibleRatio > 0) {
const visibleByHealth = ratio >= this.minVisibleRatio;
// 位置更新側の画面内判定とANDを取りたいので、ここではフラグだけにしておきたいが、
// node.activeを直接いじるとカメラ判定と競合する可能性があるため、
// isVisibleByHealth()で評価し、updatePosition()側で最終的なactiveを決定する。
// ここでは特に何もしない(視覚更新のみ)。
if (!visibleByHealth) {
// ただし、すぐに非表示にしたい場合は以下を有効化しても良い
// this.node.active = false;
}
}
}
/**
* HP割合に基づいて可視かどうかを返す。
*/
private isVisibleByHealth(): boolean {
if (this.minVisibleRatio <= 0) {
return true;
}
const ratio = this.maxHealth > 0 ? (this.currentHealth / this.maxHealth) : 0;
return ratio >= this.minVisibleRatio;
}
// ===== 公開API: 他のスクリプトからHPを操作するためのメソッド群 =====
/**
* HPを絶対値で設定します。
* @param value 新しい現在HP値
*/
public setHealth(value: number) {
this.currentHealth = value;
this.updateHealthVisual();
}
/**
* HPを最大値ごと設定します(最大値変更時に現在値もクランプされます)。
* @param max 最大HP値
* @param current 現在HP値(省略時はmaxと同じ)
*/
public setMaxHealth(max: number, current?: number) {
this.maxHealth = Math.max(1, max);
if (current !== undefined) {
this.currentHealth = current;
} else if (this.currentHealth > this.maxHealth) {
this.currentHealth = this.maxHealth;
}
this.updateHealthVisual();
}
/**
* HPを減少させます(ダメージ処理用)。
* @param amount 減少量(正の値)
*/
public applyDamage(amount: number) {
this.setHealth(this.currentHealth - Math.abs(amount));
}
/**
* HPを回復させます。
* @param amount 回復量(正の値)
*/
public heal(amount: number) {
this.setHealth(this.currentHealth + Math.abs(amount));
}
}
コードのポイント解説
- onLoad
- 自身と Canvas の
UITransformを取得し、存在しない場合はconsole.errorで明示的に通知。 fillTransformが設定されていてfullWidth <= 0の場合、現在の幅を基準値として採用。- 初期 HP 値をクランプし、
updateHealthVisual()で見た目を初期化。
- 自身と Canvas の
- update
- 毎フレーム
updatePosition()を呼び、ターゲットの位置に追従。
- 毎フレーム
- updatePosition
- ターゲットのワールド座標 +
worldOffsetを取得。 Camera.worldToScreen()でスクリーン座標に変換。UITransform.convertToNodeSpaceAR()を使い、スクリーン座標 → Canvas ローカル座標に変換し、HP バーの位置に反映。hideWhenOffscreenが true の場合、画面外やカメラ背面にいるときはnode.active = false。- HP 割合による非表示閾値(
minVisibleRatio)も考慮し、最終的なnode.activeを決定。
- ターゲットのワールド座標 +
- updateHealthVisual
- HP 値を 0〜maxHealth にクランプ。
- HP 割合
ratioを計算し、fillTransform.width/Sprite.fillRange/ProgressBar.progressに反映。
- 公開API
setHealth,setMaxHealth,applyDamage,healを用意し、他スクリプトから簡単に HP 操作が可能。- これらはすべて
updateHealthVisual()を内部で呼び出し、見た目を自動更新。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を
FloatingHealthBar.tsにします。 - 作成された
FloatingHealthBar.tsをダブルクリックし、上記のコード全文を貼り付けて保存します。
2. シーンの準備(3Dターゲットとカメラ)
- Hierarchy で右クリック → Create → 3D Object → Capsule などを選び、テスト用の敵ノードを作成します。
- 名前を
Enemyに変更しておくと分かりやすいです。
- 名前を
- すでに 3D カメラ(デフォルトの
Main Cameraなど)があることを確認します。- なければ Hierarchy で右クリック → Create → 3D Object → Camera で作成し、名前を
Main Cameraにします。
- なければ Hierarchy で右クリック → Create → 3D Object → Camera で作成し、名前を
3. Canvas と HPバー用UIノードの作成
- Hierarchy で右クリック → Create → UI → Canvas を選択し、
Canvasノードを作成します。Canvasノードには自動的にCanvasコンポーネントとUITransformが付与されます。
Canvasノードを選択し、Inspector の Render Mode をSCREEN_SPACE_CAMERAもしくはSCREEN_SPACE_OVERLAYに設定します。- 3D シーンと UI を組み合わせる場合は、通常 SCREEN_SPACE_CAMERA を使用し、
Cameraプロパティに UI 用カメラを指定します。 - 今回はシンプルに SCREEN_SPACE_OVERLAY でも動作します。
- 3D シーンと UI を組み合わせる場合は、通常 SCREEN_SPACE_CAMERA を使用し、
Canvasノードを右クリック → Create → UI → Sprite を選択し、HP バー用のノードを作成します。- 名前を
EnemyHPBarなどに変更します。 - このノードには
SpriteとUITransformが自動で付与されます。 - Inspector の Size(Width / Height)を例えば
100 x 10などに設定します。
- 名前を
- HP バーの背景と前景を分けたい場合:
EnemyHPBarを背景用とし、その子として Create → UI → Sprite でFillノードを作成します。FillノードのSpriteに緑色のバー画像などを設定し、UITransform.widthを 100 にしておきます。
4. FloatingHealthBar コンポーネントのアタッチ
- Hierarchy で
EnemyHPBar(またはFillノード)を選択します。 - Inspector で Add Component → Custom → FloatingHealthBar を選択し、コンポーネントを追加します。
5. インスペクタでプロパティを設定
EnemyHPBar(または Fill)に追加した FloatingHealthBar コンポーネントを選択し、以下のように設定します。
- Target:
- Hierarchy から 3D の
Enemyノードをドラッグして、このフィールドにドロップします。
- Hierarchy から 3D の
- Camera:
- 3D シーンを映している
Main CameraノードのCameraコンポーネントをドラッグ&ドロップします。
- 3D シーンを映している
- Canvas:
- Hierarchy の
Canvasノードをドラッグして、このフィールドにドロップします。
- Hierarchy の
- World Offset:
- デフォルトの
(0, 2, 0)のままで問題ありませんが、キャラの身長に応じて Y 値を調整してください。
- デフォルトの
- Hide When Offscreen:
- 画面外では HP バーを隠したい場合は
チェックONのままにします。
- 画面外では HP バーを隠したい場合は
- Max Health / Current Health:
- テストとして
Max Health = 100,Current Health = 100に設定します。
- テストとして
- Fill Transform:
- バーの長さで HP を表現する場合:
- 背景ノード
EnemyHPBarの子にあるFillノードを選択し、そのUITransformをドラッグ&ドロップします。 - 今回
FloatingHealthBarをFillノードにアタッチしている場合は、そのノード自身のUITransformを指定してもOKです。
- 背景ノード
- バーの長さで HP を表現する場合:
- Fill Sprite(任意):
- Sprite の
fillRangeで表現したい場合は、FillノードのSpriteをドラッグ&ドロップし、Sprite の Type をFILLEDに設定してください。
- Sprite の
- Progress Bar(任意):
ProgressBarコンポーネントを使う場合は、それをドラッグ&ドロップします。
- Full Width:
- 0 のままにしておくと、起動時に
fillTransform.widthの値が自動的に基準として使用されます。
- 0 のままにしておくと、起動時に
- Min Visible Ratio:
- HP が 0 になってもバーを表示したい場合は
0のままにします。 - HP 1% 未満で非表示にしたい場合は
0.01などに設定します。
- HP が 0 になってもバーを表示したい場合は
6. 再生して動作確認
- 再生ボタン(▶)を押してゲームを実行します。
- Scene ビューまたは Game ビューで、
- 3D の
Enemyの頭上に HP バーが表示されていること。 - カメラを移動・回転させると、HP バーが画面上でキャラに追従すること。
- 3D の
- Inspector で実行中に
FloatingHealthBarの Current Health の値を 100 → 50 → 10 → 0 と変化させてみます。- バーの長さ / fillRange / progress が HP 割合に応じて縮むことを確認します。
7. 他のスクリプトからHPを操作してみる(任意)
FloatingHealthBar は外部シングルトンに依存せず、直接コンポーネント参照さえあれば HP 操作が可能です。例えば、敵ノード側に以下のようなテストスクリプトを追加してみます。
import { _decorator, Component, Node } from 'cc';
import { FloatingHealthBar } from './FloatingHealthBar';
const { ccclass, property } = _decorator;
@ccclass('EnemyTest')
export class EnemyTest extends Component {
@property({ type: FloatingHealthBar, tooltip: 'この敵のHPバー' })
public healthBar: FloatingHealthBar | null = null;
private _timer = 0;
update(deltaTime: number) {
if (!this.healthBar) {
return;
}
this._timer += deltaTime;
if (this._timer > 1.0) {
this._timer = 0;
// 1秒ごとに10ダメージ
this.healthBar.applyDamage(10);
}
}
}
このように、FloatingHealthBar の公開メソッドを呼ぶだけで HP 表示を制御できます。
まとめ
本記事では、Cocos Creator 3.8 / TypeScript 向けに、
- 3D 空間上のターゲットノードの頭上に HP バーを表示し、
- カメラの動きに合わせて UI 上で追従させ、
- HP 値の変更に応じてバーの見た目を自動更新する、
という機能を、1つの独立コンポーネント FloatingHealthBar だけで完結させる実装を行いました。
ポイントは以下の通りです。
- 外部依存なし: GameManager やシングルトンに依存せず、インスペクタの
@propertyだけで全設定を完結。 - 再利用性: 敵・プレイヤー・NPC など、どの 3D ノードにも簡単に使い回せる汎用設計。
- 柔軟な見た目: UITransform / Sprite / ProgressBar のいずれでも HP 表示が可能で、アート側の自由度が高い。
- 防御的実装: 必須コンポーネント(UITransform など)の有無をチェックし、エラーログで編集ミスを検出。
このコンポーネントをプロジェクトのテンプレートとして用意しておけば、新しい敵キャラを追加するたびに、「HPバー用UIノードを作って FloatingHealthBar をアタッチ → ターゲットとカメラとCanvasを指定」するだけで、頭上 HP バーがすぐに利用できます。
応用として、
- HP 以外の「頭上ゲージ」(スタミナ・シールド・キャストバーなど)
- プレイヤー名やダメージ数値のフローティングテキスト
- ターゲットアイコンやクエストマーカーの追従 UI
などにも、ほぼ同じ構造で流用できます。
本記事の FloatingHealthBar をベースに、プロジェクトに合わせたカスタム UI を作り込んでみてください。




