Unityを触り始めた頃は、つい何でもかんでも Update() の中に書いてしまいがちですよね。プレイヤーの移動、カメラの追従、入力処理、UIの更新……全部ひとつのスクリプトに詰め込んでしまうと、だんだん「どこを触ればいいのか分からない巨大スクリプト」が生まれてしまいます。

特にカメラまわりは、「プレイヤー追従カメラ」と「デバッグ用の自由カメラ」がごちゃまぜになりやすいポイントです。プレイヤーにくっついたままでは、レベル全体の確認やデバッグがしづらいですよね。

そこでこの記事では、プレイヤーからカメラを切り離し、WASD+マウスでステージ全体を自由に見回せるためのコンポーネント「FreeCamera」を用意します。カメラの役割をひとつのコンポーネントに閉じ込めることで、プレイヤー制御と綺麗に分離し、シーンの確認やレベルデザインがかなり楽になります。

【Unity】ステージを自由に飛び回る!「FreeCamera」コンポーネント

FreeCamera.cs(フルコード)


using UnityEngine;

/// <summary>
/** 
 * WASD+マウスで自由に動かせるデバッグ用カメラコンポーネント。
 * - プレイヤーからカメラを切り離して、ステージ全体を見回したいときに使う
 * - シーンビューの「Fly」操作に近い感覚
 * 
 * 主な操作:
 * - マウス移動: カメラの向き(ヨー・ピッチ)を変更
 * - W / S: 前後移動
 * - A / D: 左右移動
 * - Space / LeftControl: 上下移動
 * - Shift: 移動速度アップ(ブースト)
 */
/// </summary>
[RequireComponent(typeof(Camera))]
public class FreeCamera : MonoBehaviour
{
    [Header("移動設定")]
    [SerializeField] private float moveSpeed = 5f;          // 通常移動速度
    [SerializeField] private float boostMultiplier = 3f;    // Shift押下時の速度倍率
    [SerializeField] private float verticalMoveSpeed = 5f;  // 上下移動速度(Space / Ctrl)

    [Header("マウス視点操作")]
    [SerializeField] private float mouseSensitivity = 2f;   // マウス感度
    [SerializeField] private bool invertY = false;          // Y軸反転の有無
    [SerializeField] private float minPitch = -89f;         // 下向き最大角度
    [SerializeField] private float maxPitch = 89f;          // 上向き最大角度

    [Header("カーソルロック")]
    [SerializeField] private bool lockCursorOnStart = true; // 開始時にカーソルロックするか
    [SerializeField] private KeyCode toggleCursorKey = KeyCode.Escape; // ロック切り替えキー

    // 内部状態
    private float _yaw;   // 水平方向の回転角(ヨー)
    private float _pitch; // 垂直方向の回転角(ピッチ)

    private void Start()
    {
        // 現在の回転からヨー・ピッチを初期化
        Vector3 euler = transform.rotation.eulerAngles;
        _yaw = euler.y;
        _pitch = euler.x;

        if (lockCursorOnStart)
        {
            LockCursor(true);
        }
    }

    private void Update()
    {
        // カーソルロックの切り替え
        if (Input.GetKeyDown(toggleCursorKey))
        {
            bool shouldLock = Cursor.lockState != CursorLockMode.Locked;
            LockCursor(shouldLock);
        }

        // カーソルがロックされているときだけ視点操作&移動を受け付ける
        if (Cursor.lockState == CursorLockMode.Locked)
        {
            HandleMouseLook();
            HandleMovement();
        }
    }

    /// <summary>
    /// マウス移動による視点操作を処理する
    /// </summary>
    private void HandleMouseLook()
    {
        // マウス移動量を取得
        float mouseX = Input.GetAxisRaw("Mouse X");
        float mouseY = Input.GetAxisRaw("Mouse Y");

        // ヨー(左右回転)はX方向の移動量で決定
        _yaw += mouseX * mouseSensitivity;

        // ピッチ(上下回転)はY方向の移動量で決定
        float invert = invertY ? 1f : -1f;
        _pitch += mouseY * mouseSensitivity * invert;

        // ピッチに制限をかけて、真上真下を向きすぎないようにする
        _pitch = Mathf.Clamp(_pitch, minPitch, maxPitch);

        // 回転を反映
        Quaternion rotation = Quaternion.Euler(_pitch, _yaw, 0f);
        transform.rotation = rotation;
    }

    /// <summary>
    /// キーボード入力による移動を処理する
    /// </summary>
    private void HandleMovement()
    {
        // WASD入力
        float horizontal = Input.GetAxisRaw("Horizontal"); // A(-1) / D(+1)
        float vertical = Input.GetAxisRaw("Vertical");     // S(-1) / W(+1)

        // 上下移動(Space / LeftControl)
        float up = 0f;
        if (Input.GetKey(KeyCode.Space))
        {
            up += 1f;
        }
        if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl))
        {
            up -= 1f;
        }

        // カメラのローカル軸に沿って移動方向ベクトルを作る
        Vector3 moveDir =
            transform.forward * vertical +
            transform.right * horizontal +
            Vector3.up * up;

        // 斜め移動で速くなりすぎないように正規化
        if (moveDir.sqrMagnitude > 1f)
        {
            moveDir.Normalize();
        }

        // Shiftでブースト
        float speed = moveSpeed;
        if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
        {
            speed *= boostMultiplier;
        }

        // 実際の移動
        transform.position += moveDir * speed * Time.deltaTime;
    }

    /// <summary>
    /// カーソルロック状態を切り替える
    /// </summary>
    /// <param name="shouldLock">trueでロック、falseで解放</param>
    private void LockCursor(bool shouldLock)
    {
        if (shouldLock)
        {
            Cursor.lockState = CursorLockMode.Locked;
            Cursor.visible = false;
        }
        else
        {
            Cursor.lockState = CursorLockMode.None;
            Cursor.visible = true;
        }
    }
}

