UIをドラッグ&ドロップしよう:マウスのイベントを受け取る【Unityスタートガイド9-7】

カードにマウスを乗せると大きくなり、ドラッグして左の置き場所に置く。もう一度つかんで右の置き場所の上を通り、外で離すと左の置き場所に戻る

持ち物をドラッグして装備欄に入れる、手札のカードをつかんで場に出す・・・。ボタンを押すだけでなく、UIを「つかんで動かす」操作が欲しくなることがありますよね。UnityのUIでは、スクリプトにイベントのインターフェースを書き足すだけで、マウスを乗せた・つかんだ・離したを受け取れます。今回は、それを使ってUIのドラッグ&ドロップを作ってみたいと思います!

目次

この記事で学べること

  • インターフェースとは何か(継承との違い)
  • マウスを乗せた・外したを受け取る方法(IPointerEnterHandler・IPointerExitHandler)
  • UIをドラッグして動かす方法(IBeginDragHandler・IDragHandler・IEndDragHandler)
  • 置き場所で受け取る方法(IDropHandler)と、外で離したら元に戻す方法
  • ドラッグ中にCanvas Groupの「Blocks Raycasts」を切る理由

前提知識

プロジェクトの確認

  • Unityのバージョンは6.3 LTS
  • テンプレートはUniversal 2D(3Dでも同じように出来ます)
  • HierarchyにCanvasとEventSystemがある
    • EventSystemには、Input System UI Input Moduleが付いている

UIのイベントはどうやって受け取る?

ボタンを押したときは、Buttonコンポーネントの「On Click ()」に処理を登録しました。でも、ImageにはOn Clickのような欄がありません。

UIのマウス操作は、EventSystemが調べて、マウスの下にあるUIに「乗ったよ」「つかまれたよ」と知らせてくれます。その知らせを受け取るための印が、インターフェースです。

public class HoverScale : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
  • MonoBehaviourの後ろに、,で区切ってインターフェースの名前を並べる
  • インターフェースを書いたら、そのインターフェースが決めているメソッド(OnPointerEnterなど)を必ずpublicで書く
  • EventSystemは、そのメソッドを持っているUIにだけ知らせてくれる
継承とインターフェースの違い
  • 継承(6-14):親クラスは1つだけ。親の変数や処理の中身も受け継ぐ
  • インターフェース:いくつでも付けられる。「このメソッドを持っています」という約束だけで、中身は自分で書く

よく使うインターフェースは次のとおりです。どれもusing UnityEngine.EventSystems;が必要です。

インターフェース呼ばれるメソッドいつ呼ばれる?
IPointerEnterHandlerOnPointerEnterマウスが乗ったとき
IPointerExitHandlerOnPointerExitマウスが外れたとき
IPointerDownHandlerOnPointerDownボタンを押したとき
IPointerUpHandlerOnPointerUpボタンを離したとき
IPointerClickHandlerOnPointerClickクリックしたとき(押して、同じ場所で離した)
IBeginDragHandlerOnBeginDragドラッグを始めたとき
IDragHandlerOnDragドラッグ中、マウスが動くたび
IEndDragHandlerOnEndDragドラッグをやめたとき
IDropHandlerOnDropドラッグしてきたものを、自分の上で離されたとき

画面を用意しよう

  • Canvasの中に、UI>Image を1つ作り、名前を「Card」にする
    • Width:200、Height:280
    • Pos Y:-300(画面の下の方)
    • 子にText – TextMeshProを作り、「カード」と入れる(なくてもOK)
  • Canvasの中に、UI>Image を2つ作り、名前を「DropZoneLeft」「DropZoneRight」にする
    • Width:240、Height:320
    • Pos X:-300 と 300、Pos Y:150
  • HierarchyでCardを一番下(DropZoneより下)に移動する
    • Hierarchyで下にあるものほど、画面では手前に表示される
HierarchyのCanvasの下にDropZoneLeft・DropZoneRight・Card(子にText (TMP))が並び、Gameビューでは上に置き場所が2つ、下にカードがある

マウスを乗せたら大きくしよう

まずは一番かんたんな、マウスを乗せたら大きくなるスクリプトです。

using UnityEngine;
using UnityEngine.EventSystems;

// マウスを乗せると少し大きくなる
public class HoverScale : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    [SerializeField] private float hoverScale = 1.1f;

    public void OnPointerEnter(PointerEventData eventData)
    {
        transform.localScale = Vector3.one * hoverScale;
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        transform.localScale = Vector3.one;
    }
}
  • using UnityEngine.EventSystems;
    • イベントのインターフェースを使うために必要
  • PointerEventData eventData
    • マウスの位置や、どのボタンが押されたかなど、イベントの情報が入っている

