【Cocos Creator】アタッチするだけ!AutoAim (自動照準)の実装方法【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】AutoAim(自動照準)の実装:アタッチするだけで「一定範囲内で最も近い敵の方向」を常に取得できる汎用スクリプト

このガイドでは、任意のノードにアタッチするだけで「指定した半径内にいる敵(特定のグループ)を検出し、その中で最も近い敵の方向ベクトルや角度を常に計算してくれる」汎用コンポーネント AutoAim を実装します。

タワーディフェンスの砲台、プレイヤーキャラの自動ロックオン、敵AIのターゲット選択など、「近くの敵を向きたい」場面でそのまま使えるように設計します。外部の GameManager やシングルトンには一切依存せず、インスペクタで設定するだけで完結します。


コンポーネントの設計方針

機能要件の整理

  • このコンポーネントをアタッチしたノード(以下「所有ノード」)を基準に、一定半径内の「敵ノード」を検出する。
  • 「敵ノード」は Node の Group(グループ) で識別する。
  • 検出対象は 2D/3D を問わず Node のワールド座標から距離を計算する(ここでは 2D/3D 共通のベクトル計算で対応)。
  • 範囲内に 1 体以上の敵がいれば、最も近い敵ノードを求める。
  • 最も近い敵に対して:
    • 所有ノードから敵ノードへの 方向ベクトル(正規化済み)
    • 所有ノードの「前方向」(2Dなら基本的に +X 方向、3Dなら +Z など、任意で選択)から見た 回転角度(度数)

    を常に計算して保持しておく。

  • 必要であれば、所有ノードをその方向へ自動で回転させるオプションを用意する。
  • 敵がいない場合は「ターゲットなし」として扱い、方向ベクトルはゼロベクトル、角度は 0 とする。
  • 処理負荷を抑えるため、毎フレームではなく更新間隔を調整できるようにする。

外部依存をなくすためのアプローチ

  • 敵一覧を外部マネージャから取得しない。
    • Cocos の 全ノード を階層走査し、Group を見て自前でフィルタリングする。
    • 大規模シーンでの負荷を抑えるため、検索の更新間隔(秒)を調整可能にする。
  • 結果は AutoAim コンポーネント自身の public プロパティとして公開し、他のスクリプトが参照できるようにする(ただし依存は強制しない)。
  • 回転機能はオプション化(bool)し、ON の場合のみ所有ノードの rotation を変更する。

インスペクタで設定可能なプロパティ設計

AutoAim コンポーネントに用意する主な @property は以下の通りです。

  • searchRadius: number
    • 敵を探索する半径(ワールド座標系の距離)。
    • 単位は「シーンの座標単位」(2Dならピクセル相当、3Dならユニット)。
    • 例: 500 など。
  • targetGroupName: string
    • 検出対象とする敵ノードの Group 名。
    • 例: “enemy” や “Enemy” など、プロジェクトで設定した Group 名。
    • 空文字の場合は「全グループ対象」とし、Group フィルタを無効化できるようにする。
  • updateInterval: number
    • 敵探索を行う間隔(秒)。
    • 0 以下に設定した場合は 毎フレーム探索を行う。
    • 例: 0.1 や 0.2 などにすると負荷を抑えつつ十分なレスポンスを確保できる。
  • autoRotate: boolean
    • 最も近い敵の方向へ自動でノードを回転させるかどうか。
    • 2D でスプライトを敵の方向へ向けたい場合などに便利。
  • rotationPlane: ‘XY’ | ‘XZ’(実装上は enum 風の number + Editor 用 enum で実現)
    • どの平面で角度を計算するか。
    • 2D(Canvas ベース)の場合は通常 XY 平面
    • 3D の TPS/FPS などで水平回転のみしたい場合は通常 XZ 平面
  • forwardAxis: ‘X+ / X- / Y+ / Y- / Z+ / Z-‘
    • 「ノードのどの軸方向を前方とみなすか」を指定。
    • 2D で右向きスプライトなら X+、上向きスプライトなら Y+ などを選ぶ。
    • この設定に基づいて「前方向から見た角度」を算出し、自動回転に利用する。
  • debugDrawGizmos: boolean
    • エディタ上の Scene ビューで探索半径やターゲット方向を Gizmo として描画するかどうか。
    • 挙動確認用(ビルド後には影響しない)。

