【Cocos Creator 3.8】DestructibleWall の実装:アタッチするだけで「攻撃でHPが減り、0で砕け散って消える壁」を実現する汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで「HPを持つ壊れる壁」として機能する DestructibleWall コンポーネントを実装します。
攻撃(ダメージ)を受けるたびにHPが減り、0以下になった瞬間に「砕け散るパーティクル演出」「破壊音」「コライダー無効化」「ノード削除」までを一括で処理します。
外部の GameManager やシングルトンに一切依存せず、Inspector でプロパティを設定するだけで完結するように設計します。
コンポーネントの設計方針
1. 機能要件の整理
- 壁は HP(耐久値)を持つ。
- 外部から「ダメージを与える」メソッドを呼び出すと HP が減る。
- HP が 0 以下になった瞬間に「破壊処理」を行う。
- 同じノードに付いているコライダー(2D/3D)を無効化する。
- 任意のパーティクル(破片など)を再生する。
- 任意の破壊サウンドを再生する。
- 一定時間後にノード自体を破棄(
destroy())する。
- すべての設定は Inspector から行い、他のカスタムスクリプトには依存しない。
- 標準コンポーネントが不足している場合は、防御的にログを出す。
2. 外部依存をなくすためのアプローチ
- ダメージのトリガー方法は「汎用メソッド
applyDamage(amount: number)」を公開する形にする。- 攻撃側のスクリプトから
getComponent(DestructibleWall)?.applyDamage(10);のように呼び出せる。 - このコンポーネント側から攻撃システムには一切依存しない。
- 攻撃側のスクリプトから
- パーティクル・サウンドなどの演出は「Inspector で任意のノード / AudioSource を指定」してもらう。
- 指定されていなければ、その演出だけスキップし、ログで知らせる。
- Collider(2D / 3D)は存在する場合のみ自動で無効化し、存在しない場合はログを出す。
3. Inspector で設定可能なプロパティ
基本設定
maxHp: number- 壁の最大HP(耐久値)。初期HPもこの値になる。
- 例: 100 に設定すると、合計 100 ダメージで破壊される。
destroyDelay: number- 破壊演出を再生してからノードを削除するまでの遅延時間(秒)。
- 例: 0.5 にすると、0.5秒後に
this.node.destroy()が呼ばれる。
invincible: boolean- true の間はダメージを受け付けない(デバッグや演出用)。
演出関連
destroyEffectNode: Node | null- 破壊時に再生するパーティクル等がアタッチされたノード。
- 例: 子ノードに ParticleSystem2D や ParticleSystem を付けて指定する。
- 再生後、必要に応じて自動破棄されるようにパーティクル側を設定しておくのがおすすめ。
playEffectOnSelf: booleandestroyEffectNodeが未設定のときに、this.node上のパーティクルを探して再生するかどうか。- true の場合、同一ノードにある
ParticleSystem/ParticleSystem2Dを自動で再生。
destroySound: AudioSource | null- 破壊時に再生するサウンド。
- ノードに
AudioSourceを追加し、ここにドラッグ&ドロップで設定。
ログ・デバッグ関連
enableLog: boolean- true のとき、ダメージや破壊タイミングを
console.logで出力。
- true のとき、ダメージや破壊タイミングを
TypeScriptコードの実装
import { _decorator, Component, Node, ParticleSystem, ParticleSystem2D, AudioSource, Collider, Collider2D, ITriggerEvent, ICollisionEvent, log, warn } from 'cc';
const { ccclass, property } = _decorator;
/**
* DestructibleWall
* 任意のノードにアタッチするだけで「HPを持つ壊れる壁」として機能するコンポーネント。
* 外部の GameManager やシングルトンには依存しません。
*/
@ccclass('DestructibleWall')
export class DestructibleWall extends Component {
@property({
tooltip: '壁の最大HP(初期HP)。この値を超えて回復はしません。'
})
public maxHp: number = 100;
@property({
tooltip: '破壊演出を再生してからノードを削除するまでの遅延時間(秒)。'
})
public destroyDelay: number = 0.3;
@property({
tooltip: 'true の間はダメージを受け付けません(デバッグ・演出用)。'
})
public invincible: boolean = false;
@property({
tooltip: '破壊時に再生するエフェクトが付いたノード(任意)。未設定の場合は playEffectOnSelf の設定に従います。'
})
public destroyEffectNode: Node | null = null;
@property({
tooltip: 'destroyEffectNode が未設定のとき、同一ノード上のパーティクルを自動で再生するかどうか。'
})
public playEffectOnSelf: boolean = true;
@property({
type: AudioSource,
tooltip: '破壊時に再生するサウンド(任意)。'
})
public destroySound: AudioSource | null = null;
@property({
tooltip: 'true のとき、ダメージや破壊状況をログ出力します。'
})
public enableLog: boolean = false;
// 現在HP(実行時に maxHp から初期化)
private _currentHp: number = 0;
// 破壊済みフラグ(二重破壊防止)
private _isDestroyed: boolean = false;
onLoad() {
// HP初期化
this._currentHp = this.maxHp;
if (this.enableLog) {
log(`[DestructibleWall] onLoad: 初期HP = ${this._currentHp}`);
}
// Collider / Collider2D があるか確認(任意)
const col3d = this.getComponent(Collider);
const col2d = this.getComponent(Collider2D);
if (!col3d && !col2d) {
warn('[DestructibleWall] このノードには Collider / Collider2D がありません。物理的な当たり判定が必要な場合は追加してください。');
}
// Collider2D のイベントを使いたい場合の例(攻撃との接触で自動的にダメージを与えたいときなど)
// ここではサンプルとしてイベント登録だけ行い、実際のダメージ判定はユーザー側で拡張してください。
if (col2d) {
col2d.on('onBeginContact', this._onBeginContact2D, this);
}
if (col3d) {
col3d.on('onCollisionEnter', this._onCollisionEnter3D, this);
}
}
onDestroy() {
// イベント登録している場合は解除(メモリリーク防止)
const col3d = this.getComponent(Collider);
const col2d = this.getComponent(Collider2D);
if (col2d) {
col2d.off('onBeginContact', this._onBeginContact2D, this);
}
if (col3d) {
col3d.off('onCollisionEnter', this._onCollisionEnter3D, this);
}
}
/**
* 外部からダメージを与えるための公開メソッド。
* 攻撃側のスクリプトから呼び出してください。
* 例: targetNode.getComponent(DestructibleWall)?.applyDamage(10);
*/
public applyDamage(amount: number): void {
if (this._isDestroyed) {
return;
}
if (this.invincible) {
if (this.enableLog) {
log('[DestructibleWall] 無敵状態のためダメージを無視しました。');
}
return;
}
if (amount <= 0) {
warn('[DestructibleWall] applyDamage に 0 以下の値が渡されました。処理をスキップします。');
return;
}
const prevHp = this._currentHp;
this._currentHp -= amount;
if (this.enableLog) {
log(`[DestructibleWall] ダメージ: ${amount}, HP: ${prevHp} → ${this._currentHp}`);
}
if (this._currentHp <= 0) {
this._currentHp = 0;
this._handleDestruction();
}
}
/**
* 外部から回復させたい場合の任意メソッド(必要に応じて使用)。
*/
public heal(amount: number): void {
if (this._isDestroyed) {
return;
}
if (amount <= 0) {
warn('[DestructibleWall] heal に 0 以下の値が渡されました。処理をスキップします。');
return;
}
const prevHp = this._currentHp;
this._currentHp = Math.min(this._currentHp + amount, this.maxHp);
if (this.enableLog) {
log(`[DestructibleWall] 回復: ${amount}, HP: ${prevHp} → ${this._currentHp}`);
}
}
/**
* 現在HPを取得(読み取り専用)したいとき用。
*/
public get currentHp(): Readonly<number> {
return this._currentHp;
}
/**
* 破壊処理の本体。
*/
private _handleDestruction(): void {
if (this._isDestroyed) {
return;
}
this._isDestroyed = true;
if (this.enableLog) {
log('[DestructibleWall] HP が 0 になったため破壊処理を開始します。');
}
// コライダーの無効化(2D / 3D 両方に対応)
const col3d = this.getComponent(Collider);
const col2d = this.getComponent(Collider2D);
if (col3d) {
col3d.enabled = false;
}
if (col2d) {
col2d.enabled = false;
}
if (!col3d && !col2d) {
warn('[DestructibleWall] 破壊処理: 無効化する Collider が見つかりませんでした。');
}
// 破壊エフェクト再生
this._playDestroyEffect();
// 破壊サウンド再生
this._playDestroySound();
// 一定時間後にノード削除
if (this.destroyDelay <= 0) {
this.node.destroy();
} else {
this.scheduleOnce(() => {
if (this.node && this.node.isValid) {
this.node.destroy();
}
}, this.destroyDelay);
}
}
/**
* 破壊エフェクト再生処理。
*/
private _playDestroyEffect(): void {
// 優先: destroyEffectNode が指定されている場合
if (this.destroyEffectNode) {
// Node 上の ParticleSystem / ParticleSystem2D を探して再生
const ps3d = this.destroyEffectNode.getComponent(ParticleSystem);
const ps2d = this.destroyEffectNode.getComponent(ParticleSystem2D);
if (ps3d) {
ps3d.play();
}
if (ps2d) {
ps2d.play();
}
if (!ps3d && !ps2d) {
warn('[DestructibleWall] destroyEffectNode に ParticleSystem / ParticleSystem2D が見つかりませんでした。');
}
return;
}
// destroyEffectNode が未指定で、かつ playEffectOnSelf が true の場合
if (this.playEffectOnSelf) {
const ps3d = this.getComponent(ParticleSystem);
const ps2d = this.getComponent(ParticleSystem2D);
if (ps3d) {
ps3d.play();
}
if (ps2d) {
ps2d.play();
}
if (!ps3d && !ps2d) {
// エフェクトが無くても致命的ではないので warn に留める
warn('[DestructibleWall] 同一ノード上に再生可能なパーティクルがありませんでした。エフェクトは再生されません。');
}
}
}
/**
* 破壊サウンド再生処理。
*/
private _playDestroySound(): void {
if (!this.destroySound) {
return;
}
this.destroySound.play();
}
/**
* 2D 物理の接触開始イベント例。
* 実際のダメージ量は攻撃側のタグやコンポーネントを見て決めるなど、
* プロジェクト側で拡張してください。
*/
private _onBeginContact2D(selfCollider: Collider2D, otherCollider: Collider2D, contact: ICollisionEvent | ITriggerEvent | null): void {
// デフォルトでは何もしません。必要に応じてタグなどで判定してダメージを与えてください。
// 例:
// if (otherCollider.tag === 1) {
// this.applyDamage(10);
// }
}
/**
* 3D 物理の衝突開始イベント例。
*/
private _onCollisionEnter3D(selfCollider: Collider, otherCollider: Collider): void {
// こちらもデフォルトでは何もしません。
// 例:
// if (otherCollider.tag === 1) {
// this.applyDamage(10);
// }
}
}
コードの要点解説
onLoadmaxHpから_currentHpを初期化。Collider/Collider2Dの有無をチェックし、ない場合はwarnを出す。- 2D/3D の物理イベントに登録(サンプル用。実際のダメージ判定はユーザー側で拡張)。
applyDamage(amount)- 外部から呼び出してダメージを与えるための公開メソッド。
- 無敵フラグや 0 以下の値を防御的にチェック。
- HP が 0 以下になった場合に
_handleDestruction()を呼ぶ。
_handleDestruction()- 二重呼び出し防止のため
_isDestroyedを立てる。 - Collider / Collider2D を無効化。
- エフェクトとサウンドを再生。
destroyDelay秒後にノードをdestroy()。
- 二重呼び出し防止のため
_playDestroyEffect()destroyEffectNodeが指定されていればそこからパーティクルを探して再生。- 未指定かつ
playEffectOnSelfが true なら、自ノードのパーティクルを再生。
_playDestroySound()destroySoundに設定されたAudioSourceを再生。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ上部の Assets パネルで、任意のフォルダ(例:
assets/scripts)を選択します。 - 右クリック → Create → TypeScript を選択します。
- ファイル名を
DestructibleWall.tsに変更します。 - 作成した
DestructibleWall.tsをダブルクリックしてエディタで開き、既存コードをすべて削除して、前章の TypeScript コードを丸ごと貼り付けて保存します。
2. テスト用の壁ノードを作成
2Dプロジェクトの例で説明します(3Dでも流れは同様です)。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、名前を
Wallに変更します。 - Inspector で Sprite の画像(Texture)を適当な壁の画像に設定します。
- 同じく Inspector の Add Component ボタンをクリックし、Physics 2D → BoxCollider2D を追加します。
- これで攻撃側の Rigidbody2D と物理的に当たる壁になります。
3. DestructibleWall コンポーネントをアタッチ
- Hierarchy で先ほど作成した
Wallノードを選択します。 - Inspector で Add Component → Custom → DestructibleWall を選択します。
- DestructibleWall のプロパティを設定します。
- Max Hp: 100(例)
- Destroy Delay: 0.3(例)
- Invincible: チェックを外す(false)
- Destroy Effect Node: ひとまず空のままでOK(後で設定)。
- Play Effect On Self: true のまま。
- Destroy Sound: ひとまず空のままでもOK。
- Enable Log: 動作確認のためにチェックを入れておくとログが見やすくなります。
4. 破壊エフェクトの設定(任意)
簡単なパーティクルを同じノードに付ける例です。
Wallノードを選択した状態で、Add Component → Effects → ParticleSystem2D を追加します。- ParticleSystem2D の設定で
- Play On Load: チェックを外す(false)。
- 破片っぽい見た目になるよう、Texture や Emission などを調整します(任意)。
- DestructibleWall の Destroy Effect Node は空のまま、Play Effect On Self を true にしておきます。
- 破壊時に
Wallノード上の ParticleSystem2D が自動でplay()されます。
- 破壊時に
別ノードにエフェクトを置きたい場合:
- Hierarchy で
Wallの子として Create → Empty Node でWallEffectノードを作成。 WallEffectに ParticleSystem / ParticleSystem2D を追加し、見た目を調整。- DestructibleWall の Destroy Effect Node に
WallEffectをドラッグ&ドロップ。 - 必要に応じて Play Effect On Self は false にしても構いません。
5. 破壊サウンドの設定(任意)
Wallノードを選択し、Add Component → Audio → AudioSource を追加します。- AudioSource の Clip に、破壊音の AudioClip を設定します。
- AudioSource の Play On Load のチェックを外します。
- DestructibleWall の Destroy Sound に、今追加した AudioSource をドラッグ&ドロップします。
6. 攻撃側からのダメージ呼び出し(簡易テスト)
ここでも外部依存を増やさないため、簡単な「キーを押すとダメージを与える」テストスクリプトを例として示します(任意)。
- Assets パネルで右クリック → Create → TypeScript を選択し、
TestDamageSender.tsを作成。 - 以下のような簡易スクリプトを書きます。
import { _decorator, Component, Node, input, Input, KeyCode, EventKeyboard } from 'cc';
import { DestructibleWall } from './DestructibleWall';
const { ccclass, property } = _decorator;
@ccclass('TestDamageSender')
export class TestDamageSender extends Component {
@property({
type: Node,
tooltip: 'ダメージを与えたい DestructibleWall が付いたノード'
})
public targetNode: Node | null = null;
@property({
tooltip: '1回の入力で与えるダメージ量'
})
public damageAmount: number = 25;
onEnable() {
input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
}
onDisable() {
input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
}
private _onKeyDown(event: EventKeyboard) {
if (event.keyCode === KeyCode.SPACE) {
if (!this.targetNode) {
return;
}
const wall = this.targetNode.getComponent(DestructibleWall);
if (wall) {
wall.applyDamage(this.damageAmount);
}
}
}
}
テスト手順:
- Hierarchy で空のノード
DamageTesterを作成。 DamageTesterにTestDamageSenderをアタッチ。- Target Node に先ほどの
Wallノードをドラッグ&ドロップ。 - プレビューを開始し、ゲームウィンドウがアクティブな状態で Space キー を押すと、1回につき 25 ダメージが入り、4回で壊れることを確認できます。
7. 実行時の確認ポイント
- Console に
[DestructibleWall]からのログが出ているか(Enable Log が true の場合)。 - HP が 0 になると
- コライダーが無効化されているか(物理的にすり抜けるようになる)。
- パーティクルが再生されるか。
- サウンドが再生されるか。
destroyDelay秒後にノードが Hierarchy から消えるか。
まとめ
この DestructibleWall コンポーネントは、
- HP 管理(ダメージ・回復)
- 破壊トリガー(HP 0 以下)
- 破壊演出(パーティクル・サウンド)
- 物理的な当たり判定の無効化
- ノードの自動削除
といった「壊れるオブジェクト」に必要な処理をすべて内包しつつ、外部の GameManager 等に一切依存しないように設計しています。
攻撃側は単に applyDamage() を呼び出すだけでよく、複数のステージ・シーンで再利用しやすい構成になっています。
応用例としては、
- 木箱・岩・障害物など、HP を持つあらゆるオブジェクトへの適用
- ボスの弱点パーツ(特定部位のみ破壊可能)としての利用
- 特定のギミック(スイッチを攻撃して壊すと扉が開くなど)のトリガーとして利用
などが考えられます。
演出はすべて Inspector から差し替えられるため、アート側が自由にパーティクルやサウンドを調整でき、ゲーム開発の効率化にもつながります。
このコンポーネントをベースに、例えば「破壊時にドロップアイテムを生成する」「破壊前に点滅する」といった拡張も、同じ方針(外部依存を持たず、Inspector で設定)で容易に追加できます。




