UnityでRTSやシミュレーションゲームを作り始めると、ついつい「とりあえず Update に全部書く」スタイルになりがちですよね。
マウス入力の取得、ドラッグ中の矩形描画、ユニットの判定、選択状態の切り替え……と、全部を1つの巨大スクリプトにまとめてしまうと、

  • 何がどの責務なのか分からない
  • バグが出たときに原因を特定しづらい
  • 別のシーン・別のゲームで再利用しづらい

といった問題が一気に噴出します。

そこでこの記事では、「マウスドラッグで矩形を描き、その中にあるユニットを選択状態にする」機能を
DragSelect コンポーネントとしてきれいに分離してみましょう。
入力処理・矩形描画・ユニットの判定を1つの責務にまとめつつ、他のコンポーネント(ユニット側の選択表示など)と連携しやすい形にしていきます。

【Unity】RTS風の範囲選択をサクッと実装!「DragSelect」コンポーネント

ここでは以下を行う DragSelect コンポーネントを作ります。

  • マウス左ボタンのドラッグでスクリーン上に矩形を描画(UI Image を使用)
  • ドラッグ完了時に、矩形内にある「選択可能ユニット」をまとめて選択
  • Shiftキーを押しながらなら「追加選択」、押していなければ「選択リセット」

ユニット側には「自分が選択された/解除された」を受け取るシンプルなインターフェースを用意し、
DragSelect は「どのユニットが矩形内に入っているか」を判定するだけに集中させます。

前提:ユニット側のインターフェース

範囲選択する対象には、次のインターフェースを実装してもらいます。

using UnityEngine;

/// <summary>範囲選択の対象が実装すべきインターフェース</summary>
public interface ISelectable
{
    /// <summary>選択状態が変化したときに呼ばれる</summary>
    /// <param name="isSelected">true なら選択中、false なら非選択</param>
    void SetSelected(bool isSelected);

    /// <summary>選択判定に使うワールド座標上の位置を返す</summary>
    Vector3 GetSelectionPosition();
}

例えば、ユニットの見た目を変えるだけの簡単な実装はこんな感じです。

using UnityEngine;

/// <summary>シンプルな選択可能ユニットの例</summary>
[RequireComponent(typeof(Renderer))]
public class SimpleSelectableUnit : MonoBehaviour, ISelectable
{
    [SerializeField] private Color _normalColor = Color.white;
    [SerializeField] private Color _selectedColor = Color.green;

    private Renderer _renderer;
    private bool _isSelected;

    private void Awake()
    {
        _renderer = GetComponent<Renderer>();
        ApplyColor();
    }

    public void SetSelected(bool isSelected)
    {
        if (_isSelected == isSelected) return;
        _isSelected = isSelected;
        ApplyColor();
    }

    public Vector3 GetSelectionPosition()
    {
        // 基本は Transform の位置でOK
        return transform.position;
    }

    private void ApplyColor()
    {
        if (_renderer != null && _renderer.material != null)
        {
            _renderer.material.color = _isSelected ? _selectedColor : _normalColor;
        }
    }
}

このように「選ばれたらどうするか」はユニット側に任せて、
DragSelect は「どれが選ばれるか」の判定だけを担当します。

DragSelect 本体のフルコード

以下が、Unity6 で動作する DragSelect コンポーネントのフルコードです。
新しい Input System ではなく、標準の Input API を使っています(プロジェクト設定で有効にしておきましょう)。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// マウスドラッグで矩形を描き、その範囲にある ISelectable を選択するコンポーネント。
/// - 画面上に UI Image で矩形を表示
/// - ドラッグ終了時にワールド上のユニットを判定して選択
/// - Shift キー押しながらなら「追加選択」、押していなければ「新規選択」
///
/// ※このコンポーネントは、
///   - Canvas(Screen Space - Overlay 推奨)
///   - 矩形表示用の Image
/// を使って矩形を描画します。
/// </summary>
public class DragSelect : MonoBehaviour
{
    [Header("UI 設定")]
    [SerializeField]
    private Canvas _canvas; // Screen Space Overlay 推奨

    [SerializeField]
    private Image _selectionImage; // 矩形表示用の Image(Panel など)

    [Header("選択対象設定")]
    [SerializeField]
    private LayerMask _selectableLayerMask = ~0; 
    // 選択対象のレイヤー。Physics.Raycast や Overlap ではなく、
    // カメラからのスクリーン座標変換を使うため、ここではフィルタ用途(任意)。