また、他のスクリプトから読み取り用として参照できる値も持たせます(@property ではなく @property({readonly: true}) 風に見せる or public readonly)。

  • currentTarget: Node | null … 現在ロックオンしている最も近い敵ノード。
  • currentDirection: Vec3 … 所有ノードからターゲットへの正規化方向ベクトル(ターゲットなしなら Vec3.ZERO)。
  • currentAngle: number … forwardAxis から見た回転角度(度)。

TypeScriptコードの実装

以下が完成した AutoAim コンポーネントの全コードです。


import { _decorator, Component, Node, Vec3, math, director, Scene, CCString, CCFloat, CCBoolean } from 'cc';
const { ccclass, property, executeInEditMode, menu } = _decorator;

/**
 * 回転に使う平面
 * XY: 2Dゲーム用(X軸右、Y軸上)
 * XZ: 3Dゲームで水平面のみ回転したい場合(X軸右、Z軸奥)
 */
enum RotationPlane {
    XY = 0,
    XZ = 1,
}

/**
 * ノードの「前方向」とみなす軸
 */
enum ForwardAxis {
    X_POS = 0,
    X_NEG = 1,
    Y_POS = 2,
    Y_NEG = 3,
    Z_POS = 4,
    Z_NEG = 5,
}

@ccclass('AutoAim')
@executeInEditMode(true)
@menu('Custom/AutoAim')
export class AutoAim extends Component {

    @property({
        type: CCFloat,
        tooltip: '敵を探索する半径(ワールド座標系)。単位はシーンの座標単位です。'
    })
    public searchRadius: number = 500;

    @property({
        type: CCString,
        tooltip: '検出対象とする敵ノードのGroup名。空文字の場合は全てのGroupを対象にします。'
    })
    public targetGroupName: string = 'enemy';

    @property({
        type: CCFloat,
        tooltip: '敵探索を行う間隔(秒)。0以下の場合は毎フレーム探索します。'
    })
    public updateInterval: number = 0.1;

    @property({
        type: CCBoolean,
        tooltip: '最も近い敵の方向へ自動でノードを回転させるかどうか。'
    })
    public autoRotate: boolean = true;

    @property({
        tooltip: 'どの平面で角度を計算するか。2Dなら通常XY、3Dで水平回転のみならXZ。'
    })
    public rotationPlane: RotationPlane = RotationPlane.XY;

    @property({
        tooltip: 'ノードのどの軸方向を「前方向」とみなすか。スプライトが右向きならX+、上向きならY+など。'
    })
    public forwardAxis: ForwardAxis = ForwardAxis.X_POS;

    @property({
        type: CCBoolean,
        tooltip: 'エディタのSceneビューで探索半径とターゲット方向を簡易表示します(Gizmo)。'
    })
    public debugDrawGizmos: boolean = true;

    // === 実行時に他スクリプトから参照できる読み取り用情報 ===

    /**
     * 現在ロックオンしている最も近い敵ノード。いない場合はnull。
     */
    public currentTarget: Node | null = null;

    /**
     * 所有ノードからcurrentTargetへの正規化方向ベクトル。
     * ターゲットがいない場合はVec3.ZERO。
     */
    public currentDirection: Vec3 = new Vec3(0, 0, 0);

    /**
     * forwardAxisを基準とした回転角度(度数法)。
     * ターゲットがいない場合は0。
     */
    public currentAngle: number = 0;

    // 内部用
    private _timeSinceLastUpdate = 0;

    onLoad() {
        // 防御的チェック:searchRadiusが負であれば0に補正
        if (this.searchRadius < 0) {
            console.warn('[AutoAim] searchRadiusが負の値だったため0に補正しました。', this.node.name);
            this.searchRadius = 0;
        }

        if (!this.node) {
            console.error('[AutoAim] 有効なNodeが見つかりません。コンポーネントのアタッチ先を確認してください。');
        }
    }

