Unity UI Toolkit でのタップ操作検出

初めに

UI Toolkit にてタップとかドラッグとかピンチとかを検出する方法です。 外部ライブラリに依存しています。

サンプルコード

using UnityEngine;

namespace Assets.Scenes
{
    public interface IScreenPointerAction
    {
        void PointerDown(int pointerID, Vector2 pos);
        void PointerMove(int pointerID, Vector2 pos);
        void PointerUp(int pointerID, Vector2 pos);
    }
}
using UnityEngine;
using UnityEngine.UIElements;
using VContainer;
using Assets.Scripts;

namespace Assets.Scenes
{
    public class ScreenPointerCollecter : PointerManipulator
    {
        readonly IScreenPointerAction pointerAction;

        [Inject]
        public ScreenPointerCollecter(IScreenPointerAction pointerAction)
        {
            this.pointerAction = pointerAction;
        }

        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)
        {
            target.CapturePointer(evt.pointerId);
            pointerAction.PointerDown(evt.GetPointerID(), FlipY(evt.position));
        }

        private void OnPointerMove(PointerMoveEvent evt)
        {
            if (!target.HasPointerCapture(evt.pointerId)) return;

            pointerAction.PointerMove(evt.GetPointerID(), FlipY(evt.position));
        }

        private void OnPointerUp(PointerUpEvent evt)
        {
            if (!target.HasPointerCapture(evt.pointerId)) return;

            pointerAction.PointerUp(evt.GetPointerID(), FlipY(evt.position));
            target.ReleasePointer(evt.pointerId);
        }

        static Vector2 FlipY(Vector2 position)
        {
            position.y = Screen.height - position.y;
            return position;
        }
    }
}
using R3;
using UnityEngine;

namespace Assets.Scenes
{
    public class ScreenPointer
    {
        public ScreenPointer(ref DisposableBag disposableBag)
        {
            pressed.AddTo(ref disposableBag);
            start.AddTo(ref disposableBag);
            current.AddTo(ref disposableBag);
            delta.AddTo(ref disposableBag);
            totalMove.AddTo(ref disposableBag);
            startTime.AddTo(ref disposableBag);
            currentTime.AddTo(ref disposableBag);
            tap.AddTo(ref disposableBag);
            tappingTime.AddTo(ref disposableBag);
            drag.AddTo(ref disposableBag);
        }

        public ReactiveProperty<bool> pressed { get; } = new();
        public bool Pressed
        {
            get => pressed.CurrentValue;
            private set => pressed.Value = value;
        }

        public ReactiveProperty<Vector2> start { get; } = new();
        public Vector2 Start
        {
            get => start.CurrentValue;
            private set => start.Value = value;
        }

        public ReactiveProperty<Vector2> current { get; } = new();
        public Vector2 Current
        {
            get => current.CurrentValue;
            private set => current.Value = value;
        }

        public ReactiveProperty<Vector2> delta { get; } = new();
        public Vector2 Delta
        {
            get => delta.CurrentValue;
            private set => delta.Value = value;
        }

        public ReactiveProperty<float> totalMove { get; } = new();
        public float TotalMove
        {
            get => totalMove.CurrentValue;
            private set => totalMove.Value = value;
        }

        public ReactiveProperty<float> startTime { get; } = new();
        public float StartTime
        {
            get => startTime.CurrentValue;
            private set => startTime.Value = value;
        }

        public ReactiveProperty<float> currentTime { get; } = new();
        public float CurrentTime
        {
            get => currentTime.CurrentValue;
            private set => currentTime.Value = value;
        }

        public float PressedDistance
        {
            get => (Current - Start).magnitude;
        }

        public float PressedTime
        {
            get => CurrentTime - StartTime;
        }

        public ReactiveProperty<Vector2> tap { get; } = new();
        public Vector2 Tap
        {
            get => tap.CurrentValue;
            private set => tap.Value = value;
        }

        public ReactiveProperty<float> tappingTime { get; } = new();
        public float TappingTime
        {
            get => tappingTime.CurrentValue;
            private set => tappingTime.Value = value;
        }

        public ReactiveProperty<Vector2> drag { get; } = new();
        public Vector2 Drag
        {
            get => drag.CurrentValue;
            private set => drag.Value = value;
        }

        public ReactiveProperty<bool> isDragging { get; } = new();
        public bool IsDragging
        {
            get => isDragging.CurrentValue;
            private set => isDragging.Value = value;
        }

        // ドラッグ開始とみなす距離
        const float dragLength = 5f;

        public void PointerDown(Vector2 pos)
        {
            Start = pos;
            Current = pos;
            Delta = Vector2.zero;
            TotalMove = 0f;

            StartTime = Time.time;
            CurrentTime = Time.time;

            Pressed = true;
        }

        public void PointerMove(Vector2 pos)
        {
            Delta = pos - Current;
            Current = pos;
            TotalMove += Delta.magnitude;

            CurrentTime = Time.time;

            if (dragLength < TotalMove)
            {
                Drag = Delta;

                if (!IsDragging) IsDragging = true;
            }
        }

        public void PointerUp(Vector2 pos)
        {
            PointerMove(pos);

            Pressed = false;

            if (TotalMove < dragLength)
            {
                Tap = Current;
                TappingTime = CurrentTime - StartTime;
            }

            if (IsDragging) IsDragging = false;
        }
    }
}
using System;
using UnityEngine;
using R3;

