【Cocos Creator 3.8】MineLayer の実装:アタッチするだけで「踏むと爆発する地雷エリア」を自動生成する汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで「足元に地雷(Area2D)を設置し、踏まれたら爆発して一定時間で消える」動きを実現する汎用コンポーネント MineLayer を実装します。
プレイヤーや敵キャラなど「地雷をばらまくキャラ」にこのコンポーネントを付けるだけで、一定間隔で足元に爆発判定エリアを生成できるようになります。
目次
コンポーネントの設計方針
1. 機能要件の整理
- このコンポーネントをアタッチしたノード(例: プレイヤー)の足元に「地雷ノード」を生成する。
- 地雷は 2D物理のトリガー判定(
Collider2D)を用いて、「誰かが踏んだ」ことを検出する。 - 踏まれたら爆発状態になり、爆発エフェクトを表示しつつ、一定時間後に自動で消滅する。
- 地雷の見た目(Sprite)・爆発エフェクト(Sprite / アニメーション)・コリジョン形状・サイズなどは、インスペクタのプロパティで調整可能にする。
- 一定間隔で自動的に地雷を設置する「オート設置モード」と、スクリプトから
placeMine()を呼ぶことで設置する「手動設置モード」の両方に対応する。 - 外部のカスタムスクリプトには一切依存せず、このコンポーネント単体で完結させる。
2. 外部依存をなくすためのアプローチ
- 地雷ノードは コード内で新規生成し、必要なコンポーネント(
Sprite,CircleCollider2D,RigidBody2Dなど)をaddComponentで自前で追加する。 - 爆発演出も、別の「ExplosionManager」などには頼らず、地雷ノード自身が見た目を切り替え、一定時間後に自壊する。
- 「誰が踏んだか」を判定するためにタグや専用スクリプトには依存せず、
onBeginContactで「何かが触れたら爆発」というシンプルな仕様にする。
3. インスペクタで設定可能なプロパティ設計
以下のプロパティを用意し、全て @property で Inspector から編集できるようにします。
- autoPlace (boolean)
- 地雷を自動設置するかどうか。
true: 一定間隔で足元に地雷を自動生成。false: 自動では生成しない。placeMine()を他スクリプトから呼ぶことで設置(本記事では主に自動モードを説明)。
- placeInterval (number)
- 自動設置モード時の、地雷を設置する間隔(秒)。
- 例: 1.5 にすると、1.5秒ごとに足元に新しい地雷を生成。
- maxMines (number)
- 同時に存在できる地雷の最大数。
- 0 以下を指定した場合は「無制限」とみなす。
- mineOffsetY (number)
- 設置位置の Y オフセット。キャラの足元に合わせて微調整するための値。
- 例: -20 にすると、キャラの原点より少し下に地雷を設置。
- mineRadius (number)
- 地雷の当たり判定の半径(
CircleCollider2D.radius)。 - 例: 20 〜 40 くらいで調整。
- 地雷の当たり判定の半径(
- mineSpriteFrame (
SpriteFrame)- 地雷待機中の見た目に使うスプライト。
- 未設定の場合は簡易な警告ログを出すが、Sprite 自体は空のまま生成される。
- explosionSpriteFrame (
SpriteFrame)- 爆発時の見た目に使うスプライト。
- 未設定の場合は、爆発しても見た目は変わらない。
- mineLifetime (number)
- 踏まれなかった場合でも、この秒数経過後に自動消滅する。
- 0 以下で「時間経過では消えない」。
- explosionDuration (number)
- 爆発状態を維持する時間(秒)。この時間経過後に地雷ノードを破棄。
- debugLog (boolean)
- 地雷設置・爆発・破棄などのログを
console.logに出すかどうか。
- 地雷設置・爆発・破棄などのログを
TypeScriptコードの実装
以下が完成した MineLayer.ts の全コードです。
import {
_decorator,
Component,
Node,
Vec3,
Sprite,
SpriteFrame,
UITransform,
Color,
RigidBody2D,
ERigidBody2DType,
CircleCollider2D,
Contact2DType,
IPhysics2DContact,
director,
} from 'cc';
const { ccclass, property } = _decorator;
/**
* MineLayer
* 任意のノードにアタッチすると、足元に「踏むと爆発する地雷エリア」を設置できるコンポーネント。
* 2D物理(Collider2D + RigidBody2D)を利用したトリガー判定で爆発を検出します。
*/
@ccclass('MineLayer')
export class MineLayer extends Component {
// =======================
// インスペクタ設定項目
// =======================
@property({
tooltip: 'true にすると、自動的に一定間隔で足元に地雷を設置します。',
})
public autoPlace: boolean = true;
@property({
tooltip: '自動設置モード時の、地雷設置間隔(秒)。autoPlace = true のときのみ有効です。',
min: 0.1,
})
public placeInterval: number = 1.5;
@property({
tooltip: '同時に存在できる地雷の最大数。0 以下で無制限。',
min: 0,
})
public maxMines: number = 5;
@property({
tooltip: '地雷の設置位置の Y オフセット。負の値でキャラの少し下(足元)に配置されます。',
})
public mineOffsetY: number = -20;
@property({
tooltip: '地雷の当たり判定(CircleCollider2D)の半径。',
min: 1,
})
public mineRadius: number = 25;
@property({
tooltip: '地雷待機中の見た目に使用する SpriteFrame。未設定でも動作しますが、見た目が表示されません。',
type: SpriteFrame,
})
public mineSpriteFrame: SpriteFrame | null = null;
@property({
tooltip: '爆発時の見た目に使用する SpriteFrame。未設定の場合は見た目は変化しません。',
type: SpriteFrame,
})
public explosionSpriteFrame: SpriteFrame | null = null;
@property({
tooltip: '踏まれなかった場合でも、この秒数経過後に自動で地雷を破棄します。0 以下で時間経過では消えません。',
min: 0,
})
public mineLifetime: number = 5;
@property({
tooltip: '爆発状態を維持する時間(秒)。この時間後に地雷ノードを破棄します。',
min: 0.05,
})
public explosionDuration: number = 0.3;
@property({
tooltip: 'true にすると、地雷設置・爆発・破棄のログをコンソールに出力します。',
})
public debugLog: boolean = false;
// =======================
// 内部状態
// =======================
private _timer: number = 0;
private _activeMines: Set<Node> = new Set();
// =======================
// ライフサイクル
// =======================
onLoad() {
// 特別な依存はないが、2D物理が有効かの簡易チェックとして PhysicsSystem2D の存在を確認してもよい。
if (!director.getScene()) {
console.error('[MineLayer] Scene が存在しません。正しいシーン上のノードにアタッチしてください。');
}
}
start() {
this._timer = 0;
if (this.debugLog) {
console.log('[MineLayer] start: autoPlace =', this.autoPlace);
}
}
update(dt: number) {
if (!this.autoPlace) {
return;
}
// 最大地雷数に達している場合は新規設置しない
if (this.maxMines > 0 && this._activeMines.size >= this.maxMines) {
return;
}
this._timer += dt;
if (this._timer >= this.placeInterval) {
this._timer = 0;
this.placeMine();
}
}
// =======================
// パブリック API
// =======================
/**
* 即座に足元に地雷を1つ設置します。
* autoPlace が false の場合でも利用できます。
*/
public placeMine(): void {
// 最大数制限チェック
if (this.maxMines > 0 && this._activeMines.size >= this.maxMines) {
if (this.debugLog) {
console.log('[MineLayer] placeMine: maxMines に達しているため設置しません');
}
return;
}
const mineNode = new Node('Mine');
const parent = this.node.parent ?? this.node.scene;
if (!parent) {
console.error('[MineLayer] 親ノードまたはシーンが見つからないため、地雷を設置できません。');
return;
}
parent.addChild(mineNode);
// 設置位置を計算(自身と同じ X/Z、Y はオフセット)
const worldPos = this.node.worldPosition.clone();
worldPos.y += this.mineOffsetY;
mineNode.setWorldPosition(worldPos);
// 見た目(Sprite)を追加
const sprite = mineNode.addComponent(Sprite);
if (this.mineSpriteFrame) {
sprite.spriteFrame = this.mineSpriteFrame;
} else {
// 見た目が未設定の場合は警告のみ
console.warn('[MineLayer] mineSpriteFrame が設定されていません。地雷の見た目が表示されません。');
}
sprite.color = Color.WHITE;
// UITransform を設定(スプライトサイズに応じて自動でも良いが、ここでは半径の2倍を基準にする)
const uiTransform = mineNode.addComponent(UITransform);
const size = this.mineRadius * 2;
uiTransform.setContentSize(size, size);
// 物理ボディ(静的ボディ + センサー)を追加
const body = mineNode.addComponent(RigidBody2D);
body.type = ERigidBody2DType.Static;
const collider = mineNode.addComponent(CircleCollider2D);
collider.radius = this.mineRadius;
collider.sensor = true; // センサーにすることで「めり込み」ではなくトリガー判定だけにする
// コリジョンコールバックを登録
collider.on(Contact2DType.BEGIN_CONTACT, this._onMineContact, this);
// 地雷の寿命タイマーを管理するためのメタ情報をノードに保存
const now = director.getTotalTime() / 1000; // 秒単位
(mineNode as any)._mineData = {
createdAt: now,
exploded: false,
};
this._activeMines.add(mineNode);
if (this.debugLog) {
console.log('[MineLayer] 地雷を設置しました。activeMines =', this._activeMines.size);
}
// 毎フレーム監視する必要はないので、mineLifetime/爆発後の破棄は scheduleOnce で行う
if (this.mineLifetime > 0) {
this.scheduleOnce(() => {
this._tryDestroyMine(mineNode, 'lifetime');
}, this.mineLifetime);
}
}
// =======================
// 地雷のコールバック処理
// =======================
/**
* 何かが地雷のセンサーに触れたときに呼ばれる。
*/
private _onMineContact(
selfCollider: CircleCollider2D,
otherCollider: any,
contact: IPhysics2DContact | null
) {
const mineNode = selfCollider.node;
const data = (mineNode as any)._mineData as { createdAt: number; exploded: boolean } | undefined;
if (!data) {
console.warn('[MineLayer] _onMineContact: mineData が見つかりません。');
return;
}
if (data.exploded) {
// すでに爆発処理中であれば何もしない
return;
}
data.exploded = true;
// 見た目を爆発用に切り替え
const sprite = mineNode.getComponent(Sprite);
if (this.explosionSpriteFrame && sprite) {
sprite.spriteFrame = this.explosionSpriteFrame;
} else {
// 爆発用スプライトが無い場合は、色を赤くして「爆発っぽさ」を出す
if (sprite) {
sprite.color = Color.RED;
}
}
// コリジョンを無効化して、二重で踏まれないようにする
selfCollider.enabled = false;
if (this.debugLog) {
console.log('[MineLayer] 地雷が爆発しました。爆発を検知したコライダー:', otherCollider.node.name);
}
// explosionDuration 秒後にノードを破棄
this.scheduleOnce(() => {
this._tryDestroyMine(mineNode, 'explosion');
}, this.explosionDuration);
}
/**
* 地雷ノードを安全に破棄する。
* すでに破棄されている場合や、アクティブリストに無い場合は何もしない。
*/
private _tryDestroyMine(mineNode: Node, reason: 'lifetime' | 'explosion') {
if (!mineNode.isValid) {
return;
}
if (!this._activeMines.has(mineNode)) {
// すでに別経路で削除済み
return;
}
this._activeMines.delete(mineNode);
if (this.debugLog) {
console.log(`[MineLayer] 地雷を破棄します。理由: ${reason} / 残り activeMines =`, this._activeMines.size);
}
// コールバック登録を解除(念のため)
const collider = mineNode.getComponent(CircleCollider2D);
if (collider) {
collider.off(Contact2DType.BEGIN_CONTACT, this._onMineContact, this);
}
mineNode.destroy();
}
// =======================
// コンポーネント破棄時
// =======================
onDestroy() {
// すべての地雷をクリーンアップ
this._activeMines.forEach((mine) => {
if (!mine.isValid) return;
const collider = mine.getComponent(CircleCollider2D);
if (collider) {
collider.off(Contact2DType.BEGIN_CONTACT, this._onMineContact, this);
}
mine.destroy();
});
this._activeMines.clear();
if (this.debugLog) {
console.log('[MineLayer] onDestroy: すべての地雷を破棄しました。');
}
}
}
コードの主要ポイント解説
- onLoad
- 特別な初期化はしていませんが、シーンが存在するかの簡易チェックを行っています。
- start
- 内部タイマー
_timerをリセットし、デバッグログが有効な場合に現在設定を出力します。
- 内部タイマー
- update
autoPlaceがtrueの場合のみ動作し、placeIntervalごとにplaceMine()を呼び出します。maxMinesを超えている場合は新しい地雷を設置しません。
- placeMine()
- 足元の位置(
mineOffsetY)に新しいノードを生成し、Sprite・UITransform・RigidBody2D・CircleCollider2Dを追加します。 CircleCollider2D.sensor = trueとして「トリガー判定のみ」にしています。Contact2DType.BEGIN_CONTACTに_onMineContactを登録し、誰かが踏んだ瞬間に爆発処理を行います。mineLifetime> 0 の場合はscheduleOnceで寿命タイマーを設定し、時間経過で自動破棄します。
- 足元の位置(
- _onMineContact()
- 爆発済みでないことを確認した上で、
explosionSpriteFrameによる見た目の切り替え、または色変更を行います。 - コライダーを無効化して二重判定を防ぎます。
explosionDuration後に_tryDestroyMine()を呼び出し、地雷ノードを破棄します。
- 爆発済みでないことを確認した上で、
- _tryDestroyMine()
- 内部の
_activeMines管理用 Set からノードを削除し、コールバックを解除した上でdestroy()します。
- 内部の
- onDestroy()
- MineLayer コンポーネントが削除された際に、残っているすべての地雷ノードをクリーンアップします。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタの Assets パネルで、地雷用スクリプトを置きたいフォルダ(例:
assets/scripts)を選択します。 - 右クリック → Create → TypeScript を選択します。
- ファイル名を
MineLayer.tsに変更します。 - 作成された
MineLayer.tsをダブルクリックして開き、上記のコード全文を貼り付けて保存します。
2. テスト用シーンとノードの準備
- 2Dプロジェクト であることを確認します(2D物理を使うため)。
- Hierarchy パネルで右クリック → Create → UI → Canvas を作成(すでにある場合は不要)。
- Canvas の子として、テスト用のキャラノードを作成します。
- Canvas を右クリック → Create → UI → Sprite。
- 名前を
Playerなどに変更します。
- Inspector で Player ノードを選択し、Add Component → Custom → MineLayer を選択してアタッチします。
3. プロパティの設定
Player ノードにアタッチされた MineLayer コンポーネントを選択し、Inspector で以下のように設定してみてください。
- Auto Place: チェックを入れる(true)。
- Place Interval:
1.0(1秒ごとに地雷を設置)。 - Max Mines:
5(同時に5個まで)。 - Mine Offset Y:
-30(Player の少し下に設置)。 - Mine Radius:
25。 - Mine Sprite Frame:
- 事前に地雷用の画像(例:
mine.png)を Assets にインポートしておきます。 - その画像から作成された
SpriteFrameをドラッグ&ドロップでここに設定します。
- 事前に地雷用の画像(例:
- Explosion Sprite Frame:
- 爆発用の画像(例:
explosion.png)を SpriteFrame として用意し、同様に設定します。
- 爆発用の画像(例:
- Mine Lifetime:
5(5秒経っても踏まれなかったら自動で消える)。 - Explosion Duration:
0.3(0.3秒だけ爆発表示)。 - Debug Log: 必要に応じてチェック(true にするとログが出ます)。
4. 2D物理の有効化
このコンポーネントは 2D物理エンジン を利用するため、プロジェクト設定で Physics2D を有効にしておきます。
- メニューから Project → Project Settings を開きます。
- 左側のカテゴリから Physics → 2D を選択します。
- 「Enable」や「Use 2D Physics」などの有効化チェックがあればオンにします(バージョンにより表記が多少異なります)。
- 重力などは今回は特に重要ではありませんが、デフォルトのままで問題ありません。
5. 動作確認
- シーンを保存します(Ctrl+S / Cmd+S)。
- Play ボタンを押してゲームを実行します。
- ゲーム開始後、Player ノードの足元に 1秒ごとに地雷が生成されていくのが確認できます。
- 地雷は 2D 物理のトリガー判定で「何かが重なった瞬間」に爆発します。
- テスト用に、別のノードに
RigidBody2D+Collider2Dを付けて動かすと、踏んだ瞬間に爆発する様子が確認できます。
- テスト用に、別のノードに
- Debug Log をオンにしている場合は、コンソールに
[MineLayer] 地雷を設置しました。[MineLayer] 地雷が爆発しました。[MineLayer] 地雷を破棄します。理由: explosion
などのログが出力されます。
6. 手動設置モードを試す
自動設置ではなく、任意のタイミングで地雷を設置したい場合は以下のようにします。
- MineLayer コンポーネントの Auto Place のチェックを外し、
falseにします。 - 別のスクリプト(例: プレイヤー操作スクリプト)から、以下のように呼び出します。
const mineLayer = this.node.getComponent(MineLayer); if (mineLayer) { mineLayer.placeMine(); } - 例えば「スペースキーを押したら地雷を設置する」といった操作に簡単に対応できます。
まとめ
この MineLayer コンポーネントは、任意のノードにアタッチするだけで
「足元に踏むと爆発するエリアを設置する」挙動を完結させる、再利用性の高いスクリプトです。
- 外部の GameManager や専用シングルトンに依存せず、この1ファイルだけで動作します。
- 地雷の見た目・当たり判定・寿命・爆発時間・自動設置間隔・最大数などをすべて Inspector から調整可能にしているため、ゲームごとにバランスを取りやすくなっています。
- 2D物理のトリガー判定を利用しているので、「誰が踏んだか」をタグや別スクリプトに頼らずに実装でき、シンプルな仕組みで動作します。
このコンポーネントをベースに、
- 爆発時にダメージを与える(爆心地からの距離でダメージ計算)
- アニメーションやパーティクルを再生する
- 特定のレイヤー(プレイヤーだけ、敵だけ)にのみ反応する
といった機能を拡張していけば、さまざまなアクションゲームの地雷ギミックを簡単に実現できます。
まずは本記事の MineLayer をそのまま導入し、足元に地雷が並ぶ様子と爆発挙動を確認してみてください。
