【Cocos Creator 3.8】StickyBomb(吸着爆弾)の実装:アタッチするだけで「敵にくっついてから数秒後に爆発してダメージを与える」汎用スクリプト
この記事では、任意のノードにアタッチするだけで「当たった相手に吸着し、数秒後に爆発してダメージを与える」挙動を実現する StickyBomb コンポーネントを実装します。
敵やプレイヤーなど、Collider2D を持つどんなノードにも対応できるように設計し、ダメージ量・爆発までの時間・爆発範囲・ヒット対象のレイヤーなどをインスペクタから調整できるようにします。外部の GameManager やシングルトンに依存しないため、このスクリプト単体で完結して再利用できるのがポイントです。
コンポーネントの設計方針
機能要件の整理
- StickyBomb をアタッチしたノードは、物理衝突(Collider2D) を検知できる。
- 最初に衝突した相手に対して:
- そのノードの 子ノード になる(吸着)。
- 自分の位置を相対的に固定(吸着後は物理挙動を止める)。
- 一定時間後に「爆発」する。
- 爆発時の挙動:
- 爆心地(StickyBomb の位置)から 一定半径内の Collider2D を持つノード を検出。
- 検出されたノードに ダメージイベント(カスタムイベント) を送信する。
- 爆発エフェクト用の Prefab を任意で再生可能。
- 爆発後、StickyBomb 自身は自動的に破棄される。
- 外部スクリプト(GameManager, Health コンポーネントなど)には依存しない:
- ダメージの適用は イベント通知 までに留める。
- 受け手側がイベントを受け取ってどう処理するかは自由。
物理・衝突まわりの前提
- StickyBomb ノードには Collider2D(例:CircleCollider2D)を付ける。
- 物理挙動が必要なら RigidBody2D を付ける(任意)。
- 爆発範囲の判定には PhysicsSystem2D.testAABB を使い、円に近似した AABB で検出する。
インスペクタで設定可能なプロパティ
StickyBomb コンポーネントに定義するプロパティと役割は以下の通りです。
- fuseTime: number
- 爆発までの時間(秒)。
- 例:2.0 にすると吸着してから 2 秒後に爆発。
- explosionRadius: number
- 爆発の有効半径(ワールド座標系の単位)。
- 例:2.0 なら、爆心地から半径 2 の範囲にいる Collider2D にダメージイベントを送信。
- damage: number
- 爆発時にターゲットへ通知するダメージ量。
- 実際の HP 計算は受け手側が行う。
- stickOnFirstHitOnly: boolean
- true の場合:最初にぶつかった相手にのみ吸着し、それ以降の衝突は無視。
- false の場合:吸着前の複数回の衝突で最後に触れた相手にくっつけたい場合などに拡張可能(本実装では主に true 想定)。
- autoStartFuseOnStick: boolean
- true の場合:吸着した瞬間に自動でタイマー開始。
- false の場合:外部から
startFuse()メソッドを呼び出す設計にしたい場合に利用。
- destroyOnExplode: boolean
- true の場合:爆発後に StickyBomb ノード自体を自動削除。
- false の場合:爆発後も残したい(デバッグや特殊ギミック用)。
- explosionEffectPrefab: Prefab | null
- 任意の爆発エフェクト用 Prefab。
- 設定されていれば、爆発位置にインスタンスを生成。
- explosionEffectLifetime: number
- 生成した爆発エフェクトを何秒後に自動削除するか。
- 0 以下なら削除しない。
- targetLayerMask: number
- 爆発ダメージの対象とする 物理レイヤーのビットマスク。
- PhysicsSystem2D のレイヤー設定に応じて、敵だけ/プレイヤーだけなどを指定可能。
- debugDrawGizmo: boolean
- エディタ上で爆発半径を簡易表示したい場合に利用(本実装ではログ中心にし、必要最低限の利用)。
- logVerbose: boolean
- true の場合:衝突検出・吸着・爆発・ダメージ通知などを
console.logで詳細に出力(デバッグ用)。
- true の場合:衝突検出・吸着・爆発・ダメージ通知などを
防御的実装のポイント
- onLoad で Collider2D の取得を試みる。存在しない場合はエラーログを出す。
- RigidBody2D がある場合のみ物理挙動を制御し、無い場合でも最低限動作するようにする。
- 爆発時に PhysicsSystem2D が無効な場合はログを出し、処理をスキップする。
- explosionEffectPrefab が未設定でもエラーにならないようにする。
TypeScriptコードの実装
以下が、StickyBomb コンポーネントの完全な実装コードです。
import { _decorator, Component, Node, Collider2D, Contact2DType, IPhysics2DContact, RigidBody2D, Vec2, Vec3, PhysicsSystem2D, Rect, Prefab, instantiate, director } from 'cc';
const { ccclass, property } = _decorator;
/**
* StickyBomb
* - Collider2D を持つノードにアタッチして使用。
* - 最初に衝突した相手ノードに吸着し、一定時間後に爆発してダメージイベントを送信する。
*
* ダメージ通知仕様:
* - 爆発時、範囲内のターゲットノードに対して以下のイベントを emit します。
* node.emit('StickyBombHit', {
* damage: number,
* source: Node, // 爆弾ノード
* position: Vec3 // 爆心地(ワールド座標)
* });
*
* 受け手側は、任意のコンポーネントで以下のようにリスナーを登録できます:
* this.node.on('StickyBombHit', (payload) => {
* const dmg = payload.damage;
* // HP 減算などをここで実装
* }, this);
*/
@ccclass('StickyBomb')
export class StickyBomb extends Component {
@property({
tooltip: '吸着してから爆発するまでの時間(秒)。0 以下の場合は即時爆発します。'
})
public fuseTime: number = 2.0;
@property({
tooltip: '爆発の有効半径(ワールド座標系の単位)。この半径内の Collider2D を持つノードにダメージイベントを送信します。'
})
public explosionRadius: number = 2.0;
@property({
tooltip: '爆発時に対象へ通知するダメージ量。実際の HP 計算は受け手側が行います。'
})
public damage: number = 10;
@property({
tooltip: 'true の場合、最初に衝突した相手にのみ吸着し、それ以降の衝突は無視します。'
})
public stickOnFirstHitOnly: boolean = true;
@property({
tooltip: 'true の場合、吸着した瞬間に自動でタイマーを開始して爆発します。false の場合は startFuse() を手動で呼び出す前提です。'
})
public autoStartFuseOnStick: boolean = true;
@property({
tooltip: 'true の場合、爆発後にこの StickyBomb ノードを自動的に削除します。'
})
public destroyOnExplode: boolean = true;
@property({
type: Prefab,
tooltip: '任意の爆発エフェクト用 Prefab。設定されていれば、爆発位置にインスタンスを生成します。'
})
public explosionEffectPrefab: Prefab | null = null;
@property({
tooltip: '生成した爆発エフェクトを何秒後に自動削除するか。0 以下の場合は削除しません。'
})
public explosionEffectLifetime: number = 1.0;
@property({
tooltip: '爆発ダメージの対象とする物理レイヤーのビットマスク。PhysicsSystem2D のレイヤー設定に従います。0 の場合はすべてのレイヤーを対象とします。'
})
public targetLayerMask: number = 0;
@property({
tooltip: 'true の場合、衝突検出・吸着・爆発・ダメージ通知などの詳細ログをコンソールに出力します。'
})
public logVerbose: boolean = false;
private _collider: Collider2D | null = null;
private _rigidBody: RigidBody2D | null = null;
private _stuck: boolean = false;
private _fuseStarted: boolean = false;
private _elapsedFuseTime: number = 0;
private _stuckTarget: Node | null = null;
onLoad() {
// 必須ではないが、衝突検出のために Collider2D を期待する
this._collider = this.getComponent(Collider2D);
if (!this._collider) {
console.error('[StickyBomb] Collider2D がアタッチされていません。このコンポーネントを使用するノードには必ず Collider2D を追加してください。');
} else {
// 衝突コールバックを登録
this._collider.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
}
// RigidBody2D があれば取得(任意)
this._rigidBody = this.getComponent(RigidBody2D);
}
start() {
if (this.logVerbose) {
console.log('[StickyBomb] start: fuseTime=', this.fuseTime, 'explosionRadius=', this.explosionRadius);
}
}
onDestroy() {
if (this._collider) {
this._collider.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
}
}
update(deltaTime: number) {
if (!this._fuseStarted) {
return;
}
this._elapsedFuseTime += deltaTime;
if (this.logVerbose) {
// 短時間に大量ログを避けるなら条件を入れてもよいが、ここではシンプルに。
}
if (this._elapsedFuseTime >= this.fuseTime) {
this._fuseStarted = false;
this._explode();
}
}
/**
* 衝突開始時のコールバック。
* 最初に衝突した相手に吸着し、必要であれば fuse を開始します。
*/
private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
if (this._stuck && this.stickOnFirstHitOnly) {
// すでに吸着済みで、最初の一回だけ吸着する設定なら無視
return;
}
const otherNode = otherCollider.node;
// 自分自身やすでに吸着しているターゲットには反応しない
if (otherNode === this.node || otherNode === this._stuckTarget) {
return;
}
if (this.logVerbose) {
console.log('[StickyBomb] OnBeginContact with', otherNode.name);
}
this._stickToTarget(otherNode);
}
/**
* 指定ノードに吸着する。
*/
private _stickToTarget(target: Node) {
this._stuck = true;
this._stuckTarget = target;
// 物理挙動を止める(RigidBody2D がある場合のみ)
if (this._rigidBody) {
this._rigidBody.linearVelocity = new Vec2(0, 0);
this._rigidBody.angularVelocity = 0;
this._rigidBody.enabled = false;
}
// ワールド座標を保持したまま、ターゲットの子ノードにする
const worldPos = this.node.worldPosition.clone();
this.node.parent = target;
this.node.worldPosition = worldPos;
if (this.logVerbose) {
console.log('[StickyBomb] Stuck to target:', target.name);
}
if (this.autoStartFuseOnStick) {
this.startFuse();
}
}
/**
* 外部から手動で fuse(爆発タイマー)を開始したい場合に使用。
* autoStartFuseOnStick が false のときに利用する想定。
*/
public startFuse() {
if (this._fuseStarted) {
return;
}
this._fuseStarted = true;
this._elapsedFuseTime = 0;
if (this.logVerbose) {
console.log('[StickyBomb] Fuse started. Will explode in', this.fuseTime, 'seconds.');
}
// fuseTime が 0 以下なら即時爆発
if (this.fuseTime <= 0) {
this._fuseStarted = false;
this._explode();
}
}
/**
* 爆発処理。
* - explosionRadius 内の Collider2D を検索し、ダメージイベントを送信。
* - explosionEffectPrefab があれば再生。
* - destroyOnExplode が true なら自分を削除。
*/
private _explode() {
const worldPos = this.node.worldPosition.clone();
if (this.logVerbose) {
console.log('[StickyBomb] Explode at', worldPos.toString(), 'radius=', this.explosionRadius);
}
// 爆発エフェクトの生成
this._spawnExplosionEffect(worldPos);
// 物理システムが有効か確認
const physics = PhysicsSystem2D.instance;
if (!physics) {
console.error('[StickyBomb] PhysicsSystem2D.instance が取得できません。爆発ダメージ判定をスキップします。');
} else {
// 爆発半径を含む AABB を作成
const half = this.explosionRadius;
const minX = worldPos.x - half;
const minY = worldPos.y - half;
const maxX = worldPos.x + half;
const maxY = worldPos.y + half;
const aabb = new Rect(minX, minY, maxX - minX, maxY - minY);
const results: Collider2D[] = [];
physics.testAABB(aabb, results);
if (this.logVerbose) {
console.log('[StickyBomb] testAABB found', results.length, 'colliders in AABB.');
}
for (const col of results) {
const targetNode = col.node;
// 自分自身はスキップ
if (targetNode === this.node) {
continue;
}
// レイヤーマスクのチェック(0 の場合はすべて許可)
if (this.targetLayerMask !== 0) {
const layerBit = 1 << targetNode.layer;
if ((this.targetLayerMask & layerBit) === 0) {
continue;
}
}
// 中心からの距離で円形判定(AABB だけだと角を拾うので、距離チェックで円に近づける)
const targetWorldPos = targetNode.worldPosition;
const dx = targetWorldPos.x - worldPos.x;
const dy = targetWorldPos.y - worldPos.y;
const distSq = dx * dx + dy * dy;
if (distSq > this.explosionRadius * this.explosionRadius) {
continue;
}
// ダメージイベントを送信
const payload = {
damage: this.damage,
source: this.node,
position: worldPos.clone(),
};
if (this.logVerbose) {
console.log('[StickyBomb] Damage emit to', targetNode.name, 'payload=', payload);
}
targetNode.emit('StickyBombHit', payload);
}
}
// 爆発後の処理
if (this.destroyOnExplode) {
// 一旦 disable にしてから削除
this.node.active = false;
this.scheduleOnce(() => {
if (this.node && this.node.isValid) {
this.node.destroy();
}
}, 0);
}
}
/**
* 爆発エフェクトを生成し、必要であれば一定時間後に削除する。
*/
private _spawnExplosionEffect(worldPos: Vec3) {
if (!this.explosionEffectPrefab) {
return;
}
const effectNode = instantiate(this.explosionEffectPrefab);
if (!effectNode) {
console.error('[StickyBomb] explosionEffectPrefab のインスタンス化に失敗しました。');
return;
}
// シーンのルートに配置
const scene = director.getScene();
if (!scene) {
console.error('[StickyBomb] シーンがロードされていないため、爆発エフェクトを配置できません。');
return;
}
scene.addChild(effectNode);
effectNode.worldPosition = worldPos;
if (this.explosionEffectLifetime > 0) {
this.scheduleOnce(() => {
if (effectNode && effectNode.isValid) {
effectNode.destroy();
}
}, this.explosionEffectLifetime);
}
}
}
コードの要点解説
- onLoad
- Collider2D を取得し、存在しない場合は
console.errorで警告。 - Collider2D がある場合、
BEGIN_CONTACTイベントに_onBeginContactを登録。 - RigidBody2D があれば取得(なくても動作はする)。
- Collider2D を取得し、存在しない場合は
- _onBeginContact
- 最初に衝突した相手ノードを取得し、
_stickToTargetを呼び出して吸着。 stickOnFirstHitOnlyが true の場合、2 回目以降の衝突は無視。
- 最初に衝突した相手ノードを取得し、
- _stickToTarget
- RigidBody2D があれば無効化して物理挙動を停止。
- 現在のワールド座標を保持しつつ、ターゲットの子ノードに変更して「くっついた」状態を再現。
autoStartFuseOnStickが true ならstartFuse()を呼んでタイマー開始。
- update
_fuseStartedが true のときだけ経過時間をカウント。- 経過時間が
fuseTimeに達したら_explode()を呼び出し、爆発処理へ。
- _explode
- 爆発エフェクトを生成(
_spawnExplosionEffect)。 - PhysicsSystem2D.testAABB で爆発範囲の AABB 内にある Collider2D を取得。
- レイヤーマスクと距離チェックで「爆発半径内」のターゲットだけに絞り込み。
- 該当ノードに
StickyBombHitイベントを emit(ダメージ量や爆心地情報を含む)。 destroyOnExplodeが true の場合、自身のノードを削除。
- 爆発エフェクトを生成(
- _spawnExplosionEffect
- explosionEffectPrefab が設定されていれば instantiate してシーンに追加。
- explosionEffectLifetime > 0 の場合、指定秒数後にエフェクトノードを自動削除。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- 作成されたスクリプトの名前を StickyBomb.ts に変更します。
- ダブルクリックしてエディタ(VS Code など)で開き、既存コードをすべて削除して、上記の
StickyBombコードを貼り付けて保存します。
2. 爆弾用ノードの作成
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、爆弾オブジェクト用のノードを作成します。
- 名前例:StickyBombTest
- 作成したノードを選択し、Inspector パネルで以下を設定します。
- Add Component → Physics 2D → Collider から、CircleCollider2D など任意の Collider2D を追加します。
- 必要に応じて Add Component → Physics 2D → RigidBody2D を追加し、爆弾を物理的に飛ばしたい場合は BodyType や重力などを調整します。
- 同じく Inspector の Add Component → Custom から StickyBomb を選択し、このノードにアタッチします。
3. StickyBomb プロパティの設定
StickyBomb コンポーネントを選択すると、Inspector に以下のプロパティが表示されます。テスト用に次のような値を設定してみましょう。
- Fuse Time(fuseTime):
2.0- くっついてから 2 秒後に爆発。
- Explosion Radius(explosionRadius):
2.0- 爆発半径 2 の範囲にいる Collider2D にダメージイベント。
- Damage(damage):
20 - Stick On First Hit Only(stickOnFirstHitOnly):
ON - Auto Start Fuse On Stick(autoStartFuseOnStick):
ON - Destroy On Explode(destroyOnExplode):
ON - Explosion Effect Prefab(explosionEffectPrefab):任意(なければ空で構いません)
- Explosion Effect Lifetime(explosionEffectLifetime):
1.0 - Target Layer Mask(targetLayerMask):
0- まずは 0(すべてのレイヤー対象)でテスト。
- Log Verbose(logVerbose):
ON- 挙動確認のためにログを有効にしておくと便利です。
4. 衝突相手(敵ノード)の準備
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、敵用ノードを作成します。
- 名前例:EnemyDummy
- EnemyDummy ノードを選択し、Inspector で以下を設定します。
- Add Component → Physics 2D → Collider から、BoxCollider2D など任意の Collider2D を追加。
- 必要に応じて RigidBody2D も追加(Static / Dynamic はお好みで)。
- ダメージイベントを受け取る簡易テスト用スクリプト(任意)を作成します。
- Assets パネルで右クリック → Create → TypeScript を選択し、StickyBombHitLogger.ts と名付けます。
- 以下のような簡単なコードを貼り付けます。
import { _decorator, Component } from 'cc'; const { ccclass } = _decorator; @ccclass('StickyBombHitLogger') export class StickyBombHitLogger extends Component { onLoad() { this.node.on('StickyBombHit', (payload: any) => { console.log('[StickyBombHitLogger]', this.node.name, 'got hit. damage=', payload.damage, 'from=', payload.source.name); }, this); } } - EnemyDummy ノードを選択し、Add Component → Custom → StickyBombHitLogger をアタッチします。
5. テストシーンのセットアップ
- StickyBombTest(爆弾)と EnemyDummy(敵)をシーン内に配置し、適度な距離を置いておきます。
- 物理挙動で爆弾を飛ばしたい場合:
- StickyBombTest の RigidBody2D に初速度(Linear Velocity)を設定して、敵に向かって飛ぶようにします。
- または、簡易的に Update スクリプトで移動させても構いません。
- 上部ツールバーの Play ボタンを押してゲームを再生します。
6. 動作確認
ゲーム再生中に以下を確認してみてください。
- StickyBombTest が EnemyDummy に衝突した瞬間:
- StickyBombTest ノードが EnemyDummy の子ノードになり、位置が EnemyDummy に対して相対的に固定されます。
- RigidBody2D が付いていれば無効化され、物理挙動が止まります。
- コンソールに
[StickyBomb] Stuck to target: EnemyDummyといったログが出力されます(logVerbose が ON の場合)。
- 吸着してから 2 秒後(fuseTime の値に応じて):
- StickyBombTest の位置を中心に爆発判定が行われます。
- 爆発半径内に EnemyDummy の Collider2D が存在すれば、EnemyDummy ノードに
StickyBombHitイベントが emit されます。 - EnemyDummy にアタッチした StickyBombHitLogger がイベントを受け取り、コンソールに
[StickyBombHitLogger] EnemyDummy got hit. damage= 20 from= StickyBombTestのようなログが出力されます。
- destroyOnExplode が ON の場合、爆発後に StickyBombTest ノードは自動的にシーンから削除されます。
爆発エフェクト用の Prefab を設定している場合は、爆発の瞬間にエフェクトが再生され、explosionEffectLifetime 秒後に自動削除されます。
まとめ
今回実装した StickyBomb コンポーネントは、
- Collider2D を持つ任意のノードにアタッチするだけで
- 衝突 → 吸着 → 一定時間後に爆発 → 範囲内にダメージ通知
という一連の処理を完結させる汎用スクリプトです。
- 外部の GameManager や Health コンポーネントに依存せず、ダメージはイベント通知 に留めているため、どんなプロジェクトにも組み込みやすい設計になっています。
- fuseTime / explosionRadius / damage / targetLayerMask / explosionEffectPrefab などをインスペクタから調整するだけで、さまざまなバリエーションの吸着爆弾を簡単に作れます。
応用例としては:
- プレイヤーが投げるグレネードを StickyBomb に置き換えて、「敵にくっつく爆弾」にする。
- ステージギミックとして、壁や床にくっつく地雷を実装する(衝突対象のレイヤーを壁用レイヤーにする)。
- 敵同士を巻き込む爆発を作りたい場合、targetLayerMask を「敵レイヤー」に限定する。
このように、StickyBomb.ts という 単一スクリプトだけで完結した汎用コンポーネント を用意しておくと、ゲーム内のさまざまなオブジェクトに簡単に「吸着して爆発する」挙動を付与でき、プロトタイピングや量産が大幅に効率化されます。
必要に応じて、
- 爆発時のノックバック(RigidBody2D への力の付与)
- 味方・敵・中立など、タグや追加レイヤーによる判定
- ネットワーク同期(爆発イベントの送信)
といった機能を、このコンポーネントをベースに拡張していくことも容易です。まずはこの記事のコードをそのまま貼り付けて、シーンにアタッチして動作を確認してみてください。




