【Cocos Creator 3.8】AggroLink(仲間呼び)の実装:アタッチするだけで「攻撃された敵が周囲の仲間も一斉にプレイヤーを追跡し始める」汎用スクリプト
このコンポーネントは、「敵Aを攻撃したら、近くにいる敵B・Cも一緒にプレイヤーを追いかけ始める」といった、いわゆる「仲間呼び(アグロリンク)」を簡単に実装するためのものです。各敵ノードにこの AggroLink をアタッチし、インスペクタでいくつかのパラメータを設定するだけで、外部のゲームマネージャや専用のAIスクリプトに依存せず、攻撃イベントの通知と仲間呼びが完結します。
コンポーネントの設計方針
1. 要件整理
- 敵ノードにアタッチして使う。
- 「攻撃を受けた」ことを何らかの形で検知したら、周囲の仲間(同じグループの敵)をアクティブ化する。
- アクティブ化された敵は「追跡モード」になるが、本コンポーネント単体で完結させるため、シンプルな追跡処理を内部に持つ。
- プレイヤーへの参照や、攻撃イベントの発火はインスペクタやAPIから設定・呼び出し可能にする。
- 外部のカスタムスクリプト(GameManager、EnemyController など)には一切依存しない。
2. 設計アプローチ
外部依存をなくしつつ柔軟に使えるよう、以下のような設計にします。
- 攻撃検知:
- ゲーム側の攻撃判定スクリプトから
onDamaged()を呼び出すだけで「攻撃された」ことを通知できるようにする。 - 将来、コリジョンイベントなどからも呼べるよう、公開メソッドとして実装する。
- ゲーム側の攻撃判定スクリプトから
- 仲間の探索:
- 「同じ親ノード配下にいる敵」を「仲間」とみなす汎用設計。
- 同じ
groupIdを持つAggroLinkコンポーネントを持つノードを仲間とする。 - 一定の半径(
linkRadius)内にいる仲間だけを起こす。
- 追跡モード:
- 内部に簡易な「ターゲット追跡」ロジックを持たせる。
- ターゲット(通常はプレイヤー)は
@property(Node)で指定。 - 追跡速度・開始遅延などもインスペクタから調整可能にする。
- イベント通知:
- 「自分がアクティブになった」「仲間を起こした」といったタイミングを
EventTargetで外部に通知できるようにする(任意で利用)。 - ただし他のカスタムスクリプトを前提にはしない。
- 「自分がアクティブになった」「仲間を起こした」といったタイミングを
3. インスペクタで設定可能なプロパティ設計
AggroLink コンポーネントで用意する @property は以下の通りです。
- groupId: number
- 同じ値を持つ敵同士が「仲間」とみなされるグループID。
- 例: ゴブリン系は 1、スケルトン系は 2 など。
- autoRegisterToParent: boolean
- 親ノード配下の仲間探索を行うかどうか。
- true の場合、
start()時に同じ親の子ノードからAggroLinkを自動でリストアップする。 - 通常は true のままでOK。
- linkRadius: number
- 仲間呼びの有効半径(ワールド座標ベース)。
- この半径以内にいる同じ
groupIdの敵が一斉にアクティブ化される。
- activateSelfOnDamaged: boolean
onDamaged()が呼ばれたとき、自分自身もアクティブ化するかどうか。- 通常は true(攻撃された本人も追跡を開始)。
- activateDelay: number
- アクティブ化までの遅延秒数。
- 0 なら即座に追跡開始、0.5 なら 0.5 秒後に追跡開始。
- target: Node | null
- 追跡対象のノード(通常はプレイヤー)。
- null の場合は追跡しない(アクティブフラグだけ立つ)。
- moveSpeed: number
- ターゲットに向かって移動する速度(単位:ユニット/秒)。
- 0 にすると移動はしない(アクティブ状態のフラグ管理だけになる)。
- maxChaseDistance: number
- ターゲットとの距離がこの値を超えたら追跡をやめる(0 以下なら無制限)。
- 離れすぎたらアクティブを解除したい場合に使用。
- debugDrawRadius: boolean
- 有効半径
linkRadiusをGizmosで可視化するかどうか。 - シーンビューで調整しやすくなる。
- 有効半径
また、スクリプト内で以下のような公開メソッドを提供します。
onDamaged(): void- 外部から「攻撃を受けた」と通知するためのメソッド。
- これを呼ぶと、自分と周囲の仲間がアクティブ化される。
setTarget(target: Node | null): void- 追跡対象を動的に変更するためのメソッド。
isActive(): boolean- 現在この敵がアクティブ(追跡モード)かどうかを取得できる。
TypeScriptコードの実装
import { _decorator, Component, Node, Vec3, math, EventTarget, sys, director, Gizmos, Color } from 'cc';
const { ccclass, property, executeInEditMode, menu } = _decorator;
/**
* AggroLink
* - 攻撃を受けた敵から周囲の仲間(同じ groupId を持つ AggroLink)をアクティブ化する。
* - 追跡対象(target)に向かってシンプルに移動する追跡ロジックを内蔵。
* - 外部のカスタムスクリプトには依存せず、アタッチするだけで利用可能。
*/
@ccclass('AggroLink')
@executeInEditMode(true)
@menu('Custom/AggroLink')
export class AggroLink extends Component {
@property({
tooltip: '同じグループIDを持つ敵同士が仲間としてリンクします。\n例: ゴブリン系=1, スケルトン系=2 など。'
})
public groupId: number = 1;
@property({
tooltip: 'true の場合、同じ親ノード配下の子ノードから自動的に仲間(AggroLink)を探索します。\n通常は true のままで問題ありません。'
})
public autoRegisterToParent: boolean = true;
@property({
tooltip: '仲間呼びの有効半径(ワールド座標)。この半径以内の仲間だけをアクティブ化します。'
})
public linkRadius: number = 600;
@property({
tooltip: '自分が攻撃を受けたとき、自分自身もアクティブ化するかどうか。'
})
public activateSelfOnDamaged: boolean = true;
@property({
tooltip: 'アクティブ化までの遅延秒数。0 なら即時、0.5 なら 0.5 秒後に追跡開始。'
})
public activateDelay: number = 0.0;
@property({
tooltip: '追跡対象となるノード(通常はプレイヤー)。\n未設定の場合、アクティブ化しても移動は行いません。'
})
public target: Node | null = null;
@property({
tooltip: 'ターゲットに向かって移動する速度(ユニット/秒)。0 の場合は移動せず、アクティブ状態のフラグ管理のみ行います。'
})
public moveSpeed: number = 200;
@property({
tooltip: 'ターゲットとの距離がこの値を超えたら追跡を終了します。0 以下なら無制限に追跡します。'
})
public maxChaseDistance: number = 0;
@property({
tooltip: 'シーンビューで linkRadius を円として可視化します(デバッグ用)。'
})
public debugDrawRadius: boolean = true;
/**
* アクティブ状態かどうか(追跡モード中か)
*/
private _isActive: boolean = false;
/**
* 自動探索した仲間候補のキャッシュ。
* 同じ親の子ノードにある AggroLink を保持します。
*/
private _cachedAllies: AggroLink[] = [];
/**
* 外部にイベントを通知したい場合に使える EventTarget。
* - 'activated' : (self: AggroLink) => void
* - 'linkedAllies' : (self: AggroLink, allies: AggroLink[]) => void
*
* 直接使わなくても構いません。
*/
public readonly events: EventTarget = new EventTarget();
// ワーク用ベクトル(GC削減)
private static _tempVecA: Vec3 = new Vec3();
private static _tempVecB: Vec3 = new Vec3();
onLoad() {
// エディタ上での安全対策:linkRadius が負値にならないように補正
if (this.linkRadius < 0) {
this.linkRadius = 0;
}
if (this.moveSpeed < 0) {
this.moveSpeed = 0;
}
}
start() {
// 同じ親ノード配下の仲間を自動探索
if (this.autoRegisterToParent) {
this._refreshAlliesFromParent();
}
}
update(deltaTime: number) {
// 実行時のみ追跡ロジックを動かす(エディタでは動かさない)
if (!director.isPaused() && !sys.isBrowser && !sys.isMobile) {
// ネイティブ環境かどうかの判定に依存しないようにするため、
// ここでは単純に _isActive のみで判断する。
}
if (!this._isActive) {
return;
}
// ターゲットがいない場合は移動しない
if (!this.target || !this.target.isValid) {
return;
}
if (this.moveSpeed <= 0) {
return;
}
const selfPos = AggroLink._tempVecA;
const targetPos = AggroLink._tempVecB;
this.node.getWorldPosition(selfPos);
this.target.getWorldPosition(targetPos);
// ターゲットまでの距離
const dist = Vec3.distance(selfPos, targetPos);
// 最大追跡距離を超えたらアクティブ解除
if (this.maxChaseDistance > 0 && dist > this.maxChaseDistance) {
this._setActive(false);
return;
}
if (dist <= 0.0001) {
return;
}
// ターゲット方向の正規化ベクトル
const dir = targetPos.subtract(selfPos);
dir.normalize();
// 移動量 = 方向 * 速度 * dt
const move = dir.multiplyScalar(this.moveSpeed * deltaTime);
// 新しい位置
selfPos.add(move);
this.node.setWorldPosition(selfPos);
}
/**
* 外部から「攻撃を受けた」と通知するための公開メソッド。
* これを呼ぶと、自分と周囲の仲間をアクティブ化します。
*/
public onDamaged(): void {
// 自分をアクティブ化
if (this.activateSelfOnDamaged) {
this._activateWithDelay();
}
// 仲間を探索してアクティブ化
const linkedAllies = this._activateAlliesWithinRadius();
if (linkedAllies.length > 0) {
this.events.emit('linkedAllies', this, linkedAllies);
}
}
/**
* 追跡対象を動的に設定する。
*/
public setTarget(target: Node | null): void {
this.target = target;
}
/**
* 現在アクティブ状態かどうかを返す。
*/
public isActive(): boolean {
return this._isActive;
}
/**
* 同じ親ノード配下から仲間(AggroLink)をキャッシュする。
*/
private _refreshAlliesFromParent(): void {
this._cachedAllies.length = 0;
const parent = this.node.parent;
if (!parent) {
console.warn(`[AggroLink] ${this.node.name} には親ノードがありません。autoRegisterToParent を利用する場合は親ノード配下に配置してください。`);
return;
}
const children = parent.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child === this.node) {
continue;
}
const ally = child.getComponent(AggroLink);
if (ally) {
this._cachedAllies.push(ally);
}
}
}
/**
* 有効半径内にいる仲間をアクティブ化する。
* @returns アクティブ化された仲間の配列
*/
private _activateAlliesWithinRadius(): AggroLink[] {
// キャッシュが空で、かつ autoRegisterToParent が true なら再探索
if (this.autoRegisterToParent && this._cachedAllies.length === 0) {
this._refreshAlliesFromParent();
}
const activatedAllies: AggroLink[] = [];
const selfPos = AggroLink._tempVecA;
this.node.getWorldPosition(selfPos);
for (let i = 0; i < this._cachedAllies.length; i++) {
const ally = this._cachedAllies[i];
if (!ally || !ally.node || !ally.node.isValid) {
continue;
}
// グループIDが同じでなければ仲間扱いしない
if (ally.groupId !== this.groupId) {
continue;
}
const allyPos = AggroLink._tempVecB;
ally.node.getWorldPosition(allyPos);
const dist = Vec3.distance(selfPos, allyPos);
if (dist <= this.linkRadius) {
ally._activateWithDelay();
activatedAllies.push(ally);
}
}
return activatedAllies;
}
/**
* 遅延付きでアクティブ化する。
*/
private _activateWithDelay(): void {
if (this.activateDelay <= 0) {
this._setActive(true);
return;
}
// すでにアクティブなら再度遅延しない
if (this._isActive) {
return;
}
this.scheduleOnce(() => {
this._setActive(true);
}, this.activateDelay);
}
/**
* アクティブ状態の内部セット+イベント発火。
*/
private _setActive(active: boolean): void {
if (this._isActive === active) {
return;
}
this._isActive = active;
if (active) {
this.events.emit('activated', this);
} else {
this.events.emit('deactivated', this);
}
}
/**
* エディタ上で linkRadius を可視化するための Gizmos 描画。
* Cocos Creator 3.8 では Editor 拡張側で描画するのが正式ですが、
* ここでは簡易な例として実装しています。
*
* 実際のプロジェクトでは、必要に応じて Editor 拡張に移してください。
*/
onDrawGizmos() {
if (!this.debugDrawRadius) {
return;
}
if (!this.node) {
return;
}
const worldPos = this.node.worldPosition;
const color = new Color(0, 255, 0, 128); // 半透明の緑
// 簡易な円描画(32分割)
const segments = 32;
const angleStep = (Math.PI * 2) / segments;
for (let i = 0; i < segments; i++) {
const a1 = angleStep * i;
const a2 = angleStep * (i + 1);
const p1 = new Vec3(
worldPos.x + Math.cos(a1) * this.linkRadius,
worldPos.y + Math.sin(a1) * this.linkRadius,
worldPos.z
);
const p2 = new Vec3(
worldPos.x + Math.cos(a2) * this.linkRadius,
worldPos.y + Math.sin(a2) * this.linkRadius,
worldPos.z
);
Gizmos.drawLine(p1, p2, color);
}
}
}
コードのポイント解説
onLoad()- プロパティの最低限のバリデーション(負値の補正)を行っています。
start()autoRegisterToParentが true のとき、同じ親ノード配下から仲間候補(AggroLink)を探索してキャッシュします。- これにより、毎フレームの探索コストを削減します。
update(deltaTime)_isActiveが true のときのみ追跡ロジックを実行。- ターゲットが設定されていない、または
moveSpeed <= 0の場合は移動しません(フラグ管理のみ)。 maxChaseDistanceを超えたらアクティブを解除して追跡終了。
onDamaged()- 外部(攻撃判定スクリプトなど)から呼び出すことで、「攻撃を受けた」ことを通知します。
- 自分自身のアクティブ化(
activateSelfOnDamagedが true の場合)と、周囲の仲間のアクティブ化を行います。
_refreshAlliesFromParent()- 親ノードの子ノードから
AggroLinkを持つノードをすべて探索し、_cachedAlliesに格納します。 - 「同じ親の下にいる敵は同じエリアの仲間」というよくある構造にマッチします。
- 親ノードの子ノードから
_activateAlliesWithinRadius()- キャッシュされた仲間の中から、
groupIdが同じで、かつlinkRadius以内にいるものだけをアクティブ化します。 - アクティブ化は
_activateWithDelay()経由で行われるため、遅延も反映されます。
- キャッシュされた仲間の中から、
onDrawGizmos()- シーンビュー上で仲間呼びの半径を可視化するための簡易描画です。
debugDrawRadiusが true のときのみ描画されます。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を
AggroLink.tsに変更します。 - 作成された
AggroLink.tsをダブルクリックしてエディタで開き、上記のコードをすべて貼り付けて保存します。
2. テスト用のシーン構築
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択して、敵用のノードを3つほど作成します。
- 例:
Enemy_A,Enemy_B,Enemy_C
- 例:
- これらの敵ノードを 1 つの親ノードの子にまとめます。
- Hierarchy で右クリック → Create → Empty Node で
EnemyGroupを作成。 Enemy_A,Enemy_B,Enemy_CをドラッグしてEnemyGroupの子にします。
- Hierarchy で右クリック → Create → Empty Node で
- プレイヤー役のノードを 1 つ作成します。
- Hierarchy で右クリック → Create → 2D Object → Sprite で
Playerノードを作成。 - シーン上の適当な位置に配置します。
- Hierarchy で右クリック → Create → 2D Object → Sprite で
3. AggroLink コンポーネントのアタッチ
- Hierarchy で
Enemy_Aを選択します。 - Inspector の下部にある Add Component ボタンをクリック → Custom → AggroLink を選択します。
- 同様に、
Enemy_B,Enemy_CにもAggroLinkを追加します。
4. インスペクタでプロパティを設定
各敵ノード(Enemy_A, Enemy_B, Enemy_C)の AggroLink を次のように設定します。
- groupId:
1- 3体とも同じグループIDにして、仲間としてリンクさせます。
- autoRegisterToParent:
trueEnemyGroupの子同士で自動的に仲間を探索します。
- linkRadius:
600(シーンのスケールに合わせて調整)- 3体を適度に離して配置し、この半径内に収まるようにします。
- activateSelfOnDamaged:
true- 攻撃された本人も追跡モードに入るようにします。
- activateDelay:
0.2など- 少しだけ遅れて仲間が動き出す演出にできます。
- target:
Playerノードをドラッグ&ドロップで指定。 - moveSpeed:
200〜400くらいでお好みで。 - maxChaseDistance:
0(無制限追跡)または1500など。 - debugDrawRadius:
true- シーンビューで緑色の円として
linkRadiusが表示されます。
- シーンビューで緑色の円として
5. 攻撃イベントから onDamaged() を呼ぶ
このコンポーネントは「攻撃された」ことを自前で検知しないため、攻撃判定側のスクリプトから onDamaged() を呼び出す必要があります。
例えば、簡単なテスト用の「クリックした敵を攻撃する」スクリプトを作成して、Enemy_A にだけアタッチしてみます。
import { _decorator, Component, Node, Input, input, EventMouse, Camera, PhysicsSystem2D, Vec2 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('SimpleClickDamage')
export class SimpleClickDamage extends Component {
@property({ tooltip: 'シーン内の2Dカメラを指定してください。' })
public camera2D: Camera | null = null;
start() {
input.on(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
}
onDestroy() {
input.off(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
}
private onMouseDown(event: EventMouse) {
if (!this.camera2D) {
console.warn('[SimpleClickDamage] camera2D が設定されていません。');
return;
}
// クリック位置からレイを飛ばし、Enemy_A に当たったら onDamaged() を呼ぶ、などの処理を書く
const aggro = this.getComponent('AggroLink') as any;
if (aggro && typeof aggro.onDamaged === 'function') {
aggro.onDamaged();
}
}
}
上記はあくまで「呼び出し方の例」です。実際のゲームでは、ダメージ判定やHP管理スクリプトの中で、敵がダメージを受けたタイミングで aggroLink.onDamaged() を呼び出してください。
6. 実行して動作確認
- シーンを保存します。
- エディタ右上の Play ボタンを押してゲームを実行します。
- 攻撃判定スクリプトから
onDamaged()が呼ばれるようにしておけば、- 攻撃された敵(例:
Enemy_A)がアクティブ化してPlayerに向かって移動を開始します。 linkRadius内にいるEnemy_B,Enemy_Cも、同じくプレイヤーを追いかけ始めます。maxChaseDistanceを設定している場合、一定距離以上離れると追跡をやめます。
- 攻撃された敵(例:
まとめ
AggroLink コンポーネントは、
- 各敵ノードにアタッチして、
- グループID・仲間呼び半径・追跡対象・移動速度などをインスペクタで設定し、
- 攻撃判定側から
onDamaged()を呼び出すだけ
で、「一体を攻撃したら周囲の仲間も一斉にプレイヤーを追いかけてくる」振る舞いを簡単に実現できます。
外部の GameManager や専用の AI スクリプトに依存せず、このスクリプト単体で完結しているため、
- 小さなテストシーンでの検証
- プロトタイプ段階の素早い実装
- 既存プロジェクトへの後付け
など、さまざまな場面で再利用しやすい構成になっています。
応用としては、
groupIdを使って「エリアごと」「敵種族ごと」に別々の仲間呼びグループを作る。eventsの'activated'をフックして、アニメーションの再生や SE 再生をトリガーする。activateDelayをランダム化して、仲間がバラバラのタイミングで動き出すようにする。
といった拡張も簡単に行えます。まずはこの記事のコードをそのまま導入し、シーン上で挙動を確認しながら、自分のゲームに合わせてチューニングしてみてください。