使い方の手順

  1. カメラにコンポーネントを追加する
    シーン内の Main Camera(または自由カメラにしたいカメラ)を選択し、
    「Add Component」から FreeCamera を追加します。
    [RequireComponent(typeof(Camera))] を付けているので、カメラが必ず一緒に存在する状態になります。
  2. プレイヤー追従カメラと切り替える
    例えば、普段はプレイヤー追従カメラを使い、デバッグ時だけ FreeCamera を有効にしたい場合は、
    – プレイヤー追従カメラ用オブジェクト(例: PlayerCamera
    – 自由カメラ用オブジェクト(例: FreeCamera
    の2つを用意しておき、インスペクターから GameObject.SetActive のON/OFFで切り替えます。
    たとえば、プレイヤーにくっついたカメラは通常ON、レベルチェック時はそれをOFFにしてこの FreeCamera をONにする、という運用ですね。
  3. パラメータを調整する
    • Move Speed: 通常の移動速度。ステージのスケールに合わせて 5~20 くらいで調整
    • Boost Multiplier: Shift押下時の倍率。大きなマップなら 5~10 くらいにすると快適
    • Vertical Move Speed: 上下移動の速度。Move Speed と同じか、やや遅めがおすすめ
    • Mouse Sensitivity: マウス感度。好みに合わせて 1.0~3.0 くらい
    • Invert Y: フライトゲーム風にしたいならチェックを入れてY軸反転
    • Lock Cursor On Start: 再生開始と同時にマウスカーソルをロックするかどうか
    • Toggle Cursor Key: デフォルトは Esc。一時的にカーソルを解放したいときに使います
  4. 実際に操作してみる(具体例)
    例えば以下のようなシーンで使うと便利です。
    • プレイヤーの移動範囲を確認したいとき
      プレイヤーとは別に FreeCamera を持つカメラを用意し、
      デバッグ中に FreeCamera の GameObject を有効化してステージ全体を見回します。
      崖のコライダー漏れや、敵の湧き位置、動く床の動作などを上空からチェックできます。
    • 敵AIの行動範囲を俯瞰したいとき
      敵のパトロールルートや索敵範囲が正しく機能しているかを、
      上から自由に見下ろして確認できます。
      NavMeshエージェントの経路がおかしい箇所も見つけやすくなります。
    • 巨大なステージのレベルデザイン
      広いマップを作っている場合、プレイヤーを走らせて確認するのはかなり大変です。
      FreeCamera なら、空中から高速で移動して、
      「この丘の高さは適切か」「このエリアから次のエリアへの見通しはどうか」などを素早くチェックできます。

メリットと応用

FreeCamera を独立したコンポーネントとして用意しておくと、次のようなメリットがあります。

  • プレイヤー制御とカメラ制御を完全に分離できる
    プレイヤーのスクリプトに「デバッグ用カメラ機能」を足してしまうと、
    本番ビルドで無効化し忘れたり、入力が競合したりとトラブルの元になります。
    カメラはカメラ、プレイヤーはプレイヤーで責務を分けておくと、コードの見通しがかなり良くなります。
  • プレハブ化してどのプロジェクトでも再利用できる
    FreeCamera を付けたカメラを1つプレハブにしておけば、
    新しいシーンや新プロジェクトにドラッグ&ドロップするだけで、すぐに自由カメラ環境が整います。
    レベルデザイン用の「ツール」として持ち歩けるのが便利です。
  • レベルデザインの試行錯誤が圧倒的に速くなる
    「この足場、プレイヤーからちゃんと見えるかな?」
    「この敵、出現位置からプレイヤーをちゃんと視認できるかな?」
    といった確認を、プレイヤーを動かさずにサクサク行えます。
    ステージを作る人とプレイヤー挙動を作る人が別でも、それぞれが自分の作業に集中できるのも嬉しいポイントです。

さらに、コンポーネント指向の思想に従って、必要に応じて小さく改造していくのもおすすめです。
例えば「特定のキーを押している間だけ高速移動する」のではなく、「マウスホイールで速度を可変にしたい」場合、次のような関数を追加してみるのも良いですね。


    /// <summary>
    /// マウスホイールで移動速度を可変にする(応用例)
    /// </summary>
    private void HandleSpeedByScroll()
    {
        // マウスホイールの入力値(上スクロールで正、下スクロールで負)
        float scroll = Input.GetAxis("Mouse ScrollWheel");

        if (Mathf.Approximately(scroll, 0f))
        {
            return;
        }

        // スクロール量に応じて移動速度を増減
        moveSpeed += scroll * 10f;

        // 負の値にならないように下限を設定
        moveSpeed = Mathf.Clamp(moveSpeed, 1f, 100f);
    }

Update() の中から HandleSpeedByScroll() を呼び出すだけで、
「今は細かく調整したいから低速で」「マップを大きく移動したいから高速で」といった操作が簡単にできます。
このように、FreeCamera というひとつの責務を持ったコンポーネントをベースに、必要な機能を小さく積み上げていくと、プロジェクト全体の設計もぐっと綺麗になりますね。