Unityで2Dゲームを作っていると、つい Update() の中に「カメラの追従」「移動制限」「ズーム処理」などを全部まとめて書いてしまいがちですよね。最初は動くのですが、あとからタイルマップのサイズを変えたり、カメラの挙動を変えたりするときに、1つの巨大スクリプトを開いてゴリゴリ修正するハメになります。

とくに「カメラの移動制限」を Update() 内で毎フレーム計算していたり、シーンごとに制限値を手打ちしていると、レベルデザインのたびに調整が必要になってしまいます。

そこでこの記事では、タイルマップのサイズから自動的に カメラの移動限界(Limit) を計算してくれるコンポーネント 「LimitSetter」 を用意して、

  • タイルマップを変えたら、カメラの制限も自動で追従
  • カメラのロジック本体と「制限値の計算」を分離
  • プレハブ化して、どのステージでも再利用

といった、コンポーネント指向な構成にしていきましょう。

【Unity】タイルマップにピッタリ追従するカメラ制限!「LimitSetter」コンポーネント

ここでは、「タイルマップのサイズ」から「カメラの移動限界(min/max X,Y)」を自動計算して、2Dカメラに渡すコンポーネントを実装します。

カメラ本体の移動処理(プレイヤー追従など)は別コンポーネントに任せて、LimitSetter はあくまで「境界情報の提供」に専念させるのがポイントです。

前提:シンプルな 2D カメラコントローラー

LimitSetter が設定する「移動限界」を受け取るために、まずはシンプルな 2D カメラコントローラーを1つ用意します。このコンポーネントは「プレイヤーに追従し、指定された範囲内にカメラ位置をクランプする」役割だけを持ちます。


using UnityEngine;

/// <summary>カメラの移動可能範囲を表す構造体</summary>
[System.Serializable]
public struct CameraLimits
{
    public float minX;
    public float maxX;
    public float minY;
    public float maxY;
}

/// <summary>
/// プレイヤーに追従しつつ、指定された範囲内に収まる 2D カメラコントローラー。
/// LimitSetter から CameraLimits を受け取って使用します。
/// </summary>
[RequireComponent(typeof(Camera))]
public class SimpleCamera2DController : MonoBehaviour
{
    [Header("追従ターゲット")]
    [SerializeField] private Transform target; // 例: プレイヤーの Transform

    [Header("追従スムーズさ")]
    [SerializeField] private float followSmoothTime = 0.15f;

    [Header("移動限界(Limit)")]
    [SerializeField] private bool useLimits = true;
    [SerializeField] private CameraLimits limits;

    private Vector3 _currentVelocity;
    private Camera _camera;

    private void Awake()
    {
        _camera = GetComponent<Camera>();
    }

    private void LateUpdate()
    {
        if (target == null)
        {
            return;
        }

        // ターゲット位置を基準にカメラ位置を計算
        Vector3 targetPos = target.position;
        Vector3 desiredPos = new Vector3(targetPos.x, targetPos.y, transform.position.z);

        // スムーズに追従
        Vector3 smoothedPos = Vector3.SmoothDamp(
            transform.position,
            desiredPos,
            ref _currentVelocity,
            followSmoothTime
        );

        if (useLimits)
        {
            // カメラの表示範囲を考慮してクランプ
            float camHeight = _camera.orthographicSize * 2f;
            float camWidth = camHeight * _camera.aspect;

            float halfWidth = camWidth * 0.5f;
            float halfHeight = camHeight * 0.5f;

            float clampedX = Mathf.Clamp(
                smoothedPos.x,
                limits.minX + halfWidth,
                limits.maxX - halfWidth
            );
            float clampedY = Mathf.Clamp(
                smoothedPos.y,
                limits.minY + halfHeight,
                limits.maxY - halfHeight
            );

            smoothedPos = new Vector3(clampedX, clampedY, smoothedPos.z);
        }

        transform.position = smoothedPos;
    }

    /// <summary>
    /// 外部(LimitSetter など)から移動限界を設定するための関数。
    /// </summary>
    public void SetLimits(CameraLimits newLimits)
    {
        limits = newLimits;
    }