    [SerializeField]
    private float _selectionRadius = 0.5f;
    // 各ユニットの「選択判定半径」。GetSelectionPosition() からの距離で判定。

    [Header("入力設定")]
    [SerializeField]
    private KeyCode _selectionKey = KeyCode.Mouse0; // 左クリックでドラッグ
    [SerializeField]
    private KeyCode _addSelectionModifierKey = KeyCode.LeftShift; // 追加選択用

    [Header("カメラ設定")]
    [SerializeField]
    private Camera _camera; // 未指定なら自動で Camera.main を使用

    // 内部状態
    private Vector2 _dragStartScreenPos;
    private Vector2 _dragEndScreenPos;
    private bool _isDragging;

    // 現在選択中のユニット
    private readonly HashSet<ISelectable> _currentSelection = new HashSet<ISelectable>();

    // シーン内に存在する ISelectable をキャッシュ(頻繁に Find しないため)
    private readonly List<ISelectable> _allSelectables = new List<ISelectable>();

    private void Awake()
    {
        if (_camera == null)
        {
            _camera = Camera.main;
        }

        if (_selectionImage != null)
        {
            _selectionImage.gameObject.SetActive(false);
        }

        CacheAllSelectables();
    }

    private void Update()
    {
        HandleInput();
    }

    /// <summary>
    /// シーン内の ISelectable をすべて探してキャッシュする
    /// </summary>
    private void CacheAllSelectables()
    {
        _allSelectables.Clear();

        // MonoBehaviour を継承している ISelectable 実装を全て取得
        var monoBehaviours = FindObjectsOfType<MonoBehaviour>();
        foreach (var mb in monoBehaviours)
        {
            if (mb is ISelectable selectable)
            {
                // レイヤーマスクでフィルタリング(任意)
                if (IsInLayerMask(mb.gameObject.layer, _selectableLayerMask))
                {
                    _allSelectables.Add(selectable);
                }
            }
        }
    }

    /// <summary>
    /// レイヤーがマスクに含まれているかチェック
    /// </summary>
    private bool IsInLayerMask(int layer, LayerMask mask)
    {
        return (mask.value & (1 << layer)) != 0;
    }

    /// <summary>
    /// マウス入力の処理
    /// </summary>
    private void HandleInput()
    {
        if (_camera == null || _canvas == null || _selectionImage == null)
        {
            return; // 必要な参照がなければ何もしない
        }

        // マウスボタン押下でドラッグ開始
        if (Input.GetKeyDown(_selectionKey))
        {
            _isDragging = true;
            _dragStartScreenPos = Input.mousePosition;
            _dragEndScreenPos = _dragStartScreenPos;

            BeginSelectionRect();
        }

        // ドラッグ中は矩形を更新
        if (_isDragging && Input.GetKey(_selectionKey))
        {
            _dragEndScreenPos = Input.mousePosition;
            UpdateSelectionRect();
        }

        // マウスボタンを離したらドラッグ終了
        if (_isDragging && Input.GetKeyUp(_selectionKey))
        {
            _isDragging = false;
            EndSelectionRect();

            bool addToSelection = Input.GetKey(_addSelectionModifierKey);
            ApplySelection(addToSelection);
        }
    }

    /// <summary>
    /// 矩形描画の開始処理
    /// </summary>
    private void BeginSelectionRect()
    {
        if (_selectionImage == null) return;

        _selectionImage.gameObject.SetActive(true);
        UpdateSelectionRect();
    }

