【Cocos Creator 3.8】TargetFollower の実装:アタッチするだけで「指定ターゲットを追尾する敵AI移動」を実現する汎用スクリプト
このガイドでは、任意のノード(敵キャラなど)にアタッチするだけで、指定したターゲット(プレイヤーなど)へ向かって自動で移動する「TargetFollower」コンポーネントを実装します。外部の GameManager やシングルトンに一切依存せず、インスペクタからターゲットや移動速度、追尾方法を調整するだけで使える、汎用的な敵AI移動コンポーネントを目指します。
コンポーネントの設計方針
機能要件の整理
- 指定したターゲット Node に向かって親ノード(このコンポーネントをアタッチしたノード)を移動させる。
- 2D / 3D どちらでも利用できるよう、基本は「XZ 平面 or XY 平面でのベクター移動」とし、回転は任意。
- ターゲットが未指定の場合でもゲームがクラッシュしないよう、防御的に動作(警告ログのみ)。
- 追尾の ON/OFF、追尾開始距離 / 停止距離の設定など、よくある敵AI挙動をプロパティで調整可能にする。
- 外部スクリプトへの依存を完全になくし、すべてインスペクタから設定できるようにする。
インスペクタで設定可能なプロパティ設計
以下のようなプロパティを用意します。
- target: Node | null
追尾対象となるノード(プレイヤーなど)。インスペクタからドラッグ&ドロップで指定。 - moveSpeed: number
追尾速度(単位: ユニット/秒)。
例: 2.0 ~ 10.0 あたりを目安に調整。 - enableFollow: boolean
追尾機能の有効 / 無効フラグ。
ゲーム中に一時停止したい場合に便利。 - useWorldSpace: boolean
- true: ワールド座標で追尾(通常はこちら)。
- false: 親ノードからのローカル座標で追尾。
2Dゲームでも 3Dゲームでも扱いやすいように、座標系を明示的に選択できるようにします。
- followAxisX: boolean
X 軸方向に追尾するかどうか。 - followAxisY: boolean
Y 軸方向に追尾するかどうか(2Dゲームでは高さ方向)。 - followAxisZ: boolean
Z 軸方向に追尾するかどうか(3Dゲームで前後方向)。 - startFollowDistance: number
ターゲットとの距離がこの値以下になったら追尾を開始する。
0 以下の場合は、常に追尾。 - stopDistance: number
ターゲットとの距離がこの値以下になったら、それ以上近づかない(停止)。
例: 0.5 ~ 1.0 で「近接攻撃の間合い」などを表現。 - smoothTurn: boolean
見た目の回転をターゲット方向に向けるかどうか。
2D の場合は Y 軸回転(回転スプライト)、3D の場合は Y 軸を中心に回頭する想定。 - turnSpeed: number
回転速度(度/秒)。smoothTurn が有効なときのみ使用。 - debugLog: boolean
ターゲット未設定や距離判定などのログを出すかどうか。開発時のみ ON にする想定。
これらを組み合わせることで、以下のような挙動を簡単に実現できます。
- 2D 横スクロールゲームで、X のみ追尾する敵。
- 見下ろし型 2D/3D で、X/Z 平面上を追尾する敵。
- 一定距離まで近づくと止まり、攻撃モーションに移行する前段階の移動AI。
TypeScriptコードの実装
以下が完成した TargetFollower.ts の全コードです。
import { _decorator, Component, Node, Vec3, math, Quat, warn, log } from 'cc';
const { ccclass, property } = _decorator;
/**
* TargetFollower
* 任意のノードにアタッチして、指定ターゲットへ向かって移動させる汎用コンポーネント。
* - 外部スクリプトへの依存なし
* - インスペクタからターゲットや速度、追尾軸などを調整可能
*/
@ccclass('TargetFollower')
export class TargetFollower extends Component {
@property({
type: Node,
tooltip: '追尾対象となるノード(プレイヤーなど)。\n空の場合は追尾しません。'
})
public target: Node | null = null;
@property({
tooltip: '追尾速度(ユニット/秒)。\n値が大きいほど速く移動します。'
})
public moveSpeed: number = 3.0;
@property({
tooltip: '追尾機能の有効/無効。\nゲーム中に一時停止したい場合などに使用します。'
})
public enableFollow: boolean = true;
@property({
tooltip: 'true: ワールド座標で追尾。\nfalse: ローカル座標で追尾。'
})
public useWorldSpace: boolean = true;
@property({
tooltip: 'X軸方向に追尾するかどうか。'
})
public followAxisX: boolean = true;
@property({
tooltip: 'Y軸方向に追尾するかどうか。\n2Dゲームで高さ方向を無視したい場合はOFFにします。'
})
public followAxisY: boolean = true;
@property({
tooltip: 'Z軸方向に追尾するかどうか。\n2Dゲーム(横スクロール等)では通常OFFにします。'
})
public followAxisZ: boolean = true;
@property({
tooltip: 'この距離以下になったら追尾を開始します。\n0以下の場合は常に追尾します。'
})
public startFollowDistance: number = 0.0;
@property({
tooltip: 'この距離以下には近づきません(停止距離)。\n0の場合は完全に重なるまで近づきます。'
})
public stopDistance: number = 0.5;
@property({
tooltip: 'ターゲット方向へ見た目の回転を行うかどうか。'
})
public smoothTurn: boolean = false;
@property({
tooltip: '回転速度(度/秒)。\n smoothTurn が有効なときのみ使用されます。'
})
public turnSpeed: number = 360.0;
@property({
tooltip: 'デバッグログを出力するかどうか。\n開発時のみONにすることを推奨します。'
})
public debugLog: boolean = false;
// 作業用の一時ベクター(GC削減のために再利用)
private _currentPos: Vec3 = new Vec3();
private _targetPos: Vec3 = new Vec3();
private _direction: Vec3 = new Vec3();
private _up: Vec3 = new Vec3(0, 1, 0);
private _tmpQuat: Quat = new Quat();
onLoad() {
// onLoad では、主に設定チェックとログ出力のみ行います。
if (!this.target) {
warn('[TargetFollower] target が設定されていません。このノードは追尾しません。', this.node.name);
}
if (this.moveSpeed < 0) {
warn('[TargetFollower] moveSpeed が負の値です。0 にリセットします。');
this.moveSpeed = 0;
}
if (this.stopDistance < 0) {
warn('[TargetFollower] stopDistance が負の値です。0 にリセットします。');
this.stopDistance = 0;
}
if (this.debugLog) {
log(`[TargetFollower] onLoad: node=${this.node.name}, moveSpeed=${this.moveSpeed}`);
}
}
start() {
// start では特別な初期化は行わず、主にログのみ。
if (this.debugLog) {
log('[TargetFollower] start: ready on node', this.node.name);
}
}
update(deltaTime: number) {
// 毎フレーム、追尾処理を行います。
if (!this.enableFollow) {
return;
}
if (!this.target) {
// ターゲットが設定されていない場合は何もしない(警告は onLoad で1回のみ)
return;
}
// 現在位置とターゲット位置の取得(ワールド or ローカル)
if (this.useWorldSpace) {
this.node.getWorldPosition(this._currentPos);
this.target.getWorldPosition(this._targetPos);
} else {
this.node.getPosition(this._currentPos);
this.target.getPosition(this._targetPos);
}
// 方向ベクトル = ターゲット - 自分
Vec3.subtract(this._direction, this._targetPos, this._currentPos);
// 追尾軸の制御(不要な軸は0にする)
if (!this.followAxisX) this._direction.x = 0;
if (!this.followAxisY) this._direction.y = 0;
if (!this.followAxisZ) this._direction.z = 0;
// 距離の計算(方向ベクトルの長さ)
const distance = this._direction.length();
// 開始距離チェック
if (this.startFollowDistance > 0 && distance > this.startFollowDistance) {
// まだ追尾開始距離に入っていない場合は何もしない
if (this.debugLog) {
log(`[TargetFollower] waiting: distance=${distance.toFixed(2)} > startFollowDistance=${this.startFollowDistance}`);
}
return;
}
// 停止距離チェック
if (distance <= this.stopDistance) {
// これ以上近づかない
if (this.debugLog) {
log(`[TargetFollower] stopped: distance=${distance.toFixed(2)} <= stopDistance=${this.stopDistance}`);
}
// 回転だけは行いたい場合があるので、smoothTurn は継続
if (this.smoothTurn && distance > 0.0001) {
this._applySmoothTurn(deltaTime);
}
return;
}
// 正規化して進行方向を得る
if (distance > 0.0001) {
this._direction.normalize();
} else {
// ほぼ同じ位置の場合は移動不要
return;
}
// 移動量 = 方向 * 速度 * 経過時間
const moveStep = this.moveSpeed * deltaTime;
Vec3.scaleAndAdd(this._currentPos, this._currentPos, this._direction, moveStep);
// 新しい位置を適用
if (this.useWorldSpace) {
this.node.setWorldPosition(this._currentPos);
} else {
this.node.setPosition(this._currentPos);
}
// 見た目の回転処理
if (this.smoothTurn) {
this._applySmoothTurn(deltaTime);
}
}
/**
* ターゲット方向へスムーズに回転させる処理。
* 2D/3D ともに、Y軸を上方向とした回転を想定しています。
*/
private _applySmoothTurn(deltaTime: number) {
if (!this.target) {
return;
}
// 位置の再取得(追尾と同じ座標系を使用)
if (this.useWorldSpace) {
this.node.getWorldPosition(this._currentPos);
this.target.getWorldPosition(this._targetPos);
} else {
this.node.getPosition(this._currentPos);
this.target.getPosition(this._targetPos);
}
// ターゲット方向のベクトル
Vec3.subtract(this._direction, this._targetPos, this._currentPos);
// 追尾軸の制御(移動と同じルール)
if (!this.followAxisX) this._direction.x = 0;
if (!this.followAxisY) this._direction.y = 0;
if (!this.followAxisZ) this._direction.z = 0;
if (this._direction.length() <= 0.0001) {
return;
}
this._direction.normalize();
// 現在の回転と目標回転を取得
const currentRot = this.node.getWorldRotation();
const targetRot = new Quat();
// upベクトルはY軸固定(2D/3D共通で扱いやすい)
Quat.fromViewUp(targetRot, this._direction, this._up);
// slerp でスムーズに補間
const t = math.clamp01(this.turnSpeed * deltaTime / 360.0);
Quat.slerp(this._tmpQuat, currentRot, targetRot, t);
// 回転を適用
this.node.setWorldRotation(this._tmpQuat);
}
}
コードの主要部分の解説
- onLoad()
- ターゲット未設定や負の速度など、明らかにおかしい設定値をチェックし、警告ログを出します。
- ゲーム開始前に一度だけ呼ばれるため、ここでエラーに気づきやすくなります。
- start()
- 今回は特別な初期化は不要なので、デバッグログのみ。
- 将来的に「初期位置の記録」などが必要になった場合はここに追加できます。
- update(deltaTime)
enableFollowが false の場合は何もしません。targetが null の場合も安全に return します。- ワールド座標 / ローカル座標を選択して、現在位置とターゲット位置を取得します。
- 方向ベクトル(ターゲット – 自分)を計算し、追尾しない軸は 0 にします。
- 距離を計算し、
startFollowDistanceとstopDistanceに基づいて「追尾開始 / 停止」を制御します。 - 距離が十分離れている場合のみ、正規化した方向ベクトルに速度と deltaTime を掛けて移動します。
smoothTurnが true の場合は、移動後に_applySmoothTurn()で見た目の回転を行います。
- _applySmoothTurn(deltaTime)
- ターゲット方向のベクトルを再計算し、
Quat.fromViewUp()を使って「その方向を向く回転」を算出します。 - 現在の回転と目標回転を
Quat.slerp()で補間し、turnSpeed(度/秒)に応じて滑らかに回頭します。 - Y軸を上方向とした回転を想定しているため、2D/3D の多くのケースで自然な挙動になります。
- ターゲット方向のベクトルを再計算し、
使用手順と動作確認
ここからは、実際に Cocos Creator 3.8.7 のエディタ上でこのコンポーネントを使う手順を説明します。
1. スクリプトファイルの作成
- Assets パネルで右クリックします。
Create → TypeScriptを選択します。- ファイル名を
TargetFollower.tsにします。 - 作成された
TargetFollower.tsをダブルクリックして開き、既存のコードをすべて削除し、前述のコードをそのまま貼り付けて保存します。
2. テスト用シーンとノードの準備
ここでは 2D ゲーム風の簡単なテストを例に説明します(3D でも手順はほぼ同じです)。
- Hierarchy パネルで右クリック →
Create → 2D Object → Spriteを選択し、プレイヤー用ノードPlayerを作成します。 - 同様に、敵キャラ用ノードとして
Enemyという Sprite ノードを作成します。 - Scene ビュー上で
PlayerとEnemyの位置を少し離して配置します(例: Player を x=0, Enemy を x=-5 に配置)。
3. TargetFollower コンポーネントをアタッチ
- Hierarchy で
Enemyノードを選択します。 - Inspector の下部にある
Add Componentボタンをクリックします。 Custom → TargetFollowerを選択してアタッチします。
4. インスペクタでプロパティを設定
Enemy ノードの Inspector に表示された TargetFollower のプロパティを次のように設定してみましょう。
- target:
Playerノードをドラッグ&ドロップで指定。 - moveSpeed:
3(お好みで 1~10 の範囲で調整)。 - enableFollow: チェック ON。
- useWorldSpace: チェック ON(2D なら基本 ON でOK)。
- followAxisX: チェック ON。
- followAxisY: チェック OFF(横スクロール想定で高さを無視)。
- followAxisZ: チェック OFF(2DなのでZは使わない)。
- startFollowDistance:
0(常に追尾開始)。 - stopDistance:
0.5(ある程度近づいたら停止)。 - smoothTurn: 2D でスプライトを回転させたい場合のみ ON(不要なら OFF)。
- turnSpeed:
360(smoothTurn を ON にした場合のみ有効)。 - debugLog: 動作確認でログを見たい場合だけ ON。
5. 再生して動作確認
- エディタ右上の
Playボタンをクリックしてゲームを実行します。 - シーンが再生されると、
Enemyが自動的にPlayerの方向へ移動し、stopDistanceで指定した距離まで近づいたところで停止するはずです。 - もし
Playerをキーボード入力などで動かすスクリプトを別途付けていれば、Enemy が常に Player を追いかけ続ける挙動が確認できます。
6. 3D シーンでの利用例(任意)
3D の TPS や見下ろし型ゲームで使う場合は、次のように設定すると扱いやすくなります。
- useWorldSpace: ON
- followAxisX: ON
- followAxisY: OFF(高さを無視して水平面のみ追尾)
- followAxisZ: ON
- smoothTurn: ON(敵がプレイヤーの方向を向くように)
- turnSpeed: 180 ~ 360 程度
この設定により、敵キャラが XZ 平面上でプレイヤーを追いかけつつ、常にプレイヤーの方向を向く自然な挙動になります。
まとめ
今回実装した TargetFollower コンポーネントは、
- ターゲット指定と速度・距離・軸の設定だけで、敵AIの「追尾移動」を簡単に実現できる。
- 外部の GameManager やシングルトンに依存せず、このスクリプト単体で完結している。
- 2D / 3D を問わず、座標系と追尾軸を切り替えるだけで様々なゲームに再利用できる。
- 開始距離 / 停止距離 / 回転速度などをインスペクタから調整できるため、デザイナーもパラメータ調整しやすい。
このような「アタッチするだけで使える汎用コンポーネント」をプロジェクト内にストックしておくと、敵の種類を増やしたり、新しいステージを作る際に「移動ロジックを毎回書き直す」必要がなくなり、ゲーム開発のスピードと安定性が大きく向上します。
ここからさらに発展させたい場合は、
- 追尾中にアニメーション(走る / 止まる)を切り替える。
- 視界角度や障害物判定を追加して「見えているときだけ追尾」する。
- ランダムな待機時間や、パトロールポイントとの組み合わせでより複雑なAIを構築する。
といった機能も、今回の TargetFollower をベースに、同じく「独立した汎用コンポーネント」として積み上げていくと管理しやすくなります。




