【Cocos Creator 3.8】TeleportAI の実装:アタッチするだけで「プレイヤーの背後や死角にワープ移動する敵AI」を実現する汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで、一定間隔でプレイヤーの背後や死角へワープ移動する AI を実装します。
敵キャラクターなどに取り付けることで、「歩かない瞬間移動タイプの敵」「視界外から突然現れる敵」といった挙動を簡単に再現できます。
外部の GameManager やシングルトンには一切依存せず、プレイヤーのノード参照や各種パラメータはすべてインスペクタから設定できるように設計します。
目次
コンポーネントの設計方針
実現したい挙動
- このコンポーネントをアタッチしたノード(敵など)が、一定間隔でプレイヤーの周囲にワープする。
- ワープ先は主に「背後」や「死角」を狙う:
- プレイヤーが向いている方向の逆側(背後)に出現しやすくする。
- プレイヤーの視界角度(FOV)を設定し、その外側にワープすることで「死角」感を演出する。
- ワープ時に、敵がプレイヤーから近すぎたり遠すぎたりしないよう、最小距離・最大距離を設定できる。
- ワープするごとに、オプションでエフェクトノードの表示 / 非表示や、サウンド再生(将来拡張用のフック)を想定した簡単なイベントを用意。
外部依存をなくすためのアプローチ
- プレイヤーノードは @property(Node) としてインスペクタから直接指定する。
- プレイヤーの「向き」は、2Dゲームを想定し、
- 主に プレイヤーノードの rotation / angle か、
- もしくは「右向きベクトル」を
Vec3.RIGHTとして扱い、プレイヤーのworldRotationを使って回転させる。
- ワープロジックはコンポーネント内部に完結させ、GameManager などの他スクリプトを一切参照しない。
- エフェクト用ノード(ワープ時に一瞬表示したいパーティクルなど)は、任意指定(null 許容)として、防御的にチェックする。
インスペクタで設定可能なプロパティ設計
以下のようなパラメータを用意します。
- playerNode: Node
- ワープの基準となるプレイヤーのノード。
- 必須。未設定の場合はワープ処理を行わず、エラーログを出す。
- teleportInterval: number
- ワープを行う間隔(秒)。
- 例: 2.0 にすると、約 2 秒ごとにワープ候補を生成。
- 0 以下の値が設定された場合は、自動的に 0.1 秒にクランプして安全に動作させる。
- minDistance: number
- プレイヤーからの最小距離。これより近い位置にはワープしない。
- 例: 1.0 〜 2.0 程度。
- maxDistance: number
- プレイヤーからの最大距離。これより遠い位置にはワープしない。
- 例: 3.0 〜 6.0 程度。
- minDistance > maxDistance になっている場合は、自動的に入れ替えて防御的に処理。
- backPreference: number (0〜1)
- どれくらい「背後」を優先するか。
- 1.0 ならほぼ背後のみ、0.0 なら背後優先なし(全周ランダム)。
- playerFovAngle: number
- プレイヤーの視界角度(度数法)。
- 例: 90 にすると、前方 90° を視界とみなす。
- この角度の外側を「死角」として優先的にワープ位置に選ぶ。
- randomHeightOffset: number
- 2D/3D どちらでも使えるよう、Y 方向のランダムオフセットの最大値。
- 0 の場合は、プレイヤーと同じ高さにワープ。
- 例: 0.5 にすると、±0.5 の範囲でランダムに高さをずらす。
- useWorldSpace: boolean
- true: プレイヤーと敵の位置を ワールド座標 で扱う。
- false: 親ノードを共有しているローカル座標として扱う。
- 通常は true 推奨。
- teleportEffectNode: Node | null
- 任意指定。ワープ時に一瞬表示するエフェクトノード。
- 指定されている場合、ワープの瞬間にそのノードを敵の位置に移動して有効化 → 一定時間後に自動で非表示。
- effectDuration: number
- teleportEffectNode を表示しておく時間(秒)。
- 0 以下の場合は無視。
- debugLog: boolean
- ワープ位置やエラーなどのログをコンソールに出すかどうか。
- 開発中は true、本番ビルドでは false 推奨。
TypeScriptコードの実装
import { _decorator, Component, Node, Vec3, Quat, math, tween } from 'cc';
const { ccclass, property } = _decorator;
/**
* TeleportAI
* プレイヤーの背後や死角にワープする汎用 AI コンポーネント
*/
@ccclass('TeleportAI')
export class TeleportAI extends Component {
@property({
type: Node,
tooltip: 'ワープの基準となるプレイヤーノード。\nここで指定したノードの位置・向きを元にワープ位置を計算します。'
})
public playerNode: Node | null = null;
@property({
tooltip: 'ワープを行う間隔(秒)。\n例: 2.0 で約2秒ごとにワープします。0以下は自動的に0.1秒にクランプされます。'
})
public teleportInterval: number = 2.0;
@property({
tooltip: 'プレイヤーからの最小距離。\nこれより近い場所にはワープしません。'
})
public minDistance: number = 2.0;
@property({
tooltip: 'プレイヤーからの最大距離。\nこれより遠い場所にはワープしません。'
})
public maxDistance: number = 4.0;
@property({
range: [0, 1, 0.01],
tooltip: '背後へのワープをどれくらい優先するか(0〜1)。\n1に近いほどプレイヤーの背後に出現しやすくなります。'
})
public backPreference: number = 0.8;
@property({
tooltip: 'プレイヤーの視界角度(度)。\nこの角度の外側(死角)にワープ位置を優先的に選びます。'
})
public playerFovAngle: number = 90;
@property({
tooltip: 'Y方向のランダムオフセットの最大値。\n0の場合はプレイヤーと同じ高さにワープします。'
})
public randomHeightOffset: number = 0.0;
@property({
tooltip: 'true: ワールド座標で計算します。\nfalse: 親ノードを共有している前提でローカル座標で計算します。'
})
public useWorldSpace: boolean = true;
@property({
type: Node,
tooltip: 'ワープ時に表示するエフェクトノード(任意)。\n指定された場合、ワープの瞬間にこのノードを敵の位置に移動し、有効化します。'
})
public teleportEffectNode: Node | null = null;
@property({
tooltip: 'エフェクトノードを表示しておく時間(秒)。\n0以下の場合は即座に非表示にします。'
})
public effectDuration: number = 0.3;
@property({
tooltip: 'デバッグログを有効にするかどうか。開発中のみtrue推奨。'
})
public debugLog: boolean = false;
// 内部用タイマー
private _timer: number = 0;
// 再利用用ベクトル(GC削減)
private _tempVecA: Vec3 = new Vec3();
private _tempVecB: Vec3 = new Vec3();
onLoad() {
// パラメータの防御的補正
if (this.teleportInterval <= 0) {
if (this.debugLog) {
console.warn('[TeleportAI] teleportInterval が 0 以下のため 0.1 に補正します。');
}
this.teleportInterval = 0.1;
}
if (this.minDistance < 0) {
if (this.debugLog) {
console.warn('[TeleportAI] minDistance が 0 未満のため 0 に補正します。');
}
this.minDistance = 0;
}
if (this.maxDistance <= 0) {
if (this.debugLog) {
console.warn('[TeleportAI] maxDistance が 0 以下のため 1 に補正します。');
}
this.maxDistance = 1;
}
if (this.minDistance > this.maxDistance) {
if (this.debugLog) {
console.warn('[TeleportAI] minDistance > maxDistance のため値を入れ替えます。');
}
const tmp = this.minDistance;
this.minDistance = this.maxDistance;
this.maxDistance = tmp;
}
if (this.playerFovAngle <= 0) {
// 0以下なら視界なし扱い(常に死角とみなす)
if (this.debugLog) {
console.warn('[TeleportAI] playerFovAngle が 0 以下のため 0 に補正します(全周死角扱い)。');
}
this.playerFovAngle = 0;
}
// エフェクトノードは任意なので存在チェックのみ
if (this.teleportEffectNode) {
// 最初は非表示にしておく
this.teleportEffectNode.active = false;
}
}
start() {
if (!this.playerNode) {
console.error('[TeleportAI] playerNode が設定されていません。このコンポーネントは動作しません。');
return;
}
if (this.debugLog) {
console.log('[TeleportAI] 開始しました。プレイヤー:', this.playerNode.name);
}
// 最初のワープまでのタイマーをリセット
this._timer = 0;
}
update(deltaTime: number) {
if (!this.playerNode) {
// 毎フレームエラーを出すと煩雑なので、一度だけ onLoad/start で出している。
return;
}
this._timer += deltaTime;
if (this._timer >= this.teleportInterval) {
this._timer = 0;
this.performTeleport();
}
}
/**
* 実際にワープを行う処理
*/
private performTeleport() {
const owner = this.node;
const player = this.playerNode!;
const outPos = this._tempVecA;
const success = this.calculateTeleportPosition(player, outPos);
if (!success) {
if (this.debugLog) {
console.warn('[TeleportAI] 有効なワープ位置を計算できませんでした。');
}
return;
}
// 位置の適用
if (this.useWorldSpace) {
owner.worldPosition = outPos;
} else {
owner.position = outPos;
}
if (this.debugLog) {
console.log('[TeleportAI] ワープしました。新しい位置:', outPos);
}
// エフェクトを再生
this.playTeleportEffect();
}
/**
* プレイヤーの背後・死角を優先したワープ位置を計算
* @param player プレイヤーノード
* @param outPos 計算結果の位置を格納する Vec3
* @returns 成功したら true
*/
private calculateTeleportPosition(player: Node, outPos: Vec3): boolean {
// プレイヤーの位置を取得
const playerPos = this._tempVecB;
if (this.useWorldSpace) {
player.getWorldPosition(playerPos);
} else {
player.getPosition(playerPos);
}
// プレイヤーの「前方向」を計算(2D想定: XZ平面 or XY平面)
// ここでは「右方向(Vec3.RIGHT)をプレイヤーの回転で回したもの」を前方向とみなす。
const forward = this.getPlayerForward(player);
// ランダムな距離
const distance = math.lerp(this.minDistance, this.maxDistance, Math.random());
// ワープ角度の決定
const angleRad = this.chooseTeleportAngle(forward);
// 2D(XY)前提での方向ベクトルを作成(右手座標系: cos= x, sin= y)
const dir = new Vec3(Math.cos(angleRad), Math.sin(angleRad), 0);
// ワープ先位置 = プレイヤー位置 + dir * distance
Vec3.scaleAndAdd(outPos, playerPos, dir, distance);
// 高さオフセット(Y)を加える(XY2Dなら不要だが、3Dや上下演出用に残す)
if (this.randomHeightOffset > 0) {
const offset = (Math.random() * 2 - 1) * this.randomHeightOffset;
outPos.y += offset;
}
return true;
}
/**
* プレイヤーの前方向ベクトル(2D想定)を取得
* ここでは「ローカルの右方向(Vec3.RIGHT)」を回転させて前方向とみなす。
*/
private getPlayerForward(player: Node): Vec3 {
const forward = new Vec3(1, 0, 0); // 右方向を基準
const rot = new Quat();
if (this.useWorldSpace) {
player.getWorldRotation(rot);
} else {
player.getRotation(rot);
}
// forward を rot で回転させる
Vec3.transformQuat(forward, forward, rot);
// 2D(XY)用にZ成分は無視
forward.z = 0;
if (!forward.equals(Vec3.ZERO)) {
forward.normalize();
}
return forward;
}
/**
* 背後・死角を優先したワープ角度(ラジアン)を決定する。
* @param forward プレイヤーの前方向ベクトル
*/
private chooseTeleportAngle(forward: Vec3): number {
// プレイヤーの向きの角度(ラジアン)
const playerAngle = Math.atan2(forward.y, forward.x);
// 背後方向 = プレイヤー角度 + 180°
const backAngle = playerAngle + Math.PI;
// 背後をどれくらい優先するか (0〜1)
const r = Math.random();
let baseAngle: number;
if (r < this.backPreference) {
// 背後付近のランダム角度(±45°程度)
const spread = math.toRadian(45);
baseAngle = backAngle + (Math.random() * 2 - 1) * spread;
} else {
// 死角(視界外)を優先的に選択
const halfFov = math.toRadian(this.playerFovAngle) * 0.5;
if (this.playerFovAngle > 0 && this.playerFovAngle < 360) {
// 視界外の角度レンジを2つに分けてランダム選択
// [playerAngle + halfFov, playerAngle + PI] と [playerAngle - PI, playerAngle - halfFov]
const chooseBackSide = Math.random() < 0.5;
if (chooseBackSide) {
const minA = playerAngle + halfFov;
const maxA = playerAngle + Math.PI;
baseAngle = math.lerp(minA, maxA, Math.random());
} else {
const minA = playerAngle - Math.PI;
const maxA = playerAngle - halfFov;
baseAngle = math.lerp(minA, maxA, Math.random());
}
} else {
// FOVが0 or 360 なら全周ランダム
baseAngle = Math.random() * Math.PI * 2;
}
}
return baseAngle;
}
/**
* ワープエフェクトの再生
*/
private playTeleportEffect() {
if (!this.teleportEffectNode) {
return;
}
const effect = this.teleportEffectNode;
// エフェクトノードを敵の位置に移動
if (this.useWorldSpace) {
effect.setWorldPosition(this.node.worldPosition);
} else {
effect.setPosition(this.node.position);
}
effect.active = true;
if (this.effectDuration <= 0) {
// 即座に非表示
effect.active = false;
return;
}
// 一定時間後に非表示にする(tween を利用)
tween(effect)
.delay(this.effectDuration)
.call(() => {
effect.active = false;
})
.start();
}
}
主要メソッドの解説
- onLoad()
- インスペクタから設定された値を防御的に補正(負の値の修正、min/max 入れ替えなど)。
- teleportEffectNode が指定されていれば、最初は非表示にしておく。
- start()
- playerNode が設定されていない場合は
console.errorを出し、以降の処理は行わない。 - ワープ用タイマーを初期化。
- playerNode が設定されていない場合は
- update(deltaTime)
- 毎フレーム
_timerを加算し、teleportIntervalを超えたらperformTeleport()を呼び出す。 - playerNode が未設定の場合は何もしない。
- 毎フレーム
- performTeleport()
calculateTeleportPosition()でワープ先を計算。- 成功したら、自身のノードの position / worldPosition を更新。
- ワープ後に
playTeleportEffect()でエフェクトを再生。
- calculateTeleportPosition()
- プレイヤーの位置と向きを取得。
- プレイヤーの前方向ベクトルを
getPlayerForward()で計算。 chooseTeleportAngle()で「背後・死角」を優先した角度(ラジアン)を決定。- ランダムな距離(minDistance〜maxDistance)と組み合わせて、ワープ先座標を算出。
- getPlayerForward()
- プレイヤーの回転を取得し、
Vec3.RIGHTをその回転で変換して前方向ベクトルを得る。 - 2D ゲームを想定して Z 成分は無視し、XY のみを使用。
- プレイヤーの回転を取得し、
- chooseTeleportAngle()
backPreferenceに応じて、背後付近(±45°)を優先的に選択。- それ以外の場合は、
playerFovAngleで定義された視界外(死角)からランダムに角度を選択。 - playerFovAngle が 0 or 360 の場合は全周ランダム。
- playTeleportEffect()
- teleportEffectNode が指定されていれば、敵の位置へ移動し
active = true。 effectDuration秒後に自動で非表示にするため、tweenを使用。
- teleportEffectNode が指定されていれば、敵の位置へ移動し
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を TeleportAI.ts に変更します。
- 作成された
TeleportAI.tsをダブルクリックして開き、中身をすべて削除してから、上記の TypeScript コードを丸ごと貼り付けて保存します。
2. テスト用シーンとノードの準備
ここでは 2D ゲーム(Canvas 上のスプライト)を例に説明しますが、3D でも同様に使えます。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、プレイヤー用ノードを作成します。
- 名前を Player に変更します。
- Inspector で Sprite の画像を設定しておくと視認しやすくなります。
- 同様に、敵用ノードを作成します。
- Hierarchy で右クリック → Create → 2D Object → Sprite。
- 名前を Enemy に変更します。
- 色や画像を変えてプレイヤーと区別しやすくしておきます。
3. TeleportAI コンポーネントをアタッチ
- Hierarchy で Enemy ノードを選択します。
- Inspector の一番下にある Add Component ボタンをクリックします。
- Custom カテゴリ(またはスクリプト一覧)から TeleportAI を選択してアタッチします。
4. インスペクタでプロパティを設定
Enemy ノードを選択した状態で、Inspector に表示される TeleportAI コンポーネントの各プロパティを設定します。
- Player Node:
- Hierarchy から Player ノードをドラッグ&ドロップして、このフィールドにセットします。
- Teleport Interval:
- 例: 2 と入力すると、約 2 秒ごとにワープします。
- Min Distance / Max Distance:
- 例: Min = 2, Max = 4 に設定。
- これでプレイヤーから 2〜4 単位の距離にワープするようになります。
- Back Preference:
- 例: 0.8 にすると、ほとんど背後に出現するようになります。
- Player Fov Angle:
- 例: 90 とすると、前方 90° を視界とみなし、それ以外を死角として優先的にワープします。
- Random Height Offset:
- 2D XY のみで使う場合は 0 のままで構いません。
- 上下に少しランダムさを出したい場合は 0.5 などに設定します。
- Use World Space:
- 通常は チェックを入れたまま(true) にしておきます。
- Player と Enemy が同じ親ノードのローカル座標で動いている場合は、false にしても動作します。
- Teleport Effect Node(任意):
- ワープ時にエフェクトを出したい場合、別途エフェクト用のノードを用意してここに指定します。
- 例:
- Hierarchy で右クリック → Create → 2D Object → Sprite で TeleportEffect ノードを作成。
- 色を薄い青などにして、少し小さめのスプライトにする。
- 最初は active = false にしておく(Inspector のチェックを外す)。
- Enemy の TeleportAI の Teleport Effect Node に、この TeleportEffect ノードをドラッグ&ドロップ。
- Effect Duration:
- 例: 0.3 とすると、ワープのたびに 0.3 秒間だけエフェクトが表示されます。
- Debug Log:
- 挙動を確認したい場合は チェックを入れて true にします。
- Console にワープ位置などが出力されます。
5. プレイヤーの向き(回転)を確認
TeleportAI はプレイヤーの「前方向」を基準に背後や死角を計算します。
- 2D ゲームの場合:
- Player ノードを選択し、Inspector の Rotation (または Angle) を変えると、前方向が変わります。
- シーン再生中に Player を回転させると、Enemy のワープ位置がそれに応じて変わることを確認できます。
6. 再生して動作確認
- エディタ上部の ▶(Play) ボタンをクリックしてシーンを再生します。
- 数秒ごとに Enemy ノードが Player の周囲にワープする様子が確認できます。
- Back Preference を 1.0 に近づけると、より背後に出現しやすくなります。
- Player Fov Angle を変えることで、「前方にはほとんど現れず、側面〜背後に現れる」ような死角感を調整できます。
- Teleport Effect Node を設定している場合は、ワープの瞬間にエフェクトが一瞬表示されることを確認します。
まとめ
この TeleportAI コンポーネントは、
- プレイヤーノードをインスペクタで指定するだけで、
- 一定間隔でプレイヤーの背後や死角にワープする挙動を自動的に実現
できるように設計されています。
外部の GameManager やシングルトンに一切依存せず、このスクリプト単体で完結しているため、
- 別プロジェクトへの移植
- 他の敵キャラクターへの再利用
- 複数の TeleportAI を同一シーンで同時使用
といったケースでも、そのままドラッグ&ドロップで使い回すことができます。
応用例としては、
- teleportInterval を短くして「高速テレポートボス」を作る
- maxDistance を大きくして「遠くから突然迫ってくる敵」を演出する
- Teleport Effect Node にパーティクルやアニメーションを設定して、派手なワープ演出を加える
などが考えられます。
このように、汎用的で外部依存のないコンポーネントを積み重ねていくことで、Cocos Creator 3.8 でのゲーム開発をシンプルかつ効率的に進めることができます。ぜひプロジェクトに組み込んで、さまざまな敵 AI 演出に活用してみてください。