namespace Assets.Scenes
{
    public class ScreenPointerDataSource : IScreenPointerAction, IDisposable
    {
        public ScreenPointerDataSource()
        {
            Datas = new[]
            {
                new ScreenPointer(ref disposableBag),
                new ScreenPointer(ref disposableBag),
            };

            Observable.Merge(Datas[0].current, Datas[1].current)
                .Where(_ => Datas[0].Pressed && Datas[1].Pressed)
                .Subscribe(_ => Pinch = (Datas[0].Current - Datas[1].Current).magnitude)
                .AddTo(ref disposableBag);
        }

        public ScreenPointer[] Datas {  get; private set; }

        public ReactiveProperty<float> pinch { get; } = new();
        public float Pinch
        {
            get => pinch.CurrentValue;
            private set => pinch.Value = value;
        }

        DisposableBag disposableBag = new();

        public void Dispose()
        {
            disposableBag.Dispose();
        }

        public void PointerDown(int pointerID, Vector2 pos)
        {
            if (pointerID < 0 || Datas.Length <= pointerID) return;
            Datas[pointerID].PointerDown(pos);
        }

        public void PointerMove(int pointerID, Vector2 pos)
        {
            if (pointerID < 0 || Datas.Length <= pointerID) return;
            Datas[pointerID].PointerMove(pos);
        }

        public void PointerUp(int pointerID, Vector2 pos)
        {
            if (pointerID < 0 || Datas.Length <= pointerID) return;
            Datas[pointerID].PointerUp(pos);
        }
    }
}
using System;
using System.Collections.Generic;

namespace Assets.Scenes
{
    public class ScreenPointerFactory: IDisposable
    {
        public enum AreaType
        {
            Main,
            Overlay,
        }

        public ScreenPointerFactory()
        {
            foreach (AreaType areaType in Enum.GetValues(typeof(AreaType)))
            {
                dataSources[areaType] = new ScreenPointerDataSource();
                collecters[areaType] = new ScreenPointerCollecter(dataSources[areaType]);
            }
        }

        readonly Dictionary<AreaType, ScreenPointerCollecter> collecters = new();
        readonly Dictionary<AreaType, ScreenPointerDataSource> dataSources = new();

        public ScreenPointerCollecter GetCollecter(AreaType areaType) => collecters[areaType];

        public ScreenPointerDataSource GetDataSource(AreaType areaType) => dataSources[areaType];

        public void Dispose()
        {
            collecters.Clear();
            foreach(var dataSource in dataSources.Values)
            {
                dataSource.Dispose();
            }
            dataSources.Clear();
        }
    }
}
using VContainer;
using VContainer.Unity;
using Assets.Scenes;

public class MySceneLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<ScreenPointerFactory>(Lifetime.Singleton);
        builder.RegisterEntryPoint<MySceneEntryPoint>();
    }
}
using UnityEngine;
using UnityEngine.UIElements;
using Assets.Scenes;
using VContainer;

public class MyUI : MonoBehaviour
{
    public UIDocument UIDocument;

    [Inject]
    public void Construct(ScreenPointerFactory screenPointerFactory)
    {
        var screenPointerCollecter = screenPointerFactory.GetCollecter(ScreenPointerFactory.AreaType.Main);

        var root = UIDocument.rootVisualElement.Q("root");

        // タップ操作可能な場所にタップ処理を設定
        root.Q("header").AddManipulator(screenPointerCollecter);
        root.Q("main").AddManipulator(screenPointerCollecter);
    }
}
using System;
using System.Linq;
using R3;
using UnityEngine;
using VContainer;
using VContainer.Unity;

namespace Assets.Scenes
{
    class MySceneEntryPoint: IStartable, IDisposable
    {
        readonly ScreenPointerDataSource screenPointerDataSource;

        [Inject]
        public MySceneEntryPoint(ScreenPointerFactory screenPointerFactory)
        {
            screenPointerDataSource = screenPointerFactory.GetDataSource(ScreenPointerFactory.AreaType.Main);
        }

        DisposableBag disposableBag = new();

        public void Start()
        {
            screenPointerDataSource.Datas[0].tap
                .Skip(1) // 初期値を無視
                .Subscribe(OnTap)
                .AddTo(ref disposableBag);

            screenPointerDataSource.pinch
                .Subscribe(OnPinch)
                .AddTo(ref disposableBag);
        }

        public void Dispose()
        {
            disposableBag.Dispose();
        }

        void OnTap(Vector2 screenPos)
        {
            // TODO タップ処理
        }

        void OnPinch(float pinch)
        {
            // TODO ピンチ処理
        }
    }
}

top

その他の投稿
20250812-01 Unity UI Toolkit でマップ画面 with RenderTexture
20250811-01 Unity UI Toolkit での座標変換
20250721-01 Unityでカメラが平行投影の場合にScreenToWorldPointがズレる
20250712-01 Unity既存プロジェクトにURP追加
20250320-01 Unity のナビゲーションシステム 追記
20241013-01 Google Play で開発者情報が公開される件
20240824-01 Unity UI Toolkit カスタムコントロールサンプル
20240720-01 Unity NavMesh サンプル
20240529-02 Unity ECS の Android ビルドでエラー
20240529-01 Unity ECS で、Prefab を使用した場合に Android ビルドでエラー