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 を用意する
- Hierarchy で右クリック → UI → Canvas を作成
- Render Mode を Screen Space – Overlay にする
- Canvas の子として UI → Image を作成し、名前を「SelectionImage」に変更
- 色を半透明に(例: 緑: R=0, G=1, B=0, A=0.2)
- Raycast Target のチェックを外す
手順②:DragSelect コンポーネントを配置する
- Empty GameObject を作成し、名前を「InputSystem」や「SelectionManager」などにする
- 上記 GameObject に
DragSelectスクリプトを追加 - Inspector で以下を設定
- Canvas:作成した Canvas をドラッグ&ドロップ
- Selection Image:SelectionImage をドラッグ&ドロップ
- Camera:メインカメラ(未設定なら自動で Camera.main)
- Selectable Layer Mask:ユニット用のレイヤーを指定しておくと便利
手順③:ユニットに ISelectable 実装を付ける
プレイヤーユニット、敵ユニット、動くオブジェクトなど、「範囲選択の対象にしたい」オブジェクトには、
先ほどの SimpleSelectableUnit のように ISelectable を実装したコンポーネントを付けます。
- ユニット用の Prefab(例:「Unit」)を作成
- Prefab の Root に
SimpleSelectableUnitを追加 - Renderer コンポーネントを持っていることを確認(MeshRenderer や SpriteRenderer など)
- 必要に応じて、ユニット用のレイヤー(例:「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 クラスにせず、小さなコンポーネントの組み合わせでゲームの操作系を組み立てていきましょう。
