【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

【Unity】【UI Toolkit】Visual Elementのマウスによるクリック判定・範囲を制御する

UnityのUI ToolkitでVisual Elementのマウスによるクリック判定・範囲を制御する方法です。

Unity 2022.2.19

マウスイベントに反応しないようにする

VisualElement.pickingModePickingMode.Ignoreに設定すると、そのVisualElementはマウスイベントに反応しなくなります。
この挙動を確かめるために以下のように、マウスイベントに反応した場合にログ出力を行う、PickingModeIgnoreVisualElementを作成します。

using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleElement : VisualElement
{
    public ExampleElement()
    {
        // マウスイベントに反応しないようにする
        pickingMode = PickingMode.Ignore;
    }

    protected override void ExecuteDefaultActionAtTarget(EventBase evt)
    {
        base.ExecuteDefaultActionAtTarget(evt);

        // クリックイベントが来たらログを出力
        if (evt.eventTypeId == MouseDownEvent.TypeId())
            Debug.Log("Mouse Down");
    }
}

これを以下のように適当なEditorWindowに表示します。
わかりやすいように赤い色をつけています。

using UnityEditor;
using UnityEngine;

public sealed class ExampleWindow : EditorWindow
{
    private void OnEnable()
    {
        var element = new ExampleElement();
        element.style.backgroundColor = Color.red;
        element.style.width = 100;
        element.style.height = 100;
        rootVisualElement.Add(element);
    }

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

Window > Exampleからウィンドウを開き、赤いVisual Elementをクリックすると、クリックイベントに反応しないことを確認できます。

反応しない

マウスイベントに反応する範囲を指定する

VisualElement.ContainsPointをオーバーライドすると、マウスイベントに反応する範囲をカスタムすることができます。
以下は、Paddingを除いた領域(ContentRect)のみマウスイベントに反応するVisualElementの例です。

using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleElement : VisualElement
{
    public ExampleElement()
    {
        style.paddingLeft = 25;
        style.paddingRight = 25;
        style.paddingTop = 25;
        style.paddingBottom = 25;
    }

    protected override void ExecuteDefaultActionAtTarget(EventBase evt)
    {
        base.ExecuteDefaultActionAtTarget(evt);

        // クリックイベントが来たらログを出力
        if (evt.eventTypeId == MouseDownEvent.TypeId())
            Debug.Log("Mouse Down");
    }

    public override bool ContainsPoint(Vector2 localPoint)
    {
        // contentRect (paddingを除いた領域) のみをクリック判定に含める
        return contentRect.Contains(localPoint);
    }
}

Window > Exampleからウィンドウを開き、赤いVisual Elementをクリックすると、Paddingを除いた中央に近い部分のみクリックに反応することを確認できます。

参考

docs.unity3d.com

【Unity】【UI Toolkit】独自のVisual ElementにDefault Actionを使ってイベントターゲットになった時の処理を定義する

UnityのUI Toolkitで独自のVisual ElementにDefault Actionを使ってイベントターゲットになった時の処理を定義する方法です。

  • Default Actionとは?
  • Default Actionの定義方法
  • ターゲットフェーズでDefault Actionを実行する
  • 参考
続きを読む