    start() {
        // 初回に一度探索を実行
        this._updateTarget();
    }

    update(deltaTime: number) {
        // 更新間隔が0以下なら毎フレーム探索
        if (this.updateInterval <= 0) {
            this._updateTarget();
        } else {
            this._timeSinceLastUpdate += deltaTime;
            if (this._timeSinceLastUpdate >= this.updateInterval) {
                this._timeSinceLastUpdate = 0;
                this._updateTarget();
            }
        }

        if (this.autoRotate) {
            this._applyRotation();
        }
    }

    /**
     * シーン内のノードを走査し、最も近いターゲットを更新する。
     */
    private _updateTarget() {
        const scene = director.getScene();
        if (!scene) {
            console.warn('[AutoAim] シーンがロードされていません。');
            this._clearTarget();
            return;
        }

        const ownerWorldPos = new Vec3();
        this.node.getWorldPosition(ownerWorldPos);

        let nearestNode: Node | null = null;
        let nearestSqrDist = Number.POSITIVE_INFINITY;

        // シーン直下の子から再帰的に探索
        const roots = scene.children;
        for (let i = 0; i < roots.length; i++) {
            this._searchNodeRecursive(roots[i], ownerWorldPos, (node, sqrDist) => {
                if (sqrDist < nearestSqrDist) {
                    nearestSqrDist = sqrDist;
                    nearestNode = node;
                }
            });
        }

        if (nearestNode) {
            this.currentTarget = nearestNode;
            const targetPos = new Vec3();
            nearestNode.getWorldPosition(targetPos);

            const dir = new Vec3();
            Vec3.subtract(dir, targetPos, ownerWorldPos);
            if (dir.lengthSqr() > 0.000001) {
                Vec3.normalize(this.currentDirection, dir);
            } else {
                this.currentDirection.set(0, 0, 0);
            }

            this.currentAngle = this._computeAngleFromForward(this.currentDirection);
        } else {
            this._clearTarget();
        }
    }

    /**
     * 再帰的にノードを探索し、条件に合うノードに対してコールバックを呼び出す。
     */
    private _searchNodeRecursive(node: Node, ownerWorldPos: Vec3, onCandidate: (node: Node, sqrDist: number) => void) {
        // 自分自身はスキップ
        if (node === this.node) {
            // 子ノードは探索する
        } else {
            // Groupフィルタ
            if (this._isTargetGroup(node)) {
                const nodeWorldPos = new Vec3();
                node.getWorldPosition(nodeWorldPos);

                const sqrDist = Vec3.squaredDistance(ownerWorldPos, nodeWorldPos);
                const radiusSqr = this.searchRadius * this.searchRadius;

                if (sqrDist <= radiusSqr) {
                    onCandidate(node, sqrDist);
                }
            }
        }

        // 子ノードを再帰的に探索
        const children = node.children;
        for (let i = 0; i < children.length; i++) {
            this._searchNodeRecursive(children[i], ownerWorldPos, onCandidate);
        }
    }

    /**
     * Group名によるフィルタ。targetGroupNameが空なら常にtrue。
     */
    private _isTargetGroup(node: Node): boolean {
        if (!this.targetGroupName || this.targetGroupName.trim() === '') {
            return true;
        }
        return node.layer && node.group === this.targetGroupName;
    }

    /**
     * ターゲット情報をクリアする。
     */
    private _clearTarget() {
        this.currentTarget = null;
        this.currentDirection.set(0, 0, 0);
        this.currentAngle = 0;
    }