    /// <summary>
    /// 矩形描画の更新処理
    /// </summary>
    private void UpdateSelectionRect()
    {
        if (_selectionImage == null || _canvas == null) return;

        // スクリーン座標からキャンバス座標へ変換
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            _canvas.transform as RectTransform,
            _dragStartScreenPos,
            _canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : _camera,
            out Vector2 startPos);

        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            _canvas.transform as RectTransform,
            _dragEndScreenPos,
            _canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : _camera,
            out Vector2 endPos);

        // 左下とサイズを計算
        Vector2 size = endPos - startPos;
        Vector2 center = startPos + size / 2f;

        RectTransform rectTransform = _selectionImage.rectTransform;
        rectTransform.anchoredPosition = center;
        rectTransform.sizeDelta = new Vector2(Mathf.Abs(size.x), Mathf.Abs(size.y));
    }

    /// <summary>
    /// 矩形描画の終了処理
    /// </summary>
    private void EndSelectionRect()
    {
        if (_selectionImage == null) return;

        _selectionImage.gameObject.SetActive(false);
    }

    /// <summary>
    /// ドラッグで描いた矩形内にある ISelectable を選択状態にする
    /// </summary>
    /// <param name="addToSelection">true なら既存の選択に追加、false なら選択をリセットしてから追加</param>
    private void ApplySelection(bool addToSelection)
    {
        if (!addToSelection)
        {
            ClearSelection();
        }

        // ドラッグ距離がほぼゼロなら「クリック扱い」にして 1 体だけ選択したい場合は、
        // ここで距離チェックして分岐しても良い。
        // 今回はそのまま矩形選択として扱う。

        Rect selectionRect = GetScreenRect(_dragStartScreenPos, _dragEndScreenPos);

        foreach (var selectable in _allSelectables)
        {
            if (selectable == null) continue;

            Vector3 worldPos = selectable.GetSelectionPosition();
            Vector3 screenPos = _camera.WorldToScreenPoint(worldPos);

            // カメラの前方にあるか(背面は除外)
            if (screenPos.z < 0f) continue;

            // スクリーン座標上で矩形内にあるかチェック
            if (selectionRect.Contains(screenPos))
            {
                // さらに「半径」での簡易チェック(カメラからの距離を考慮したい場合など)
                if (_selectionRadius > 0f)
                {
                    // ここでは簡易的に、矩形内に入っていれば OK とし、
                    // _selectionRadius は拡張用のパラメータとして用意しています。
                }

                AddToSelection(selectable);
            }
        }
    }

    /// <summary>
    /// スクリーン座標 2 点から Rect を作成
    /// </summary>
    private Rect GetScreenRect(Vector2 start, Vector2 end)
    {
        // 左下と右上を計算
        float xMin = Mathf.Min(start.x, end.x);
        float xMax = Mathf.Max(start.x, end.x);
        float yMin = Mathf.Min(start.y, end.y);
        float yMax = Mathf.Max(start.y, end.y);

        return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
    }

    /// <summary>
    /// 現在の選択をすべて解除
    /// </summary>
    private void ClearSelection()
    {
        foreach (var selectable in _currentSelection)
        {
            if (selectable != null)
            {
                selectable.SetSelected(false);
            }
        }
        _currentSelection.Clear();
    }

    /// <summary>
    /// 1 つの ISelectable を選択リストに追加
    /// </summary>
    private void AddToSelection(ISelectable selectable)
    {
        if (_currentSelection.Contains(selectable)) return;

        _currentSelection.Add(selectable);
        selectable.SetSelected(true);
    }

    /// <summary>
    /// 外部から現在の選択一覧を取得したいとき用
    /// </summary>
    public IReadOnlyCollection<ISelectable> GetCurrentSelection()
    {
        return _currentSelection;
    }

    /// <summary>
    /// シーン内の ISelectable 一覧を再スキャンする(ユニットが動的に増減するゲーム用)
    /// </summary>
    public void RefreshSelectableCache()
    {
        CacheAllSelectables();
    }
}

補足:矩形用 Image の簡単な設定例

  • Canvas(Render Mode: Screen Space – Overlay)を作成
  • Canvas の子に UI > Image を作成(名前は「SelectionImage」など)
  • 色は半透明の緑や青に設定(例: RGBA = (0, 1, 0, 0.2))
  • Raycast Target は OFF にしておく(選択操作をブロックしないため)

この Image を DragSelect の _selectionImage にアサインします。


使い方の手順

ここからは、具体的な使い方をステップごとに見ていきましょう。
例として「RTS風のユニット範囲選択」を想定します。

手順①:シーンに Canvas と SelectionImage を用意する

  1. Hierarchy で右クリック → UI → Canvas を作成
    • Render Mode を Screen Space – Overlay にする
  2. Canvas の子として UI → Image を作成し、名前を「SelectionImage」に変更
    • 色を半透明に(例: 緑: R=0, G=1, B=0, A=0.2)
    • Raycast Target のチェックを外す

手順②:DragSelect コンポーネントを配置する

  1. Empty GameObject を作成し、名前を「InputSystem」や「SelectionManager」などにする
  2. 上記 GameObject に DragSelect スクリプトを追加
  3. Inspector で以下を設定
    • Canvas:作成した Canvas をドラッグ&ドロップ
    • Selection Image:SelectionImage をドラッグ&ドロップ
    • Camera:メインカメラ(未設定なら自動で Camera.main)
    • Selectable Layer Mask:ユニット用のレイヤーを指定しておくと便利

