【Cocos Creator 3.8】ChainLightning(連鎖雷)の実装:敵にヒット後、自動で近くの敵へ次々とダメージを連鎖させる汎用スクリプト
このガイドでは、1つの敵にヒットしたあと、周囲の敵へ一定回数まで自動で雷が飛び移る「連鎖雷」コンポーネントを実装します。
攻撃オブジェクト(弾・スキルエフェクト・近接攻撃の当たり判定など)にアタッチするだけで、「最初に当たった敵から、射程内の敵へ順番にダメージが連鎖する」挙動を実現できます。
敵管理用のGameManagerなどに依存せず、「敵」ノードに enemyTag で指定したタグ名を付けるだけで動くように設計します。
コンポーネントの設計方針
1. 機能要件の整理
- 攻撃側ノード(弾・スキル・近接攻撃など)にアタッチして使用する。
- 最初の敵ヒットは、以下の2パターンをサポートする:
- 自ノードの位置から一番近い敵を自動で検索して最初のターゲットにする
- 外部から最初のターゲットを指定する(メソッド呼び出し)
- 最初のターゲットにダメージを与えた後、その敵の周囲から次の敵を探し、雷を連鎖させる。
- 連鎖は最大ヒット数(または最大連鎖回数)までで終了する。
- 同じ敵には1回だけヒットする(既にヒット済みの敵はスキップ)。
- 敵の検索は ワールド座標空間 で行い、
enemyTagとmaxChainDistanceを用いてフィルタリングする。 - 各ヒット間に わずかなディレイ を入れて、連鎖している感じを演出できるようにする。
- ダメージは カスタムイベント と ログ で通知し、敵側の実装に依存しない。
- (オプション)雷のエフェクト表示用に、Line-like 表現のための Lineノード(Sprite など) を指定できるようにし、あればそれを伸縮・回転して表示する。
敵側のスクリプトや GameManager などの外部依存を一切なくすため、「敵の判定」=タグと距離による検索に限定し、ダメージ処理はイベント通知に留めます。
敵ノードが 'TakeDamage' などのイベントをリッスンしていればダメージを受けられますし、そうでなくてもコンソールログで動作を確認できます。
2. インスペクタで設定可能なプロパティ設計
以下のプロパティを用意します。
enemyTag: string- 敵ノードに付与するタグ名。
- このタグと一致するノードのみを「敵」とみなして連鎖対象にする。
- 例:
"Enemy"
damage: number- 1ヒットあたりのダメージ量。
- イベントパラメータとして敵側に渡される。
- 例:
10
maxHits: number- 最大ヒット数(最初のターゲットを含む)。
- 例:
5にすると、最大 5 体の敵にヒットした時点で連鎖終了。
maxChainDistance: number- 1回の連鎖で次の敵を探す最大距離(ワールド座標上の距離)。
- この距離より遠い敵は次のターゲット候補にならない。
- 例:
400
searchRadius: number- 最初のターゲットを「自動検索」する際の探索半径。
- 自ノードの位置からこの半径内で最も近い敵を最初のターゲットにする。
- 例:
600
autoStart: booleantrue:start()時に自動で最初のターゲットを検索して連鎖を開始する。false: 外部からbeginChainFrom(node)を呼び出したときだけ動作する。
hitInterval: number- 各ヒット間のディレイ時間(秒)。
- 0 にすると即座に次の敵へ連鎖する。
- 例:
0.08
destroyOnComplete: boolean- 連鎖処理が完了したときに、この攻撃ノードを自動で
destroy()するかどうか。 - 弾や一時的なエフェクトの場合は
true推奨。
- 連鎖処理が完了したときに、この攻撃ノードを自動で
linePrefab: Prefab | null- 雷のビームを表現するためのプレハブ(任意)。
- 例: 細長い Sprite を用意し、これを回転・スケールして「線」に見せる。
- 指定されていれば、各ヒット間にこのプレハブをインスタンス化して表示する。
lineDuration: number- 1本の雷ビームを表示しておく時間(秒)。
linePrefabを使う場合のみ有効。
debugLog: booleantrueの場合、連鎖の進行状況やエラーをconsole.logで詳細に出力する。
また、防御的な実装として、以下の点に注意します。
enemyTagが空文字の場合はエラーログを出し、連鎖を開始しない。- 敵が1体も見つからなかった場合は、その旨をログに出して終了する。
linePrefabが指定されている場合、インスタンス生成時にエラーがあればログ出力する。
TypeScriptコードの実装
import { _decorator, Component, Node, Vec3, math, Prefab, instantiate, tween, Tween, director } from 'cc';
const { ccclass, property } = _decorator;
/**
* ChainLightning
* - 指定した最初のターゲット、または自ノード周辺から見つけた敵を起点に
* 近くの敵へ順番に雷を連鎖させるコンポーネント。
* - 敵は Node.tag === enemyTag で判定する。
* - ダメージは敵ノードに対して "TakeDamage" カスタムイベントを emit する。
*/
@ccclass('ChainLightning')
export class ChainLightning extends Component {
@property({
tooltip: '敵ノードに設定されているタグ名。このタグと一致するノードのみを連鎖対象とします。',
})
public enemyTag: string = 'Enemy';
@property({
tooltip: '1ヒットあたりのダメージ量。敵ノードには "TakeDamage" イベントで通知されます。',
})
public damage: number = 10;
@property({
tooltip: '最大ヒット数(最初のターゲットを含む)。この回数に達すると連鎖を終了します。',
min: 1,
})
public maxHits: number = 5;
@property({
tooltip: '1回の連鎖で次の敵を探す最大距離(ワールド座標上の距離)。',
min: 0,
})
public maxChainDistance: number = 400;
@property({
tooltip: 'autoStart が true のとき、最初のターゲットを自ノード周辺から検索する半径。',
min: 0,
})
public searchRadius: number = 600;
@property({
tooltip: 'true の場合、start() 時に自動で最初のターゲットを検索して連鎖を開始します。',
})
public autoStart: boolean = true;
@property({
tooltip: '各ヒット間のディレイ時間(秒)。0 にすると即座に次の敵へ連鎖します。',
min: 0,
})
public hitInterval: number = 0.08;
@property({
tooltip: '連鎖処理が完了したときに、このノードを自動で destroy() するかどうか。',
})
public destroyOnComplete: boolean = true;
@property({
type: Prefab,
tooltip: '雷ビームを表現するためのプレハブ(任意)。\n細長い Sprite などを想定しています。指定されていれば、各ヒット間にインスタンスを生成して表示します。',
})
public linePrefab: Prefab | null = null;
@property({
tooltip: '1本の雷ビームを表示しておく時間(秒)。linePrefab を使用する場合のみ有効です。',
min: 0,
})
public lineDuration: number = 0.12;
@property({
tooltip: 'true の場合、連鎖の進行状況やエラーを console.log で詳細に出力します。',
})
public debugLog: boolean = false;
// 内部状態管理
private _isRunning = false;
private _hitTargets: Node[] = [];
private _currentHitCount = 0;
private _activeTweens: Tween<any>[] = [];
onLoad() {
// 特に必須コンポーネントはないが、enemyTag のチェックなどを行う
if (!this.enemyTag || this.enemyTag.trim().length === 0) {
console.error('[ChainLightning] enemyTag が設定されていません。このコンポーネントは動作しません。', this.node.name);
}
}
start() {
if (this.autoStart) {
this.tryAutoStartChain();
}
}
onDisable() {
// ノードが無効化されたら進行中の tween を全て停止する
this._stopAllTweens();
this._isRunning = false;
}
onDestroy() {
this._stopAllTweens();
}
/**
* autoStart が true のときに呼び出される、自動連鎖開始処理。
*/
private tryAutoStartChain() {
if (!this._validateConfig()) {
return;
}
const firstTarget = this._findClosestEnemyAroundNode(this.node, this.searchRadius);
if (!firstTarget) {
if (this.debugLog) {
console.warn('[ChainLightning] 自動開始しようとしましたが、searchRadius 内に敵が見つかりませんでした。', this.node.name);
}
this._onChainComplete();
return;
}
this.beginChainFrom(firstTarget);
}
/**
* 連鎖雷を特定の最初のターゲットから開始する。
* 外部スクリプトからも呼び出し可能。
*/
public beginChainFrom(firstTarget: Node) {
if (!this._validateConfig()) {
return;
}
if (!firstTarget || !firstTarget.isValid) {
console.error('[ChainLightning] beginChainFrom に無効なターゲットが渡されました。');
return;
}
if (this._isRunning) {
if (this.debugLog) {
console.warn('[ChainLightning] すでに連鎖処理が実行中です。二重起動は無視されます。');
}
return;
}
this._isRunning = true;
this._hitTargets.length = 0;
this._currentHitCount = 0;
if (this.debugLog) {
console.log('[ChainLightning] 連鎖開始: first target =', firstTarget.name);
}
// 最初のヒット処理を開始
this._chainFromTarget(firstTarget);
}
/**
* 設定値の妥当性チェック。
*/
private _validateConfig(): boolean {
if (!this.enemyTag || this.enemyTag.trim().length === 0) {
console.error('[ChainLightning] enemyTag が空です。敵ノードのタグを設定してください。');
return false;
}
if (this.maxHits <= 0) {
console.error('[ChainLightning] maxHits が 0 以下です。1 以上の値を設定してください。');
return false;
}
if (this.maxChainDistance < 0) {
console.error('[ChainLightning] maxChainDistance が負の値です。0 以上の値を設定してください。');
return false;
}
return true;
}
/**
* 一つのターゲットから次のターゲットへ連鎖させるメイン処理。
*/
private _chainFromTarget(currentTarget: Node) {
if (!currentTarget || !currentTarget.isValid) {
if (this.debugLog) {
console.warn('[ChainLightning] currentTarget が無効です。連鎖を終了します。');
}
this._onChainComplete();
return;
}
this._currentHitCount++;
this._hitTargets.push(currentTarget);
// ダメージ適用(イベント通知)
this._applyDamage(currentTarget);
if (this.debugLog) {
console.log(`[ChainLightning] Hit #${this._currentHitCount} :`, currentTarget.name);
}
// 最大ヒット数に達したら終了
if (this._currentHitCount >= this.maxHits) {
if (this.debugLog) {
console.log('[ChainLightning] maxHits に達したため連鎖終了。');
}
this._onChainComplete();
return;
}
// 次のターゲットを検索
const nextTarget = this._findNextTarget(currentTarget);
if (!nextTarget) {
if (this.debugLog) {
console.log('[ChainLightning] 次のターゲットが見つからないため連鎖終了。');
}
this._onChainComplete();
return;
}
// 雷ビームの表示(任意)
this._spawnLineBetween(currentTarget, nextTarget);
// ディレイ後に次のターゲットへ連鎖
if (this.hitInterval > 0) {
const t = tween({})
.delay(this.hitInterval)
.call(() => {
this._removeTween(t);
if (!this._isRunning) return;
this._chainFromTarget(nextTarget);
});
this._activeTweens.push(t);
t.start();
} else {
// 即座に次へ
this._chainFromTarget(nextTarget);
}
}
/**
* 敵ノードにダメージを適用する。
* - "TakeDamage" イベントを emit する。
* - debugLog が true の場合は console.log も出力する。
*/
private _applyDamage(target: Node) {
if (!target.isValid) return;
// カスタムイベントでダメージ通知
target.emit('TakeDamage', {
amount: this.damage,
source: this.node,
type: 'ChainLightning',
});
if (this.debugLog) {
console.log(`[ChainLightning] ${target.name} に ${this.damage} ダメージを適用 (イベント: TakeDamage)`);
}
}
/**
* currentTarget から一定距離内にいる、まだヒットしていない最も近い敵を探す。
*/
private _findNextTarget(currentTarget: Node): Node | null {
const allEnemies = this._findAllEnemies();
if (allEnemies.length === 0) return null;
const currentWorldPos = new Vec3();
currentTarget.getWorldPosition(currentWorldPos);
let bestTarget: Node | null = null;
let bestDistSq = Number.MAX_VALUE;
for (const enemy of allEnemies) {
if (!enemy.isValid) continue;
if (this._hitTargets.includes(enemy)) continue; // 既にヒット済み
const pos = new Vec3();
enemy.getWorldPosition(pos);
const distSq = Vec3.squaredDistance(currentWorldPos, pos);
if (distSq > this.maxChainDistance * this.maxChainDistance) {
continue; // 射程外
}
if (distSq < bestDistSq) {
bestDistSq = distSq;
bestTarget = enemy;
}
}
return bestTarget;
}
/**
* 自ノードの周辺から searchRadius 内で一番近い敵を探す。
* autoStart 用の最初のターゲット検索に使用。
*/
private _findClosestEnemyAroundNode(originNode: Node, radius: number): Node | null {
const allEnemies = this._findAllEnemies();
if (allEnemies.length === 0) return null;
const originPos = new Vec3();
originNode.getWorldPosition(originPos);
let bestTarget: Node | null = null;
let bestDistSq = Number.MAX_VALUE;
const radiusSq = radius * radius;
for (const enemy of allEnemies) {
if (!enemy.isValid) continue;
const pos = new Vec3();
enemy.getWorldPosition(pos);
const distSq = Vec3.squaredDistance(originPos, pos);
if (distSq > radiusSq) continue;
if (distSq < bestDistSq) {
bestDistSq = distSq;
bestTarget = enemy;
}
}
return bestTarget;
}
/**
* シーン内の全ノードから、enemyTag を持つノードを収集する。
* - director.getScene() から再帰的に探索する。
* - 外部の GameManager 等に依存しない汎用的な検索。
*/
private _findAllEnemies(): Node[] {
const scene = director.getScene();
if (!scene) {
console.error('[ChainLightning] シーンが取得できませんでした。');
return [];
}
const result: Node[] = [];
this._collectEnemiesRecursive(scene, result);
return result;
}
private _collectEnemiesRecursive(node: Node, out: Node[]) {
if (node.tag === this.enemyTag) {
out.push(node);
}
for (const child of node.children) {
this._collectEnemiesRecursive(child, out);
}
}
/**
* 2つのノードの間に雷ビーム用の linePrefab を表示する(任意)。
*/
private _spawnLineBetween(from: Node, to: Node) {
if (!this.linePrefab) return;
if (!from.isValid || !to.isValid) return;
const scene = director.getScene();
if (!scene) return;
const lineNode = instantiate(this.linePrefab);
if (!lineNode) {
console.error('[ChainLightning] linePrefab のインスタンス化に失敗しました。');
return;
}
// シーンのルートに配置(UI 系なら Canvas 配下など、用途に合わせて変更可能)
scene.addChild(lineNode);
const fromPos = new Vec3();
const toPos = new Vec3();
from.getWorldPosition(fromPos);
to.getWorldPosition(toPos);
// 中点に配置
const midPos = new Vec3(
(fromPos.x + toPos.x) * 0.5,
(fromPos.y + toPos.y) * 0.5,
(fromPos.z + toPos.z) * 0.5,
);
lineNode.setWorldPosition(midPos);
// 距離と角度を計算して、線を伸ばす
const dir = new Vec3();
Vec3.subtract(dir, toPos, fromPos);
const length = Vec3.len(dir);
// Y 軸方向を基準とした回転(2D 前提の簡易実装)
const angleRad = Math.atan2(dir.y, dir.x);
const angleDeg = math.toDegree(angleRad);
lineNode.setRotationFromEuler(0, 0, angleDeg);
// X スケールを長さに合わせる想定(linePrefab 側のサイズに合わせて調整)
const originalScale = lineNode.scale.clone();
if (length > 0.001) {
lineNode.setScale(length / 100, originalScale.y, originalScale.z);
}
// 一定時間後に自動で破棄
const t = tween(lineNode)
.delay(this.lineDuration)
.call(() => {
this._removeTween(t);
if (lineNode.isValid) {
lineNode.destroy();
}
});
this._activeTweens.push(t);
t.start();
}
/**
* 連鎖がすべて終了したときに呼び出される。
*/
private _onChainComplete() {
this._isRunning = false;
if (this.debugLog) {
console.log('[ChainLightning] 連鎖処理が完了しました。総ヒット数:', this._currentHitCount);
}
if (this.destroyOnComplete && this.node.isValid) {
this.node.destroy();
}
}
/**
* 登録済みの Tween を停止し、配列から除去する。
*/
private _stopAllTweens() {
for (const t of this._activeTweens) {
t.stop();
}
this._activeTweens.length = 0;
}
private _removeTween(t: Tween<any>) {
const idx = this._activeTweens.indexOf(t);
if (idx !== -1) {
this._activeTweens.splice(idx, 1);
}
}
}
コードの主要ポイント解説
onLoadenemyTagが空でないかチェックし、問題があればエラーログを出します。- 他に必須コンポーネントはないため、ここでの検証は最低限です。
startautoStartがtrueの場合、tryAutoStartChain()を呼び出します。- 自ノード周辺(
searchRadius)から最初のターゲットを検索し、見つかれば連鎖を開始します。
beginChainFrom(firstTarget)- 外部から「最初のターゲット」を指定して連鎖を開始したい場合に呼び出します。
- 設定値の検証・重複起動の防止・内部状態の初期化を行ったのち、
_chainFromTargetを呼びます。
_chainFromTarget(currentTarget)- 連鎖のメインループです。
- 現在のターゲットにダメージを適用し、
maxHitsに達していなければ次のターゲットを検索します。 - 次のターゲットが見つからなければ連鎖終了。
hitIntervalに応じて、ディレイ付きまたは即時で次へ連鎖します。
_findAllEnemies/_collectEnemiesRecursivedirector.getScene()からシーン全体を再帰的に探索し、node.tag === enemyTagのノードを収集します。- GameManager などの外部スクリプトに依存せず、「タグ」だけで敵を判定する設計です。
_spawnLineBetween(from, to)linePrefabが指定されている場合のみ呼ばれます。- 2つのノードのワールド座標から中点・角度・距離を計算し、線状のビームを表示します。
lineDuration秒後に自動的に破棄されます。
_applyDamage(target)- 敵ノードに対して
TakeDamageイベントをemitします。 - 敵側スクリプトは、必要に応じて
node.on('TakeDamage', ...)で受け取り、HP 減少などを実装できます。
- 敵ノードに対して
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を
ChainLightning.tsにします。 - 自動生成されたコードをすべて削除し、本記事の 「TypeScriptコードの実装」 セクションのコードを丸ごと貼り付けて保存します。
2. 敵ノードの用意(タグ設定が重要)
このコンポーネントは「敵タグ」で敵を判定するため、まず敵ノードを準備します。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、敵用ノードを複数作成します。
- 例:
Enemy_1,Enemy_2,Enemy_3…
- 例:
- 各敵ノードを選択し、Inspector の Tag フィールドを
Enemyに設定します。- Tag フィールドが見当たらない場合は、Node のプロパティ欄をスクロールして確認してください。
- 本スクリプトのデフォルト
enemyTagは"Enemy"なので、ここを揃えることが重要です。
- テストしやすいように、敵ノードを適度に離して配置しておきます(例: 200〜300px 間隔)。
(任意)敵側のダメージ処理を確認したい場合は、シンプルなコンポーネントを追加してログを出すと分かりやすくなります。
// 簡易的なダメージ表示用コンポーネント例(EnemyDamageLogger.ts)
import { _decorator, Component, Node } from 'cc';
const { ccclass } = _decorator;
@ccclass('EnemyDamageLogger')
export class EnemyDamageLogger extends Component {
onLoad() {
this.node.on('TakeDamage', (data: any) => {
console.log(`[EnemyDamageLogger] ${this.node.name} がダメージを受けました:`, data);
});
}
}
このスクリプトを敵ノードにアタッチしておくと、連鎖雷からのダメージイベントがコンソールに表示されます。
3. 連鎖雷を発生させるノードの作成
- Hierarchy パネルで右クリック → Create → 2D Object → Empty Node を選択し、ノード名を
ChainLightningTesterなどにします。 ChainLightningTesterノードを選択し、Inspector の Add Component → Custom → ChainLightning を選択してコンポーネントを追加します。- Inspector で ChainLightning の各プロパティを設定します。
- Enemy Tag:
Enemy(敵ノードの Tag と一致させる) - Damage:
10(お好みで) - Max Hits:
5 - Max Chain Distance:
400 - Search Radius:
600 - Auto Start:
✓(チェック ON) - Hit Interval:
0.08 - Destroy On Complete:
✓(テスト用ノードなら OFF でも可) - Line Prefab: (後述、まずは
nullのままで OK) - Line Duration:
0.12 - Debug Log:
✓(挙動確認のため ON 推奨)
- Enemy Tag:
ChainLightningTesterノードを、敵たちの近くに配置します(searchRadius内に少なくとも1体は入るように)。
4. プレビューで自動連鎖を確認する
- エディタ右上の ▶(Play) ボタンを押して、エディタ内プレビューまたはブラウザプレビューを開始します。
- ゲーム開始直後、
ChainLightningTesterの位置から最も近い敵が最初のターゲットとして選ばれ、そこから周囲の敵へ順番に雷が連鎖していきます。 - コンソール(Console パネル)で以下のようなログが出ていることを確認します。
[ChainLightning] 連鎖開始: first target = Enemy_2[ChainLightning] Hit #1 : Enemy_2[ChainLightning] Enemy_2 に 10 ダメージを適用 (イベント: TakeDamage)- …(連鎖が続く)…
[ChainLightning] 連鎖処理が完了しました。総ヒット数: 4
- 敵ノードに
EnemyDamageLoggerを付けている場合は、そのログも合わせて表示されます。
5. 雷ビームのエフェクトを追加する(任意)
見た目を分かりやすくするため、雷ビーム用の Line プレハブを作成して linePrefab に設定してみましょう。
- Assets パネルで右クリック → Create → 2D Object → Sprite を選択し、名前を
LightningLineにします。 - Inspector で Sprite に細長いテクスチャを設定するか、単色の白い画像を指定して Width を大きく / Height を小さく 調整して「線」に見えるようにします。
- 例: Width: 100, Height: 8
LightningLineノードを選択した状態で、Assets パネルにドラッグ&ドロップし、Prefab 化します。- Hierarchy 上の
LightningLineノードは削除して構いません(Prefab が残っていればOK)。 ChainLightningTesterノードを選択し、Inspector の Line Prefab に先ほど作成したLightningLineプレハブをドラッグ&ドロップします。- Line Duration を
0.1〜0.2秒程度に設定します。 - 再度プレイして、連鎖のたびに敵同士を結ぶビームが一瞬表示されることを確認します。
6. スクリプトから任意タイミングで連鎖を開始する(応用)
autoStart を OFF にし、外部スクリプトから beginChainFrom() を呼び出すことで、「弾が特定の敵に当たった瞬間から連鎖開始」といった挙動を作れます。
ChainLightningTesterノードの Auto Start のチェックを外します。- 例えば「弾」ノードに以下のようなスクリプトを付け、敵に当たったときに呼び出します。
// Bullet.ts の一部サンプル
import { _decorator, Component, Node, Collider2D, Contact2DType, IPhysics2DContact } from 'cc';
import { ChainLightning } from './ChainLightning';
const { ccclass, property } = _decorator;
@ccclass('Bullet')
export class Bullet extends Component {
@property(ChainLightning)
chainLightning: ChainLightning | null = null;
onLoad() {
const col = this.getComponent(Collider2D);
if (col) {
col.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
}
}
private onBeginContact(selfCol: Collider2D, otherCol: Collider2D, contact: IPhysics2DContact | null) {
const otherNode = otherCol.node;
// ここでは単純に敵タグで判定
if (otherNode.tag === 'Enemy' && this.chainLightning) {
this.chainLightning.beginChainFrom(otherNode);
}
}
}
このようにすれば、「弾が当たった敵から連鎖雷をスタート」という使い方も簡単に実現できます。
まとめ
この ChainLightning コンポーネントは、
- 敵管理用の GameManager や外部シングルトンに一切依存せず、
- 敵ノードの Tag と距離情報だけで連鎖対象を自動判定し、
- ダメージは カスタムイベントで通知するだけのシンプルな設計
になっているため、
- 2D アクションゲームのスキル(チェインライトニング、チェインフレイムなど)
- タワーディフェンスの「雷塔」や「範囲連鎖攻撃タワー」
- RPG の範囲魔法・状態異常の伝播表現
など、さまざまなジャンルに そのまま再利用できます。
調整可能なプロパティ(最大ヒット数・連鎖距離・ディレイ・ビームエフェクトなど)もすべてインスペクタから変更できるため、デザイナーやレベルデザイナーがコードに触れずにバランス調整できる点も大きなメリットです。
このコンポーネントをベースに、
- 属性ごとに異なるエフェクトプレハブを使い分ける
- 同じ敵に複数回ヒットさせるモードを追加する
- 敵の「優先度」や「HP」などを見て次のターゲットを選ぶ
といった拡張も容易に行えます。
まずは本記事の実装をそのまま導入し、シーン内に敵ノードとテストノードを配置して、「アタッチするだけで連鎖雷が動く」感覚を体験してみてください。