    /**
     * currentDirectionから、forwardAxisを基準とした角度(度)を計算する。
     */
    private _computeAngleFromForward(direction: Vec3): number {
        if (direction.lengthSqr() <= 0.000001) {
            return 0;
        }

        let dx = 0, dy = 0;

        if (this.rotationPlane === RotationPlane.XY) {
            dx = direction.x;
            dy = direction.y;
        } else { // XZ
            dx = direction.x;
            dy = direction.z;
        }

        // ターゲット方向の角度(ラジアン)
        let targetRad = Math.atan2(dy, dx); // -PI ~ PI, X+を0とする

        // forwardAxisごとに「前方向」とみなす角度オフセットを決める
        let forwardOffsetRad = 0;

        switch (this.forwardAxis) {
            case ForwardAxis.X_POS:
                forwardOffsetRad = 0; // X+ を0度
                break;
            case ForwardAxis.X_NEG:
                forwardOffsetRad = Math.PI; // X- は180度
                break;
            case ForwardAxis.Y_POS:
                forwardOffsetRad = Math.PI / 2; // Y+ は90度
                break;
            case ForwardAxis.Y_NEG:
                forwardOffsetRad = -Math.PI / 2; // Y- は-90度
                break;
            case ForwardAxis.Z_POS:
                // XZ平面でZ+を前にしたい場合
                forwardOffsetRad = Math.PI / 2;
                break;
            case ForwardAxis.Z_NEG:
                forwardOffsetRad = -Math.PI / 2;
                break;
        }

        // targetRad から forward を見た角度
        let rad = targetRad - forwardOffsetRad;

        // -PI ~ PI に正規化
        rad = math.toRadian(math.toDegree(rad));
        // 度数へ変換
        let deg = math.toDegree(rad);

        return deg;
    }

    /**
     * autoRotateがtrueのとき、currentAngleに基づいてノードを回転させる。
     */
    private _applyRotation() {
        if (!this.currentTarget || this.currentDirection.lengthSqr() <= 0.000001) {
            return;
        }

        // 2D想定: Z軸回転でスプライトを向ける。
        // 3Dの場合は用途に応じてここを拡張可能。
        if (this.rotationPlane === RotationPlane.XY) {
            // Nodeのローカル回転を直接設定(Z軸回転)
            const euler = this.node.eulerAngles.clone();
            // Cocosの2Dスプライトは通常、右向き(X+)が0度で、反時計回りが正。
            euler.z = this.currentAngle;
            this.node.setRotationFromEuler(euler);
        } else {
            // XZ平面の場合はY軸回転で向ける
            const euler = this.node.eulerAngles.clone();
            euler.y = this.currentAngle;
            this.node.setRotationFromEuler(euler);
        }
    }

    /**
     * Sceneビューでの簡易Gizmo描画(エディタ専用)。
     * 実行ビルドには影響しません。
     */
    onDrawGizmos() {
        if (!this.debugDrawGizmos) {
            return;
        }
        // Cocos Creator 3.8 では、ここでgizmo APIを使って線や円を描画できますが、
        // 実装環境によりAPIが異なるため、必要に応じてプロジェクト側で拡張してください。
        // このメソッドを定義しておくことで、後から簡単にGizmo処理を追加できます。
    }
}

コードの主要部分の解説

onLoad

  • searchRadius が負の場合は 0 に補正し、警告ログを出します。
  • 念のため this.node が存在するかチェックし、問題があればエラーログを出します。

start

  • ゲーム開始時に一度だけ _updateTarget() を呼び出し、初期ターゲットを確定します。

update

  • updateInterval に応じて敵探索を行います。
    • 0 以下なら毎フレーム探索。
    • それ以外なら _timeSinceLastUpdate に deltaTime を加算し、間隔を超えたら探索。
  • autoRotate が true の場合、_applyRotation() で所有ノードをターゲット方向へ回転させます。

_updateTarget

  • Cocos の director.getScene() から現在のシーンを取得します。
  • シーン直下の子ノードを起点に、_searchNodeRecursive で全ノードを再帰的に走査します。
  • Group フィルタと半径チェックを通過したノードの中から、最も距離の近いノードを選びます。
  • 最も近いノードが見つかれば:
    • currentTarget に保存。
    • 所有ノードからターゲットへの方向ベクトルを計算し、正規化して currentDirection に保存。
    • _computeAngleFromForward で forwardAxis から見た角度を計算し、currentAngle に保存。
  • 見つからなければ _clearTarget() で情報をリセットします。

