【Cocos Creator 3.8】SmoothPursuit の実装:アタッチするだけで「カメラや任意ノードをターゲットに滑らか追従」させる汎用スクリプト
このコンポーネントは、自分自身(多くはカメラノード)を、指定したターゲットノードの位置へ Lerp(線形補間)で滑らかに追従させるための汎用スクリプトです。
カメラにアタッチすれば「カメラ追従」、UI ノードにアタッチすれば「UI フォロワー」のように、アタッチしてターゲットを指定するだけで使えます。
コンポーネントの設計方針
要件整理
- 自分自身(このコンポーネントを付けたノード)の座標を、ターゲットノードの座標へ滑らかに追従させる。
- 追従には
Vec3.lerpを用いて「遅れて追いかける」感を出す。 - 主にカメラノードに付けて使うことを想定するが、任意のノードに付けても機能する汎用設計にする。
- 外部の GameManager やシングルトンに一切依存しない。必要な情報はすべてインスペクタで設定できるようにする。
- ターゲット未設定時はエラーログを出し、追従処理は行わない安全設計にする。
- 2D/3D 双方で使えるように、XYZ 各軸ごとに「追従する/しない」を選べるようにする。
- 追従の強さ(補間係数)をインスペクタから調整できるようにする。
- 一定距離以内なら瞬時に追従してしまう「スナップ距離」を設定できるようにする。
インスペクタで設定可能なプロパティ
以下のプロパティを @property として公開します。
- target (
Node | null)
追従対象となるターゲットノード。カメラ追従なら「プレイヤー」ノードなどを指定します。- 未設定の場合はエラーログを出して追従しないようにします。
- followX (
boolean, デフォルト:true)
X 軸方向に追従するかどうか。 - followY (
boolean, デフォルト:true)
Y 軸方向に追従するかどうか。 - followZ (
boolean, デフォルト:false)
Z 軸方向に追従するかどうか。2D カメラなど Z を固定したい場合はfalseのままで OK。 - lerpFactor (
number, デフォルト:5.0)
追従の強さ(補間係数)。- 値が大きいほどターゲットに素早く追いつく(キビキビした追従)。
- 値が小さいほどゆっくり追従する(ヌルっとしたカメラ)。
- 内部では
t = clamp01(lerpFactor * dt)としてフレームレートに依存しないようにしています。
- snapDistance (
number, デフォルト:0.01)
ターゲットとの距離がこの値以下になった場合、補間せずにターゲット位置へスナップします。- ごく小さい値にすることで、微妙な振動を防ぎつつ、見た目は滑らかに保てます。
- 0 以下にするとスナップを無効化します。
- useWorldSpace (
boolean, デフォルト:true)true: ワールド座標で追従(カメラなどに推奨)。false: ローカル座標で追従(親子関係の中で相対的に追従させたい UI など)。
- offset (
Vec3, デフォルト: (0, 0, 0))
ターゲット位置に対して加えるオフセット。- 例えば 2D カメラでプレイヤーより少し上を映したい場合は (0, 100, 0) など。
- enableOnStart (
boolean, デフォルト:true)true:start()時に自動で追従を有効化。false: 初期状態では追従しない(スクリプトからsetEnabled(true)を呼ぶ前提のときに使用)。
また、スクリプト内で以下のような公開メソッドも用意します。
setEnabled(enabled: boolean): void
追従のオン/オフをコードから切り替えたい場合に使用します。setTarget(target: Node | null): void
ランタイムに追従対象を切り替えたい場合に使用します。
TypeScriptコードの実装
import { _decorator, Component, Node, Vec3, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* SmoothPursuit
* 任意のノードをターゲットへ滑らかに追従させる汎用コンポーネント。
* - カメラや 2D/3D オブジェクトにアタッチして使用可能
* - X/Y/Z 軸ごとに追従の有無を切り替え可能
* - ワールド座標 / ローカル座標を選択可能
* - オフセット指定可能
*/
@ccclass('SmoothPursuit')
export class SmoothPursuit extends Component {
@property({
type: Node,
tooltip: '追従対象となるターゲットノード。\nカメラ追従ならプレイヤーノードなどを指定します。'
})
public target: Node | null = null;
@property({
tooltip: 'X 軸方向に追従するかどうか。2D ゲームで横スクロールなら true 推奨。'
})
public followX: boolean = true;
@property({
tooltip: 'Y 軸方向に追従するかどうか。2D ゲームで縦方向も追従させたい場合は true。'
})
public followY: boolean = true;
@property({
tooltip: 'Z 軸方向に追従するかどうか。2D カメラなら false のままで問題ありません。'
})
public followZ: boolean = false;
@property({
tooltip: '追従の強さ(補間係数)。\n値が大きいほど素早く追いつきます(キビキビ)。\n値が小さいほどゆっくり追従します(ヌルヌル)。\n内部では lerpFactor * dt を 0〜1 にクランプして使用します。',
min: 0.0
})
public lerpFactor: number = 5.0;
@property({
tooltip: 'スナップ距離。ターゲットとの距離がこの値以下なら、補間せずにターゲット位置へスナップします。\n0 以下にするとスナップを無効化します。',
min: 0.0
})
public snapDistance: number = 0.01;
@property({
tooltip: 'true: ワールド座標で追従(カメラ向け)。\nfalse: ローカル座標で追従(UI などの相対追従向け)。'
})
public useWorldSpace: boolean = true;
@property({
type: Vec3,
tooltip: 'ターゲット位置に対して加えるオフセット。\n例: (0, 100, 0) でターゲットより少し上を追従。'
})
public offset: Vec3 = new Vec3(0, 0, 0);
@property({
tooltip: 'コンポーネント開始時(start)に追従を有効化するかどうか。'
})
public enableOnStart: boolean = true;
// 内部で使い回す一時ベクトル(GC 削減)
private _currentPos: Vec3 = new Vec3();
private _targetPos: Vec3 = new Vec3();
private _tempPos: Vec3 = new Vec3();
private _isActive: boolean = false;
onLoad() {
// onLoad ではまだ target が未設定の可能性もあるので、
// ここでは特別な処理は行わず、防御的なログのみ出す。
if (!this.target) {
console.warn('[SmoothPursuit] target が設定されていません。このままでは追従しません。', this.node);
}
}
start() {
// 開始時に有効化フラグを設定
this._isActive = this.enableOnStart;
if (!this.target) {
console.warn('[SmoothPursuit] start 時点で target が未設定です。Inspector から設定してください。', this.node);
}
}
update(deltaTime: number) {
if (!this._isActive) {
return;
}
if (!this.target) {
// 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.add(this._targetPos, this._targetPos, this.offset);
// 追従しない軸は、現在位置の値をそのまま使う
if (!this.followX) {
this._targetPos.x = this._currentPos.x;
}
if (!this.followY) {
this._targetPos.y = this._currentPos.y;
}
if (!this.followZ) {
this._targetPos.z = this._currentPos.z;
}
// スナップ距離チェック
if (this.snapDistance > 0) {
const dist = Vec3.distance(this._currentPos, this._targetPos);
if (dist
コードのポイント解説
onLoad()- ここでは
targetが未設定の場合に警告ログを出すのみ。 - 外部依存は一切なく、このコンポーネント単体で完結しています。
- ここでは
start()enableOnStartの値に応じて内部フラグ_isActiveを設定します。- このタイミングでターゲット未設定なら再度警告ログを出します。
update(deltaTime)_isActiveがfalseまたはtargetがnullの場合は何もしません。useWorldSpaceに応じてgetWorldPosition/getPositionを使い分けます。- オフセット
offsetをターゲット位置に加算します。 followX/Y/Zがfalseの軸は現在位置の値をそのまま維持します。snapDistance以内ならスナップして終了し、細かい振動を防ぎます。lerpFactor * deltaTimeをmath.clamp01で 0〜1 に制限し、フレームレートに依存しない補間を実現しています。Vec3.lerpでcurrent → targetを補間し、その位置をsetWorldPosition/setPositionで反映します。
- 一時ベクトルの再利用
_currentPos,_targetPos,_tempPosをフィールドとして使い回し、毎フレームのnew Vec3()を避けています。
- 公開メソッド
setEnabledで追従のオン/オフを切り替え可能。setTargetでランタイムにターゲットを変更可能。- どちらも外部のマネージャーに依存せず、呼び出す側のスクリプトから直接利用できます。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリックします。
Create > TypeScriptを選択します。- ファイル名を
SmoothPursuit.tsとして作成します。 - 作成された
SmoothPursuit.tsをダブルクリックして開き、中身をすべて削除してから、上記のコードを貼り付けて保存します。
2. テスト用シーンの準備(2D カメラ追従例)
ここでは、2D ゲームで「カメラがプレイヤーを追いかける」ケースを例に説明します。
- Hierarchy パネルで右クリックし、
Create > 2D Object > Canvasを作成します(既にあれば不要)。 - 同様に Hierarchy パネルで右クリックし、
Create > 2D Object > Spriteを作成し、名前をPlayerに変更します。
(プレイヤーの見た目用。画像は任意で OK) - Hierarchy で
Main Camera(または使用しているカメラノード)を選択します。
3. カメラに SmoothPursuit をアタッチ
Main Cameraノードを選択した状態で、Inspector パネル右下のAdd Componentボタンをクリックします。Custom Component(またはCustom)の中からSmoothPursuitを選択します。
4. プロパティの設定
Inspector で SmoothPursuit コンポーネントの各プロパティを設定します。
- Target:
- フィールド右側の「丸いピッカーアイコン」をクリックし、Hierarchy から
Playerノードを選択します。
- フィールド右側の「丸いピッカーアイコン」をクリックし、Hierarchy から
- Follow X / Follow Y / Follow Z:
- 2D 横スクロールであれば:
Follow X: ON(チェック)Follow Y: 必要に応じて ON/OFF(プレイヤーの上下動にカメラを合わせたいかどうか)Follow Z: OFF(2D カメラなら通常は固定)
- 2D 横スクロールであれば:
- Lerp Factor:
- まずは
5.0のままで試し、早すぎる/遅すぎる場合は 2〜10 の範囲で調整してみてください。
- まずは
- Snap Distance:
- デフォルトの
0.01で問題ないことが多いです。 - カメラがガタガタするようなら、
0.1や1.0など少し大きめにしてみてください。
- デフォルトの
- Use World Space:
- カメラ追従の場合は
true(ON)のままで OK です。
- カメラ追従の場合は
- Offset:
- プレイヤーより少し上を映したい場合:
X: 0Y: 100(ゲームのスケールに合わせて調整)Z: 0
- プレイヤーより少し上を映したい場合:
- Enable On Start:
- 自動で追従を開始したい場合は ON(デフォルト)。
- ゲーム開始後しばらくしてから追従を始めたいなど、スクリプトから制御する場合は OFF にしておき、別スクリプトから
setEnabled(true)を呼び出してください。
5. 動作確認
Playerノードに簡単な移動スクリプトを付けるか、エディタの Preview でキーボード入力や自作の移動ロジックを使って動かします。- ゲームを再生(
▶︎ボタン)します。 - プレイヤーが動くと、それを追いかけるようにカメラが滑らかに追従していることを確認してください。
- Inspector で
Lerp Factorを変更しながら、追従の「キビキビ感」「ヌルヌル感」の違いを確認すると、調整の感覚が掴みやすいです。
応用例:UI ノードの追従(ローカル座標モード)
例えば、ワールド上の敵に対して、その上に HP バー(UI)を追従させたい場合:
- Canvas 配下に HP バー用のノード(
HpBarなど)を作成します。 HpBarノードにSmoothPursuitをアタッチします。Targetに敵キャラのノードを指定します。Use World Spaceを OFF にして、ローカル座標での追従に切り替えます。- 必要に応じて
Offsetの Y を上方向に少し上げて、敵の頭上に来るように調整します。
このように、ワールド座標 / ローカル座標・追従軸・オフセットを組み合わせることで、様々な追従パターンを簡単に実現できます。
まとめ
SmoothPursuitは、任意のノードをターゲットへ滑らかに追従させる汎用コンポーネントです。- 外部の GameManager やシングルトンに一切依存せず、このスクリプト単体で完結するように設計しています。
- インスペクタから:
- ターゲットノード
- 追従軸(X/Y/Z)
- 追従の強さ(Lerp Factor)
- スナップ距離
- ワールド/ローカル座標の切替
- オフセット
- 開始時に有効化するかどうか
を柔軟に調整できます。
- カメラ追従だけでなく、UI の追従や 3D オブジェクト同士の追尾など、幅広いシーンで再利用可能です。
- ゲームごとにカメラロジックを書き直す必要がなくなり、「アタッチしてプロパティを少し調整するだけ」で追従挙動を実現できるため、プロトタイピングから本番開発まで効率が大きく向上します。
このコンポーネントをプロジェクトの「標準追従スクリプト」としてライブラリ化しておくと、新しいシーンや別プロジェクトでもすぐに再利用できるようになります。ぜひ自分のゲームに合わせてデフォルト値を調整しながら活用してみてください。




