【Cocos Creator】アタッチするだけ!AggroLink (仲間呼び)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【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
    • 有効半径 linkRadiusGizmos で可視化するかどうか。
    • シーンビューで調整しやすくなる。

また、スクリプト内で以下のような公開メソッドを提供します。

  • 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. スクリプトファイルの作成

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を AggroLink.ts に変更します。
  3. 作成された AggroLink.ts をダブルクリックしてエディタで開き、上記のコードをすべて貼り付けて保存します。

2. テスト用のシーン構築

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択して、敵用のノードを3つほど作成します。
    • 例: Enemy_A, Enemy_B, Enemy_C
  2. これらの敵ノードを 1 つの親ノードの子にまとめます。
    • Hierarchy で右クリック → Create → Empty NodeEnemyGroup を作成。
    • Enemy_A, Enemy_B, Enemy_C をドラッグして EnemyGroup の子にします。
  3. プレイヤー役のノードを 1 つ作成します。
    • Hierarchy で右クリック → Create → 2D Object → SpritePlayer ノードを作成。
    • シーン上の適当な位置に配置します。
  1. Hierarchy で Enemy_A を選択します。
  2. Inspector の下部にある Add Component ボタンをクリック → CustomAggroLink を選択します。
  3. 同様に、Enemy_B, Enemy_C にも AggroLink を追加します。

4. インスペクタでプロパティを設定

各敵ノード(Enemy_A, Enemy_B, Enemy_C)の AggroLink を次のように設定します。

  • groupId1
    • 3体とも同じグループIDにして、仲間としてリンクさせます。
  • autoRegisterToParenttrue
    • EnemyGroup の子同士で自動的に仲間を探索します。
  • linkRadius600(シーンのスケールに合わせて調整)
    • 3体を適度に離して配置し、この半径内に収まるようにします。
  • activateSelfOnDamagedtrue
    • 攻撃された本人も追跡モードに入るようにします。
  • activateDelay0.2 など
    • 少しだけ遅れて仲間が動き出す演出にできます。
  • targetPlayer ノードをドラッグ&ドロップで指定。
  • moveSpeed200400 くらいでお好みで。
  • maxChaseDistance0(無制限追跡)または 1500 など。
  • debugDrawRadiustrue
    • シーンビューで緑色の円として 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. 実行して動作確認

  1. シーンを保存します。
  2. エディタ右上の Play ボタンを押してゲームを実行します。
  3. 攻撃判定スクリプトから onDamaged() が呼ばれるようにしておけば、
    • 攻撃された敵(例: Enemy_A)がアクティブ化して Player に向かって移動を開始します。
    • linkRadius 内にいる Enemy_B, Enemy_C も、同じくプレイヤーを追いかけ始めます。
    • maxChaseDistance を設定している場合、一定距離以上離れると追跡をやめます。

まとめ

AggroLink コンポーネントは、

  • 各敵ノードにアタッチして、
  • グループID・仲間呼び半径・追跡対象・移動速度などをインスペクタで設定し、
  • 攻撃判定側から onDamaged() を呼び出すだけ

で、「一体を攻撃したら周囲の仲間も一斉にプレイヤーを追いかけてくる」振る舞いを簡単に実現できます。

外部の GameManager や専用の AI スクリプトに依存せず、このスクリプト単体で完結しているため、

  • 小さなテストシーンでの検証
  • プロトタイプ段階の素早い実装
  • 既存プロジェクトへの後付け

など、さまざまな場面で再利用しやすい構成になっています。

応用としては、

  • groupId を使って「エリアごと」「敵種族ごと」に別々の仲間呼びグループを作る。
  • events'activated' をフックして、アニメーションの再生や SE 再生をトリガーする。
  • activateDelay をランダム化して、仲間がバラバラのタイミングで動き出すようにする。

といった拡張も簡単に行えます。まずはこの記事のコードをそのまま導入し、シーン上で挙動を確認しながら、自分のゲームに合わせてチューニングしてみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!