【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- 死亡時に再生するアニメーションステート名。
useDeathAnimationがtrueのときのみ有効。- 空文字の場合、警告を出してアニメーション再生をスキップする。
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コンポーネントを取得し、useDeathAnimationがtrueなのに見つからなければwarnを出します。startHealthが-1の場合はmaxHealthを使用し、それ以外は0 ~ maxHealthにクランプして_currentHealthを初期化します。- 初期HPが0以下なら
_isDeadをtrueにします。
start()- 開始時点で HP が 0 以下なのに
_isDeadがfalseの場合(異常系)には念のため_handleDeath()を呼びます。
- 開始時点で HP が 0 以下なのに
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イベントを発行します。useDeathAnimationとdeathAnimationState、Animationコンポーネントの有無から「アニメーションを再生すべきか」を判定します。destroyOnDeathとwaitDeathAnimationToDestroyに応じて:- アニメーション完了を待ってから
destroy()する。 - アニメーションを再生しつつ、すぐに
destroy()する。 - アニメーションのみ再生し、ノードは削除しない。
- アニメーションを再生せずにすぐ
destroy()する。
- アニメーション完了を待ってから
- アニメーション再生で例外が起きた場合でも
errorログを出して他の処理が止まらないようにしています。
使用手順と動作確認
1. スクリプトファイルを作成する
- エディタ上部の Assets パネルで、任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create → TypeScript を選択します。
- 新しく作成されたスクリプトの名前を
HealthManager.tsに変更します。 - ダブルクリックしてエディタ(VS Codeなど)で開き、内容をすべて削除して、前章の
HealthManagerのコードを貼り付けて保存します。
2. テスト用ノードを作成する
まずは簡単に挙動を確認するためのノードを作成します。
- Hierarchy パネルで右クリックし、Create → 2D Object → Sprite(または Node)を選択します。
- 作成されたノードの名前を分かりやすく
TestEnemyなどに変更します。 - もし死亡アニメーションを試したい場合は、このノードに Animation コンポーネントとアニメーションクリップを事前に設定しておきます。
- Inspector → Add Component → Animation を追加。
- 任意のアニメーションクリップ(例:
enemy_die)をClipsに登録し、ステート名を確認しておきます(通常はクリップ名と同じ)。
3. HealthManager コンポーネントをアタッチする
- Hierarchy で先ほど作成した
TestEnemyノードを選択します。 - Inspector パネル下部の Add Component ボタンをクリックします。
- Custom Component(または Custom カテゴリ)から HealthManager を選択します。
4. インスペクタでプロパティを設定する
TestEnemy ノードを選択した状態で、Inspector に表示される HealthManager の項目を次のように設定してみます。
Max Health:100Start Health:100(または-1にしてMax Healthと同じにする)Destroy On Death:trueUse Death Animation:- アニメーションを試したい場合:
true - まずはシンプルに削除だけ試したい場合:
false
- アニメーションを試したい場合:
Death Animation State:- 例えば
enemy_dieなど、Animation コンポーネントに設定したステート名。 Use Death Animationをtrueにした場合のみ必須。
- 例えば
Wait Death Animation To Destroy:- アニメーション完了後に消したい場合:
true - アニメーションを再生しつつ即座に消したい場合:
false
- アニメーション完了後に消したい場合:
Log Debug:true(挙動確認のため最初はオンにするのがおすすめ)
5. 動作確認(コンソールからダメージを与えてみる)
エディタのプレビューで実際にダメージを与えて挙動を確認します。
- エディタ上部の Preview ボタン(再生マーク)を押してゲームを実行します。
- ゲームビューが表示されたら、Developer Tools(ブラウザのコンソール)を開きます。
- ブラウザプレビューの場合:
F12またはCtrl+Shift+Iなどで開き、Console タブを選択。
- ブラウザプレビューの場合:
- コンソールから
TestEnemyのHealthManagerを取得してダメージを与えます。- 例:
// ノードを名前から取得(同名ノードが複数ある場合は注意) const node = cc.director.getScene().getChildByName('TestEnemy'); const hm = node.getComponent('HealthManager'); // 現在HPを確認 hm.getCurrentHealth(); // 30ダメージを与える hm.applyDamage(30); // 即死させる hm.kill(); Log Debugをtrueにしていれば、コンソールに HP の変化や死亡ログが表示されます。
- 例:
- 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システムへ発展させることも容易です。まずはこのシンプルな形から、プロジェクトに合わせてカスタマイズしてみてください。




