Unityを触り始めたころは、つい何でもかんでも Update() に書いてしまいがちですよね。プレイヤーの移動、カメラの追従、UIの更新、敵AI、さらにはミニマップの処理まで、全部ひとつのスクリプトやひとつの Update() に押し込んでしまうと、次のような問題が出てきます。

  • どこを直せばどの挙動が変わるのか分かりにくい
  • ちょっとした修正のつもりが別の機能に影響してバグを生む
  • プレハブごとにカスタマイズしたいのに、巨大スクリプトが邪魔をする

こうした「Godクラス」化を防ぐには、機能ごとに小さなコンポーネントに分割していくのが効果的です。この記事では、ミニマップ専用のコンポーネントを用意して、メインロジックからきれいに分離する方法を紹介します。

Unity 6 では SubViewport(サブビューポート) を使うことで、メインカメラとは別のカメラ映像を UI の一部として簡単に表示できます。ここでは、上空からの簡易カメラ映像を画面右上にミニマップとして表示する「Minimap」コンポーネントを作っていきましょう。

【Unity】上空カメラでサクッとミニマップ!「Minimap」コンポーネント

フルコード:Minimap コンポーネント

以下は、SubViewport を使ってミニマップ用カメラを制御し、その映像を UI の右上に表示するためのコンポーネントです。プレイヤーの位置を追従しつつ、回転は任意で固定または追従が選べるようにしてあります。


using UnityEngine;
using UnityEngine.UI;

namespace Sample.Minimap
{
    /// <summary>
    /// 上空からのカメラ映像を SubViewport 経由で
    /// 画面右上にミニマップとして表示するコンポーネント。
    /// 
    /// ・プレイヤーなどのターゲットを追従
    /// ・回転を固定 or ターゲットに合わせる
    /// ・UI の RectTransform を使って右上に表示
    /// </summary>
    [RequireComponent(typeof(Camera))]
    public class Minimap : MonoBehaviour
    {
        [Header("ミニマップの基本設定")]
        [SerializeField]
        private Transform target; 
        // ミニマップが追従するターゲット(プレイヤーなど)

        [SerializeField]
        private float height = 20f;
        // ターゲットからどれくらい上空にカメラを配置するか

        [SerializeField]
        private float followSmoothTime = 0.15f;
        // 追従のスムージング時間(0 でカクっと追従、値を大きくするとふわっと追従)

        [SerializeField]
        private bool rotateWithTarget = true;
        // true: ターゲットのY回転に合わせる / false: カメラ回転を固定

        [SerializeField]
        private float fixedNorthRotation = 0f;
        // rotateWithTarget = false のときに使う、北向きなどの固定角度(Y軸)

        [Header("SubViewport / UI 設定")]
        [SerializeField]
        private RectTransform minimapRect;
        // ミニマップを表示する UI の RectTransform(右上のパネルなど)

        [SerializeField]
        private Canvas rootCanvas;
        // ミニマップを含む Canvas(スクリーンスペースの大きさ計算に使用)

        [SerializeField]
        private Camera mainCamera;
        // メインカメラ(SubViewport 比率を決めるために使用)

        [SerializeField]
        private float orthographicSize = 20f;
        // ミニマップカメラの表示範囲(正射影サイズ)

        [SerializeField]
        private Color backgroundColor = Color.black;
        // ミニマップエリアの背景色

        // 内部用
        private Camera minimapCamera;
        private Vector3 currentVelocity; // SmoothDamp 用

        private void Awake()
        {
            // このコンポーネントが付いているオブジェクトの Camera を取得
            minimapCamera = GetComponent<Camera>();

            // ミニマップは真上から見下ろすので正射影にしておく
            minimapCamera.orthographic = true;
            minimapCamera.orthographicSize = orthographicSize;
            minimapCamera.backgroundColor = backgroundColor;
            minimapCamera.clearFlags = CameraClearFlags.SolidColor;

            // 透視投影にしたい場合は上記を調整してください
        }

        private void Start()
        {
            // 参照チェック
            if (target == null)
            {
                Debug.LogWarning("[Minimap] target が設定されていません。追従対象を設定してください。", this);
            }

            if (minimapRect == null)
            {
                Debug.LogWarning("[Minimap] minimapRect が設定されていません。UI の RectTransform を設定してください。", this);
            }

            if (rootCanvas == null)
            {
                // 自動で親 Canvas を探す(見つからない場合もあるので警告は残す)
                rootCanvas = GetComponentInParent<Canvas>();
                if (rootCanvas == null)
                {
                    Debug.LogWarning("[Minimap] rootCanvas が設定されていません。Canvas を設定してください。", this);
                }
            }

            if (mainCamera == null)
            {
                // メインカメラを自動で探す
                mainCamera = Camera.main;
                if (mainCamera == null)
                {
                    Debug.LogWarning("[Minimap] mainCamera が設定されていません。MainCamera タグ付きのカメラを探しましたが見つかりませんでした。", this);
                }
            }

            // 初期位置をターゲットに合わせておく
            if (target != null)
            {
                Vector3 startPos = target.position + Vector3.up * height;
                transform.position = startPos;
                UpdateRotationImmediate();
            }

            // SubViewport の矩形を UI の RectTransform から計算して適用
            UpdateSubViewportRect();
        }