手順③:ユニットに ISelectable 実装を付ける

プレイヤーユニット、敵ユニット、動くオブジェクトなど、「範囲選択の対象にしたい」オブジェクトには、
先ほどの SimpleSelectableUnit のように ISelectable を実装したコンポーネントを付けます。

  1. ユニット用の Prefab(例:「Unit」)を作成
  2. Prefab の Root に SimpleSelectableUnit を追加
  3. Renderer コンポーネントを持っていることを確認(MeshRenderer や SpriteRenderer など)
  4. 必要に応じて、ユニット用のレイヤー(例:「Unit」レイヤー)を作成し、
    DragSelect の _selectableLayerMask でそのレイヤーだけを選択

これで、ゲーム再生中にマウス左ドラッグで矩形を描くと、その範囲にあるユニットが色付きで選択されるようになります。
Shift キーを押しながらドラッグすれば、既存の選択にユニットを追加できます。

手順④:具体的な使用例

  • プレイヤーユニットの一括移動
    • DragSelect で選択されたユニット一覧を取得し、右クリック地点へ移動命令を出す
    • 移動処理は別コンポーネント(UnitMover など)に分離しておくときれいです
  • 敵ユニットの一括ターゲット指定
    • 選択中ユニットに対して「攻撃対象」を設定するコマンドを送る
  • 動く床・ギミックの一括オン/オフ
    • 床やギミックにも ISelectable を実装しておけば、範囲選択で一括制御が可能

メリットと応用

DragSelect をコンポーネントとして分離しておくと、

  • プレハブ管理が楽になる
    • ユニット側は「選択されたときの見た目/挙動」だけに集中できる
    • 選択ロジックは DragSelect に集約されるので、他のシーンでもそのまま使い回せる
  • レベルデザインがしやすい
    • レベルデザイナーは「ユニットに ISelectable 実装を付ける」だけで範囲選択対応にできる
    • 矩形の色や太さ、ドラッグ開始条件などは DragSelect の Inspector から調整可能
  • 責務が明確でテストしやすい
    • 「ドラッグ矩形の描画」と「ユニットの選択状態管理」が別コンポーネントに分かれているため、
      どこを修正すべきか迷いにくい

改造案:クリック時は単体選択にする

現在の実装では、ドラッグ距離が短くても矩形選択として扱っています。
「ほとんど動いていない場合は『クリック扱い』にして、一番近いユニットだけを選択したい」というケースも多いので、
そのための改造例を1つ紹介します。

DragSelect クラス内に、次のようなメソッドを追加してみてください。

/// <summary>
/// クリック(ほぼドラッグしていない)とみなせる距離かどうかを判定し、
/// 単体選択を行うサンプル。
/// </summary>
private void ApplyClickSelectionIfNeeded(float clickThreshold = 5f)
{
    float distance = Vector2.Distance(_dragStartScreenPos, _dragEndScreenPos);
    if (distance > clickThreshold)
    {
        // クリックではなくドラッグとみなす
        return;
    }

    // クリック位置に最も近いユニットを 1 体だけ選択
    ClearSelection();

    ISelectable closest = null;
    float closestSqrDist = float.MaxValue;

    foreach (var selectable in _allSelectables)
    {
        if (selectable == null) continue;

        Vector3 worldPos = selectable.GetSelectionPosition();
        Vector3 screenPos = _camera.WorldToScreenPoint(worldPos);
        if (screenPos.z < 0f) continue;

        float sqrDist = (new Vector2(screenPos.x, screenPos.y) - _dragStartScreenPos).sqrMagnitude;
        if (sqrDist < closestSqrDist)
        {
            closestSqrDist = sqrDist;
            closest = selectable;
        }
    }

    if (closest != null)
    {
        AddToSelection(closest);
    }
}

そして ApplySelection を呼ぶ直前で、

  • ドラッグ距離が短ければ ApplyClickSelectionIfNeeded() を呼ぶ
  • そうでなければ従来の矩形選択を行う

と分岐させれば、「クリックで単体選択 / ドラッグで範囲選択」という、RTS でよくある操作感に近づけられます。

このように、DragSelect を1つの責務にまとめておくと、
入力方式を変えたり、選択ルールを差し替えたりするのがとても楽になります。
巨大な God クラスにせず、小さなコンポーネントの組み合わせでゲームの操作系を組み立てていきましょう。