【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します。
- ノードの Euler 角の Z 成分を
- 3D(XZ 平面)の場合:
- ノードの Euler 角の Y 成分を
currentAngleに設定してsetRotationFromEulerします。
- ノードの Euler 角の Y 成分を
使用手順と動作確認
ここからは、Cocos Creator 3.8.7 エディタ上で実際に AutoAim を使う手順を具体的に説明します。
1. スクリプトファイルの作成
- エディタの Assets パネルで、AutoAim を置きたいフォルダ(例:
assets/scripts)を右クリックします。 - Create > TypeScript を選択します。
- 新しく作成されたスクリプトファイルの名前を AutoAim.ts に変更します。
- 作成した
AutoAim.tsをダブルクリックしてエディタ(VSCode など)で開き、上記の TypeScript コード全文を貼り付けて保存します。
2. テスト用シーンとノードの準備
ここでは 2D ゲームを想定した簡単な例を紹介します。
- 2Dシーンを用意
- まだシーンがない場合は、File > New Scene で新規シーンを作成し、
AutoAimTest.sceneなどの名前で保存します。
- まだシーンがない場合は、File > New Scene で新規シーンを作成し、
- プレイヤー(照準元)ノードの作成
- Hierarchy パネルで右クリック → Create > 2D Object > Sprite を選択します。
- 作成された Sprite の名前を Player に変更します。
- Inspector の Sprite コンポーネントで任意の画像を設定し、プレイヤーの見た目を用意します。
- Scene ビューで Player ノードを中央付近に配置します。
- 敵ノードの作成
- 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 を例にします。
- メインメニューから Project > Project Settings… を開きます。
- 左側のリストから Group を選択します。
- 空いているスロットに enemy という名前の Group を追加します。
- OK またはウィンドウを閉じて設定を保存します。
- Hierarchy で Enemy1, Enemy2, Enemy3 をそれぞれ選択し、Inspector の Node セクションにある Group のプルダウンから enemy を選択します。
これで敵ノードの Group が “enemy” に設定されました。
4. AutoAim コンポーネントのアタッチ
- Hierarchy で Player ノードを選択します。
- Inspector の下部にある Add Component ボタンをクリックします。
- Custom カテゴリの中から AutoAim を選択して追加します。
- もし Custom → AutoAim が見つからない場合は:
- スクリプト名(クラス名)が
@ccclass('AutoAim')と一致しているか確認。 - スクリプトを保存した後、Cocos Creator を一度再読み込みしてみてください。
- スクリプト名(クラス名)が
- もし Custom → AutoAim が見つからない場合は:
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. 実行して動作を確認する
- エディタ上部の Play ボタンをクリックしてゲームをプレビューします。
- シーンが再生されると、Player ノードが自動的に最も近い enemy Group のノードの方向を向くようになります。
- Scene ビューまたはゲームビューで、敵ノード(Enemy1, Enemy2, Enemy3)をドラッグで移動させてみてください。
- Player が常に「一番近くにいる敵」の方向を向くことを確認できます。
- 敵の一体を 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 の設計を保ったまま発展させることができます。
このコンポーネントをベースに、自分のゲームに合わせた「賢い自動照準」を自由に組み立ててみてください。