        private void LateUpdate()
        {
            // 1. ターゲット追従
            UpdateFollow();

            // 2. 回転制御
            UpdateRotation();

            // 3. SubViewport の位置・サイズを UI に合わせて更新
            //   (解像度変更や UI レイアウト変更に追従したい場合)
            UpdateSubViewportRect();
        }

        /// <summary>
        /// ターゲットの位置へスムーズに追従する処理
        /// </summary>
        private void UpdateFollow()
        {
            if (target == null)
            {
                return;
            }

            // 目標位置 = ターゲットの真上
            Vector3 targetPos = target.position + Vector3.up * height;

            // スムーズに追従させる
            transform.position = Vector3.SmoothDamp(
                transform.position,
                targetPos,
                ref currentVelocity,
                followSmoothTime
            );
        }

        /// <summary>
        /// カメラの回転を更新する処理
        /// </summary>
        private void UpdateRotation()
        {
            if (rotateWithTarget && target != null)
            {
                // ターゲットの Y 回転だけを取り出して、真上から見下ろす
                float yRotation = target.eulerAngles.y;
                transform.rotation = Quaternion.Euler(90f, yRotation, 0f);
            }
            else
            {
                // 固定方向(北向きなど)から真下を向く
                transform.rotation = Quaternion.Euler(90f, fixedNorthRotation, 0f);
            }
        }

        /// <summary>
        /// 即座に回転だけ更新したいとき用(初期化など)
        /// </summary>
        private void UpdateRotationImmediate()
        {
            if (rotateWithTarget && target != null)
            {
                float yRotation = target.eulerAngles.y;
                transform.rotation = Quaternion.Euler(90f, yRotation, 0f);
            }
            else
            {
                transform.rotation = Quaternion.Euler(90f, fixedNorthRotation, 0f);
            }
        }

        /// <summary>
        /// SubViewport の矩形を UI の RectTransform に合わせて更新する。
        /// 
        /// ・Canvas の描画モードが Screen Space - Overlay / Camera の場合を想定
        /// ・minimapRect の Rect をスクリーン座標に変換し、Camera.rect に 0-1 で設定
        /// </summary>
        private void UpdateSubViewportRect()
        {
            if (minimapRect == null || rootCanvas == null || mainCamera == null)
            {
                return;
            }

            // Canvas 全体のサイズ(ピクセル)
            RectTransform canvasRect = rootCanvas.GetComponent<RectTransform>();
            if (canvasRect == null)
            {
                return;
            }

            Vector2 canvasSize = canvasRect.rect.size;

            // minimapRect の四隅を Canvas ローカル座標で取得
            Vector3[] corners = new Vector3[4];
            minimapRect.GetWorldCorners(corners);

            // Canvas の座標系に変換(World -> Canvas Local)
            for (int i = 0; i < 4; i++)
            {
                corners[i] = canvasRect.InverseTransformPoint(corners[i]);
            }

            // 左下と右上を取得
            Vector3 bottomLeft = corners[0];
            Vector3 topRight   = corners[2];

            // Canvas ローカル座標 (-width/2 ~ width/2) を 0 ~ width に変換
            float xMin = bottomLeft.x + canvasSize.x * 0.5f;
            float yMin = bottomLeft.y + canvasSize.y * 0.5f;
            float xMax = topRight.x   + canvasSize.x * 0.5f;
            float yMax = topRight.y   + canvasSize.y * 0.5f;

            // 0 ~ 1 に正規化して Camera.rect 用の値に変換
            float normalizedX = xMin / canvasSize.x;
            float normalizedY = yMin / canvasSize.y;
            float normalizedW = (xMax - xMin) / canvasSize.x;
            float normalizedH = (yMax - yMin) / canvasSize.y;

            minimapCamera.rect = new Rect(normalizedX, normalizedY, normalizedW, normalizedH);
        }

        /// <summary>
        /// 外部からターゲットを差し替えたいとき用の API
        /// (プレイヤーが乗り物に乗ったときなど)
        /// </summary>
        /// <param name="newTarget">新しい追従対象</param>
        public void SetTarget(Transform newTarget)
        {
            target = newTarget;
            if (target != null)
            {
                // 位置と回転を即座に合わせる
                transform.position = target.position + Vector3.up * height;
                UpdateRotationImmediate();
            }
        }