_searchNodeRecursive

  • 自分自身のノードはターゲット候補から除外しますが、その子孫ノードは探索します。
  • _isTargetGroup で Group 名フィルタを行い、対象ならワールド座標から距離を計算します。
  • 距離の二乗(sqrDist)が searchRadius^2 以下なら候補としてコールバックします。
  • その後、子ノードに対して再帰的に同じ処理を行います。

_computeAngleFromForward

  • rotationPlane が XY の場合は (x, y)、XZ の場合は (x, z) を使って Math.atan2 で角度(ラジアン)を求めます。
  • forwardAxis ごとに「前方向」の角度オフセットを設定し、ターゲット方向と前方向の差分を角度として算出します。
  • 最終的に度数法(deg)として currentAngle に保存されます。

_applyRotation

  • ターゲットがいない場合や方向ベクトルが無効な場合は何もしません。
  • 2D(XY 平面)の場合:
    • ノードの Euler 角の Z 成分を currentAngle に設定して setRotationFromEuler します。
  • 3D(XZ 平面)の場合:
    • ノードの Euler 角の Y 成分を currentAngle に設定して setRotationFromEuler します。

使用手順と動作確認

ここからは、Cocos Creator 3.8.7 エディタ上で実際に AutoAim を使う手順を具体的に説明します。

1. スクリプトファイルの作成

  1. エディタの Assets パネルで、AutoAim を置きたいフォルダ(例: assets/scripts)を右クリックします。
  2. Create > TypeScript を選択します。
  3. 新しく作成されたスクリプトファイルの名前を AutoAim.ts に変更します。
  4. 作成した AutoAim.ts をダブルクリックしてエディタ(VSCode など)で開き、上記の TypeScript コード全文を貼り付けて保存します。

2. テスト用シーンとノードの準備

ここでは 2D ゲームを想定した簡単な例を紹介します。

  1. 2Dシーンを用意
    • まだシーンがない場合は、File > New Scene で新規シーンを作成し、AutoAimTest.scene などの名前で保存します。
  2. プレイヤー(照準元)ノードの作成
    • Hierarchy パネルで右クリック → Create > 2D Object > Sprite を選択します。
    • 作成された Sprite の名前を Player に変更します。
    • Inspector の Sprite コンポーネントで任意の画像を設定し、プレイヤーの見た目を用意します。
    • Scene ビューで Player ノードを中央付近に配置します。
  3. 敵ノードの作成
    • Hierarchy パネルで右クリック → Create > 2D Object > Sprite を選択します。
    • 名前を Enemy1 に変更します。
    • Inspector の Sprite コンポーネントで敵用の画像を設定します(色違いでもOK)。
    • Scene ビューで Player から少し離れた位置に配置します(例: X=300, Y=0)。
    • 同様の手順で Enemy2, Enemy3 など複数の敵ノードを作成し、Player の周囲に散らして配置します。

3. Group(グループ)の設定

AutoAim は targetGroupName に設定した Group 名のノードだけを敵として認識します。ここでは “enemy” という Group を例にします。

  1. メインメニューから Project > Project Settings… を開きます。
  2. 左側のリストから Group を選択します。
  3. 空いているスロットに enemy という名前の Group を追加します。
  4. OK またはウィンドウを閉じて設定を保存します。
  5. Hierarchy で Enemy1, Enemy2, Enemy3 をそれぞれ選択し、Inspector の Node セクションにある Group のプルダウンから enemy を選択します。

これで敵ノードの Group が “enemy” に設定されました。

4. AutoAim コンポーネントのアタッチ

  1. Hierarchy で Player ノードを選択します。
  2. Inspector の下部にある Add Component ボタンをクリックします。
  3. Custom カテゴリの中から AutoAim を選択して追加します。
    • もし Custom → AutoAim が見つからない場合は:
      • スクリプト名(クラス名)が @ccclass('AutoAim') と一致しているか確認。
      • スクリプトを保存した後、Cocos Creator を一度再読み込みしてみてください。

