Unity UI Toolkit でのタップ操作検出
初めに
UI Toolkit にてタップとかドラッグとかピンチとかを検出する方法です。 外部ライブラリに依存しています。
- Unity 6000.1.15f1
- R3 1.3.0
- VContainer 1.17.0
サンプルコード
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 ピンチ処理
}
}
}