【Unity】【UI Toolkit】マウス操作やジェスチャに関する処理を切り離して実装できるManipulatorの使い方

UnityのUI Toolkitでマウス操作やジェスチャに関する処理を切り離して実装できるManipulatorの使い方についてまとめました。

Unity 2022.2.19

はじめに

UI Toolkitでは、以下のようにマウスなどのイベントのコールバックをUIに登録することで、イベントに応じて任意の処理を実行できます。

using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleElement : VisualElement
{
    public ExampleElement()
    {
        RegisterCallback<ClickEvent>(OnClick, TrickleDown.TrickleDown);
    }

    private void OnClick(ClickEvent evt)
    {
        if (evt.propagationPhase != PropagationPhase.AtTarget)
            return;

        Debug.Log("Clicked");
    }
}

しかしながら、たとえばドラッグ&ドロップなどのように、イベントに対して複雑なハンドリングを行う必要がある場合、これを全てUIのコードに書くと煩雑になり可読性が低下しがちです。

このようなケースでは、Manipulatorを使うことでイベントに関する処理を切り出すことができます。

Clickableマニピュレータ

UI Toolkitでは、このManipulatorがあらかじめいくつか用意されています。
代表的なManipulatorにClickableマニピュレータがあります。
これを使うと、クリックに関する処理を以下のように書けるようになります。

using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleElement : VisualElement
{
    public ExampleElement()
    {
        var clickable = new Clickable(() => Debug.Log("Clicked"));
        this.AddManipulator(clickable);

        // Manipulatorを削除する場合は以下のようにする
        //this.RemoveManipulator();
    }
}

独自のManipulatorを作る

独自のManipulatorを作ることもできます。
以下はドラッグにより対象のUIを移動するManipulatorです。

using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleManipulator : PointerManipulator // PointerManipulatorを継承する
{
    private Vector3 _elementStartPosition;
    private Vector3 _pointerStartPosition;

    public ExampleManipulator(VisualElement target)
    {
        // 操作対象のVisualElementがあればtargetに入れる
        this.target = target;

        // マウスの左ボタンだけ反応するようにManipulatorActivationFilterを追加する
        activators.Add(new ManipulatorActivationFilter
        {
            button = MouseButton.LeftMouse
        });
    }

    protected override void RegisterCallbacksOnTarget()
    {
        // 各種ポインターイベントを登録
        target.RegisterCallback<PointerDownEvent>(OnPointerDown);
        target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
        target.RegisterCallback<PointerUpEvent>(OnPointerUp);
    }

    protected override void UnregisterCallbacksFromTarget()
    {
        target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
        target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
        target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
    }

    // ポインター押下時の処理
    private void OnPointerDown(PointerDownEvent evt)
    {
        // コンストラクタで登録したactivatorsにイベントがマッチすれば(今回の場合左クリックだったら)処理を通す
        if (!CanStartManipulation(evt))
            return;

        _pointerStartPosition = evt.position;
        _elementStartPosition = target.transform.position;
        target.CapturePointer(evt.pointerId);
    }

    // ポインター移動時の処理
    private void OnPointerMove(PointerMoveEvent evt)
    {
        if (!target.HasPointerCapture(evt.pointerId))
            return;

        // ポインターの位置にUIを移動する
        var pointerDelta = evt.position - _pointerStartPosition;
        target.transform.position = _elementStartPosition + pointerDelta;
    }

    // ポインターを話した時の理
    private void OnPointerUp(PointerUpEvent evt)
    {
        if (!CanStopManipulation(evt))
            return;

        if (target.HasPointerCapture(evt.pointerId))
            target.ReleasePointer(evt.pointerId);
    }
}

説明はコメントに書いた通りです。

対象のUIをドラッグした場所に移動できるようにしています。

独自のManipulatorの動作確認

最後に、前節で作成した独自のManipulatorの動作確認を行います。

まずUI Builderで、下図のように一つだけUIを配置したレイアウトを作成します。

レイアウトを作成

UXMLは以下の通りです。

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
    <ui:VisualElement name="circle" style="flex-grow: 0; background-color: rgb(255, 0, 0); width: 50px; height: 50px; border-top-left-radius: 25px; border-bottom-left-radius: 25px; border-top-right-radius: 25px; border-bottom-right-radius: 25px;" />
</ui:UXML>

次にこれを表示するためのEditorWindowを作成します。

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleWindow : EditorWindow
{
    [SerializeField] private VisualTreeAsset visualTreeAsset;

    private void CreateGUI()
    {
        visualTreeAsset.CloneTree(rootVisualElement);
        var circle = rootVisualElement.Q<VisualElement>("circle");
        
        // circleにExampleManipulatorを追加する
        var _ = new ExampleManipulator(circle);
    }

    [MenuItem("Window/Example")]
    private static void Open()
    {
        GetWindow<ExampleWindow>();
    }
}

レイアウトのcircleオブジェクトに対して、前節で作成したExampleManipulatorを追加しています。

あとはこのEditorWindowのスクリプトのInspectorから、visualTreeAssetに先ほどのUXMLファイルをアサインし、Window > Exampleからウィンドウを開くと以下の結果が得られます。

結果

独自のManipulatorを使ってドラッグで移動できていることを確認できました。

参考

docs.unity.cn