5. AutoAim プロパティの設定例

Player の Inspector に追加された AutoAim コンポーネントの各プロパティを次のように設定してみましょう。

  • Search Radius: 600
    • Player を中心に半径 600 の範囲内にいる敵を検出します。
  • Target Group Name: enemy
    • 先ほど設定した Group 名と一致させます。
  • Update Interval: 0.1
    • 0.1 秒ごとに敵の探索を行います。レスポンスと負荷のバランスが良い値です。
  • Auto Rotate: ON(チェック)
    • Player スプライトが自動的に最も近い敵の方向を向くようになります。
  • Rotation Plane: XY
    • 2D ゲームなので XY 平面で角度を計算します。
  • Forward Axis: X+(X_POS)
    • Player スプライトの画像が「右向き」の場合、X+ を前方向として扱うと自然です。
    • もし「上向き」の画像なら Y+ を選んでください。
  • Debug Draw Gizmos: ON(チェック)
    • Gizmo 表示の実装を追加すれば、探索範囲や方向の可視化ができます。現状はプレースホルダですが、後から拡張しやすい構造になっています。

6. 実行して動作を確認する

  1. エディタ上部の Play ボタンをクリックしてゲームをプレビューします。
  2. シーンが再生されると、Player ノードが自動的に最も近い enemy Group のノードの方向を向くようになります。
  3. Scene ビューまたはゲームビューで、敵ノード(Enemy1, Enemy2, Enemy3)をドラッグで移動させてみてください。
    • Player が常に「一番近くにいる敵」の方向を向くことを確認できます。
  4. 敵の一体を Player から遠ざけたり近づけたりすると、ロックオン対象が切り替わる様子が分かります。

7. 他スクリプトからの利用(オプション)

このコンポーネントは単体で完結しますが、他のスクリプトから「今どの方向を向くべきか」を参照することもできます。


// 例: 同じノードにアタッチした別スクリプトから参照
import { _decorator, Component } from 'cc';
import { AutoAim } from './AutoAim';
const { ccclass } = _decorator;

@ccclass('Shooter')
export class Shooter extends Component {
    private _autoAim: AutoAim | null = null;

    start() {
        this._autoAim = this.getComponent(AutoAim);
        if (!this._autoAim) {
            console.error('[Shooter] AutoAimコンポーネントが見つかりません。');
        }
    }

    update() {
        if (!this._autoAim) return;

        const dir = this._autoAim.currentDirection;
        const target = this._autoAim.currentTarget;

        // dir や target を使って弾の発射方向を決めるなど
    }
}

このように、AutoAim 自体は他のスクリプトに依存せず、必要であれば他スクリプトが AutoAim の結果を読むだけ、という構造にすることで再利用性を高めています。


まとめ

  • AutoAim コンポーネントは、任意のノードにアタッチするだけで
    • 指定半径内の敵(Group 名で指定)を自動探索
    • 最も近い敵ノードをロックオン
    • 方向ベクトルと角度を常に更新
    • オプションでノードを自動回転

    してくれる汎用的な「自動照準」機能を提供します。

  • 外部の GameManager やシングルトンに一切依存せず、インスペクタのプロパティだけで完結するため、どのプロジェクトにもそのまま持ち込んで利用できます。
  • タワーディフェンスの砲台、プレイヤーのオートロックオン、敵AIのターゲット選択など、多くのゲームで共通する「近くの敵を向く」処理を一箇所に集約できるため、ゲーム開発の効率化とバグ低減に大きく貢献します。
  • 今後の拡張としては、
    • タグやカスタムコンポーネントによるフィルタリング
    • 視野角(FOV)による絞り込み
    • 障害物(壁)による視線遮蔽チェック

    などを追加しても、AutoAim の設計を保ったまま発展させることができます。

このコンポーネントをベースに、自分のゲームに合わせた「賢い自動照準」を自由に組み立ててみてください。

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をコピーしました!