Unityを触り始めた頃によくやってしまうのが、「Update に全部書いてしまう」スタイルですよね。
カメラ操作、プレイヤー入力、UI更新、エフェクト制御……全部ひとつのスクリプトに詰め込んでしまうと、少し仕様を変えたいだけでもコードのどこを触ればいいのか分からなくなってしまいます。
カメラの「ちょっとした演出」も同じで、「プレイヤー制御スクリプトの中にカメラの揺れやズレの処理を全部書く」ような実装だと、すぐにGodクラス化してしまいます。
そこで今回は、
- 「特定キーを押している間だけ」
- 「マウス位置の方向に」
- 「カメラをふわっとずらす」
という機能だけに責務を絞ったコンポーネント
「MousePeek(マウス視点)」 を作って、カメラにアタッチするだけで使えるようにしてみましょう。
【Unity】マウス方向にチラ見カメラ!「MousePeek」コンポーネント
以下が、Unity6(新Input System前提)で動作する「MousePeek」コンポーネントのフルコードです。
カメラにアタッチすることで、「指定キーを押している間だけ」マウス方向にカメラをオフセットさせます。
using UnityEngine;
using UnityEngine.InputSystem; // 新Input System 用
/// <summary>
/// 特定キーを押している間、マウス位置の方向へカメラをオフセットするコンポーネント。
/// カメラにアタッチして使います。
/// </summary>
[RequireComponent(typeof(Camera))]
public class MousePeek : MonoBehaviour
{
// --- 設定項目(インスペクターから調整) ---
[Header("基本設定")]
[SerializeField]
private float maxOffsetDistance = 2.0f;
// カメラが最大でどれくらいずれるか(ワールド座標の距離)
[SerializeField]
private float moveSpeed = 10.0f;
// 現在位置から目標オフセット位置への追従速度(Lerp用)
[SerializeField]
private float returnSpeed = 10.0f;
// キーを離したときに元の位置へ戻る速度(Lerp用)
[SerializeField]
private float depthFromCamera = 10.0f;
// マウスのワールド座標を取得する際に使う深度(カメラからの距離)
[Header("入力設定")]
[SerializeField]
private Key peekKey = Key.Mouse2;
// マウス中ボタンなど。Keyboardのキーも指定可能(Key.LeftShift など)
[SerializeField]
private bool useScreenEdgeNormalization = true;
// true: 画面中心からの相対位置でオフセット
// false: ワールド座標ベースでオフセット
[Header("デバッグ")]
[SerializeField]
private bool drawGizmos = true;
// シーンビューにオフセット位置を表示するか
// --- 内部状態 ---
private Camera _camera;
private Vector3 _basePosition;
// カメラの「基準位置」(オフセット前の位置)
private bool _isPeeking;
// 現在「覗き込み中」かどうか
private void Awake()
{
_camera = GetComponent<Camera>();
_basePosition = transform.position;
}
private void OnEnable()
{
// 有効化時に基準位置を更新しておく
_basePosition = transform.position;
}
private void Update()
{
if (Mouse.current == null)
{
// マウスが存在しない環境(ゲームパッドのみ等)では何もしない
return;
}
// キー入力の状態を取得
bool isKeyPressed = Keyboard.current != null && Keyboard.current[peekKey].isPressed
|| Mouse.current != null && Mouse.current[peekKey].isPressed;
_isPeeking = isKeyPressed;
// 現在のカメラ位置
Vector3 currentPosition = transform.position;
// 目標位置を計算
Vector3 targetPosition = _basePosition;
if (_isPeeking)
{
// マウス方向にオフセットを計算
Vector3 offset = CalculateOffsetFromMouse();
targetPosition = _basePosition + offset;
// 覗き込み中は moveSpeed で追従
transform.position = Vector3.Lerp(
currentPosition,
targetPosition,
1f - Mathf.Exp(-moveSpeed * Time.deltaTime)
);
}
else
{
// 覗き込みをしていないときは basePosition に戻る
transform.position = Vector3.Lerp(
currentPosition,
_basePosition,
1f - Mathf.Exp(-returnSpeed * Time.deltaTime)
);
}
}
/// <summary>
/// マウス位置からカメラのオフセットベクトルを計算する。
/// </summary>
private Vector3 CalculateOffsetFromMouse()
{
// 画面上のマウス位置(ピクセル座標)
Vector2 mousePos = Mouse.current.position.ReadValue();
Vector3 offset = Vector3.zero;
if (useScreenEdgeNormalization)
{
// --- 画面中心からの相対位置でオフセットするモード ---
// 画面サイズ
float width = Screen.width;
float height = Screen.height;
// 画面中心を(0,0)、四隅を(-1,-1)〜(1,1)に正規化
Vector2 normalized = new Vector2(
(mousePos.x - width * 0.5f) / (width * 0.5f),
(mousePos.y - height * 0.5f) / (height * 0.5f)
);
// 正規化された値をクランプ(安全のため)
normalized = Vector2.ClampMagnitude(normalized, 1f);
// カメラの右方向と上方向を使ってオフセットを作る
Vector3 right = transform.right;
Vector3 up = transform.up;
offset = (right * normalized.x + up * normalized.y) * maxOffsetDistance;
}
else
{
// --- ワールド座標ベースでオフセットするモード ---
// マウス位置をワールド座標に変換
Vector3 screenPoint = new Vector3(mousePos.x, mousePos.y, depthFromCamera);
Vector3 worldPoint = _camera.ScreenToWorldPoint(screenPoint);
// カメラ基準位置から見たマウス方向
Vector3 dir = (worldPoint - _basePosition);
dir.z = 0f; // 2DゲームなどでZ方向に動かしたくない場合は0に固定
if (dir.sqrMagnitude > 0.0001f)
{
dir.Normalize();
offset = dir * maxOffsetDistance;
}
}
return offset;
}
/// <summary>
/// シーンビュー上でオフセット位置を可視化する(デバッグ用)。
/// </summary>
private void OnDrawGizmosSelected()
{
if (!drawGizmos)
{
return;
}
// 再生中かどうかで基準位置を決定
Vector3 basePos = Application.isPlaying ? _basePosition : transform.position;
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(basePos, 0.1f);
// 想定される最大オフセット範囲を円で表示
Gizmos.color = new Color(0f, 1f, 1f, 0.25f);
Gizmos.DrawWireSphere(basePos, maxOffsetDistance);
}
/// <summary>
/// ゲーム中に「今の位置を基準位置として登録し直す」ためのメソッド。
/// 例えばカメラの追従ターゲットが変わったときに外部から呼ぶ想定。
/// </summary>
public void ResetBasePositionToCurrent()
{
_basePosition = transform.position;
}
}
使い方の手順
ここでは、典型的な「プレイヤー追従カメラ」に MousePeek を足して、
「中ボタンを押している間だけ、マウス方向にカメラがチラ見する」例を想定して説明します。
-
コンポーネントを作成する
- Unity の Project ビューで
MousePeek.csを作成し、上記コードをコピペします。 - 保存したら、コンパイルエラーが出ていないことを確認します。
- Unity の Project ビューで
-
カメラにアタッチする
- シーン内のカメラ(例:
Main Camera)を選択します。 - Inspector の「Add Component」から
MousePeekを追加します。 [RequireComponent(typeof(Camera))]を付けているので、カメラ以外にアタッチしようとすると警告が出ます。
- シーン内のカメラ(例:
-
インスペクターでパラメータを調整する
例として、次のように設定してみましょう。Max Offset Distance: 1.5〜3.0(カメラがどれくらい動くか。3Dなら小さめ、2Dならやや大きめがおすすめ)Move Speed: 10〜20(覗き込み開始の追従速度)Return Speed: 10〜20(元位置に戻る速度)Depth From Camera: 10(カメラから10ユニット先の平面でマウス位置を解釈)Peek Key:Mouse2(マウス中ボタン)やLeftShiftなど好みで変更Use Screen Edge Normalization: ON なら「画面端に行くほど強くオフセット」、OFF なら「ワールド座標方向ベース」
-
実際のゲームで試す
具体的な使用例として、次のようなシチュエーションで試すと分かりやすいです。-
プレイヤーキャラの追従カメラ
- プレイヤーを追いかけるカメラに
MousePeekを付けると、 - 「中ボタンを押している間だけ、マウス方向に画面を少しずらして先を確認できる」
- という「チラ見」機能になります。見通しの悪いダンジョンや横スクロールの先読みなどに便利です。
- プレイヤーを追いかけるカメラに
-
タワーディフェンスやRTSの俯瞰カメラ
- 通常は WASD やマウスドラッグでスクロールしつつ、
- 一時的に
MousePeekでマウス方向に画面を寄せることで、素早く戦況を確認できます。
-
動く床やギミックのプレビュー
- 2Dアクションで、足場の先にあるトラップや動く床を、
- 「覗き込みキー」を押している間だけチラ見できるようにすると、理不尽さを減らしつつ緊張感を保てます。
-
プレイヤーキャラの追従カメラ
メリットと応用
カメラの「覗き込み」機能を MousePeek という単一コンポーネントに切り出すことで、次のようなメリットがあります。
- プレイヤー制御スクリプトを肥大化させない
カメラの演出ロジックをプレイヤーの移動スクリプトから完全に分離できるので、
「移動は移動」「カメラ演出はカメラ演出」と責務を分けて考えられます。 - プレハブ化してどのシーンでも再利用できる
MousePeekを付けたカメラをプレハブ化しておけば、
新しいシーンにそのままドラッグ&ドロップするだけで「覗き込みカメラ」が完成します。
レベルデザイナーがパラメータを触るだけで演出の強さを調整できるのもポイントです。 - 他のカメラ演出コンポーネントと組み合わせやすい
例えば、カメラシェイク用コンポーネント、ズーム制御コンポーネントなどと併用しても、
それぞれが小さな責務に分かれているので、バグの原因を追いやすくなります。
応用として、「覗き込み中は少しだけカメラをズームインする」などの演出も簡単に追加できます。
例えば、次のようなメソッドを MousePeek に追加し、Update から呼び出すと、
覗き込み中に FOV を変化させることができます。
/// <summary>
/// 覗き込み中にカメラのFOVを少し変化させる応用例。
/// Camera.main ではなく、自分の Camera コンポーネントを直接操作します。
/// </summary>
private void UpdatePeekFov(float normalFov = 60f, float peekFov = 55f, float fovLerpSpeed = 5f)
{
if (_camera == null) return;
float targetFov = _isPeeking ? peekFov : normalFov;
_camera.fieldOfView = Mathf.Lerp(
_camera.fieldOfView,
targetFov,
1f - Mathf.Exp(-fovLerpSpeed * Time.deltaTime)
);
}
このように、小さなコンポーネントとしてカメラの「覗き込み」だけを切り出しておくと、
あとから「ちょっとズームを足す」「ちょっとシェイクを足す」といった改造がしやすくなります。
Update に全部書き込むスタイルから卒業して、責務ごとにコンポーネントを分けていきましょう。