CardにHoverScaleを付けて再生し、マウスを乗せてみてください。少し大きくなって、外すと元に戻れば成功!

カードにマウスを乗せると少し大きくなり、外すと元の大きさに戻る

何も起きないときは、ImageのRaycast Targetにチェックが入っているか確認しましょう。チェックを外したUIには、EventSystemが知らせてくれません。

ドラッグして動かそう

次は、カードをつかんで動かせるようにします。

  • CardにAdd Component>Canvas Group を付ける(理由はあとで説明します)
  • 次のスクリプトを作って、Cardに付ける
using UnityEngine;
using UnityEngine.EventSystems;

// ドラッグで動かせる UI。置き場所以外で離したら元の位置に戻る
[RequireComponent(typeof(CanvasGroup))]
public class DragItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    private RectTransform rectTransform;
    private CanvasGroup canvasGroup;
    private Canvas canvas;
    private Vector2 startPosition;
    private bool isDropped;

    private void Awake()
    {
        rectTransform = GetComponent<RectTransform>();
        canvasGroup = GetComponent<CanvasGroup>();
        canvas = GetComponentInParent<Canvas>();
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        startPosition = rectTransform.anchoredPosition;
        isDropped = false;
        canvasGroup.blocksRaycasts = false; // 自分の下にある置き場所に、マウスが届くようにする
        transform.SetAsLastSibling();       // ほかの UI より手前に表示する
    }

    public void OnDrag(PointerEventData eventData)
    {
        // マウスが動いた分だけ動かす(Canvas の拡大率で割る)
        rectTransform.anchoredPosition += eventData.delta / canvas.scaleFactor;
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        canvasGroup.blocksRaycasts = true;
        if (!isDropped)
        {
            rectTransform.anchoredPosition = startPosition; // 置き場所以外なら元に戻す
        }
    }

    // 置き場所(DropZone)から呼ばれる
    public void PlaceAt(RectTransform zone)
    {
        isDropped = true;
        rectTransform.position = zone.position;
    }
}
  • [RequireComponent(typeof(CanvasGroup))]
    • このスクリプトを付けると、Canvas Groupも自動で付く(付け忘れ防止)
  • OnBeginDrag
    • つかんだときの位置を覚えておく。SetAsLastSiblingで、Hierarchyの一番下(=一番手前)に移す
  • OnDrag
    • eventData.deltaは、前のフレームからマウスが動いた量(ピクセル)
    • Canvas ScalerのUI Scale ModeをScale With Screen Sizeにしていると、UIは画面に合わせて拡大・縮小される。canvas.scaleFactor(拡大率)で割って、マウスとぴったり同じだけ動くようにする
    • 作ったばかりのCanvas(Constant Pixel Size)では拡大率は1なので、割っても変わらない。あとで設定を変えてもずれないように、最初から割っておく
  • OnEndDrag
    • 置き場所に置かれなかったら、覚えておいた位置に戻す
  • PlaceAt
    • このあと作る置き場所から呼ばれる。置かれた印(isDropped)を付けて、置き場所の真ん中に動かす
CardのInspector。Image、Hover Scale、Canvas Group、Drag Itemが付いている

Blocks Raycastsを切る理由

ドラッグ中のカードは、ちょうどマウスの真下にあります。そのままだと、EventSystemが「マウスの下にあるのはカード」と判断して、カードの下にある置き場所には何も知らせてくれません。

Canvas GroupのBlocks Raycastsをfalseにすると、そのUIは(子も含めて)マウスに反応しなくなり、下にある置き場所に知らせが届くようになります。離したらtrueに戻して、またつかめるようにしています。

スライム

これを知らなくて、「置き場所のOnDropが呼ばれない!」ってずっと悩んでました・・・

フェアリー

ドラッグ&ドロップで一番つまずくところだよね。「つかんでいる間は、マウスを下に通す」って覚えておこう!

置き場所で受け取ろう

最後に、置き場所のスクリプトです。ドラッグ中に上に来たら光って、離されたらカードを受け取ります。

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

// ドラッグしてきた UI を受け取る置き場所
public class DropZone : MonoBehaviour, IDropHandler, IPointerEnterHandler, IPointerExitHandler
{
    [SerializeField] private Color normalColor = new Color(1f, 1f, 1f, 0.3f);
    [SerializeField] private Color highlightColor = new Color(1f, 0.9f, 0.3f, 0.6f);

    private Image image;

