【Cocos Creator 3.8】PoisonEffect(毒状態)の実装:アタッチするだけで一定間隔ごとに継続ダメージを与える汎用スクリプト
このガイドでは、任意のキャラクターや敵ノードにアタッチするだけで「毒状態(DoT:Damage over Time)」を実現できる PoisonEffect コンポーネントを実装します。
本来であれば「HealthManager」のような別スクリプトと連携させるケースが多い機能ですが、本記事のコンポーネントは完全に独立して動作するように設計します。
内部にシンプルな HP 管理機能を持たせ、インスペクタから HP や毒ダメージ量・間隔を調整できるため、プレイヤー・敵・ギミックなど様々なノードに再利用できます。
コンポーネントの設計方針
1. 要件整理
- 一定時間ごとに「毒ダメージ」を与える。
- 毒は「総継続時間」または「回数」で制御できる。
- 毒の有効/無効をスクリプトまたはインスペクタから切り替え可能。
- 他のカスタムスクリプト(HealthManager 等)に依存しない。
- 内部に簡易 HP 管理を持ち、0 以下になったら「死亡フラグ」を立てる。
- Editor 上で挙動を確認しやすいように、ログ出力をオプションで有効化できる。
今回、「親の HealthManager を呼び出す」という要件は:
- 外部依存を禁止する条件と衝突するため、
- コンポーネント内部に簡易 HealthManager 的な仕組みを内包する形で解決します。
将来的に本格的な HealthManager と連携したくなった場合も、applyDamage メソッドをフックポイントとして、そこだけ書き換えれば簡単に統合できるような構造にしておきます。
2. インスペクタで設定可能なプロパティ設計
以下のプロパティを用意します。
- maxHealth: number
- 初期 HP(最大 HP)を指定します。
- ゲーム開始時に
currentHealthがこの値で初期化されます。
- startWithFullHealth: boolean
- チェック時、
onLoadでcurrentHealth = maxHealthに自動設定します。 - 外部から HP を事前にセットしたい場合はオフにします。
- チェック時、
- currentHealth: number
- 現在 HP。インスペクタから直接変更可能にし、デバッグにも利用します。
- 実行中に毒ダメージで減少していきます。
- enablePoisonOnStart: boolean
- チェック時、
start()で自動的に毒状態を開始します。 - スクリプトから手動で開始したい場合はオフにします。
- チェック時、
- damagePerTick: number
- 1 回の毒ダメージ量。
- 正の値で HP が減少します(0 以下を設定してもダメージは発生しません)。
- tickInterval: number
- 毒ダメージが発生する間隔(秒)。
- 例:0.5 なら 0.5 秒ごとに 1 回ダメージ。
- totalDuration: number
- 毒の総継続時間(秒)。
- 0 以下の場合は「時間制限なし」として扱い、
isPoisonActiveが true の間ずっと継続します。
- maxTicks: number
- 毒ダメージが発生する最大回数。
- 0 以下の場合は「回数制限なし」として扱います。
totalDurationとmaxTicksの両方が設定されている場合、どちらかの条件を満たした時点で毒が終了します。
- stopWhenDead: boolean
- HP が 0 以下になったときに毒を自動停止するかどうか。
- チェック時、死亡と同時に毒も止まります。
- autoDestroyOnDeath: boolean
- 死亡時にこのノード自体を
destroy()するかどうか。 - チェック時、HP が 0 以下になった瞬間にノードが破棄されます。
- 死亡時にこのノード自体を
- logDebug: boolean
- チェック時、コンソールに毒ダメージや HP 変化のログを出力します。
- 開発・デバッグ時に有効化し、リリース時はオフにする想定です。
また、スクリプト内から利用する以下の状態変数も用意します(インスペクタには公開しない):
private _isPoisonActive: boolean… 現在毒状態かどうか。private _elapsedSinceLastTick: number… 最後のダメージからの経過時間。private _elapsedTotal: number… 毒開始からの総経過時間。private _appliedTicks: number… すでに適用されたダメージ回数。private _isDead: boolean… HP が 0 以下になったかどうか。
TypeScriptコードの実装
import { _decorator, Component, Node, log, warn } from 'cc';
const { ccclass, property } = _decorator;
/**
* PoisonEffect
* 任意のノードにアタッチして使用する「毒状態(継続ダメージ)」コンポーネント。
* 外部の HealthManager などに依存せず、内部で簡易 HP 管理を行います。
*/
@ccclass('PoisonEffect')
export class PoisonEffect extends Component {
// ====== HP 管理関連 ======
@property({
tooltip: '最大HP(初期HP)。startWithFullHealth が有効な場合、ゲーム開始時に currentHealth にこの値が設定されます。'
})
public maxHealth: number = 100;
@property({
tooltip: '有効にすると、onLoad で currentHealth が maxHealth に自動設定されます。'
})
public startWithFullHealth: boolean = true;
@property({
tooltip: '現在のHP。毒ダメージで減少していきます。デバッグ用にインスペクタから直接変更も可能です。'
})
public currentHealth: number = 100;
@property({
tooltip: '有効にすると、start() 時に自動的に毒状態を開始します。'
})
public enablePoisonOnStart: boolean = true;
// ====== 毒ダメージ関連 ======
@property({
tooltip: '1回の毒ダメージ量。正の値でHPが減少します。0以下の場合はダメージが発生しません。'
})
public damagePerTick: number = 5;
@property({
tooltip: '毒ダメージが発生する間隔(秒)。例:0.5なら0.5秒ごとに1回ダメージ。'
})
public tickInterval: number = 1.0;
@property({
tooltip: '毒の総継続時間(秒)。0以下の場合は時間制限なしとして扱います。'
})
public totalDuration: number = 5.0;
@property({
tooltip: '毒ダメージの最大回数。0以下の場合は回数制限なしとして扱います。'
})
public maxTicks: number = 0;
@property({
tooltip: 'HPが0以下になったときに毒を自動停止するかどうか。'
})
public stopWhenDead: boolean = true;
@property({
tooltip: 'HPが0以下になったときにこのノードを自動的にdestroy()するかどうか。'
})
public autoDestroyOnDeath: boolean = false;
@property({
tooltip: '有効にすると、毒ダメージやHP変化などのデバッグログをコンソールに出力します。'
})
public logDebug: boolean = true;
// ====== 内部状態 ======
private _isPoisonActive: boolean = false;
private _elapsedSinceLastTick: number = 0;
private _elapsedTotal: number = 0;
private _appliedTicks: number = 0;
private _isDead: boolean = false;
// ====== ライフサイクル ======
onLoad() {
// HP初期化
if (this.startWithFullHealth) {
this.currentHealth = this.maxHealth;
}
// 不正値に対する防御的処理
if (this.tickInterval <= 0) {
warn('[PoisonEffect] tickInterval が 0 以下です。1秒に補正します。');
this.tickInterval = 1.0;
}
if (this.damagePerTick < 0) {
warn('[PoisonEffect] damagePerTick が負の値です。正の値にしてください。現在値:', this.damagePerTick);
}
if (this.logDebug) {
log(`[PoisonEffect] onLoad: maxHealth=${this.maxHealth}, currentHealth=${this.currentHealth}`);
}
}
start() {
// 自動で毒を開始する設定なら有効化
if (this.enablePoisonOnStart) {
this.startPoison();
}
}
update(deltaTime: number) {
// 毒が有効でなければ何もしない
if (!this._isPoisonActive) {
return;
}
// すでに死亡していて、かつ「死亡で停止」が有効なら何もしない
if (this._isDead && this.stopWhenDead) {
return;
}
// 経過時間の更新
this._elapsedSinceLastTick += deltaTime;
this._elapsedTotal += deltaTime;
// totalDuration が設定されていて、超過した場合は毒終了
if (this.totalDuration > 0 && this._elapsedTotal >= this.totalDuration) {
if (this.logDebug) {
log('[PoisonEffect] totalDuration に達したため毒を終了します。');
}
this.stopPoison();
return;
}
// tickInterval を超えたらダメージ適用
if (this._elapsedSinceLastTick >= this.tickInterval) {
this._elapsedSinceLastTick -= this.tickInterval;
this.applyPoisonTick();
}
}
// ====== 公開メソッド ======
/**
* 毒状態を開始します。
* enablePoisonOnStart が false の場合、スクリプトや他コンポーネントから明示的に呼び出してください。
*/
public startPoison(): void {
if (this._isPoisonActive) {
if (this.logDebug) {
log('[PoisonEffect] すでに毒状態です。startPoison() は無視されました。');
}
return;
}
this._isPoisonActive = true;
this._elapsedSinceLastTick = 0;
this._elapsedTotal = 0;
this._appliedTicks = 0;
if (this.logDebug) {
log('[PoisonEffect] 毒状態を開始しました。');
}
}
/**
* 毒状態を停止します。
*/
public stopPoison(): void {
if (!this._isPoisonActive) {
return;
}
this._isPoisonActive = false;
if (this.logDebug) {
log('[PoisonEffect] 毒状態を停止しました。');
}
}
/**
* 現在の毒状態かどうかを返します。
*/
public isPoisonActive(): boolean {
return this._isPoisonActive;
}
/**
* 外部から任意のダメージを与えたい場合に使用できます。
* 今回は毒ダメージ内部からもこのメソッドを利用します。
*/
public applyDamage(amount: number): void {
if (amount <= 0) {
return;
}
if (this._isDead) {
// すでに死亡している場合はダメージを無視
return;
}
const prevHealth = this.currentHealth;
this.currentHealth -= amount;
if (this.logDebug) {
log(`[PoisonEffect] ダメージ適用: -${amount} (HP: ${prevHealth} → ${this.currentHealth})`);
}
if (this.currentHealth <= 0) {
this.currentHealth = 0;
this.handleDeath();
}
}
/**
* 現在のHPが0以下かどうかを返します。
*/
public isDead(): boolean {
return this._isDead;
}
// ====== 内部処理 ======
/**
* 1回分の毒ダメージ処理。
*/
private applyPoisonTick(): void {
// maxTicks が設定されていて、すでに上限に達している場合は何もしない
if (this.maxTicks > 0 && this._appliedTicks >= this.maxTicks) {
if (this.logDebug) {
log('[PoisonEffect] maxTicks に達したため毒を終了します。');
}
this.stopPoison();
return;
}
if (this.damagePerTick <= 0) {
if (this.logDebug) {
warn('[PoisonEffect] damagePerTick が 0 以下のため、毒ダメージは発生しません。');
}
return;
}
this._appliedTicks++;
if (this.logDebug) {
log(`[PoisonEffect] 毒ダメージ発生 (${this._appliedTicks} 回目)。`);
}
this.applyDamage(this.damagePerTick);
// ダメージ適用後に死亡し、かつ stopWhenDead が有効なら毒を止める
if (this._isDead && this.stopWhenDead) {
if (this.logDebug) {
log('[PoisonEffect] 死亡したため毒を停止します。');
}
this.stopPoison();
}
}
/**
* HPが0以下になったときの処理。
*/
private handleDeath(): void {
if (this._isDead) {
return;
}
this._isDead = true;
if (this.logDebug) {
log('[PoisonEffect] HP が 0 になりました。死亡状態です。');
}
if (this.autoDestroyOnDeath) {
if (this.logDebug) {
log('[PoisonEffect] autoDestroyOnDeath が有効のため、このノードを destroy() します。');
}
this.node.destroy();
}
}
}
コードの要点解説
- onLoad
startWithFullHealthが true の場合、currentHealthをmaxHealthで初期化。tickIntervalが 0 以下なら 1 秒に補正し、警告ログを出します。- デバッグ用に初期状態をログ出力(
logDebugが true のとき)。
- start
enablePoisonOnStartが true の場合、自動でstartPoison()を呼び出し、毒を開始します。
- update
- 毒が有効でない場合は即 return。
- 死亡済みかつ
stopWhenDeadが true の場合も処理を中断。 _elapsedSinceLastTickと_elapsedTotalにdeltaTimeを加算。totalDurationを超えたら毒を停止。tickIntervalを超えるたびにapplyPoisonTick()を呼び出してダメージを与えます。
- applyPoisonTick
maxTicksに達していれば毒を終了。damagePerTickが 0 以下なら警告して何もしない。- 有効な場合は
_appliedTicksをインクリメントし、applyDamage()で HP を減らします。 - ダメージ後に死亡し、
stopWhenDeadが true なら毒を停止。
- applyDamage
- 0 以下のダメージは無視。
- すでに死亡している場合も無視。
- HP を減算し、0 以下になったら
handleDeath()を呼び出します。
- handleDeath
_isDeadを true に設定。autoDestroyOnDeathが true の場合、ノードをdestroy()。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ上部メニューまたは Assets パネルで、Assets フォルダを右クリックします。
- Create → TypeScript を選択します。
- 新しく作成されたスクリプトファイルの名前を
PoisonEffect.tsに変更します。 - ダブルクリックしてエディタ(VS Code 等)で開き、既存のコードをすべて削除して、前述の
PoisonEffectコードを貼り付けて保存します。
2. テスト用ノードの作成
- Hierarchy パネルで右クリックし、Create → 3D Object → Cube など、任意のノードを作成します。
- 2D プロジェクトの場合は Create → 2D Object → Sprite などでも構いません。
- 作成したノードの名前を分かりやすく
PoisonTestTargetなどに変更しておきます。
3. コンポーネントのアタッチ
- Hierarchy で先ほど作成した
PoisonTestTargetノードを選択します。 - Inspector パネル下部の Add Component ボタンをクリックします。
- Custom Component または Custom カテゴリから PoisonEffect を選択します。
- リストに見つからない場合は、スクリプトの保存とコンパイルが完了しているかを確認し、エディタを一度再起動してみてください。
4. プロパティの設定例
Inspector で PoisonEffect コンポーネントに以下のように設定してみましょう。
- maxHealth:
100 - startWithFullHealth:
チェック - currentHealth:
100(自動で 100 になっているはず) - enablePoisonOnStart:
チェック(ゲーム開始と同時に毒開始) - damagePerTick:
10 - tickInterval:
1.0(1 秒ごとにダメージ) - totalDuration:
0(0 なので時間制限なし) - maxTicks:
10(10 回ダメージを与えたら毒終了) - stopWhenDead:
チェック - autoDestroyOnDeath:
任意(動きを見たい場合はオフにしておくとノードが残ります) - logDebug:
チェック(コンソールで挙動を確認するためオン推奨)
この設定では:
- ゲーム開始と同時に毒状態が開始されます。
- 1 秒ごとに 10 ダメージが 10 回発生します(合計 100 ダメージ)。
- HP は 100 → 0 まで減少し、10 回目のダメージで死亡します。
stopWhenDeadがオンなので、死亡と同時に毒も停止します。
5. 再生して動作確認
- エディタ上部の Play ボタンを押してゲームを再生します。
- 再生中に Console パネル(Developer → Console など)を開きます。
- 設定どおりであれば、次のようなログが 1 秒ごとに出力されます(
logDebugが true の場合):[PoisonEffect] 毒状態を開始しました。[PoisonEffect] 毒ダメージ発生 (1 回目)。[PoisonEffect] ダメージ適用: -10 (HP: 100 → 90)- …
[PoisonEffect] 毒ダメージ発生 (10 回目)。[PoisonEffect] ダメージ適用: -10 (HP: 10 → 0)[PoisonEffect] HP が 0 になりました。死亡状態です。[PoisonEffect] 死亡したため毒を停止します。
- ゲームビューから再生を停止すると、編集モードに戻ります。
6. 応用的な使い方
- スクリプトから毒のオン/オフを制御する
- 別のコンポーネントから
getComponent(PoisonEffect)で取得し、startPoison()やstopPoison()を呼び出せます。 - 例:プレイヤーが毒沼に入ったときだけ毒を開始し、抜けたら停止するなど。
- 別のコンポーネントから
- 時間制限付きの毒
totalDuration = 5、tickInterval = 0.5、maxTicks = 0とすると、5 秒間だけ 0.5 秒ごとにダメージが発生します。
- 回数制限のみで制御する毒
totalDuration = 0、maxTicks = 3、tickInterval = 2とすると、合計 3 回だけ 2 秒ごとにダメージが発生します。
まとめ
この PoisonEffect コンポーネントは、
- 内部に簡易 HP 管理機能を持ち、
- 毒ダメージ量・間隔・継続時間・回数・死亡時の挙動などをインスペクタから柔軟に調整でき、
- 他のカスタムスクリプトに一切依存せず、任意のノードにアタッチするだけで「毒状態(継続ダメージ)」を実現
できる汎用コンポーネントです。
ゲーム開発では、「プレイヤー」「敵」「トラップ」「ギミック」など、多くのオブジェクトが継続ダメージ系の効果を共有します。本記事のように、外部依存を排しつつ再利用性の高いコンポーネントとして実装しておくことで、
- 新しい敵を追加するときにアタッチするだけで毒耐性テストができる
- ダメージ量や間隔をインスペクタで調整しながらゲームバランスを素早く検証できる
- 将来、本格的な HealthManager を導入する際も
applyDamage()だけを書き換えることで移行しやすい
といったメリットがあります。
このパターンをベースに、「燃焼(Burn)」「出血(Bleed)」「リジェネ(回復)」「シールド」など、他の状態異常・バフ系コンポーネントも同様の設計で増やしていくと、Cocos Creator 3.8 プロジェクト全体の保守性・拡張性が大きく向上します。