    /// <summary>
    /// 外部から「移動限界を使うかどうか」を切り替える関数。
    /// </summary>
    public void SetUseLimits(bool enabled)
    {
        useLimits = enabled;
    }
}

本題:LimitSetter コンポーネント(タイルマップから自動設定)

次に、タイルマップ(Grid + Tilemap)のサイズから CameraLimits を自動計算して、SimpleCamera2DController に渡す LimitSetter を実装します。


using UnityEngine;
using UnityEngine.Tilemaps;

/// <summary>
/// タイルマップのサイズから 2D カメラの移動限界(Limit)を自動計算して設定するコンポーネント。
/// タイルマップの Bounds を元に CameraLimits を算出し、SimpleCamera2DController に渡します。
/// </summary>
[DisallowMultipleComponent]
public class LimitSetter : MonoBehaviour
{
    [Header("参照するタイルマップ")]
    [SerializeField] private Tilemap targetTilemap;

    [Header("制限を適用するカメラコントローラー")]
    [SerializeField] private SimpleCamera2DController cameraController;

    [Header("外枠に余白を追加する(タイル単位)")]
    [Tooltip("タイルマップの外側にどれだけ余白を追加するか(タイル数単位)")]
    [SerializeField] private Vector2Int tileMargin = Vector2Int.zero;

    [Header("起動時に自動で設定する")]
    [SerializeField] private bool applyOnStart = true;

    private void Reset()
    {
        // コンポーネントがアタッチされたときに、よく使う参照を自動取得しておく

        // 同じ GameObject に Tilemap があれば取得
        if (targetTilemap == null)
        {
            targetTilemap = GetComponent<Tilemap>();
        }

        // メインカメラにある SimpleCamera2DController を探す
        if (cameraController == null && Camera.main != null)
        {
            cameraController = Camera.main.GetComponent<SimpleCamera2DController>();
        }
    }

    private void Start()
    {
        if (applyOnStart)
        {
            ApplyLimits();
        }
    }

    /// <summary>
    /// タイルマップのサイズからカメラの移動限界を計算して、カメラコントローラーに適用します。
    /// </summary>
    [ContextMenu("Apply Limits Now")]
    public void ApplyLimits()
    {
        if (targetTilemap == null)
        {
            Debug.LogWarning($"[LimitSetter] タイルマップが設定されていません: {name}", this);
            return;
        }

        if (cameraController == null)
        {
            Debug.LogWarning("[LimitSetter] Camera Controller が設定されていません。SimpleCamera2DController を参照してください。", this);
            return;
        }

        // タイルマップのワールド座標での境界を取得
        // ※ cellBounds はセル座標、localBounds / bounds はワールド座標基準
        Bounds worldBounds = targetTilemap.localBounds;

        // 余白をタイル単位からワールド単位(セルサイズ)に変換
        Vector3 cellSize = targetTilemap.cellSize;
        float marginX = tileMargin.x * cellSize.x;
        float marginY = tileMargin.y * cellSize.y;

        // min/max を計算(余白込み)
        float minX = worldBounds.min.x - marginX;
        float maxX = worldBounds.max.x + marginX;
        float minY = worldBounds.min.y - marginY;
        float maxY = worldBounds.max.y + marginY;

        // 結果を CameraLimits 構造体に格納
        CameraLimits limits = new CameraLimits
        {
            minX = minX,
            maxX = maxX,
            minY = minY,
            maxY = maxY
        };

        // カメラコントローラーに適用
        cameraController.SetLimits(limits);
        cameraController.SetUseLimits(true);

        Debug.Log($"[LimitSetter] カメラ移動限界を設定しました: " +
                  $"minX={minX:F2}, maxX={maxX:F2}, minY={minY:F2}, maxY={maxY:F2}", this);
    }
}