        /// <summary>
        /// ミニマップの表示範囲(正射影サイズ)を変更する
        /// (ズームイン・アウト用)
        /// </summary>
        public void SetOrthographicSize(float size)
        {
            orthographicSize = Mathf.Max(0.1f, size);
            if (minimapCamera != null)
            {
                minimapCamera.orthographicSize = orthographicSize;
            }
        }
    }
}

使い方の手順

ここでは、プレイヤーキャラクターの上空にミニマップカメラを配置し、その映像を画面右上の UI パネルに表示する例で説明します。

  1. UI(ミニマップ枠)の準備
    • Hierarchy で Canvas を作成(既にあればそれを利用)
    • Canvas の Render Mode は Screen Space - OverlayScreen Space - Camera を推奨
    • Canvas の子に UI > Image を作成し、名前を MinimapPanel などに変更
    • MinimapPanel の RectTransform を右上に配置(Anchor を Right-Top にして、Width/Height を 200×200 など)
    • この RectTransform がミニマップの表示枠になります
  2. ミニマップ用カメラの作成
    • Hierarchy で Camera を新規作成し、名前を MinimapCamera に変更
    • MinimapCamera に上記の Minimap コンポーネントを追加
    • インスペクターで以下を設定:
      • Target:プレイヤーキャラクターの Transform
      • Height:カメラの高さ(例: 20)
      • Rotate With Target:プレイヤーの向きに合わせたい場合はチェック
      • Minimap Rect:先ほど作成した MinimapPanel の RectTransform
      • Root Canvas:Canvas の参照(未設定なら自動検出されますが、明示的に設定推奨)
      • Main Camera:メインカメラを設定(Camera.main でも自動検出されます)
      • Orthographic Size:ミニマップの表示範囲(例: 20)
    • MinimapCamera の Culling Mask で、ミニマップに映したいレイヤーだけを ON にしても良いです(UI を映さないなど)
  3. プレイヤー(ターゲット)の設定
    • プレイヤーオブジェクトに Player などの名前を付けておきます
    • Minimap コンポーネントの Target に、このプレイヤーの Transform をドラッグ&ドロップ
    • ゲームを再生すると、プレイヤーの真上に MinimapCamera が追従し、右上のパネルにミニマップが表示されます
  4. 応用例:敵や動く床の確認にも使う
    • 敵キャラクターや動く床などにも通常どおり MeshRenderer / SpriteRenderer を設定しておけば、ミニマップにもそのまま映ります
    • 「ミニマップ専用アイコン」を表示したい場合は、専用レイヤーを作り、そのレイヤーだけを MinimapCamera の Culling Mask で ON にする構成もおすすめです
    • たとえば、プレイヤーに小さな矢印アイコンを子オブジェクトとして付けて「MinimapIcon」レイヤーにし、MinimapCamera では「MinimapIcon」だけを映す、といった使い方ができます

メリットと応用

この Minimap コンポーネントを使うことで、ミニマップの責務をひとつの小さなスクリプトに閉じ込めることができます。メインのゲームロジック(プレイヤー移動や敵 AI)からは「ミニマップのことは Minimap に任せる」構造になるので、次のようなメリットがあります。

  • プレハブ管理が楽になる:プレイヤープレハブに MinimapCamera を子として仕込んでおけば、どのシーンでも同じミニマップ挙動を再利用できます。
  • レベルデザイン時に視認性をすぐ確認できる:シーンに MinimapCamera をポンと置いてターゲットを指定するだけで、レベル全体の見え方をミニマップから確認できます。
  • 責務が明確:追従・回転・表示領域の計算といった「ミニマップに関する処理」がコンポーネントに集約されるため、バグ調査の範囲が限定されます。

さらに、応用として次のような改造も簡単に行えます。

  • プレイヤーが特定のキーを押したときにミニマップをズームイン / アウトする
  • ダンジョンに入ったら自動的にミニマップを非表示にする
  • プレイヤーが乗り物に乗ったときだけターゲットを乗り物に切り替える

例えば、マウスホイールでミニマップをズームイン / アウトする簡単な改造は、以下のようなメソッドを Minimap クラスに追加するだけで実現できます(新 Input System ではなく、古典的な Input API を使った例):


        /// <summary>
        /// マウスホイールでミニマップのズームを操作する例。
        /// Update や LateUpdate から呼び出して使います。
        /// </summary>
        private void HandleZoomByMouseWheel()
        {
            // マウスホイールの入力値を取得
            float scroll = Input.GetAxis("Mouse ScrollWheel"); // 新InputSystemの場合は別途バインドが必要

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

            // スクロール方向に応じてサイズを変更
            float zoomSpeed = 10f;
            float newSize = orthographicSize - scroll * zoomSpeed;

            // SetOrthographicSize を使って安全に反映
            SetOrthographicSize(newSize);
        }

このように、小さな責務に分割されたコンポーネントにしておくと、必要な機能を足したり引いたりするのがとても楽になります。ミニマップも「巨大な管理クラス」に押し込むのではなく、専用コンポーネントとして育てていきましょう。