【Cocos Creator】アタッチするだけ!FloatingHealthBar (頭上HPバー)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【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% 未満で非表示。

また、防御的実装として以下を行います。

  • onLoadcanvasUITransform があるか確認し、なければエラーログ。
  • 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() で見た目を初期化。
  • 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. スクリプトファイルの作成

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を FloatingHealthBar.ts にします。
  3. 作成された FloatingHealthBar.ts をダブルクリックし、上記のコード全文を貼り付けて保存します。

2. シーンの準備(3Dターゲットとカメラ)

  1. Hierarchy で右クリック → Create → 3D Object → Capsule などを選び、テスト用の敵ノードを作成します。
    • 名前を Enemy に変更しておくと分かりやすいです。
  2. すでに 3D カメラ(デフォルトの Main Camera など)があることを確認します。
    • なければ Hierarchy で右クリック → Create → 3D Object → Camera で作成し、名前を Main Camera にします。

3. Canvas と HPバー用UIノードの作成

  1. Hierarchy で右クリック → Create → UI → Canvas を選択し、Canvas ノードを作成します。
    • Canvas ノードには自動的に Canvas コンポーネントと UITransform が付与されます。
  2. Canvas ノードを選択し、Inspector の Render ModeSCREEN_SPACE_CAMERA もしくは SCREEN_SPACE_OVERLAY に設定します。
    • 3D シーンと UI を組み合わせる場合は、通常 SCREEN_SPACE_CAMERA を使用し、Camera プロパティに UI 用カメラを指定します。
    • 今回はシンプルに SCREEN_SPACE_OVERLAY でも動作します。
  3. Canvas ノードを右クリック → Create → UI → Sprite を選択し、HP バー用のノードを作成します。
    • 名前を EnemyHPBar などに変更します。
    • このノードには SpriteUITransform が自動で付与されます。
    • Inspector の Size(Width / Height)を例えば 100 x 10 などに設定します。
  4. HP バーの背景と前景を分けたい場合:
    • EnemyHPBar を背景用とし、その子として Create → UI → SpriteFill ノードを作成します。
    • Fill ノードの Sprite に緑色のバー画像などを設定し、UITransform.width を 100 にしておきます。

4. FloatingHealthBar コンポーネントのアタッチ

  1. Hierarchy で EnemyHPBar(または Fill ノード)を選択します。
  2. Inspector で Add Component → Custom → FloatingHealthBar を選択し、コンポーネントを追加します。

5. インスペクタでプロパティを設定

EnemyHPBar(または Fill)に追加した FloatingHealthBar コンポーネントを選択し、以下のように設定します。

  1. Target:
    • Hierarchy から 3D の Enemy ノードをドラッグして、このフィールドにドロップします。
  2. Camera:
    • 3D シーンを映している Main Camera ノードの Camera コンポーネントをドラッグ&ドロップします。
  3. Canvas:
    • Hierarchy の Canvas ノードをドラッグして、このフィールドにドロップします。
  4. World Offset:
    • デフォルトの (0, 2, 0) のままで問題ありませんが、キャラの身長に応じて Y 値を調整してください。
  5. Hide When Offscreen:
    • 画面外では HP バーを隠したい場合は チェックON のままにします。
  6. Max Health / Current Health:
    • テストとして Max Health = 100, Current Health = 100 に設定します。
  7. Fill Transform:
    • バーの長さで HP を表現する場合:
      • 背景ノード EnemyHPBar の子にある Fill ノードを選択し、その UITransform をドラッグ&ドロップします。
      • 今回 FloatingHealthBarFill ノードにアタッチしている場合は、そのノード自身の UITransform を指定してもOKです。
  8. Fill Sprite(任意):
    • Sprite の fillRange で表現したい場合は、Fill ノードの Sprite をドラッグ&ドロップし、Sprite の TypeFILLED に設定してください。
  9. Progress Bar(任意):
    • ProgressBar コンポーネントを使う場合は、それをドラッグ&ドロップします。
  10. Full Width:
    • 0 のままにしておくと、起動時に fillTransform.width の値が自動的に基準として使用されます。
  11. Min Visible Ratio:
    • HP が 0 になってもバーを表示したい場合は 0 のままにします。
    • HP 1% 未満で非表示にしたい場合は 0.01 などに設定します。

6. 再生して動作確認

  1. 再生ボタン(▶)を押してゲームを実行します。
  2. Scene ビューまたは Game ビューで、
    • 3D の Enemy の頭上に HP バーが表示されていること。
    • カメラを移動・回転させると、HP バーが画面上でキャラに追従すること。
  3. Inspector で実行中に FloatingHealthBarCurrent 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 を作り込んでみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!