ポイント解説

  • [DisallowMultipleComponent]
    LimitSetter を同じ GameObject に複数つけてしまう事故を防ぎます。カメラ制限は1つで十分ですね。
  • Reset() での自動参照取得
    コンポーネントをアタッチした瞬間に、同じオブジェクトの TilemapCamera.mainSimpleCamera2DController を自動で拾うようにして、セットアップを楽にしています。
  • ApplyLimits()[ContextMenu]
    インスペクターの右クリックメニューから「今すぐ適用」を実行できるので、プレイ前に境界の確認・調整がしやすくなります。
  • tileMargin
    ステージの端ギリギリまでカメラが行くのではなく、少し余白を持たせたいときに便利です。タイル数で指定できるので、レベルデザイナーにも分かりやすいです。

使い方の手順

  1. タイルマップを用意する
    例として、2Dのステージを Grid > Tilemap で作成し、床や壁のタイルを敷き詰めておきます。
    – GameObject 構成例:
    • Grid
      • Tilemap (Ground)
  2. カメラに SimpleCamera2DController を付ける
    メインカメラ(Main Camera)を選択し、SimpleCamera2DController をアタッチします。
    Target にはプレイヤーの Transform をドラッグ&ドロップ
    – 2Dゲームなのでカメラは Orthographic にしておきましょう
  3. LimitSetter をタイルマップに付ける
    Tilemap (Ground) など、ステージの範囲を表しているタイルマップの GameObject に LimitSetter をアタッチします。
    Target Tilemap:通常は自動で自分自身の Tilemap が入ります
    Camera Controller:メインカメラ上の SimpleCamera2DController をドラッグ&ドロップ(または Reset 時に自動で入ります)
    Tile Margin:例えば (1,1) にすると、上下左右1タイル分だけ余白を追加できます
  4. プレイして動作確認する
    再生ボタンを押すと、Start()ApplyLimits() が呼ばれ、タイルマップのサイズからカメラの移動限界が自動設定されます。
    プレイヤーをステージの端まで移動させて、カメラがそれ以上外へ行かないことを確認してみましょう。

具体的な使用例

  • プレイヤー用ステージ
    横スクロールアクションゲームで、プレイヤーが左右に移動するステージの Ground タイルマップに LimitSetter を付ければ、ステージの長さを変えてもカメラ制限が自動で追従します。
    レベルデザイナーは単にタイルを増やしたり減らしたりするだけでOKです。
  • 敵専用のアリーナ
    ボス戦用の小さなアリーナタイルマップに LimitSetter を付けて、カメラがアリーナの外に出ないように制限することもできます。
    シーン切り替え時に別のタイルマップ+LimitSetter を用意すれば、ステージごとに自動でカメラ制限が変わります。
  • 動く床+パララックス背景
    背景用の大きなタイルマップに LimitSetter を付けて、その範囲内でカメラを動かしつつ、手前に動く床(Platform)を配置することで、背景とゲームプレイ空間の境界を一括管理できます。

メリットと応用

LimitSetter コンポーネントを導入することで、以下のようなメリットがあります。

  • レベルデザインがタイルマップ中心になる
    ステージのサイズを「タイルマップの大きさ」として自然に表現でき、そのままカメラ制限に反映されます。
    シーンごとに「minX, maxX いくつだったっけ?」と悩む必要がなくなります。
  • カメラロジックと制限ロジックの分離
    SimpleCamera2DController は「どう追いかけるか」だけに集中し、LimitSetter は「どこまで動いてよいか」だけを担当します。
    単一責任のコンポーネントに分割されているので、あとから仕様変更が入っても影響範囲を限定しやすいです。
  • プレハブ化で再利用が簡単
    「ステージ用タイルマップ + LimitSetter」をプレハブにしておけば、別シーンにドラッグ&ドロップするだけで、カメラ制限付きのステージが量産できます。

改造案:ゲーム中にタイルマップを差し替えたときに再適用する

例えば、ゲーム中にステージを拡張したり、別のタイルマップに切り替えたりする場合、LimitSetter に「再計算用の関数」を追加しておくと便利です。


public void SetTilemapAndReapply(Tilemap newTilemap)
{
    // タイルマップを差し替えて、すぐに限界を再計算する
    targetTilemap = newTilemap;
    ApplyLimits();
}

この関数を呼び出すだけで、動的にステージを切り替えてもカメラの移動限界が自動で更新されるようになります。
LimitSetter を「境界情報のハブ」として育てていくと、シーン構成がかなりスッキリしますね。