在现代 Unity3D 中捏合和其他多指手势?
Pinch, and other multi-finger gestures, in modern Unity3D?
在现代 Unity3D 中,我们使用 IPointerDownHandler 系列调用。
关于 IPointerDownHandler
调用系列,
public class FingerMove:MonoBehaviour, IPointerDownHandler...
{
public void OnPointerDown (PointerEventData data)
{
当然他们很棒
用于处理单点触摸。
但是你如何严肃地处理多次触摸?
您可以自己“手动完成所有操作”来跟踪触摸,但 Unity 会希望您为如此绝对基本的事情执行此操作似乎令人难以置信。 (我的意思是 - 它是一个游戏引擎。当然,我也可以自己编写所有渲染和物理!)
这是一个基本上是“牛仔编程”的例子,只是手工完成,没有软件工程。什么是真正的解决方案?
//
// example of programming a pinch (as well as swipes) using modern Unity
//
// here we are forced to track "by hand" in your own code
// how many fingers are down and which
// fingers belong to you etc etc:
//
// pedagogic example code:
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using UnityEngine.EventSystems;
public class FingerMove:MonoBehaviour,
IPointerDownHandler, IDragHandler, IPointerUpHandler
{
// these three for the ordinary one-finger-only drag
private Vector2 prevPoint;
private Vector2 newPoint;
private Vector2 screenTravel;
// and this one is the ordinary one-finger-only drag
private int currentMainFinger = -1;
// and this for the (strictly second finger only) drag...
private int currentSecondFinger = -1;
private Vector2 posA;
private Vector2 posB;
private float previousDistance = -1f;
private float distance;
private float pinchDelta = 0f;
public void OnPointerDown (PointerEventData data)
{
if (currentMainFinger == -1)
{
// this is the NEW currentMainFinger
currentMainFinger = data.pointerId;
prevPoint = data.position;
// and for the drag (if it becomes used)...
posA = data.position;
return;
}
if (currentSecondFinger == -1)
{
// this is the NEW currentSecondFinger
currentSecondFinger = data.pointerId;
posB = data.position;
figureDelta();
previousDistance = distance;
return;
}
Debug.Log("third+ finger! (ignore)");
}
public void OnDrag (PointerEventData data)
{
// handle single-finger moves (swipes, drawing etc) only:
if ( currentMainFinger == data.pointerId )
{
newPoint = data.position;
screenTravel = newPoint - prevPoint;
prevPoint = newPoint;
if (currentSecondFinger == -1)
{
Debug.Log("NO 2f");
_processSwipe(); // handle it your way
}
else
{
}
// and for two-finger if it becomes used next frame
// or is already being used...
posA = data.position;
}
if (currentSecondFinger == -1) return;
// handle two-finger (eg, pinch, rotate etc)...
if ( currentMainFinger == data.pointerId ) posA = data.position;
if ( currentSecondFinger == data.pointerId ) posB = data.position;
figureDelta();
pinchDelta = distance - previousDistance;
previousDistance = distance;
_processPinch(); // handle it your way
}
private void figureDelta()
{
// when/if two touches, keep track of the distance between them
distance = Vector2.Distance(posA, posB);
}
public void OnPointerUp (PointerEventData data)
{
if ( currentMainFinger == data.pointerId )
{
currentMainFinger = -1;
}
if ( currentSecondFinger == data.pointerId )
{
currentSecondFinger = -1;
}
}
private float sensitivity = 0.3f;
// in this example, the swipes/pinch affects these three calls:
public Changer orbitLR;
public Changer orbitUD;
public Changer distanceZ;
// initial values of those...
private float LR = -20f;
private float UD = 20f;
private float distanceCam = 5f;
private void _processSwipe()
{
// in this example, just left-right or up-down swipes
LR = LR + sensitivity * screenTravel.x;
UD = UD - sensitivity * screenTravel.y;
LR = Mathf.Clamp(LR, -150f, 30f);
UD = Mathf.Clamp(UD, 5f, 50f);
orbitLR.RotationY = LR;
orbitUD.RotationX = UD;
}
private void _processPinch()
{
// in this example, pinch to zoom
distanceCam = distanceCam - pinchDelta * 0.0125f;
distanceCam = Mathf.Clamp(distanceCam, 3f, 8f);
distanceZ.DistanceZ = distanceCam;
}
}
(注意,请不要回答无法使用的遗留“Touches”系统。这与正常的现代 Unity 开发有关。)
统一输入系统还不完善。您现在必须使用低级系统自己跟踪触摸。 See an example here。
实现这个并不复杂。
每次出现 OnPointerDown
事件时,使用 List
并存储 pointerId
,然后递增 touchCount
变量。如果 List
.
中已经存在,则不要将 pointerId
存储在 OnPointerDown 上
调用OnPointerUp
或有释放时,检查pointerId
是否存在。如果是,请递减 touchCount
变量。如果 not 存在于 List
中,那么不要减少任何东西。
1.实现非常简单:
public class FingerMove : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
public int touchCount;
public List<int> touchID = new List<int>(6); //6 touches limit
public void OnPointerDown(PointerEventData data)
{
Debug.Log("Pressed");
//Check If PointerId exist, if it doesn't add to list
if (touchID.Contains(data.pointerId))
{
return; //Exit if PointerId exist
}
//PointerId does not exist, add it to the list then increment touchCount
touchID.Add(data.pointerId);
touchCount++;
}
public void OnPointerUp(PointerEventData data)
{
Debug.Log("Released");
//Check If PointerId exist, if it exist remove it from list then decrement touchCount
if (touchID.Contains(data.pointerId))
{
touchID.Remove(data.pointerId);
touchCount--;
return;
}
}
void Update()
{
Debug.Log("Touch Count: " + touchCount);
}
}
2.第一个例子很简单,但可以用我们自己的界面进行改进.
此方法使用两个脚本:
IPointerCounterHandler.cs
接口:
public interface IPointerCounterHandler : IEventSystemHandler
{
void OnPointerCounterChanged(int touchCount);
void OnPointerCounterChanged(PointerCounterEventData touchCountData);
}
PointerCounterEventData.cs
脚本。
public class PointerCounterEventData : BaseEventData
{
//The callback with int parameter
public static readonly ExecuteEvents.EventFunction<IPointerCounterHandler> counterChangedV1Delegate
= delegate (IPointerCounterHandler handler, BaseEventData data)
{
//var casted = ExecuteEvents.ValidateEventData<PointerCounterEventData>(data);
handler.OnPointerCounterChanged(touchCount);
};
//The callback with PointerCounterEventData parameter
public static readonly ExecuteEvents.EventFunction<IPointerCounterHandler> counterChangedV2Delegate
= delegate (IPointerCounterHandler handler, BaseEventData data)
{
var casted = ExecuteEvents.ValidateEventData<PointerCounterEventData>(data);
handler.OnPointerCounterChanged(casted);
};
public static int touchCount = 0;
public PointerCounterInfo touchCountData = new PointerCounterInfo();
public static List<int> touchID = new List<int>(6); //6 touches limit
//Constructor with the int parameter
public PointerCounterEventData(
EventSystem eventSystem,
int tempTouchId,
PointerState pointerStat
)
: base(eventSystem)
{
//Process the Input event
processTouches(pointerStat, tempTouchId, null, CallBackType.TouchCountOnly);
}
//Constructor with the PointerEventData parameter
public PointerCounterEventData(
EventSystem eventSystem,
PointerEventData eventData,
PointerState pointerStat,
GameObject target
)
: base(eventSystem)
{
//Process the Input event
processTouches(pointerStat, eventData.pointerId, eventData, CallBackType.CounterData);
//Create new PointerCounterInfo for the OnPointerCounterChanged(PointerCounterEventData eventData) function
PointerCounterInfo pcInfo = createPointerInfo(eventData,
target, pointerStat);
//Update touchCountData
touchCountData = pcInfo;
}
void processTouches(PointerState pointerStat, int tempTouchId, PointerEventData touchCountData, CallBackType cbType)
{
if (pointerStat == PointerState.DOWN)
{
//Check If PointerId exist, if it doesn't add to list
if (touchID.Contains(tempTouchId))
{
//eventData.eventData
return; //Exit if PointerId exist
}
//PointerId does not exist, add it to the list then increment touchCount
touchID.Add(tempTouchId);
touchCount++;
}
if (pointerStat == PointerState.UP)
{
//Check If PointerId exist, if it exist remove it from list then decrement touchCount
if (touchID.Contains(tempTouchId))
{
touchID.Remove(tempTouchId);
touchCount--;
return;
}
}
}
public static void notifyPointerDown(EventSystem eventSystem, PointerEventData eventData,
GameObject target)
{
PointerState pointerStat = PointerState.DOWN;
notifyfuncs(eventSystem, eventData, target, pointerStat);
}
public static void notifyPointerUp(EventSystem eventSystem, PointerEventData eventData,
GameObject target)
{
PointerState pointerStat = PointerState.UP;
notifyfuncs(eventSystem, eventData, target, pointerStat);
}
private static void notifyfuncs(EventSystem eventSystem, PointerEventData eventData,
GameObject target, PointerState pointerStat)
{
//////////////////////Call the int parameter//////////////////////
PointerCounterEventData eventParam1 = new PointerCounterEventData(
eventSystem,
eventData.pointerId,
pointerStat);
ExecuteEvents.Execute<IPointerCounterHandler>(
target,
eventParam1,
PointerCounterEventData.counterChangedV1Delegate);
//////////////////////Call the PointerCounterEventData parameter//////////////////////
PointerCounterEventData eventParam2 = new PointerCounterEventData(
eventSystem,
eventData,
pointerStat,
target);
ExecuteEvents.Execute<IPointerCounterHandler>(
target,
eventParam2,
PointerCounterEventData.counterChangedV2Delegate);
}
//Creates PointerCounterInfo for the OnPointerCounterChanged(PointerCounterEventData eventData) function
private static PointerCounterInfo createPointerInfo(PointerEventData eventData,
GameObject target, PointerState pointerStat)
{
PointerCounterInfo pointerCounterInfo = new PointerCounterInfo();
pointerCounterInfo.pointerId = eventData.pointerId;
pointerCounterInfo.touchCount = touchCount;
pointerCounterInfo.eventData = eventData;
pointerCounterInfo.pointerState = pointerStat;
pointerCounterInfo.target = target;
return pointerCounterInfo;
}
public enum CallBackType
{
TouchCountOnly, CounterData
}
}
public enum PointerState { NONE, DOWN, UP }
public class PointerCounterInfo
{
public int pointerId = 0;
public int touchCount = 0;
public PointerEventData eventData;
public PointerState pointerState;
public GameObject target;
}
用法:
在你的脚本中实现 IPointerCounterHandler
然后覆盖
void OnPointerCounterChanged(int touchCount);
和
void OnPointerCounterChanged(PointerCounterEventData touchCountData);
函数。
最后,在OnPointerDown
函数中调用PointerCounterEventData.notifyPointerDown
,在OnPointerUp
函数中调用PointerCounterEventData.notifyPointerUp
。
测试:
public class Test : MonoBehaviour, IPointerCounterHandler, IPointerDownHandler, IPointerUpHandler
{
public void OnPointerCounterChanged(int touchCount)
{
Debug.Log("Simple Finger Counter: " + touchCount);
}
public void OnPointerCounterChanged(PointerCounterEventData touchCountData)
{
PointerCounterInfo moreEventData = touchCountData.touchCountData;
Debug.Log("Finger TouchCount: " + moreEventData.touchCount);
Debug.Log("Finger PointerId: " + moreEventData.pointerId);
Debug.Log("Finger Pointer State: " + moreEventData.pointerState);
Debug.Log("Finger Target: " + moreEventData.target.name);
//Can also access PointerEventData
PointerEventData eventData = touchCountData.touchCountData.eventData;
Debug.Log("Click Time!: " + eventData.clickTime);
}
public void OnPointerDown(PointerEventData eventData)
{
PointerCounterEventData.notifyPointerDown(EventSystem.current, eventData, this.gameObject);
}
public void OnPointerUp(PointerEventData eventData)
{
PointerCounterEventData.notifyPointerUp(EventSystem.current, eventData, this.gameObject);
}
}
您没有在 Unity 中执行此操作的预定义方法。您所能做的就是再次以面向对象的方法使用自定义解决方案。最好的办法是拆分事件检测和事件处理。
要问的主要问题是,如何以 OOP 方式表示手指、触摸、手势等。我选择这样做:
- 每当发生指针按下事件时,都会创建一个新手指。
- 创建新手指时,也会创建其与现有手指的所有可能子集的组合。这意味着,当添加第三根手指时,如果将手指标记为
f1, f2, f3
,则创建的手指组合为:f3, f1f3, f2f3, f1f2f3
。这在使用多个手指时提供了最大的灵活性。你可以做手势like this。比如做锚点手势,只需要f2f3
的手势,但f1
的手势也必须存在。在这种情况下,您可以简单地忽略 f1
。
- 移动手指时,将创建一个新手势,并使用新创建的手势触发所有组合的更改事件,具体取决于该手指。
还有您通常需要从多点触控事件中获得的信息:
- 手指的平均位置
- 手指集体旋转
- 手指代表的形状的总大小,用于缩放和填充。可以是两根手指之间向量的大小,也可以是多边形的面积
- 所有顶点的位置,如果你想做高级的东西
- 以上所有的变化(增量)
前面的长代码:
using UnityEngine;
using UnityEngine.EventSystems;
using System.Linq;
using System.Collections.Generic;
public class MultitouchHandler : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler {
public List<Finger> Fingers = new List<Finger>();
public List<FingerCombination> FingerCombinations = new List<FingerCombination>();
public FingerCombination GetFingerCombination(params int[] fingerIndices) {
var fc = FingerCombinations.Find(x => x.IDs.Count == fingerIndices.Length && fingerIndices.All(y => x.IDs.Contains(Fingers[y].ID)));
if (fc != null) return fc;
fc = new FingerCombination() {
Fingers = fingerIndices.Select(x => Fingers[x]).ToList()
};
fc.IDs = fc.Fingers.Select(x => x.ID).ToList();
fc.Data = Fingers.Select(x => x.Data).ToList();
fc.PreviousData = Fingers.Select(x => x.Data).ToList();
FingerCombinations.Add(fc);
return fc;
}
public delegate void MultitouchEventHandler(int touchCount, MultitouchHandler sender);
public event MultitouchEventHandler OnFingerAdded;
public event MultitouchEventHandler OnFingerRemoved;
public void OnDrag(PointerEventData eventData) {
var finger = Fingers.Find(x => x.ID == eventData.pointerId);
var fcs = FingerCombinations.Where(x => x.IDs.Contains(eventData.pointerId));
finger.PreviousData = finger.Data;
finger.Data = eventData;
foreach (var fc in fcs) {
fc.PreviousData = fc.Data;
fc.Data = fc.Fingers.Select(x => x.Data).ToList();
fc.PreviousGesture = fc.Gesture;
fc.Gesture = new Gesture() {
Center = fc.Center,
Size = fc.Size,
Angle = fc.Angle,
SizeDelta = 1
};
if (fc.PreviousGesture != null) {
fc.Gesture.CenterDelta = fc.Center - fc.PreviousGesture.Center;
fc.Gesture.SizeDelta = fc.Size / fc.PreviousGesture.Size;
fc.Gesture.AngleDelta = fc.Angle - fc.PreviousGesture.Angle;
}
fc.Changed();
}
}
public void OnPointerDown(PointerEventData eventData) {
var finger = new Finger() { ID = eventData.pointerId, Data = eventData };
Fingers.Add(finger);
if (OnFingerAdded != null)
OnFingerAdded(Fingers.Count, this);
}
public void OnPointerUp(PointerEventData eventData) {
Fingers.RemoveAll(x => x.ID == eventData.pointerId);
if (OnFingerRemoved != null)
OnFingerRemoved(Fingers.Count, this);
var fcs = FingerCombinations.Where(x => x.IDs.Contains(eventData.pointerId));
foreach (var fc in fcs) {
fc.Finished();
}
FingerCombinations.RemoveAll(x => x.IDs.Contains(eventData.pointerId));
}
public class Finger {
public int ID;
public PointerEventData Data;
public PointerEventData PreviousData;
}
public class FingerCombination {
public List<int> IDs = new List<int>();
public List<Finger> Fingers;
public List<PointerEventData> PreviousData;
public List<PointerEventData> Data;
public delegate void GestureEventHandler(Gesture gesture, FingerCombination sender);
public event GestureEventHandler OnChange;
public delegate void GestureEndHandler(FingerCombination sender);
public event GestureEndHandler OnFinish;
public Gesture Gesture;
public Gesture PreviousGesture;
public Vector2 Center
{
get { return Data.Aggregate(Vector2.zero, (x, y) => x + y.position) / Data.Count; }
}
public float Size
{
get
{
if (Data.Count == 1) return 0;
var magnitudeSum = 0f;
for (int i = 1; i < Data.Count; i++) {
var dif = (Data[i].position - Data[0].position);
magnitudeSum += dif.magnitude;
}
return magnitudeSum / (Data.Count - 1);
}
}
public float Angle
{
get
{
if (Data.Count == 1) return 0;
var angleSum = 0f;
for (int i = 1; i < Data.Count; i++) {
var dif = (Data[i].position - Data[0].position);
angleSum += Mathf.Atan2(dif.y, dif.x) * Mathf.Rad2Deg;
}
return angleSum / (Data.Count - 1);
}
}
internal void Changed() {
if (OnChange != null)
OnChange.Invoke(Gesture, this);
}
internal void Finished() {
if (OnFinish != null)
OnFinish.Invoke(this);
}
}
public class Gesture {
public Vector2 Center;
public float Size;
public float Angle;
public Vector2 CenterDelta;
public float SizeDelta;
public float AngleDelta;
}
}
这里有一个例子,展示了如何使用 4 个手指。
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
public class MultiTouchTest : MonoBehaviour {
public Vector2 rectSize = Vector2.one * 2;
public Vector2 skewedRectSize = Vector2.one;
public Vector2 rectPos = Vector2.zero;
public List<Vector3> Fingers = new List<Vector3>();
void Start() {
var h = GetComponent<MultitouchHandler>();
h.OnFingerAdded += OnGestureStart;
}
private void OnGestureStart(int touchCount, MultitouchHandler sender) {
if (touchCount != 4) return;
var fc = sender.GetFingerCombination(0, 1, 2, 3);
fc.OnChange += OnGesture;
}
private void OnGesture(MultitouchHandler.Gesture gesture, MultitouchHandler.FingerCombination sender) {
rectSize *= gesture.SizeDelta;
Fingers = sender.Fingers.Select(x => Camera.main.ScreenToWorldPoint(x.Data.position)).ToList();
var tan = Mathf.Tan(gesture.Angle * Mathf.Deg2Rad);
skewedRectSize = new Vector2(rectSize.x / tan, rectSize.y * tan);
rectPos += gesture.CenterDelta / 50;
}
public void OnDrawGizmos() {
Gizmos.color = Color.red;
Gizmos.DrawCube(rectPos, skewedRectSize);
Gizmos.color = Color.blue;
foreach (var finger in Fingers) Gizmos.DrawSphere(finger + Vector3.forward, 0.5f);
}
}
结果如下:
虽然这只是一个简单的例子。好的答案对于 SO 的格式来说太长了。
我不喜欢回答我自己的问题,但经过大量调查和专家输入,以下是唯一的方法。
让我们总结一下:
1。您实际上必须添加一个守护进程。因此,对于 Pinch,只需将“PinchInputModule.cs”拖放到消费者对象上 - 您就完成了。
您可能会认为“如果不添加守护进程就不能自动工作,真糟糕。”但是 - 事实上,对于 Unity 自己的,你必须添加一个守护进程,即“TouchInput”系列。 (他们有时会自动添加,有时他们会忘记,您必须这样做。)
很简单,自动魔法的“追逐”是愚蠢的,算了吧。你必须添加一个守护进程。
2。您必须从 IPointerDownHandler/etc 横向继承,因为很简单,Unity 搞砸了,您无法在 StandAloneInputModule 中正确继承。复制粘贴不是编程。
很简单,沿着子类化 StandAloneInputModule 的道路走下去并不是好的工程,因为 Unity 搞砸了。您只需在新守护程序中使用 IPointerDownHandler/etc 即可。下面对此进行更多讨论。
下面我给出了“单点触摸”和“捏合”的例子。这些是生产就绪的。您可以为其他情况编写自己的应用程序,例如四点触摸等。因此,使用捏合守护进程(字面意思是将其放在有问题的对象上),处理捏合变得异常容易:
public void OnPinchZoom (float delta)
{
_processPinch(delta);
}
很难看到它更容易。
这样做,直到 Unity 记住它的产品“用于手机”并且他们添加了捏等调用。
做一个立方体,把你自己的脚本放在上面FingerMove
。
制作脚本,说移动相机LR,UD。 (或其他 - 只是 Debug.Log 更改。)
粘贴到这个处理程序脚本中...
SingleFingerInputModule.cs
/*
ISingleFingerHandler - handles strict single-finger down-up-drag
Put this daemon ON TO the game object, with a consumer of the service.
(Note - there are many, many philosophical decisions to make when
implementing touch concepts; just some issues include what happens
when other fingers touch, can you "swap out" etc. Note that, for
example, Apple vs. Android have slightly different takes on this.
If you wanted to implement slightly different "philosophy" you'd
do that in this script.)
*/
public interface ISingleFingerHandler
{
void OnSingleFingerDown (Vector2 position);
void OnSingleFingerUp (Vector2 position);
void OnSingleFingerDrag (Vector2 delta);
}
/* note, Unity chooses to have "one interface for each action"
however here we are dealing with a consistent paradigm ("dragging")
which has three parts; I feel it's better to have one interface
forcing the consumer to have the three calls (no problem if empty) */
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
public class SingleFingerInputModule:MonoBehaviour,
IPointerDownHandler,IPointerUpHandler,IDragHandler
{
private ISingleFingerHandler needsUs = null;
// of course that would be a List,
// just one shown for simplicity in this example code
private int currentSingleFinger = -1;
private int kountFingersDown = 0;
void Awake()
{
needsUs = GetComponent(typeof(ISingleFingerHandler)) as ISingleFingerHandler;
// of course, you may prefer this to search the whole scene,
// just this gameobject shown here for simplicity
// alternately it's a very good approach to have consumers register
// for it. to do so just add a register function to the interface.
}
public void OnPointerDown(PointerEventData data)
{
kountFingersDown = kountFingersDown + 1;
if (currentSingleFinger == -1 && kountFingersDown == 1)
{
currentSingleFinger = data.pointerId;
if (needsUs != null) needsUs.OnSingleFingerDown(data.position);
}
}
public void OnPointerUp (PointerEventData data)
{
kountFingersDown = kountFingersDown - 1;
if ( currentSingleFinger == data.pointerId )
{
currentSingleFinger = -1;
if (needsUs != null) needsUs.OnSingleFingerUp(data.position);
}
}
public void OnDrag (PointerEventData data)
{
if ( currentSingleFinger == data.pointerId && kountFingersDown == 1 )
{
if (needsUs != null) needsUs.OnSingleFingerDrag(data.delta);
}
}
}
将那个守护进程与你的消费者 FingerMove
放在游戏对象上,然后忘记它。现在是
非常简单
处理拖动:
public class FingerMove:MonoBehaviour, ISingleFingerHandler
{
public void OnSingleFingerDown(Vector2 position) {}
public void OnSingleFingerUp (Vector2 position) {}
public void OnSingleFingerDrag (Vector2 delta)
{
_processSwipe(delta);
}
private void _processSwipe(Vector2 screenTravel)
{
.. move the camera or whatever ..
}
}
就像我说的,
非常简单!
现在让我们考虑两个手指的情况,捏到zoom/unzoom。
PinchInputModule.cs
/*
IPinchHandler - strict two sequential finger pinch Handling
Put this daemon ON TO the game object, with a consumer of the service.
(Note, as always, the "philosophy" of a glass gesture is up to you.
There are many, many subtle questions; eg should extra fingers block,
can you 'swap primary' etc etc etc - program it as you wish.)
*/
public interface IPinchHandler
{
void OnPinchStart ();
void OnPinchEnd ();
void OnPinchZoom (float gapDelta);
}
/* note, Unity chooses to have "one interface for each action"
however here we are dealing with a consistent paradigm ("pinching")
which has three parts; I feel it's better to have one interface
forcing the consumer to have the three calls (no problem if empty) */
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
public class PinchInputModule:MonoBehaviour,
IPointerDownHandler,IPointerUpHandler,IDragHandler
{
private IPinchHandler needsUs = null;
// of course that would be a List,
// just one shown for simplicity in this example code
private int currentFirstFinger = -1;
private int currentSecondFinger = -1;
private int kountFingersDown = 0;
private bool pinching = false;
private Vector2 positionFirst = Vector2.zero;
private Vector2 positionSecond = Vector2.zero;
private float previousDistance = 0f;
private float delta = 0f;
void Awake()
{
needsUs = GetComponent(typeof(IPinchHandler)) as IPinchHandler;
// of course, this could search the whole scene,
// just this gameobject shown here for simplicity
}
public void OnPointerDown(PointerEventData data)
{
kountFingersDown = kountFingersDown + 1;
if (currentFirstFinger == -1 && kountFingersDown == 1)
{
// first finger must be a pure first finger and that's that
currentFirstFinger = data.pointerId;
positionFirst = data.position;
return;
}
if (currentFirstFinger != -1 && currentSecondFinger == -1 && kountFingersDown == 2)
{
// second finger must be a pure second finger and that's that
currentSecondFinger = data.pointerId;
positionSecond = data.position;
FigureDelta();
pinching = true;
if (needsUs != null) needsUs.OnPinchStart();
return;
}
}
public void OnPointerUp (PointerEventData data)
{
kountFingersDown = kountFingersDown - 1;
if ( currentFirstFinger == data.pointerId )
{
currentFirstFinger = -1;
if (pinching)
{
pinching = false;
if (needsUs != null) needsUs.OnPinchEnd();
}
}
if ( currentSecondFinger == data.pointerId )
{
currentSecondFinger = -1;
if (pinching)
{
pinching = false;
if (needsUs != null) needsUs.OnPinchEnd();
}
}
}
public void OnDrag (PointerEventData data)
{
if ( currentFirstFinger == data.pointerId )
{
positionFirst = data.position;
FigureDelta();
}
if ( currentSecondFinger == data.pointerId )
{
positionSecond = data.position;
FigureDelta();
}
if (pinching)
{
if ( data.pointerId == currentFirstFinger || data.pointerId == currentSecondFinger )
{
if (kountFingersDown==2)
{
if (needsUs != null) needsUs.OnPinchZoom(delta);
}
return;
}
}
}
private void FigureDelta()
{
float newDistance = Vector2.Distance(positionFirst, positionSecond);
delta = newDistance - previousDistance;
previousDistance = newDistance;
}
}
将该守护进程放在游戏对象上,您有服务的消费者。注意,“混搭”绝对没有问题。在此示例中,让我们同时使用拖动和捏合手势。现在是
非常简单
处理夹点:
public class FingerMove:MonoBehaviour, ISingleFingerHandler, IPinchHandler
{
public void OnSingleFingerDown(Vector2 position) {}
public void OnSingleFingerUp (Vector2 position) {}
public void OnSingleFingerDrag (Vector2 delta)
{
_processSwipe(delta);
}
public void OnPinchStart () {}
public void OnPinchEnd () {}
public void OnPinchZoom (float delta)
{
_processPinch(delta);
}
private void _processSwipe(Vector2 screenTravel)
{
.. handle drag (perhaps move LR/UD)
}
private void _processPinch(float delta)
{
.. handle zooming (perhaps move camera in-and-out)
}
}
就像我说的,
非常简单! :)
要了解这有多优雅,请考虑以下问题:捏合时,您希望“暂停”拖动,还是让两者都发生?令人惊奇的是,您只需在 SingleFingerInputModule.cs 中对其进行编程即可。在具体的例子中,我希望它“按住”拖动,while/if 用户正在缩放,所以上面的 SingleFingerInputModule.cs 就是这样编程的。您可以轻松修改它以进行持续拖动、更改为质心、取消拖动或任何您想要的操作。令人惊奇的是,FingerMove.cs 完全没有受到影响!非常有用的抽象!
请注意,对于上面 Gökhan 出色的四角示例,我会这样写:
public class FingerStretch:MonoBehaviour, IFourCornerHandler
{
public void OnFourCornerChange (Vector2 a, b, c, d)
{
... amazingly elegant solution
... Gökhan does all the work in FourCornerInputModule.cs
... here I just subscribe to it. amazingly simple
}
这只是一个令人难以置信的简单方法。
这太简单了:O
Gökhan 会将手指的所有逻辑封装在 FourCornerInputModule.cs 中,它有一个接口 IFourCornerHandler。请注意,FourCornerInputModule 会明智地做出所有哲学决定(例如,您必须将所有四个手指都放下,如果您有一个额外的手指,等等)。
出现的问题如下:
1。我们应该做“类似事件系统”的编程吗?
在所谓的“独立输入模块”处查看您的 Unity 项目,这是一个带有 EventSystem
和 StandAloneInputModule
的游戏对象
您实际上可以“从头开始编写”类似 SingleFingerInputModule.cs 或 PinchInputModule.cs、 的东西,这样它就可以“像”Unity 的 StandAloneInputModule 一样工作。
虽然很难做到这一点,但请注意评论中的链接 。
但是有一个特定的、可解决的问题:可笑的是,你不能使用 OO 原则:在我上面的 SingleFingerInputModule.cs
代码中,我们非常明智地- 当然 - 使用 Unity 已经完成的现有惊人的 IPointerDownHandler 等功能,我们(基本上)“子类化”,添加更多逻辑。这正是你应该做的,真的必须做的。相比之下:如果您确实决定“制作像 StandAloneInputModule 一样工作的东西”,那将是一场惨败 - 您必须重新开始,可能会复制和粘贴 Unity 的源代码(用于 IPointerDownHandler 等)并按照您的方式稍微修改它,这这当然是一个你永远不应该做软件工程的确切例子。
2。但是你必须'remember to add the daemon'?
请注意,如果您选择“制作类似于 StandAloneInputModule 的东西”路线,实际上,您仍然必须这样做!!!!!!!这有点奇怪;优势为零。
3。您必须给所有订阅者打电话吗?
如果你走“做一些像 StandAloneInputModule 一样工作的东西”的路线,Unity 有一个“执行......”调用......就是这样做的。它只不过是一个用于“调用订阅者”的宏(除了最琐碎的脚本之外,我们每天都在每个脚本中这样做);没有优势。
事实上:我个人认为 进行订阅调用 、 实际上要好得多,只需将其作为接口调用之一。我只是认为这比尝试“像”Unity 怪异的魔法调用系统(它实际上根本不起作用——无论如何你必须“记住附加”一个 StandAloneInputModule)要好得多。
综上所述,
我开始相信
(1) 实际上,建立在 IPointerDownHandler、IDragHandler、IPointerUpHandler 上绝对是正确的方法
开始重写代码以“制作像 StandAloneInputModule 一样工作的东西”无疑是一个坏主意
(2) 必须添加守护进程没有错
如果你试图“制作像 StandAloneInputModule 一样工作的东西”......你无论如何都必须“记住添加它”,看在上帝的份上。
(3) 寻找消费者完全没有错,或者更好的是,有一个订阅电话
如果您尝试“制作像 StandAloneInputModule 一样工作的东西”,那么 Unity 给您的“执行...”调用几乎不存在优势,这是一行代码与您的代码(更短,更清晰,更快)的代码行来“呼叫订阅者”。同样,简单地 会更加明显和清晰,您可以说任何非 Unity 程序员都会这样做。
所以对我来说,今天在 Unity 中最好的方法就是写 modules/interfaces 例如 SingleFingerInputModule.cs、PinchInputModule.cs、FourCornerInputModule.cs,把它放在游戏对象上你想拥有那些消费者 - 你就完成了。 “就这么简单。”
public class Zoom:MonoBehaviour, ISingleFingerHandler, IPinchHandler
{
public void OnPinchZoom (float delta)
{
...
在现代 Unity3D 中,我们使用 IPointerDownHandler 系列调用。
关于 IPointerDownHandler
调用系列,
public class FingerMove:MonoBehaviour, IPointerDownHandler...
{
public void OnPointerDown (PointerEventData data)
{
当然他们很棒
用于处理单点触摸。
但是你如何严肃地处理多次触摸?
您可以自己“手动完成所有操作”来跟踪触摸,但 Unity 会希望您为如此绝对基本的事情执行此操作似乎令人难以置信。 (我的意思是 - 它是一个游戏引擎。当然,我也可以自己编写所有渲染和物理!)
这是一个基本上是“牛仔编程”的例子,只是手工完成,没有软件工程。什么是真正的解决方案?
//
// example of programming a pinch (as well as swipes) using modern Unity
//
// here we are forced to track "by hand" in your own code
// how many fingers are down and which
// fingers belong to you etc etc:
//
// pedagogic example code:
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using UnityEngine.EventSystems;
public class FingerMove:MonoBehaviour,
IPointerDownHandler, IDragHandler, IPointerUpHandler
{
// these three for the ordinary one-finger-only drag
private Vector2 prevPoint;
private Vector2 newPoint;
private Vector2 screenTravel;
// and this one is the ordinary one-finger-only drag
private int currentMainFinger = -1;
// and this for the (strictly second finger only) drag...
private int currentSecondFinger = -1;
private Vector2 posA;
private Vector2 posB;
private float previousDistance = -1f;
private float distance;
private float pinchDelta = 0f;
public void OnPointerDown (PointerEventData data)
{
if (currentMainFinger == -1)
{
// this is the NEW currentMainFinger
currentMainFinger = data.pointerId;
prevPoint = data.position;
// and for the drag (if it becomes used)...
posA = data.position;
return;
}
if (currentSecondFinger == -1)
{
// this is the NEW currentSecondFinger
currentSecondFinger = data.pointerId;
posB = data.position;
figureDelta();
previousDistance = distance;
return;
}
Debug.Log("third+ finger! (ignore)");
}
public void OnDrag (PointerEventData data)
{
// handle single-finger moves (swipes, drawing etc) only:
if ( currentMainFinger == data.pointerId )
{
newPoint = data.position;
screenTravel = newPoint - prevPoint;
prevPoint = newPoint;
if (currentSecondFinger == -1)
{
Debug.Log("NO 2f");
_processSwipe(); // handle it your way
}
else
{
}
// and for two-finger if it becomes used next frame
// or is already being used...
posA = data.position;
}
if (currentSecondFinger == -1) return;
// handle two-finger (eg, pinch, rotate etc)...
if ( currentMainFinger == data.pointerId ) posA = data.position;
if ( currentSecondFinger == data.pointerId ) posB = data.position;
figureDelta();
pinchDelta = distance - previousDistance;
previousDistance = distance;
_processPinch(); // handle it your way
}
private void figureDelta()
{
// when/if two touches, keep track of the distance between them
distance = Vector2.Distance(posA, posB);
}
public void OnPointerUp (PointerEventData data)
{
if ( currentMainFinger == data.pointerId )
{
currentMainFinger = -1;
}
if ( currentSecondFinger == data.pointerId )
{
currentSecondFinger = -1;
}
}
private float sensitivity = 0.3f;
// in this example, the swipes/pinch affects these three calls:
public Changer orbitLR;
public Changer orbitUD;
public Changer distanceZ;
// initial values of those...
private float LR = -20f;
private float UD = 20f;
private float distanceCam = 5f;
private void _processSwipe()
{
// in this example, just left-right or up-down swipes
LR = LR + sensitivity * screenTravel.x;
UD = UD - sensitivity * screenTravel.y;
LR = Mathf.Clamp(LR, -150f, 30f);
UD = Mathf.Clamp(UD, 5f, 50f);
orbitLR.RotationY = LR;
orbitUD.RotationX = UD;
}
private void _processPinch()
{
// in this example, pinch to zoom
distanceCam = distanceCam - pinchDelta * 0.0125f;
distanceCam = Mathf.Clamp(distanceCam, 3f, 8f);
distanceZ.DistanceZ = distanceCam;
}
}
(注意,请不要回答无法使用的遗留“Touches”系统。这与正常的现代 Unity 开发有关。)
统一输入系统还不完善。您现在必须使用低级系统自己跟踪触摸。 See an example here。
实现这个并不复杂。
每次出现 OnPointerDown
事件时,使用 List
并存储 pointerId
,然后递增 touchCount
变量。如果 List
.
pointerId
存储在 OnPointerDown 上
调用OnPointerUp
或有释放时,检查pointerId
是否存在。如果是,请递减 touchCount
变量。如果 not 存在于 List
中,那么不要减少任何东西。
1.实现非常简单:
public class FingerMove : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
public int touchCount;
public List<int> touchID = new List<int>(6); //6 touches limit
public void OnPointerDown(PointerEventData data)
{
Debug.Log("Pressed");
//Check If PointerId exist, if it doesn't add to list
if (touchID.Contains(data.pointerId))
{
return; //Exit if PointerId exist
}
//PointerId does not exist, add it to the list then increment touchCount
touchID.Add(data.pointerId);
touchCount++;
}
public void OnPointerUp(PointerEventData data)
{
Debug.Log("Released");
//Check If PointerId exist, if it exist remove it from list then decrement touchCount
if (touchID.Contains(data.pointerId))
{
touchID.Remove(data.pointerId);
touchCount--;
return;
}
}
void Update()
{
Debug.Log("Touch Count: " + touchCount);
}
}
2.第一个例子很简单,但可以用我们自己的界面进行改进.
此方法使用两个脚本:
IPointerCounterHandler.cs
接口:
public interface IPointerCounterHandler : IEventSystemHandler
{
void OnPointerCounterChanged(int touchCount);
void OnPointerCounterChanged(PointerCounterEventData touchCountData);
}
PointerCounterEventData.cs
脚本。
public class PointerCounterEventData : BaseEventData
{
//The callback with int parameter
public static readonly ExecuteEvents.EventFunction<IPointerCounterHandler> counterChangedV1Delegate
= delegate (IPointerCounterHandler handler, BaseEventData data)
{
//var casted = ExecuteEvents.ValidateEventData<PointerCounterEventData>(data);
handler.OnPointerCounterChanged(touchCount);
};
//The callback with PointerCounterEventData parameter
public static readonly ExecuteEvents.EventFunction<IPointerCounterHandler> counterChangedV2Delegate
= delegate (IPointerCounterHandler handler, BaseEventData data)
{
var casted = ExecuteEvents.ValidateEventData<PointerCounterEventData>(data);
handler.OnPointerCounterChanged(casted);
};
public static int touchCount = 0;
public PointerCounterInfo touchCountData = new PointerCounterInfo();
public static List<int> touchID = new List<int>(6); //6 touches limit
//Constructor with the int parameter
public PointerCounterEventData(
EventSystem eventSystem,
int tempTouchId,
PointerState pointerStat
)
: base(eventSystem)
{
//Process the Input event
processTouches(pointerStat, tempTouchId, null, CallBackType.TouchCountOnly);
}
//Constructor with the PointerEventData parameter
public PointerCounterEventData(
EventSystem eventSystem,
PointerEventData eventData,
PointerState pointerStat,
GameObject target
)
: base(eventSystem)
{
//Process the Input event
processTouches(pointerStat, eventData.pointerId, eventData, CallBackType.CounterData);
//Create new PointerCounterInfo for the OnPointerCounterChanged(PointerCounterEventData eventData) function
PointerCounterInfo pcInfo = createPointerInfo(eventData,
target, pointerStat);
//Update touchCountData
touchCountData = pcInfo;
}
void processTouches(PointerState pointerStat, int tempTouchId, PointerEventData touchCountData, CallBackType cbType)
{
if (pointerStat == PointerState.DOWN)
{
//Check If PointerId exist, if it doesn't add to list
if (touchID.Contains(tempTouchId))
{
//eventData.eventData
return; //Exit if PointerId exist
}
//PointerId does not exist, add it to the list then increment touchCount
touchID.Add(tempTouchId);
touchCount++;
}
if (pointerStat == PointerState.UP)
{
//Check If PointerId exist, if it exist remove it from list then decrement touchCount
if (touchID.Contains(tempTouchId))
{
touchID.Remove(tempTouchId);
touchCount--;
return;
}
}
}
public static void notifyPointerDown(EventSystem eventSystem, PointerEventData eventData,
GameObject target)
{
PointerState pointerStat = PointerState.DOWN;
notifyfuncs(eventSystem, eventData, target, pointerStat);
}
public static void notifyPointerUp(EventSystem eventSystem, PointerEventData eventData,
GameObject target)
{
PointerState pointerStat = PointerState.UP;
notifyfuncs(eventSystem, eventData, target, pointerStat);
}
private static void notifyfuncs(EventSystem eventSystem, PointerEventData eventData,
GameObject target, PointerState pointerStat)
{
//////////////////////Call the int parameter//////////////////////
PointerCounterEventData eventParam1 = new PointerCounterEventData(
eventSystem,
eventData.pointerId,
pointerStat);
ExecuteEvents.Execute<IPointerCounterHandler>(
target,
eventParam1,
PointerCounterEventData.counterChangedV1Delegate);
//////////////////////Call the PointerCounterEventData parameter//////////////////////
PointerCounterEventData eventParam2 = new PointerCounterEventData(
eventSystem,
eventData,
pointerStat,
target);
ExecuteEvents.Execute<IPointerCounterHandler>(
target,
eventParam2,
PointerCounterEventData.counterChangedV2Delegate);
}
//Creates PointerCounterInfo for the OnPointerCounterChanged(PointerCounterEventData eventData) function
private static PointerCounterInfo createPointerInfo(PointerEventData eventData,
GameObject target, PointerState pointerStat)
{
PointerCounterInfo pointerCounterInfo = new PointerCounterInfo();
pointerCounterInfo.pointerId = eventData.pointerId;
pointerCounterInfo.touchCount = touchCount;
pointerCounterInfo.eventData = eventData;
pointerCounterInfo.pointerState = pointerStat;
pointerCounterInfo.target = target;
return pointerCounterInfo;
}
public enum CallBackType
{
TouchCountOnly, CounterData
}
}
public enum PointerState { NONE, DOWN, UP }
public class PointerCounterInfo
{
public int pointerId = 0;
public int touchCount = 0;
public PointerEventData eventData;
public PointerState pointerState;
public GameObject target;
}
用法:
在你的脚本中实现 IPointerCounterHandler
然后覆盖
void OnPointerCounterChanged(int touchCount);
和
void OnPointerCounterChanged(PointerCounterEventData touchCountData);
函数。
最后,在OnPointerDown
函数中调用PointerCounterEventData.notifyPointerDown
,在OnPointerUp
函数中调用PointerCounterEventData.notifyPointerUp
。
测试:
public class Test : MonoBehaviour, IPointerCounterHandler, IPointerDownHandler, IPointerUpHandler
{
public void OnPointerCounterChanged(int touchCount)
{
Debug.Log("Simple Finger Counter: " + touchCount);
}
public void OnPointerCounterChanged(PointerCounterEventData touchCountData)
{
PointerCounterInfo moreEventData = touchCountData.touchCountData;
Debug.Log("Finger TouchCount: " + moreEventData.touchCount);
Debug.Log("Finger PointerId: " + moreEventData.pointerId);
Debug.Log("Finger Pointer State: " + moreEventData.pointerState);
Debug.Log("Finger Target: " + moreEventData.target.name);
//Can also access PointerEventData
PointerEventData eventData = touchCountData.touchCountData.eventData;
Debug.Log("Click Time!: " + eventData.clickTime);
}
public void OnPointerDown(PointerEventData eventData)
{
PointerCounterEventData.notifyPointerDown(EventSystem.current, eventData, this.gameObject);
}
public void OnPointerUp(PointerEventData eventData)
{
PointerCounterEventData.notifyPointerUp(EventSystem.current, eventData, this.gameObject);
}
}
您没有在 Unity 中执行此操作的预定义方法。您所能做的就是再次以面向对象的方法使用自定义解决方案。最好的办法是拆分事件检测和事件处理。
要问的主要问题是,如何以 OOP 方式表示手指、触摸、手势等。我选择这样做:
- 每当发生指针按下事件时,都会创建一个新手指。
- 创建新手指时,也会创建其与现有手指的所有可能子集的组合。这意味着,当添加第三根手指时,如果将手指标记为
f1, f2, f3
,则创建的手指组合为:f3, f1f3, f2f3, f1f2f3
。这在使用多个手指时提供了最大的灵活性。你可以做手势like this。比如做锚点手势,只需要f2f3
的手势,但f1
的手势也必须存在。在这种情况下,您可以简单地忽略f1
。 - 移动手指时,将创建一个新手势,并使用新创建的手势触发所有组合的更改事件,具体取决于该手指。
还有您通常需要从多点触控事件中获得的信息:
- 手指的平均位置
- 手指集体旋转
- 手指代表的形状的总大小,用于缩放和填充。可以是两根手指之间向量的大小,也可以是多边形的面积
- 所有顶点的位置,如果你想做高级的东西
- 以上所有的变化(增量)
前面的长代码:
using UnityEngine;
using UnityEngine.EventSystems;
using System.Linq;
using System.Collections.Generic;
public class MultitouchHandler : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler {
public List<Finger> Fingers = new List<Finger>();
public List<FingerCombination> FingerCombinations = new List<FingerCombination>();
public FingerCombination GetFingerCombination(params int[] fingerIndices) {
var fc = FingerCombinations.Find(x => x.IDs.Count == fingerIndices.Length && fingerIndices.All(y => x.IDs.Contains(Fingers[y].ID)));
if (fc != null) return fc;
fc = new FingerCombination() {
Fingers = fingerIndices.Select(x => Fingers[x]).ToList()
};
fc.IDs = fc.Fingers.Select(x => x.ID).ToList();
fc.Data = Fingers.Select(x => x.Data).ToList();
fc.PreviousData = Fingers.Select(x => x.Data).ToList();
FingerCombinations.Add(fc);
return fc;
}
public delegate void MultitouchEventHandler(int touchCount, MultitouchHandler sender);
public event MultitouchEventHandler OnFingerAdded;
public event MultitouchEventHandler OnFingerRemoved;
public void OnDrag(PointerEventData eventData) {
var finger = Fingers.Find(x => x.ID == eventData.pointerId);
var fcs = FingerCombinations.Where(x => x.IDs.Contains(eventData.pointerId));
finger.PreviousData = finger.Data;
finger.Data = eventData;
foreach (var fc in fcs) {
fc.PreviousData = fc.Data;
fc.Data = fc.Fingers.Select(x => x.Data).ToList();
fc.PreviousGesture = fc.Gesture;
fc.Gesture = new Gesture() {
Center = fc.Center,
Size = fc.Size,
Angle = fc.Angle,
SizeDelta = 1
};
if (fc.PreviousGesture != null) {
fc.Gesture.CenterDelta = fc.Center - fc.PreviousGesture.Center;
fc.Gesture.SizeDelta = fc.Size / fc.PreviousGesture.Size;
fc.Gesture.AngleDelta = fc.Angle - fc.PreviousGesture.Angle;
}
fc.Changed();
}
}
public void OnPointerDown(PointerEventData eventData) {
var finger = new Finger() { ID = eventData.pointerId, Data = eventData };
Fingers.Add(finger);
if (OnFingerAdded != null)
OnFingerAdded(Fingers.Count, this);
}
public void OnPointerUp(PointerEventData eventData) {
Fingers.RemoveAll(x => x.ID == eventData.pointerId);
if (OnFingerRemoved != null)
OnFingerRemoved(Fingers.Count, this);
var fcs = FingerCombinations.Where(x => x.IDs.Contains(eventData.pointerId));
foreach (var fc in fcs) {
fc.Finished();
}
FingerCombinations.RemoveAll(x => x.IDs.Contains(eventData.pointerId));
}
public class Finger {
public int ID;
public PointerEventData Data;
public PointerEventData PreviousData;
}
public class FingerCombination {
public List<int> IDs = new List<int>();
public List<Finger> Fingers;
public List<PointerEventData> PreviousData;
public List<PointerEventData> Data;
public delegate void GestureEventHandler(Gesture gesture, FingerCombination sender);
public event GestureEventHandler OnChange;
public delegate void GestureEndHandler(FingerCombination sender);
public event GestureEndHandler OnFinish;
public Gesture Gesture;
public Gesture PreviousGesture;
public Vector2 Center
{
get { return Data.Aggregate(Vector2.zero, (x, y) => x + y.position) / Data.Count; }
}
public float Size
{
get
{
if (Data.Count == 1) return 0;
var magnitudeSum = 0f;
for (int i = 1; i < Data.Count; i++) {
var dif = (Data[i].position - Data[0].position);
magnitudeSum += dif.magnitude;
}
return magnitudeSum / (Data.Count - 1);
}
}
public float Angle
{
get
{
if (Data.Count == 1) return 0;
var angleSum = 0f;
for (int i = 1; i < Data.Count; i++) {
var dif = (Data[i].position - Data[0].position);
angleSum += Mathf.Atan2(dif.y, dif.x) * Mathf.Rad2Deg;
}
return angleSum / (Data.Count - 1);
}
}
internal void Changed() {
if (OnChange != null)
OnChange.Invoke(Gesture, this);
}
internal void Finished() {
if (OnFinish != null)
OnFinish.Invoke(this);
}
}
public class Gesture {
public Vector2 Center;
public float Size;
public float Angle;
public Vector2 CenterDelta;
public float SizeDelta;
public float AngleDelta;
}
}
这里有一个例子,展示了如何使用 4 个手指。
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
public class MultiTouchTest : MonoBehaviour {
public Vector2 rectSize = Vector2.one * 2;
public Vector2 skewedRectSize = Vector2.one;
public Vector2 rectPos = Vector2.zero;
public List<Vector3> Fingers = new List<Vector3>();
void Start() {
var h = GetComponent<MultitouchHandler>();
h.OnFingerAdded += OnGestureStart;
}
private void OnGestureStart(int touchCount, MultitouchHandler sender) {
if (touchCount != 4) return;
var fc = sender.GetFingerCombination(0, 1, 2, 3);
fc.OnChange += OnGesture;
}
private void OnGesture(MultitouchHandler.Gesture gesture, MultitouchHandler.FingerCombination sender) {
rectSize *= gesture.SizeDelta;
Fingers = sender.Fingers.Select(x => Camera.main.ScreenToWorldPoint(x.Data.position)).ToList();
var tan = Mathf.Tan(gesture.Angle * Mathf.Deg2Rad);
skewedRectSize = new Vector2(rectSize.x / tan, rectSize.y * tan);
rectPos += gesture.CenterDelta / 50;
}
public void OnDrawGizmos() {
Gizmos.color = Color.red;
Gizmos.DrawCube(rectPos, skewedRectSize);
Gizmos.color = Color.blue;
foreach (var finger in Fingers) Gizmos.DrawSphere(finger + Vector3.forward, 0.5f);
}
}
结果如下:
虽然这只是一个简单的例子。好的答案对于 SO 的格式来说太长了。
我不喜欢回答我自己的问题,但经过大量调查和专家输入,以下是唯一的方法。
让我们总结一下:
1。您实际上必须添加一个守护进程。因此,对于 Pinch,只需将“PinchInputModule.cs”拖放到消费者对象上 - 您就完成了。
您可能会认为“如果不添加守护进程就不能自动工作,真糟糕。”但是 - 事实上,对于 Unity 自己的,你必须添加一个守护进程,即“TouchInput”系列。 (他们有时会自动添加,有时他们会忘记,您必须这样做。)
很简单,自动魔法的“追逐”是愚蠢的,算了吧。你必须添加一个守护进程。
2。您必须从 IPointerDownHandler/etc 横向继承,因为很简单,Unity 搞砸了,您无法在 StandAloneInputModule 中正确继承。复制粘贴不是编程。
很简单,沿着子类化 StandAloneInputModule 的道路走下去并不是好的工程,因为 Unity 搞砸了。您只需在新守护程序中使用 IPointerDownHandler/etc 即可。下面对此进行更多讨论。
下面我给出了“单点触摸”和“捏合”的例子。这些是生产就绪的。您可以为其他情况编写自己的应用程序,例如四点触摸等。因此,使用捏合守护进程(字面意思是将其放在有问题的对象上),处理捏合变得异常容易:
public void OnPinchZoom (float delta)
{
_processPinch(delta);
}
很难看到它更容易。
这样做,直到 Unity 记住它的产品“用于手机”并且他们添加了捏等调用。
做一个立方体,把你自己的脚本放在上面FingerMove
。
制作脚本,说移动相机LR,UD。 (或其他 - 只是 Debug.Log 更改。)
粘贴到这个处理程序脚本中...
SingleFingerInputModule.cs
/*
ISingleFingerHandler - handles strict single-finger down-up-drag
Put this daemon ON TO the game object, with a consumer of the service.
(Note - there are many, many philosophical decisions to make when
implementing touch concepts; just some issues include what happens
when other fingers touch, can you "swap out" etc. Note that, for
example, Apple vs. Android have slightly different takes on this.
If you wanted to implement slightly different "philosophy" you'd
do that in this script.)
*/
public interface ISingleFingerHandler
{
void OnSingleFingerDown (Vector2 position);
void OnSingleFingerUp (Vector2 position);
void OnSingleFingerDrag (Vector2 delta);
}
/* note, Unity chooses to have "one interface for each action"
however here we are dealing with a consistent paradigm ("dragging")
which has three parts; I feel it's better to have one interface
forcing the consumer to have the three calls (no problem if empty) */
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
public class SingleFingerInputModule:MonoBehaviour,
IPointerDownHandler,IPointerUpHandler,IDragHandler
{
private ISingleFingerHandler needsUs = null;
// of course that would be a List,
// just one shown for simplicity in this example code
private int currentSingleFinger = -1;
private int kountFingersDown = 0;
void Awake()
{
needsUs = GetComponent(typeof(ISingleFingerHandler)) as ISingleFingerHandler;
// of course, you may prefer this to search the whole scene,
// just this gameobject shown here for simplicity
// alternately it's a very good approach to have consumers register
// for it. to do so just add a register function to the interface.
}
public void OnPointerDown(PointerEventData data)
{
kountFingersDown = kountFingersDown + 1;
if (currentSingleFinger == -1 && kountFingersDown == 1)
{
currentSingleFinger = data.pointerId;
if (needsUs != null) needsUs.OnSingleFingerDown(data.position);
}
}
public void OnPointerUp (PointerEventData data)
{
kountFingersDown = kountFingersDown - 1;
if ( currentSingleFinger == data.pointerId )
{
currentSingleFinger = -1;
if (needsUs != null) needsUs.OnSingleFingerUp(data.position);
}
}
public void OnDrag (PointerEventData data)
{
if ( currentSingleFinger == data.pointerId && kountFingersDown == 1 )
{
if (needsUs != null) needsUs.OnSingleFingerDrag(data.delta);
}
}
}
将那个守护进程与你的消费者 FingerMove
放在游戏对象上,然后忘记它。现在是
非常简单
处理拖动:
public class FingerMove:MonoBehaviour, ISingleFingerHandler
{
public void OnSingleFingerDown(Vector2 position) {}
public void OnSingleFingerUp (Vector2 position) {}
public void OnSingleFingerDrag (Vector2 delta)
{
_processSwipe(delta);
}
private void _processSwipe(Vector2 screenTravel)
{
.. move the camera or whatever ..
}
}
就像我说的,
非常简单!
现在让我们考虑两个手指的情况,捏到zoom/unzoom。
PinchInputModule.cs
/*
IPinchHandler - strict two sequential finger pinch Handling
Put this daemon ON TO the game object, with a consumer of the service.
(Note, as always, the "philosophy" of a glass gesture is up to you.
There are many, many subtle questions; eg should extra fingers block,
can you 'swap primary' etc etc etc - program it as you wish.)
*/
public interface IPinchHandler
{
void OnPinchStart ();
void OnPinchEnd ();
void OnPinchZoom (float gapDelta);
}
/* note, Unity chooses to have "one interface for each action"
however here we are dealing with a consistent paradigm ("pinching")
which has three parts; I feel it's better to have one interface
forcing the consumer to have the three calls (no problem if empty) */
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
public class PinchInputModule:MonoBehaviour,
IPointerDownHandler,IPointerUpHandler,IDragHandler
{
private IPinchHandler needsUs = null;
// of course that would be a List,
// just one shown for simplicity in this example code
private int currentFirstFinger = -1;
private int currentSecondFinger = -1;
private int kountFingersDown = 0;
private bool pinching = false;
private Vector2 positionFirst = Vector2.zero;
private Vector2 positionSecond = Vector2.zero;
private float previousDistance = 0f;
private float delta = 0f;
void Awake()
{
needsUs = GetComponent(typeof(IPinchHandler)) as IPinchHandler;
// of course, this could search the whole scene,
// just this gameobject shown here for simplicity
}
public void OnPointerDown(PointerEventData data)
{
kountFingersDown = kountFingersDown + 1;
if (currentFirstFinger == -1 && kountFingersDown == 1)
{
// first finger must be a pure first finger and that's that
currentFirstFinger = data.pointerId;
positionFirst = data.position;
return;
}
if (currentFirstFinger != -1 && currentSecondFinger == -1 && kountFingersDown == 2)
{
// second finger must be a pure second finger and that's that
currentSecondFinger = data.pointerId;
positionSecond = data.position;
FigureDelta();
pinching = true;
if (needsUs != null) needsUs.OnPinchStart();
return;
}
}
public void OnPointerUp (PointerEventData data)
{
kountFingersDown = kountFingersDown - 1;
if ( currentFirstFinger == data.pointerId )
{
currentFirstFinger = -1;
if (pinching)
{
pinching = false;
if (needsUs != null) needsUs.OnPinchEnd();
}
}
if ( currentSecondFinger == data.pointerId )
{
currentSecondFinger = -1;
if (pinching)
{
pinching = false;
if (needsUs != null) needsUs.OnPinchEnd();
}
}
}
public void OnDrag (PointerEventData data)
{
if ( currentFirstFinger == data.pointerId )
{
positionFirst = data.position;
FigureDelta();
}
if ( currentSecondFinger == data.pointerId )
{
positionSecond = data.position;
FigureDelta();
}
if (pinching)
{
if ( data.pointerId == currentFirstFinger || data.pointerId == currentSecondFinger )
{
if (kountFingersDown==2)
{
if (needsUs != null) needsUs.OnPinchZoom(delta);
}
return;
}
}
}
private void FigureDelta()
{
float newDistance = Vector2.Distance(positionFirst, positionSecond);
delta = newDistance - previousDistance;
previousDistance = newDistance;
}
}
将该守护进程放在游戏对象上,您有服务的消费者。注意,“混搭”绝对没有问题。在此示例中,让我们同时使用拖动和捏合手势。现在是
非常简单
处理夹点:
public class FingerMove:MonoBehaviour, ISingleFingerHandler, IPinchHandler
{
public void OnSingleFingerDown(Vector2 position) {}
public void OnSingleFingerUp (Vector2 position) {}
public void OnSingleFingerDrag (Vector2 delta)
{
_processSwipe(delta);
}
public void OnPinchStart () {}
public void OnPinchEnd () {}
public void OnPinchZoom (float delta)
{
_processPinch(delta);
}
private void _processSwipe(Vector2 screenTravel)
{
.. handle drag (perhaps move LR/UD)
}
private void _processPinch(float delta)
{
.. handle zooming (perhaps move camera in-and-out)
}
}
就像我说的,
非常简单! :)
要了解这有多优雅,请考虑以下问题:捏合时,您希望“暂停”拖动,还是让两者都发生?令人惊奇的是,您只需在 SingleFingerInputModule.cs 中对其进行编程即可。在具体的例子中,我希望它“按住”拖动,while/if 用户正在缩放,所以上面的 SingleFingerInputModule.cs 就是这样编程的。您可以轻松修改它以进行持续拖动、更改为质心、取消拖动或任何您想要的操作。令人惊奇的是,FingerMove.cs 完全没有受到影响!非常有用的抽象!
请注意,对于上面 Gökhan 出色的四角示例,我会这样写:
public class FingerStretch:MonoBehaviour, IFourCornerHandler
{
public void OnFourCornerChange (Vector2 a, b, c, d)
{
... amazingly elegant solution
... Gökhan does all the work in FourCornerInputModule.cs
... here I just subscribe to it. amazingly simple
}
这只是一个令人难以置信的简单方法。
这太简单了:O
Gökhan 会将手指的所有逻辑封装在 FourCornerInputModule.cs 中,它有一个接口 IFourCornerHandler。请注意,FourCornerInputModule 会明智地做出所有哲学决定(例如,您必须将所有四个手指都放下,如果您有一个额外的手指,等等)。
出现的问题如下:
1。我们应该做“类似事件系统”的编程吗?
在所谓的“独立输入模块”处查看您的 Unity 项目,这是一个带有 EventSystem
和 StandAloneInputModule
您实际上可以“从头开始编写”类似 SingleFingerInputModule.cs 或 PinchInputModule.cs、 的东西,这样它就可以“像”Unity 的 StandAloneInputModule 一样工作。
虽然很难做到这一点,但请注意评论中的链接
但是有一个特定的、可解决的问题:可笑的是,你不能使用 OO 原则:在我上面的 SingleFingerInputModule.cs
代码中,我们非常明智地- 当然 - 使用 Unity 已经完成的现有惊人的 IPointerDownHandler 等功能,我们(基本上)“子类化”,添加更多逻辑。这正是你应该做的,真的必须做的。相比之下:如果您确实决定“制作像 StandAloneInputModule 一样工作的东西”,那将是一场惨败 - 您必须重新开始,可能会复制和粘贴 Unity 的源代码(用于 IPointerDownHandler 等)并按照您的方式稍微修改它,这这当然是一个你永远不应该做软件工程的确切例子。
2。但是你必须'remember to add the daemon'?
请注意,如果您选择“制作类似于 StandAloneInputModule 的东西”路线,实际上,您仍然必须这样做!!!!!!!这有点奇怪;优势为零。
3。您必须给所有订阅者打电话吗?
如果你走“做一些像 StandAloneInputModule 一样工作的东西”的路线,Unity 有一个“执行......”调用......就是这样做的。它只不过是一个用于“调用订阅者”的宏(除了最琐碎的脚本之外,我们每天都在每个脚本中这样做);没有优势。
事实上:我个人认为 进行订阅调用 、
综上所述,
我开始相信
(1) 实际上,建立在 IPointerDownHandler、IDragHandler、IPointerUpHandler 上绝对是正确的方法
开始重写代码以“制作像 StandAloneInputModule 一样工作的东西”无疑是一个坏主意
(2) 必须添加守护进程没有错
如果你试图“制作像 StandAloneInputModule 一样工作的东西”......你无论如何都必须“记住添加它”,看在上帝的份上。
(3) 寻找消费者完全没有错,或者更好的是,有一个订阅电话
如果您尝试“制作像 StandAloneInputModule 一样工作的东西”,那么 Unity 给您的“执行...”调用几乎不存在优势,这是一行代码与您的代码(更短,更清晰,更快)的代码行来“呼叫订阅者”。同样,简单地
所以对我来说,今天在 Unity 中最好的方法就是写 modules/interfaces 例如 SingleFingerInputModule.cs、PinchInputModule.cs、FourCornerInputModule.cs,把它放在游戏对象上你想拥有那些消费者 - 你就完成了。 “就这么简单。”
public class Zoom:MonoBehaviour, ISingleFingerHandler, IPinchHandler
{
public void OnPinchZoom (float delta)
{
...