【Cocos Creator】アタッチするだけ!HealthManager (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】HealthManagerの実装:アタッチするだけでHP管理と死亡時処理(ノード削除 or アニメーション再生)を実現する汎用スクリプト

このガイドでは、どんなゲームオブジェクトにもアタッチするだけで「HPを持たせる」「ダメージ/回復」「HPが0になったら死亡処理(ノード削除 or アニメーション再生)」「死亡イベント通知」ができる汎用コンポーネント HealthManager を実装します。

敵キャラ・プレイヤー・破壊可能オブジェクトなど、あらゆるノードにそのまま使い回せるよう、外部スクリプトに一切依存しない完全独立設計にします。


コンポーネントの設計方針

機能要件の整理

  • ノードに「最大HP」「現在HP」を持たせる。
  • ダメージ/回復を行うメソッドを公開する。
  • HPが0以下になった瞬間に「死亡」と判定する。
  • 死亡時に以下のいずれかの動作を選択できるようにする:
    • ノードを即座に削除する。
    • 指定したアニメーションステートを再生し、再生完了後に削除する(任意)。
    • 何も削除せず、単に「死亡した」というイベントだけ通知する。
  • 死亡時に EventTarget を用いた died イベント(シグナル)を発行する。
  • 一度死亡したら、二重で死亡処理が走らないようにする(フラグ管理)。

外部依存をなくすための設計

  • ゲーム全体の管理クラス(GameManagerなど)には一切依存しない。
  • 必要な設定値はすべてインスペクタの @property で受け取る。
  • アニメーションを再生する場合のみ、同じノードに Animation コンポーネントが存在することを前提とするが、存在しなければエラーログを出力して処理を安全にスキップする。
  • 死亡イベント(シグナル)は EventTarget を内部に持ち、外部スクリプトから onDied / offDied メソッド経由で購読できるようにする。

インスペクタで設定可能なプロパティ設計

  • maxHealth: number
    • 最大HP。
    • 正の値で指定。初期値は 100
    • ゲーム開始時の現在HPは、この最大HPで初期化される。
  • startHealth: number
    • ゲーム開始時の現在HP。
    • 0 <= startHealth <= maxHealth の範囲に自動クランプされる。
    • 初期値は -1 とし、「-1 の場合は maxHealth で初期化」という意味にする。
  • destroyOnDeath: boolean
    • 死亡時にノードを削除するかどうか。
    • true の場合:死亡処理後に this.node.destroy() を呼び出す。
    • false の場合:ノードは残るが、死亡フラグと死亡イベントは発行される。
    • 初期値は true
  • useDeathAnimation: boolean
    • 死亡時にアニメーションを再生するかどうか。
    • true の場合:指定したアニメーションステート名を再生する。
    • false の場合:アニメーション再生は行わない。
    • 初期値は false
  • deathAnimationState: string
    • 死亡時に再生するアニメーションステート名。
    • useDeathAnimationtrue のときのみ有効。
    • 空文字の場合、警告を出してアニメーション再生をスキップする。
  • waitDeathAnimationToDestroy: boolean
    • 死亡アニメーション再生完了を待ってからノードを削除するかどうか。
    • true の場合:アニメーション完了イベントを待ってから destroy
    • false の場合:アニメーションを再生しても、すぐに destroy する。
    • destroyOnDeath === false の場合、このフラグは無視される。
    • 初期値は true
  • logDebug: boolean
    • ダメージや死亡などのログをコンソールに出すかどうか。
    • デバッグ時に true にすると挙動を追いやすい。
    • 初期値は false

公開メソッド(他スクリプトから呼び出せるAPI)

  • getCurrentHealth(): number – 現在HPを取得。
  • getMaxHealth(): number – 最大HPを取得。
  • isDead(): boolean – すでに死亡しているかどうか。
  • applyDamage(amount: number): void – ダメージを与える(内部で死亡判定)。
  • heal(amount: number): void – 回復処理(最大HPまで)。
  • setHealth(value: number): void – 現在HPを直接設定(クランプ付き)。
  • kill(): void – 強制的にHPを0にして死亡処理を行う。
  • onDied(callback: (self: HealthManager) => void, target?: any): void – 死亡イベント購読。
  • offDied(callback: (self: HealthManager) => void, target?: any): void – 死亡イベント購読解除。

TypeScriptコードの実装


import { _decorator, Component, Node, Animation, EventTarget, log, warn, error } from 'cc';
const { ccclass, property } = _decorator;

/**
 * HealthManager
 * 任意のノードにアタッチして HP 管理と死亡時処理を行う汎用コンポーネント。
 *
 * 主な機能:
 * - 最大HP / 現在HPの管理
 * - ダメージ / 回復 / 強制死亡
 * - HPが0になったときに死亡処理 (ノード削除 or アニメーション再生)
 * - 死亡イベント (died) の発行
 */
@ccclass('HealthManager')
export class HealthManager extends Component {

    @property({
        tooltip: '最大HP。ゲーム開始時のベースとなるHPです。'
    })
    public maxHealth: number = 100;

    @property({
        tooltip: '開始時の現在HP。-1 の場合は maxHealth を使用します。'
    })
    public startHealth: number = -1;

    @property({
        tooltip: '死亡時にこのノードを自動的に destroy するかどうか。'
    })
    public destroyOnDeath: boolean = true;

    @property({
        tooltip: '死亡時にアニメーションを再生するかどうか。'
    })
    public useDeathAnimation: boolean = false;

    @property({
        tooltip: '死亡時に再生する Animation のステート名。\nuseDeathAnimation が true のときのみ使用されます。'
    })
    public deathAnimationState: string = '';

    @property({
        tooltip: '死亡アニメーションの再生完了を待ってからノードを destroy するかどうか。\n※ destroyOnDeath が true の場合のみ有効です。'
    })
    public waitDeathAnimationToDestroy: boolean = true;

    @property({
        tooltip: 'HPの変化や死亡処理をデバッグログとして出力するかどうか。'
    })
    public logDebug: boolean = false;

    // ==========================
    // 内部状態
    // ==========================

    private _currentHealth: number = 0;
    private _isDead: boolean = false;
    private _eventTarget: EventTarget = new EventTarget();
    private _animation: Animation | null = null;

    // イベント名 (外部用に定数化したい場合のために公開)
    public static readonly EVENT_DIED: string = 'died';

    // ==========================
    // ライフサイクル
    // ==========================

    onLoad() {
        // Animation コンポーネント取得 (存在しない場合は null のまま)
        this._animation = this.getComponent(Animation);
        if (!this._animation && this.useDeathAnimation) {
            warn(`[HealthManager] Node "${this.node.name}" に Animation コンポーネントがありませんが、useDeathAnimation が true になっています。アニメーションは再生されません。`);
        }

        // startHealth の初期化ロジック
        if (this.startHealth < 0) {
            this._currentHealth = this.maxHealth;
        } else {
            // 0 ~ maxHealth にクランプ
            this._currentHealth = Math.max(0, Math.min(this.startHealth, this.maxHealth));
        }

        this._isDead = (this._currentHealth <= 0);

        if (this.logDebug) {
            log(`[HealthManager] onLoad - Node="${this.node.name}" maxHealth=${this.maxHealth}, currentHealth=${this._currentHealth}, isDead=${this._isDead}`);
        }
    }

    start() {
        // もし開始時点ですでに HP が 0 以下なら、即座に死亡処理を行う
        if (this._currentHealth <= 0 && !this._isDead) {
            this._handleDeath();
        }
    }

    // update() はこのコンポーネントでは使用しないため省略

    // ==========================
    // 公開 API
    // ==========================

    /**
     * 現在のHPを取得します。
     */
    public getCurrentHealth(): number {
        return this._currentHealth;
    }

    /**
     * 最大HPを取得します。
     */
    public getMaxHealth(): number {
        return this.maxHealth;
    }

    /**
     * すでに死亡しているかどうかを返します。
     */
    public isDead(): boolean {
        return this._isDead;
    }

    /**
     * ダメージを与えます。amount が 0 以下の場合は何もしません。
     * HP が 0 以下になった場合、死亡処理を行います。
     */
    public applyDamage(amount: number): void {
        if (amount <= 0) {
            return;
        }
        if (this._isDead) {
            // すでに死亡している場合は無視
            return;
        }

        const oldHealth = this._currentHealth;
        this._currentHealth = Math.max(0, this._currentHealth - amount);

        if (this.logDebug) {
            log(`[HealthManager] applyDamage - Node="${this.node.name}" damage=${amount}, HP: ${oldHealth} -> ${this._currentHealth}`);
        }

        if (this._currentHealth <= 0) {
            this._handleDeath();
        }
    }

    /**
     * HPを回復します。amount が 0 以下の場合は何もしません。
     * すでに死亡している場合は回復できません。
     */
    public heal(amount: number): void {
        if (amount <= 0) {
            return;
        }
        if (this._isDead) {
            // 死亡後の回復は許可しない
            if (this.logDebug) {
                log(`[HealthManager] heal - Node="${this.node.name}" はすでに死亡しているため、回復できません。`);
            }
            return;
        }

        const oldHealth = this._currentHealth;
        this._currentHealth = Math.min(this.maxHealth, this._currentHealth + amount);

        if (this.logDebug) {
            log(`[HealthManager] heal - Node="${this.node.name}" heal=${amount}, HP: ${oldHealth} -> ${this._currentHealth}`);
        }
    }

    /**
     * 現在HPを直接設定します。0 ~ maxHealth の範囲にクランプされます。
     * HP が 0 になった場合は死亡処理を行います。
     */
    public setHealth(value: number): void {
        if (this._isDead) {
            if (this.logDebug) {
                log(`[HealthManager] setHealth - Node="${this.node.name}" はすでに死亡しているため、HPを変更できません。`);
            }
            return;
        }

        const clamped = Math.max(0, Math.min(value, this.maxHealth));
        const oldHealth = this._currentHealth;
        this._currentHealth = clamped;

        if (this.logDebug) {
            log(`[HealthManager] setHealth - Node="${this.node.name}" HP: ${oldHealth} -> ${this._currentHealth}`);
        }

        if (this._currentHealth <= 0) {
            this._handleDeath();
        }
    }

    /**
     * 強制的にHPを0にして死亡処理を行います。
     */
    public kill(): void {
        if (this._isDead) {
            return;
        }
        if (this.logDebug) {
            log(`[HealthManager] kill - Node="${this.node.name}" 強制死亡`);
        }
        this._currentHealth = 0;
        this._handleDeath();
    }

    /**
     * 死亡イベント (died) を購読します。
     * callback には、このコンポーネント自身 (HealthManager) が渡されます。
     */
    public onDied(callback: (self: HealthManager) => void, target?: any): void {
        this._eventTarget.on(HealthManager.EVENT_DIED, callback, target);
    }

    /**
     * 死亡イベントの購読を解除します。
     */
    public offDied(callback: (self: HealthManager) => void, target?: any): void {
        this._eventTarget.off(HealthManager.EVENT_DIED, callback, target);
    }

    // ==========================
    // 内部処理
    // ==========================

    /**
     * 実際の死亡処理を行います。
     * - フラグ設定
     * - died イベント発行
     * - アニメーション再生
     * - destroyOnDeath の場合はノード削除
     */
    private _handleDeath(): void {
        if (this._isDead) {
            return;
        }

        this._isDead = true;

        if (this.logDebug) {
            log(`[HealthManager] _handleDeath - Node="${this.node.name}" が死亡しました。`);
        }

        // イベント発行
        this._eventTarget.emit(HealthManager.EVENT_DIED, this);

        // アニメーション再生が必要かどうか
        const shouldPlayAnimation = this.useDeathAnimation && !!this._animation && this.deathAnimationState.length > 0;

        if (this.useDeathAnimation && !this._animation) {
            warn(`[HealthManager] _handleDeath - Node="${this.node.name}" に Animation コンポーネントがないため、死亡アニメーションを再生できません。`);
        }
        if (this.useDeathAnimation && this.deathAnimationState.length === 0) {
            warn(`[HealthManager] _handleDeath - deathAnimationState が空です。アニメーションを再生できません。`);
        }

        // ノード削除が必要かどうか
        const shouldDestroy = this.destroyOnDeath;

        if (shouldPlayAnimation) {
            // アニメーションを再生
            try {
                this._animation!.play(this.deathAnimationState);
            } catch (e) {
                error(`[HealthManager] _handleDeath - アニメーション "${this.deathAnimationState}" の再生に失敗しました:`, e);
            }

            if (shouldDestroy && this.waitDeathAnimationToDestroy) {
                // 再生完了を待ってから destroy
                const onFinished = () => {
                    this._animation?.off(Animation.EventType.FINISHED, onFinished, this);
                    if (this.node && this.node.isValid) {
                        if (this.logDebug) {
                            log(`[HealthManager] _handleDeath - 死亡アニメーション完了後に Node="${this.node.name}" を destroy します。`);
                        }
                        this.node.destroy();
                    }
                };
                this._animation.on(Animation.EventType.FINISHED, onFinished, this);
                return;
            }
        }

        // ここまで来たらアニメーション完了を待たずに destroy するか、
        // destroy しない場合は何もしない
        if (shouldDestroy) {
            if (this.node && this.node.isValid) {
                if (this.logDebug) {
                    log(`[HealthManager] _handleDeath - Node="${this.node.name}" を即座に destroy します。`);
                }
                this.node.destroy();
            }
        }
    }
}

主要な処理の解説

  • onLoad()
    • Animation コンポーネントを取得し、useDeathAnimationtrue なのに見つからなければ warn を出します。
    • startHealth-1 の場合は maxHealth を使用し、それ以外は 0 ~ maxHealth にクランプして _currentHealth を初期化します。
    • 初期HPが0以下なら _isDeadtrue にします。
  • start()
    • 開始時点で HP が 0 以下なのに _isDeadfalse の場合(異常系)には念のため _handleDeath() を呼びます。
  • applyDamage()
    • 0以下のダメージは無視します。
    • すでに死亡している場合も無視します。
    • HPを減算し、0未満にはならないよう Math.max(0, ...) でクランプします。
    • 減算後に HP が 0 以下であれば _handleDeath() を呼びます。
  • heal()
    • 0以下の回復量は無視します。
    • 死亡後は回復不可とし、ログのみ出力します。
    • HPを加算し、maxHealth を超えないよう Math.min(maxHealth, ...) でクランプします。
  • setHealth()
    • 現在HPを直接設定するメソッドです。
    • 死亡後は変更不可にしています(ゲームデザインによってはここを変えてもOK)。
    • 設定値を 0 ~ maxHealth にクランプし、0なら _handleDeath() を呼びます。
  • kill()
    • 強制的に HP を 0 にして死亡処理を行います。
    • 即死ギミックやステージ外落下などで便利です。
  • onDied() / offDied()
    • 内部の EventTarget を通して died イベントを購読/解除できます。
    • コールバックには、このコンポーネント自身(HealthManager インスタンス)が渡されるので、そこから HP 情報やノードにアクセスできます。
  • _handleDeath()
    • 死亡判定が通ったときに一度だけ呼ばれる内部メソッドです。
    • _isDead フラグを true にし、died イベントを発行します。
    • useDeathAnimationdeathAnimationStateAnimation コンポーネントの有無から「アニメーションを再生すべきか」を判定します。
    • destroyOnDeathwaitDeathAnimationToDestroy に応じて:
      • アニメーション完了を待ってから destroy() する。
      • アニメーションを再生しつつ、すぐに destroy() する。
      • アニメーションのみ再生し、ノードは削除しない。
      • アニメーションを再生せずにすぐ destroy() する。
    • アニメーション再生で例外が起きた場合でも error ログを出して他の処理が止まらないようにしています。

使用手順と動作確認

1. スクリプトファイルを作成する

  1. エディタ上部の Assets パネルで、任意のフォルダ(例:assets/scripts)を右クリックします。
  2. Create → TypeScript を選択します。
  3. 新しく作成されたスクリプトの名前を HealthManager.ts に変更します。
  4. ダブルクリックしてエディタ(VS Codeなど)で開き、内容をすべて削除して、前章の HealthManager のコードを貼り付けて保存します。

2. テスト用ノードを作成する

まずは簡単に挙動を確認するためのノードを作成します。

  1. Hierarchy パネルで右クリックし、Create → 2D Object → Sprite(または Node)を選択します。
  2. 作成されたノードの名前を分かりやすく TestEnemy などに変更します。
  3. もし死亡アニメーションを試したい場合は、このノードに Animation コンポーネントとアニメーションクリップを事前に設定しておきます。
    • InspectorAdd ComponentAnimation を追加。
    • 任意のアニメーションクリップ(例:enemy_die)を Clips に登録し、ステート名を確認しておきます(通常はクリップ名と同じ)。

3. HealthManager コンポーネントをアタッチする

  1. Hierarchy で先ほど作成した TestEnemy ノードを選択します。
  2. Inspector パネル下部の Add Component ボタンをクリックします。
  3. Custom Component(または Custom カテゴリ)から HealthManager を選択します。

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

TestEnemy ノードを選択した状態で、Inspector に表示される HealthManager の項目を次のように設定してみます。

  • Max Health100
  • Start Health100(または -1 にして Max Health と同じにする)
  • Destroy On Deathtrue
  • Use Death Animation
    • アニメーションを試したい場合:true
    • まずはシンプルに削除だけ試したい場合:false
  • Death Animation State
    • 例えば enemy_die など、Animation コンポーネントに設定したステート名。
    • Use Death Animationtrue にした場合のみ必須。
  • Wait Death Animation To Destroy
    • アニメーション完了後に消したい場合:true
    • アニメーションを再生しつつ即座に消したい場合:false
  • Log Debugtrue(挙動確認のため最初はオンにするのがおすすめ)

5. 動作確認(コンソールからダメージを与えてみる)

エディタのプレビューで実際にダメージを与えて挙動を確認します。

  1. エディタ上部の Preview ボタン(再生マーク)を押してゲームを実行します。
  2. ゲームビューが表示されたら、Developer Tools(ブラウザのコンソール)を開きます。
    • ブラウザプレビューの場合:F12 または Ctrl+Shift+I などで開き、Console タブを選択。
  3. コンソールから TestEnemyHealthManager を取得してダメージを与えます。
    • 例:
      
      // ノードを名前から取得(同名ノードが複数ある場合は注意)
      const node = cc.director.getScene().getChildByName('TestEnemy');
      const hm = node.getComponent('HealthManager');
      
      // 現在HPを確認
      hm.getCurrentHealth();
      
      // 30ダメージを与える
      hm.applyDamage(30);
      
      // 即死させる
      hm.kill();
              
    • Log Debugtrue にしていれば、コンソールに HP の変化や死亡ログが表示されます。
  4. HP が 0 になったタイミングで:
    • Destroy On Death = true の場合:TestEnemy ノードが Hierarchy から消えます。
    • Use Death Animation = true & Wait Death Animation To Destroy = true の場合:死亡アニメーションが再生され、再生完了後にノードが削除されます。
    • Destroy On Death = false の場合:ノードは残りますが、内部的には死亡フラグが立ち、isDead()true を返します。

6. 他スクリプトから死亡イベント(died)を利用する例

同じノードに別のスクリプトをアタッチして、死亡イベントをフックする例です。


import { _decorator, Component, log } from 'cc';
import { HealthManager } from './HealthManager';
const { ccclass } = _decorator;

@ccclass('DeathLogger')
export class DeathLogger extends Component {
    private _health: HealthManager | null = null;

    onLoad() {
        this._health = this.getComponent(HealthManager);
        if (!this._health) {
            return;
        }

        // 死亡イベント購読
        this._health.onDied(this.onDied, this);
    }

    onDestroy() {
        if (this._health) {
            this._health.offDied(this.onDied, this);
        }
    }

    private onDied(health: HealthManager) {
        log(`[DeathLogger] Node="${this.node.name}" が死亡しました。最終HP=${health.getCurrentHealth()}`);
    }
}

このように、HealthManager 自体は完全に独立していながら、必要に応じて他スクリプトから柔軟に連携できます。


まとめ

  • HealthManager を任意のノードにアタッチするだけで:
    • 最大HP/現在HPの管理
    • ダメージ・回復・即死処理
    • HP 0 到達時の死亡処理(ノード削除/アニメーション再生)
    • 死亡イベント(died)の発行

    を一括で扱えるようになりました。

  • 外部の GameManager やシングルトンに一切依存せず、インスペクタプロパティだけで挙動を切り替えられるため、プレハブ化して量産する際にも非常に扱いやすい構成です。
  • 敵キャラ・プレイヤー・ギミックオブジェクトなど、HPという概念を持つすべてのノードにこのコンポーネントを再利用することで、ゲーム全体の HP 管理ロジックを統一でき、バグの混入ポイントを大きく減らせます。
  • さらに、死亡イベントに対して別コンポーネントから演出やスコア加算をフックしていけば、機能ごとに責務を分離した拡張性の高い設計が可能になります。

この HealthManager をベースに、「無敵時間」「ダメージ時のフラッシュエフェクト」「HPバーとの連動」などを追加していくことで、よりリッチな汎用HPシステムへ発展させることも容易です。まずはこのシンプルな形から、プロジェクトに合わせてカスタマイズしてみてください。

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をコピーしました!