    private void Awake()
    {
        image = GetComponent<Image>();
        image.color = normalColor;
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        // 何かをドラッグしているときだけ光らせる
        if (eventData.dragging)
        {
            image.color = highlightColor;
        }
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        image.color = normalColor;
    }

    public void OnDrop(PointerEventData eventData)
    {
        image.color = normalColor;

        // ドラッグしてきたものが DragItem なら、ここに置く
        if (eventData.pointerDrag == null)
        {
            return;
        }
        DragItem item = eventData.pointerDrag.GetComponent<DragItem>();
        if (item == null)
        {
            return;
        }
        item.PlaceAt((RectTransform)transform);
        Debug.Log($"{item.name}を{name}に置いた!");
    }
}
  • eventData.dragging
    • 何かをドラッグしている最中ならtrue。ただマウスを乗せただけでは光らない
  • eventData.pointerDrag
    • 今ドラッグされているGameObject。ここからDragItemを取り出す
  • OnDropとOnEndDragの順番
    • 離したときは、置き場所のOnDropが先、カードのOnEndDragがあとに呼ばれる
    • なので、OnDropでPlaceAt(isDroppedをtrue)→ OnEndDragで「置かれていないときだけ戻す」がうまくいく

DropZoneLeftとDropZoneRightの両方に、DropZoneを付けましょう。

置き場所を増やしたいときは、ImageにDropZoneを付けるだけでOK。置き場所とカードは、おたがいの数を知らなくても動くので、手札やインベントリのようにたくさん並べても、スクリプトはこのままで使えます。

DropZoneLeftのInspector。ImageとDrop Zoneが付き、Normal Colorは白の半透明、Highlight Colorは黄色

動かして確認してみよう!

再生して、次のことを試してみてください。

左の置き場所に置くとConsoleに「CardをDropZoneLeftに置いた!」が出る。右の置き場所の上を通って外で離すと、左の置き場所に戻る
  • マウスを乗せると、カードが少し大きくなる
  • ドラッグすると、カードがマウスにぴったりついてくる
  • 置き場所の上に来ると、置き場所が黄色く光る
  • 置き場所の上で離すと、置き場所の真ん中に置かれる。Consoleに「CardをDropZoneLeftに置いた!」
  • 置き場所の外で離すと、つかむ前の位置に戻る

置いたカードをもう一度つかんで、別の置き場所に移すことも出来ます。外で離したときは、最後に置いてあった場所に戻ります。

2Dのゲームで、UIではなくSpriteRendererのオブジェクトをドラッグしたいときは、Main CameraにPhysics 2D Raycasterを付け、オブジェクトにCollider 2Dを付けると、同じインターフェースでイベントを受け取れます。

よくあるエラーと対処法

マウスを乗せても、ドラッグしても何も起きない

  • 原因:シーンにEventSystemがない。またはImageのRaycast Targetのチェックが外れている
  • 対処法:Hierarchyに EventSystem があるか確認する(9-1)。ImageのRaycast Targetにチェックを入れる

置き場所のOnDropが呼ばれない

  • 原因:ドラッグ中もカードのBlocks Raycastsがtrueのままで、カードがマウスをさえぎっている
  • 対処法:OnBeginDragでcanvasGroup.blocksRaycasts = falseにする

カードがマウスより速く(遅く)動く

  • 原因:eventData.deltaをそのまま足している。Canvas ScalerがScale With Screen Sizeのときは、画面の大きさによってずれる
  • 対処法:canvas.scaleFactorで割る

カードが置き場所の後ろに隠れる

  • 原因:Hierarchyで、カードが置き場所より上にある(上にあるものほど奥に表示される)
  • 対処法:OnBeginDragでSetAsLastSibling()を呼ぶ

エラー:’DragItem’ does not implement interface member ‘IDragHandler.OnDrag(PointerEventData)’

  • 原因:インターフェースを書いたのに、そのメソッドを書いていない。またはpublicが付いていない
  • 対処法:public void OnDrag(PointerEventData eventData)を書く

まとめ

  • UIのマウス操作は、EventSystemが知らせてくれる。受け取るにはイベントのインターフェースを付ける
  • インターフェースはいくつでも付けられる。決められたメソッドをpublicで書く
  • ドラッグは、OnBeginDrag・OnDrag・OnEndDragの3つ。動かす量はeventData.delta / canvas.scaleFactor
  • ドラッグ中は、Canvas GroupのBlocks Raycastsを切って、下の置き場所にマウスを通す
  • 置き場所はIDropHandlerで受け取る。OnDropはOnEndDragより先に呼ばれる

インターフェースをもっと詳しく知りたい人は、UIを操作するためのイベントインターフェースも参考にしてください。

次のステップ

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次