Unityを触り始めた頃によくあるパターンとして、「とりあえず全部 Update() に書いてしまう」という実装があります。入力処理、移動処理、UI操作、エフェクト制御…すべてを1つの巨大スクリプトに押し込んでしまうと、後から読むときに「どこを触ればいいのか分からない」「ちょっと直しただけで別の機能が壊れた」という状態になりがちです。
そこでおすすめなのが、「やりたいこと1つにつき1コンポーネント」を意識した作り方です。今回の記事では、UIのパネルやウィンドウなどを「マウスで掴んでドラッグ移動できるようにする」ためのコンポーネント Draggable を用意して、ドラッグ移動という責務だけを切り出した実装にしてみましょう。
【Unity】UIをつかんでスイスイ動かす!「Draggable」コンポーネント
ここでは、親の UI 要素(RectTransform)をマウスでドラッグして動かすための Draggable コンポーネントを作ります。
「Controlノード」という表現はGodotに近いですが、Unityでは主に RectTransform を持つ UI オブジェクト(パネルやボタンなど)を対象にします。
フルコード
using UnityEngine;
using UnityEngine.EventSystems;
namespace Sample.UI
{
/// <summary>
/// マウスでUIをドラッグ移動できるようにするコンポーネント
/// 親のRectTransform(または指定したRectTransform)をドラッグ移動します。
/// </summary>
[DisallowMultipleComponent]
public class Draggable : MonoBehaviour,
IPointerDownHandler,
IPointerUpHandler,
IDragHandler
{
[Header("ドラッグ対象")]
[SerializeField]
private RectTransform targetRectTransform;
// 実際に動かすRectTransform
// 未指定の場合は、親のRectTransformを自動で使用します。
[Header("制約設定")]
[SerializeField]
private bool clampToCanvas = true;
// true の場合、ドラッグ対象をCanvasの範囲内に収めます
[SerializeField]
private bool useLocalPosition = true;
// true: anchoredPosition(ローカル座標)で移動
// false: position(ワールド座標)で移動
[Header("ドラッグ感度")]
[SerializeField]
[Tooltip("ドラッグ量に掛けるスケール。1で等倍。")]
private float dragSensitivity = 1f;
// 内部で使用する変数
private Canvas rootCanvas;
private RectTransform canvasRectTransform;
private Vector2 pointerOffset; // クリック位置とRectTransform中心との差
private Camera uiCamera;
private bool isDragging;
private void Awake()
{
// targetRectTransform が未指定なら、親の RectTransform を使う
if (targetRectTransform == null)
{
// 自身ではなく「親」を動かしたい場合
var parentRect = transform.parent as RectTransform;
if (parentRect != null)
{
targetRectTransform = parentRect;
}
else
{
// 親にRectTransformがない場合は、自身を動かす
targetRectTransform = GetComponent<RectTransform>();
}
}
// ルートCanvasを取得
rootCanvas = GetComponentInParent<Canvas>();
if (rootCanvas != null)
{
canvasRectTransform = rootCanvas.GetComponent<RectTransform>();
// Screen Space - Camera / World Space の場合はカメラを取得
if (rootCanvas.renderMode == RenderMode.ScreenSpaceCamera ||
rootCanvas.renderMode == RenderMode.WorldSpace)
{
uiCamera = rootCanvas.worldCamera;
}
}
}
/// <summary>
/// ポインタが押されたとき(マウスダウン/タッチ開始)
/// </summary>
public void OnPointerDown(PointerEventData eventData)
{
isDragging = true;
// クリック位置がRectTransform内のどこかを記録しておく
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
targetRectTransform,
eventData.position,
uiCamera,
out var localPoint))
{
// localPoint は targetRectTransform のローカル座標系でのクリック位置
// anchoredPosition とのオフセットを保存
pointerOffset = targetRectTransform.anchoredPosition - localPoint;
}
else
{
pointerOffset = Vector2.zero;
}
}
/// <summary>
/// ドラッグ中に呼ばれる
/// </summary>
public void OnDrag(PointerEventData eventData)
{
if (!isDragging || targetRectTransform == null)
{
return;
}
// スクリーン座標 → RectTransform ローカル座標に変換
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvasRectTransform != null ? canvasRectTransform : targetRectTransform,
eventData.position,
uiCamera,
out var localPoint))
{
return;
}
// 新しい座標を計算
Vector2 newAnchoredPos = localPoint * dragSensitivity + pointerOffset;
if (useLocalPosition)
{
// anchoredPosition を使って移動
targetRectTransform.anchoredPosition = newAnchoredPos;
}
else
{
// ワールド座標で動かしたい場合は、スクリーン座標から直接変換もできる
Vector3 worldPos;
if (RectTransformUtility.ScreenPointToWorldPointInRectangle(
targetRectTransform,
eventData.position,
uiCamera,
out worldPos))
{
targetRectTransform.position = worldPos;
}
}
// Canvas内に収めるオプション
if (clampToCanvas && canvasRectTransform != null)
{
ClampToCanvas();
}
}
/// <summary>
/// ポインタが離されたとき(マウスアップ/タッチ終了)
/// </summary>
public void OnPointerUp(PointerEventData eventData)
{
isDragging = false;
}
/// <summary>
/// targetRectTransform を Canvas の範囲内に収める
/// </summary>
private void ClampToCanvas()
{
if (targetRectTransform == null || canvasRectTransform == null)
{
return;
}
// RectTransform の現在のワールド四隅を取得
Vector3[] targetCorners = new Vector3[4];
Vector3[] canvasCorners = new Vector3[4];
targetRectTransform.GetWorldCorners(targetCorners);
canvasRectTransform.GetWorldCorners(canvasCorners);
// Canvasの矩形
float canvasLeft = canvasCorners[0].x;
float canvasRight = canvasCorners[2].x;
float canvasBottom = canvasCorners[0].y;
float canvasTop = canvasCorners[2].y;
// 対象Rectの矩形
float targetLeft = targetCorners[0].x;
float targetRight = targetCorners[2].x;
float targetBottom = targetCorners[0].y;
float targetTop = targetCorners[2].y;
Vector3 position = targetRectTransform.position;
// 左右のはみ出しを補正
if (targetLeft < canvasLeft)
{
float delta = canvasLeft - targetLeft;
position.x += delta;
}
else if (targetRight > canvasRight)
{
float delta = targetRight - canvasRight;
position.x -= delta;
}
// 上下のはみ出しを補正
if (targetBottom < canvasBottom)
{
float delta = canvasBottom - targetBottom;
position.y += delta;
}
else if (targetTop > canvasTop)
{
float delta = targetTop - canvasTop;
position.y -= delta;
}
targetRectTransform.position = position;
}
/// <summary>
/// 外部からドラッグの有効/無効を切り替えるAPI
/// 例: ポーズ中はドラッグ不可にするなど
/// </summary>
public void SetDraggable(bool enabled)
{
// コンポーネント自体を有効/無効にする
enabled = enabled;
if (!enabled)
{
isDragging = false;
}
}
}
}
使い方の手順
UIパネルやウィンドウをマウスでドラッグ移動できるようにする具体的な手順です。
- CanvasとUIオブジェクトを用意する
- Hierarchy で
UI > Canvasを作成します(既にある場合はそれを使ってOK)。 - Canvas の子として
UI > Panelを作成し、ウィンドウやメニューとして使う想定にします。
- Hierarchy で
- EventSystem をシーンに配置する
- UI をクリック/ドラッグするには EventSystem が必要です。
- メニューから
UI > Event Systemを追加します(通常、Canvasを作ると自動で追加されます)。
- Draggable コンポーネントを追加する
- 上記コードを
Draggable.csとしてAssetsフォルダ内に保存します。 - ドラッグしたい UI の「つまみ部分」にしたい GameObject(例: パネル上部のタイトルバー用 Image)を選択します。
- Inspector で
Add ComponentボタンからDraggableを追加します。 Target Rect Transformを空のままにしておくと、自動的に「親の RectTransform」がドラッグ対象になります。
→ タイトルバーを掴んだら親のウィンドウ全体が動く、という動作になります。
- 上記コードを
- 実行してドラッグを確認する
- 再生ボタンを押して Play モードに入ります。
- タイトルバー(Draggable を付けたオブジェクト)をマウスでクリックし、そのままドラッグしてみてください。
- ウィンドウ全体(親の RectTransform)がマウスに追従して移動すれば成功です。
具体的な使用例としては、次のようなものがあります。
- プレイヤーのインベントリウィンドウ
インベントリ画面の上部に「タイトルバー」用の Image を置き、そのオブジェクトにDraggableを付けると、ゲーム中にインベントリウィンドウを好きな位置に動かせます。 - 敵のステータス表示パネル(デバッグ用)
開発中に敵の情報を表示するデバッグUIを作り、Draggableを付けておくと、画面上で邪魔にならない位置に移動しながらテストできます。 - 動く設定メニュー
設定メニューのウィンドウにDraggableを付けることで、「ユーザーが好きな位置に配置できるUI」を簡単に実現できます。
メリットと応用
Draggable コンポーネントを用意しておくと、「ドラッグで動かしたいUI」にこのスクリプトを付けるだけで機能が完成します。プレハブとしてウィンドウを作っておけば、シーンをまたいでも同じ挙動を再利用できるので、レベルデザインやUIデザインがだいぶ楽になります。
- 巨大な UI 管理スクリプトに「ドラッグ処理」を書かなくてよい
- 「ドラッグできる」「ドラッグできない」をコンポーネントの有無で判別できる
- ウィンドウごとにドラッグ感度やキャンバス内制約の有無を切り替えられる
- プレハブ化したウィンドウを別プロジェクトに持って行っても、そのままドラッグ機能が付いてくる
特に、レベルデザインやUIモックアップの段階では「とりあえずドラッグで動かしたい」場面が多いので、こうした小さなコンポーネントを1つ持っておくとかなり捗ります。
応用として、例えば「ドラッグ終了時に位置を保存して、次回起動時に同じ位置に復元する」といった機能を追加することもできます。以下は、そのための簡単な改造案です。
/// <summary>
/// 現在位置をPlayerPrefsに保存するサンプル
/// (ウィンドウごとにキー名を変えると複数保存できます)
/// </summary>
private void SavePosition(string key)
{
if (targetRectTransform == null)
{
return;
}
Vector2 pos = targetRectTransform.anchoredPosition;
PlayerPrefs.SetFloat(key + "_x", pos.x);
PlayerPrefs.SetFloat(key + "_y", pos.y);
PlayerPrefs.Save();
}
このように、「ドラッグで動かす」という1つの責務だけを切り出したコンポーネントを作っておくと、後から機能追加や改造をする際も見通しが良くなります。
UI周りは肥大化しやすいので、こまめにコンポーネントを分けていく習慣をつけていきたいですね。
