【Cocos Creator 3.8】LaserTrap の実装:アタッチするだけで「周期的に点滅し、接触中にダメージを与えるレーザー罠」を実現する汎用スクリプト
このコンポーネントは、任意のノードにアタッチするだけで「ON/OFF を周期的に繰り返すレーザー罠」を実現します。
レーザーが ON の間だけダメージ判定が有効になり、プレイヤーなどの当たり判定ノードが触れている間、一定間隔でダメージを与えます。
ダメージ処理自体は「イベント通知」として外部に送るだけにしているため、どんなゲームでも共通で使い回せるのがポイントです。
コンポーネントの設計方針
機能要件の整理
- ノードにアタッチするだけで「レーザー罠」として動作する。
- レーザーは「ON / OFF」を一定周期で切り替える。
- ON の時間・OFF の時間を個別に設定できる。
- 開始時に ON から始めるか OFF から始めるかを選べる。
- レーザー ON の間だけ「ダメージ判定」が有効。
- ダメージ判定は 2D 物理のトリガーコリジョン(isTrigger = true)で行う。
- 接触中の相手には、一定間隔ごとにダメージを与える(継続ダメージ)。
- ダメージは「イベント通知」として送出し、実際の HP 処理は受け手側に任せる。
- LaserTrap は「どのノードにどれだけダメージを与えたか」をイベントで知らせるだけ。
- これにより、LaserTrap 自身は他のカスタムスクリプトに依存しない。
- レーザーの見た目(Sprite の ON/OFF や色・不透明度)も自動で切り替える。
- エディタ上で簡単に調整できるよう、すべてのパラメータは @property から設定可能にする。
外部依存をなくすための設計アプローチ
- GameManager や Player スクリプトなど、カスタムクラスへの参照は一切持たない。
- ダメージ処理は「イベント名」と「EventTarget(任意)」に対して通知するだけにする。
- イベント名は文字列で指定(例:
"onLaserDamage")。 - 通知先 EventTarget は Inspector から任意のノードの
ComponentやNodeにアタッチされたEventTargetを指定できるようにする。 - さらに、
Node.emitを使った「レーザー自身からのイベント発火」も行うので、
LaserTrap をアタッチしたノードに対してnode.on('LaserTrapDamage', ...)で受け取ることも可能。
- イベント名は文字列で指定(例:
- 物理コリジョンは標準コンポーネント(
Collider2D+RigidBody2D)のみを使用。- 存在しない場合は
getComponentでチェックし、エラーログを出す。 - エディタでの追加手順は後述。
- 存在しない場合は
- 見た目の制御は
SpriteとUIOpacityに限定し、存在しなければログを出すだけにとどめる。
インスペクタで設定可能なプロパティ設計
以下のプロパティを用意します。
- レーザー動作系
enabledAtStart: boolean
シーン開始時にレーザー罠を有効にするかどうか。
false の場合、LaserTrapは動作せず、見た目も OFF 状態のまま。startOn: boolean
動作開始時に ON 状態から始めるかどうか。
true なら「ON → OFF → ON → …」、false なら「OFF → ON → OFF → …」の順で点滅する。onDuration: number
レーザーが ON の状態を維持する時間(秒)。
例: 1.0 にすると、1秒間 ON。offDuration: number
レーザーが OFF の状態を維持する時間(秒)。
例: 0.5 にすると、0.5秒間 OFF。
- ダメージ系
damagePerTick: number
ダメージ 1 回あたりの量。
例: 10 にすると、1 tick ごとに 10 ダメージ。damageInterval: number
同じ対象に連続でダメージを与える間隔(秒)。
例: 0.5 にすると、対象が触れ続けている間、0.5秒ごとにダメージ。maxTargets: number
同時にダメージ判定を行う最大対象数。
0 以下なら制限なし。
パフォーマンスやゲームデザイン上、同時にダメージを受ける対象数を制限したい場合に使用。
- 対象フィルタ系
targetGroupMask: number
ダメージ対象とみなすCollider2D.groupのビットマスク。
0 の場合は全グループ対象。
例: プレイヤーをグループ 1、敵をグループ 2 として、プレイヤーだけに当てたいなら1 << 1など。ignoreTrigger: boolean
トリガー同士の接触を無視するかどうか。
true の場合、otherCollider.isTrigger == trueの相手にはダメージを与えない。
- 見た目制御系
useSpriteVisibility: boolean
ON/OFF に応じて Sprite の有効・無効を切り替えるかどうか。
true の場合、ON で表示、OFF で非表示。useOpacity: boolean
ON/OFF に応じてUIOpacityの値を切り替えるかどうか。
true の場合、ON でonOpacity、OFF でoffOpacityを適用。onOpacity: number
レーザー ON 時の不透明度(0〜255)。offOpacity: number
レーザー OFF 時の不透明度(0〜255)。
- イベント通知系
emitNodeEvents: boolean
LaserTrap をアタッチしたノード自身に対してnode.emitでダメージイベントを送るかどうか。
true の場合、this.node.emit('LaserTrapDamage', payload)が発火。eventTarget: EventTarget | null
任意で指定できる EventTarget。
設定されていれば、ダメージ発生時にeventTarget.emit(eventName, payload)を呼び出す。eventName: string
ダメージ通知に使用するイベント名。
デフォルト:"LaserTrapDamage"。
イベント通知の payload は以下の形で送られます。
interface LaserTrapDamageEvent {
trapNode: Node; // この LaserTrap がアタッチされているノード
targetNode: Node; // ダメージを受けたノード
damage: number; // 今回与えたダメージ量
totalTime: number; // シーン開始からの経過時間(秒)
}
TypeScriptコードの実装
以下が完成した LaserTrap.ts の全コードです。
import { _decorator, Component, Node, Collider2D, Contact2DType, IPhysics2DContact, RigidBody2D, Sprite, UIOpacity, EventTarget, log, warn, error } from 'cc';
const { ccclass, property } = _decorator;
interface LaserTrapDamageEvent {
trapNode: Node;
targetNode: Node;
damage: number;
totalTime: number;
}
@ccclass('LaserTrap')
export class LaserTrap extends Component {
// === レーザー動作系 ===
@property({
tooltip: 'シーン開始時にレーザー罠を有効にするかどうか。\nOFF の場合、スクリプトは動作しません。'
})
public enabledAtStart: boolean = true;
@property({
tooltip: '動作開始時に ON 状態から始めるかどうか。\nON: ON→OFF→ON...\nOFF: OFF→ON→OFF...'
})
public startOn: boolean = true;
@property({
tooltip: 'レーザー ON 状態を維持する時間(秒)。\n0 以下の場合は 0.1 に補正されます。'
})
public onDuration: number = 1.0;
@property({
tooltip: 'レーザー OFF 状態を維持する時間(秒)。\n0 以下の場合は 0.1 に補正されます。'
})
public offDuration: number = 0.5;
// === ダメージ系 ===
@property({
tooltip: '1 回あたりのダメージ量。\n0 以下の場合はダメージを与えません。'
})
public damagePerTick: number = 10;
@property({
tooltip: '同じ対象に連続でダメージを与える間隔(秒)。\n0 以下の場合は毎フレームダメージになります。'
})
public damageInterval: number = 0.5;
@property({
tooltip: '同時にダメージ判定を行う最大対象数。\n0 以下で制限なし。'
})
public maxTargets: number = 0;
// === 対象フィルタ系 ===
@property({
tooltip: 'ダメージ対象とみなす Collider2D.group のビットマスク。\n0 の場合は全グループ対象。'
})
public targetGroupMask: number = 0;
@property({
tooltip: 'true の場合、相手がトリガー(Collider2D.isTrigger)のときはダメージ対象から除外します。'
})
public ignoreTrigger: boolean = false;
// === 見た目制御系 ===
@property({
tooltip: 'ON/OFF に応じて Sprite コンポーネントの有効・無効を切り替えるかどうか。'
})
public useSpriteVisibility: boolean = true;
@property({
tooltip: 'ON/OFF に応じて UIOpacity の値を切り替えるかどうか。'
})
public useOpacity: boolean = true;
@property({
tooltip: 'レーザー ON 時の不透明度(0〜255)。',
range: [0, 255, 1],
slide: true
})
public onOpacity: number = 255;
@property({
tooltip: 'レーザー OFF 時の不透明度(0〜255)。',
range: [0, 255, 1],
slide: true
})
public offOpacity: number = 40;
// === イベント通知系 ===
@property({
tooltip: 'LaserTrap をアタッチしたノード自身に対して node.emit でダメージイベントを送るかどうか。'
})
public emitNodeEvents: boolean = true;
@property({
tooltip: '任意の EventTarget を指定して、ダメージ発生時にイベントを通知します。\n未設定でも動作します。'
})
public eventTarget: EventTarget | null = null;
@property({
tooltip: 'ダメージ通知に使用するイベント名。\n例: "LaserTrapDamage"'
})
public eventName: string = 'LaserTrapDamage';
// === 内部状態 ===
private _collider: Collider2D | null = null;
private _rigidBody: RigidBody2D | null = null;
private _sprite: Sprite | null = null;
private _uiOpacity: UIOpacity | null = null;
private _isOn: boolean = false;
private _timer: number = 0;
private _totalTime: number = 0;
// 接触中の対象ごとに、次にダメージを与えられる時刻(秒)を記録
private _touchingTargets: Map = new Map();
onLoad() {
// 必要なコンポーネントの取得
this._collider = this.getComponent(Collider2D);
if (!this._collider) {
error('[LaserTrap] Collider2D がアタッチされていません。このノードに Collider2D を追加してください。');
}
this._rigidBody = this.getComponent(RigidBody2D);
if (!this._rigidBody) {
warn('[LaserTrap] RigidBody2D がアタッチされていません。推奨: Dynamic 以外の RigidBody2D を追加してください。');
}
this._sprite = this.getComponent(Sprite);
if (!this._sprite && this.useSpriteVisibility) {
warn('[LaserTrap] Sprite コンポーネントが見つかりません。useSpriteVisibility は無視されます。');
}
this._uiOpacity = this.getComponent(UIOpacity);
if (!this._uiOpacity && this.useOpacity) {
warn('[LaserTrap] UIOpacity コンポーネントが見つかりません。useOpacity は無視されます。');
}
// コリジョンイベント登録
if (this._collider) {
this._collider.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
this._collider.on(Contact2DType.END_CONTACT, this._onEndContact, this);
// 継続判定は update 内で行うので、STAY_CONTACT は必須ではない
}
// パラメータの防御的補正
if (this.onDuration = currentDuration) {
this._timer -= currentDuration;
this._isOn = !this._isOn;
this._updateVisual();
}
// ダメージ処理
if (!this._isOn || this.damagePerTick
コードの主要部分の解説
onLoad
Collider2D,RigidBody2D,Sprite,UIOpacityをgetComponentで取得し、防御的に存在チェックを行います。Collider2Dに対してBEGIN_CONTACT,END_CONTACTイベントを登録します。onDuration,offDurationが 0 以下なら 0.1 に補正します。startOnに基づいて初期の ON/OFF 状態を設定し、見た目を更新します。
start
enabledAtStartが false の場合、以後の update で一切処理を行わないようにします。- 見た目だけ OFF にしておき、あとは外部からプロパティを変更しても動作はしません(完全停止)。
update
enabledAtStartが false の場合は即 return。_timerにdeltaTimeを加算し、onDuration/offDurationを超えたら ON/OFF をトグルします。- ON の間だけ、
_touchingTargetsに登録されている各ノードに対してダメージ判定を行います。 - 各対象ごとに「次にダメージを与えられる時刻(秒)」を記録し、
damageInterval経過ごとにダメージを与えます。 maxTargetsが正の場合、1フレーム内でダメージ処理する対象数を制限します。
_onBeginContact / _onEndContact
- 接触開始時に、対象が有効なターゲットかどうかを
_isValidTargetで判定します。 - 有効なターゲットであれば
_touchingTargetsマップに登録し、すぐにダメージを与えられるように現在時刻をセットします。 - 接触終了時には、そのノードをマップから削除します。
_isValidTarget
ignoreTriggerが true の場合、otherCollider.isTrigger == trueの相手を除外します。targetGroupMaskが 0 でない場合、Collider2D.groupをビットマスクでフィルタします。- 自分自身のノード(
this.node)は常に除外します。
_applyDamage
- ダメージ量が 0 以下なら何もしません。
LaserTrapDamageEvent型の payload を作成し、node.emitとeventTarget.emitで通知します。- このコンポーネント自体は HP を減らしたりはせず、「ダメージが発生した」という情報だけを外部に伝えます。
_updateVisual
useSpriteVisibilityが true かつSpriteが存在する場合、sprite.enabledを ON/OFF に合わせて切り替えます。useOpacityが true かつUIOpacityが存在する場合、不透明度をonOpacity/offOpacityに切り替えます。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を LaserTrap.ts に変更します。
- 作成された
LaserTrap.tsをダブルクリックし、エディタ(VS Code など)で開きます。 - 中身をすべて削除して、前述の TypeScript コードをそのまま貼り付けて保存します。
2. テスト用レーザー罠ノードの作成
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、
名前を LaserTrapTest などに変更します。 - Inspector で Sprite の SpriteFrame にレーザーっぽい画像(細長い線状のテクスチャ)を設定します。
- 同じく Inspector の Add Component ボタンをクリックし、
UI → UIOpacity を追加します(任意ですが、ON/OFF の見た目を変えやすくなります)。
3. 物理コンポーネントの設定(Collider2D / RigidBody2D)
- LaserTrapTest ノードを選択した状態で、Inspector の Add Component をクリックします。
- Physics2D → BoxCollider2D(または PolygonCollider2D)を追加します。
- レーザーの見た目に合わせて Size を調整し、細長い当たり判定にします。
- Is Trigger にチェックを入れて、トリガーコリジョンにします。
- 再度 Add Component をクリックし、Physics2D → RigidBody2D を追加します。
- Type は Static または Kinematic を推奨します(動かさない罠の場合)。
- Dynamic にすると重力などの影響を受けるので、通常のレーザー罠には向きません。
※ RigidBody2D は必須ではありませんが、Cocos の 2D 物理では Rigidbody 付き同士の方が安定してコリジョンが発生するため、追加を推奨します。
4. LaserTrap コンポーネントのアタッチ
- LaserTrapTest ノードを選択した状態で、Inspector の Add Component をクリックします。
- Custom → LaserTrap を選択します。
5. プロパティの設定例
まずは動作確認用に、以下のように設定してみます。
- レーザー動作系
- Enabled At Start: ON
- Start On: ON
- On Duration: 1.0
- Off Duration: 0.5
- ダメージ系
- Damage Per Tick: 10
- Damage Interval: 0.5
- Max Targets: 0(制限なし)
- 対象フィルタ系
- Target Group Mask: 0(とりあえず全グループ対象)
- Ignore Trigger: OFF(テスト用)
- 見た目制御系
- Use Sprite Visibility: ON
- Use Opacity: ON
- On Opacity: 255
- Off Opacity: 40
- イベント通知系
- Emit Node Events: ON
- Event Target: (空)
- Event Name: LaserTrapDamage
6. ダメージイベントの受け取りテスト
LaserTrap 自体は HP を減らさないので、イベントを受け取ってログを出す簡単なテストスクリプトを用意します。
- Assets パネルで右クリック → Create → TypeScript → ファイル名を LaserTrapDebugListener.ts にします。
- 以下のコードを貼り付けます。
import { _decorator, Component, Node, log } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('LaserTrapDebugListener')
export class LaserTrapDebugListener extends Component {
onLoad() {
// LaserTrap からのイベントを受け取る
this.node.on('LaserTrapDamage', this.onLaserDamage, this);
}
onDestroy() {
this.node.off('LaserTrapDamage', this.onLaserDamage, this);
}
private onLaserDamage(eventData: any) {
// 型チェックは簡易的に
const trapNode = eventData.trapNode as Node;
const targetNode = eventData.targetNode as Node;
const damage = eventData.damage as number;
const totalTime = eventData.totalTime as number;
log(`[LaserTrapDebugListener] time=${totalTime.toFixed(2)}s ` +
`trap=${trapNode.name} target=${targetNode.name} damage=${damage}`);
}
}
次に、このリスナーを LaserTrap ノードにアタッチします。
- Hierarchy で LaserTrapTest ノードを選択します。
- Inspector の Add Component → Custom → LaserTrapDebugListener を選択します。
- これで、LaserTrap からのイベントを同じノード上で受け取れるようになります。
7. プレイヤー役のテストノードを作成
レーザーに触れてダメージイベントが発生するかを確認するため、簡単な「当たり判定用ノード」を作ります。
- Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、
名前を PlayerDummy に変更します。 - 見た目は何でも構いません(四角い画像など)。
- Inspector の Add Component → Physics2D → CircleCollider2D を追加します。
- Is Trigger は OFF のままで構いません(LaserTrap 側が Trigger)。
- さらに Add Component → Physics2D → RigidBody2D を追加し、Type を Dynamic にします。
- シーン上で LaserTrapTest と PlayerDummy が重なるように配置します。
8. シミュレーションで動作確認
- エディタ右上の Play(▶) ボタンを押してゲームを実行します。
- Scene ビューまたは Game ビューで、LaserTrap の Sprite が
- 1秒間 ON(不透明度 255)
- 0.5秒間 OFF(不透明度 40)
を繰り返していることを確認します。
- Console パネルを開くと、PlayerDummy が LaserTrap に触れている間、
0.5秒ごとに以下のようなログが出力されるはずです。[LaserTrapDebugListener] time=1.02s trap=LaserTrapTest target=PlayerDummy damage=10 [LaserTrapDebugListener] time=1.52s trap=LaserTrapTest target=PlayerDummy damage=10 ... - LaserTrap が OFF の間はログが止まり、ON に戻ると再びダメージログが出力されることを確認します。
9. Group と Mask を使った対象フィルタの例
プレイヤーだけをダメージ対象にしたい場合の一例です。
- Cocos Creator 上部メニューから Project → Project Settings → Physics 2D を開きます。
- Collision Group に「Player」などのグループを追加し、インデックスをメモしておきます(例: 1)。
- Hierarchy で PlayerDummy を選択し、CircleCollider2D の Group を「Player」に設定します。
- LaserTrapTest を選択し、LaserTrap コンポーネントの Target Group Mask を
- 例: グループ 1 のみ対象にしたい場合は 2 を入力(1 << 1 = 2)。
- 他のグループのノードをレーザーに重ねてもダメージイベントが発生しないことを確認します。
まとめ
この LaserTrap コンポーネントは、
- 周期的な ON/OFF 切り替え(点滅)
- ON 中のみ有効なトリガーコリジョン
- 接触中の対象への継続ダメージ(一定間隔)
- グループマスクやトリガーフィルタによる対象選別
- Sprite / UIOpacity を使った簡易的な見た目制御
- イベント駆動のダメージ通知(HP 処理は外部に委譲)
をすべて 1 つのスクリプトだけ で完結させています。
GameManager や Player クラスといった外部スクリプトに依存しないため、
- どのプロジェクトにもコピペで持ち込める
- プレイヤー・敵・ギミックなど、どんな「ダメージを受ける対象」にも共通で使える
- イベントの受け取り側だけ好きなように実装すればよい
という利点があります。
応用例としては、
- ダメージではなく「ノックバック」や「スロー効果」などをイベント側で実装する
- イベント payload に「レーザーの向き」や「ID」などを追加して、より複雑なギミック管理を行う
- LaserTrap を移動させたり回転させる別コンポーネントと組み合わせて、動くレーザー罠にする
といった発展も簡単に行えます。
まずは本記事のコードをそのまま試し、プロジェクトに合わせてパラメータやイベント受け取り側の処理をカスタマイズしてみてください。




