Unityを触り始めた頃って、つい「とりあえず全部Updateに書く」実装になりがちですよね。プレイヤーの入力、移動、カメラ制御、UI更新…すべてを1つの巨大スクリプトに詰め込んでしまうと、次のような問題が出てきます。
- カメラ挙動を少し変えたいだけなのに、プレイヤーのコードまで巻き込んで修正が必要
- 役割が混ざっていて、バグが起きたときに原因箇所を特定しづらい
- プレイヤー以外(敵、NPC、乗り物など)で同じカメラ挙動を使い回しづらい
そこでおすすめなのが、「カメラの振る舞いはカメラ用コンポーネントに切り出す」という考え方です。この記事では、プレイヤーの進行方向(velocity)に応じて、カメラ位置を少し前にずらす先読みカメラを、LookAhead というコンポーネントとして実装していきます。
【Unity】進行方向を先読みして気持ちいいカメラ!「LookAhead」コンポーネント
プレイヤーが動く方向に少しだけカメラを寄せると、視界の先が見やすくなってアクションゲームや2D/3Dプラットフォーマーの操作感がぐっと良くなります。
ここでは、
- Rigidbody(または任意のTransform)の速度ベクトルを読み取り
- その方向にカメラの「ターゲット位置」をオフセット
- スムーズに補間してカメラ酔いを抑える
という動きを、単体で完結するコンポーネントとして実装します。
フルコード:LookAhead.cs
using UnityEngine;
/// <summary>
/// プレイヤーなどの移動方向(velocity)をもとに、
/// カメラを進行方向側へ少しだけ先読みオフセットするコンポーネント。
/// カメラにアタッチして使います。
/// </summary>
[DisallowMultipleComponent]
public class LookAhead : MonoBehaviour
{
// --- 参照設定 ---
[Header("ターゲット設定")]
[SerializeField]
private Transform target;
// カメラが追従する対象(プレイヤーなど)
[SerializeField]
private Rigidbody targetRigidbody;
// 速度情報を取得するRigidbody
// 無くても動作はしますが、あるとより正確な先読みが可能です。
[Header("基本オフセット")]
[SerializeField]
private Vector3 baseOffset = new Vector3(0f, 5f, -10f);
// ターゲットから見たカメラの基本位置オフセット
[Header("先読み設定")]
[SerializeField, Tooltip("速度ベクトルをどれくらい先読み位置に反映するか(スカラー)")]
private float lookAheadDistance = 2.0f;
[SerializeField, Tooltip("先読み方向のY成分を無視するか(横方向だけ先読みしたい場合にON)")]
private bool ignoreVerticalVelocity = true;
[SerializeField, Tooltip("速度がこの値以下なら、先読みを徐々にゼロに近づける")]
private float minSpeedForFullLookAhead = 1.0f;
[Header("スムージング")]
[SerializeField, Tooltip("カメラが目標位置へ追従する速さ")]
private float followSmoothTime = 0.15f;
[SerializeField, Tooltip("先読みオフセット(velocity)の変化スムージング")]
private float lookAheadSmoothTime = 0.1f;
// --- 内部状態 ---
// SmoothDamp 用の速度キャッシュ
private Vector3 currentVelocity = Vector3.zero;
// 先読みオフセット用のスムージング
private Vector3 currentLookAheadOffset = Vector3.zero;
private Vector3 lookAheadVelocity = Vector3.zero;
private void Reset()
{
// エディタでアタッチした直後に、よくある設定を自動補完する処理
// カメラ自身のTransformは自明なので何もしない
// シーン内に "Player" という名前のオブジェクトがあればターゲット候補にする
if (target == null)
{
GameObject player = GameObject.FindWithTag("Player");
if (player == null)
{
player = GameObject.Find("Player");
}
if (player != null)
{
target = player.transform;
targetRigidbody = player.GetComponent<Rigidbody>();
}
}
}
private void LateUpdate()
{
if (target == null)
{
// ターゲット未設定なら何もしない
return;
}
// 1. 速度ベクトルを取得
Vector3 velocity = GetTargetVelocity();
// 2. 速度から先読み方向ベクトルを計算
Vector3 desiredLookAhead = CalculateLookAheadOffset(velocity);
// 3. 先読みオフセット自体もスムーズに変化させる
currentLookAheadOffset = Vector3.SmoothDamp(
currentLookAheadOffset,
desiredLookAhead,
ref lookAheadVelocity,
lookAheadSmoothTime
);
// 4. ターゲットの基本追従位置 + 先読みオフセットを計算
Vector3 targetPosition = target.position + baseOffset + currentLookAheadOffset;
// 5. カメラをスムーズに移動させる
transform.position = Vector3.SmoothDamp(
transform.position,
targetPosition,
ref currentVelocity,
followSmoothTime
);
// 6. 向きの制御(必要ならターゲットを見る)
// ここでは「ターゲットの位置」を向くようにしているが、
// 固定の向きにしたい場合はコメントアウトしてもOK。
transform.LookAt(target.position);
}
/// <summary>
/// ターゲットの速度ベクトルを取得する。
/// Rigidbody があればそれを優先し、なければ前フレームからの位置差分で近似する。
/// </summary>
private Vector3 GetTargetVelocity()
{
if (targetRigidbody != null)
{
return targetRigidbody.velocity;
}
// Rigidbody が無い場合、簡易的な疑似速度を計算する
// Time.deltaTime が 0 近い場合のゼロ除算を避ける
float dt = Time.deltaTime;
if (dt <= 0f)
{
return Vector3.zero;
}
// 簡易版:ターゲットの移動方向を追跡したい場合は別途前フレーム位置を記録する設計にしても良いですが、
// ここでは「速度情報が無い場合は先読みを行わない」という方針にします。
return Vector3.zero;
}
/// <summary>
/// 速度ベクトルから先読みオフセットを算出する。
/// </summary>
private Vector3 CalculateLookAheadOffset(Vector3 velocity)
{
if (velocity.sqrMagnitude < Mathf.Epsilon)
{
// ほぼ停止中なら先読みオフセットはゼロ方向
return Vector3.zero;
}
Vector3 v = velocity;
if (ignoreVerticalVelocity)
{
// Y成分を無視して水平方向だけを先読みしたい場合
v.y = 0f;
}
float speed = v.magnitude;
if (speed < Mathf.Epsilon)
{
return Vector3.zero;
}
// 正規化して方向ベクトルにする
Vector3 direction = v / speed;
// 低速時は先読み量を徐々に減衰させる
float t = 1f;
if (speed < minSpeedForFullLookAhead)
{
// 0 ~ minSpeedForFullLookAhead の間で 0 ~ 1 に正規化
t = Mathf.InverseLerp(0f, minSpeedForFullLookAhead, speed);
}
// direction * lookAheadDistance * t が最終的な先読みオフセット
return direction * lookAheadDistance * t;
}
/// <summary>
/// 外部からターゲットを動的に差し替えたい場合に呼び出すヘルパー。
/// 例: プレイヤーが乗り物に乗ったときなど。
/// </summary>
public void SetTarget(Transform newTarget, Rigidbody newRigidbody = null)
{
target = newTarget;
targetRigidbody = newRigidbody;
// 先読み状態をリセットして、急なカメラジャンプを防ぐ
currentLookAheadOffset = Vector3.zero;
lookAheadVelocity = Vector3.zero;
currentVelocity = Vector3.zero;
}
}
使い方の手順
-
シーンにカメラとプレイヤーを用意する
例として、3Dアクションゲームを想定します。- プレイヤーオブジェクトに
Rigidbodyと移動スクリプトを設定 - プレイヤーには
Playerタグを付けておくと、Reset()時に自動検出されます
- プレイヤーオブジェクトに
-
カメラに LookAhead コンポーネントを追加
- Hierarchy でカメラを選択
- Inspector の「Add Component」から
LookAheadを追加 - スクリプトを初めてアタッチしたタイミングで、
Playerが見つかれば自動でtargetが設定されます
-
パラメータを調整する
カメラオブジェクトの Inspector で、以下の値をゲームに合わせて調整します。Base Offset:ターゲットから見たカメラの基本位置(例:(0, 5, -10))Look Ahead Distance:どれくらい先読みするか(1〜5くらいから試すと良いです)Ignore Vertical Velocity:ジャンプの上下には反応させたくない場合は ONMin Speed For Full Look Ahead:この速度までは徐々に先読みを減らすしきい値Follow Smooth Time:カメラの追従のなめらかさ(0.1〜0.3程度)Look Ahead Smooth Time:先読みオフセットの変化のなめらかさ
-
具体的な使用例
-
プレイヤーキャラクター
横スクロールアクションなら、Base Offsetを(0, 2, -10)のようにし、
Look Ahead Distanceを 3 〜 5 に設定すると、進行方向側の足場が見やすくなります。 -
敵を追うカメラ
ボス戦などで「敵の動きに合わせてカメラを少し先読みしたい」場合、
カメラのLookAheadのTargetにボスの Transform と Rigidbody を設定します。 -
動く床や乗り物
プレイヤーが乗り物に乗ったときだけカメラのターゲットを乗り物に変えることで、
乗り物の進行方向に合わせた先読みカメラに切り替えることもできます。
その場合は、ゲーム側のスクリプトからSetTarget()を呼び出します。
-
プレイヤーキャラクター
メリットと応用
LookAhead をカメラ専用のコンポーネントとして切り出しておくことで、次のようなメリットがあります。
- プレハブ管理が楽になる
カメラのプレハブにLookAheadを仕込んでおけば、どのシーンでも同じ「気持ちいいカメラ」をすぐに再利用できます。
プレイヤー側のスクリプトを一切触らずに、カメラ挙動だけをチューニングできるのがポイントです。 - レベルデザイン時の調整コスト削減
ステージによって「見せたい方向」が変わる場合でも、Base OffsetとLook Ahead Distanceをシーンごとに変えるだけで対応できます。
特定のステージでは先読みを強めにしてスピード感を演出し、別のステージでは控えめにする、といった演出も簡単です。 - 責務の分離でバグ調査がしやすい
カメラの動きがおかしいときは、LookAheadコンポーネントだけを見ればよい、という状態になります。
プレイヤーの移動ロジックとカメラロジックが分離されているので、原因の切り分けがしやすくなります。
さらに、少し手を加えるだけで、ゲームに合わせたバリエーションを作ることもできます。
改造案:特定方向の先読みを抑制する(例:後退時はあまり先読みしない)
例えば、プレイヤーが後ろ向きに移動するときには先読み量を半分にしたい、というケースを考えてみます。
プレイヤーの「正面方向」に対して速度ベクトルの向きを判定し、後退時は係数を下げる処理を追加できます。
/// <summary>
/// プレイヤーの forward と速度ベクトルのなす角度によって
/// 先読み量を調整する例。
/// target.forward と velocity が逆向き(後退)なら係数を小さくする。
/// </summary>
private Vector3 CalculateDirectionalLookAhead(Vector3 velocity)
{
if (velocity.sqrMagnitude < Mathf.Epsilon || target == null)
{
return Vector3.zero;
}
Vector3 v = velocity;
if (ignoreVerticalVelocity)
{
v.y = 0f;
}
float speed = v.magnitude;
if (speed < Mathf.Epsilon)
{
return Vector3.zero;
}
Vector3 direction = v / speed;
// forward との向きの差を -1 ~ 1 の範囲で取得(1=同方向, -1=真逆)
Vector3 forward = target.forward;
if (ignoreVerticalVelocity)
{
forward.y = 0f;
forward.Normalize();
}
float dot = Vector3.Dot(forward, direction);
// 後退(dot < 0)のときは 0.3 倍、前進(dot >= 0)のときは 1 倍で先読み
float directionalFactor = (dot < 0f) ? 0.3f : 1f;
float t = 1f;
if (speed < minSpeedForFullLookAhead)
{
t = Mathf.InverseLerp(0f, minSpeedForFullLookAhead, speed);
}
return direction * lookAheadDistance * t * directionalFactor;
}
このように、小さなコンポーネントとしてカメラ挙動を切り出しておけば、ゲームに合わせた細かい調整や差し替えが非常にやりやすくなります。
巨大な God クラスを避けて、役割ごとにスクリプトを分割していく設計を意識してみましょう。




