在现代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开发。)

我不想回答我自己的问题,但经过大量调查和专家意见后,以下是唯一的方法。


让我们总结一下:

你事实上必须添加一个守护进程。 因此,对于Pinch,只需将“PinchInputModule.cs”拖放到使用者对象上 – 您就完成了。

您可能会认为“如果不添加守护程序,它就不会自动运行”。 但实际上,使用Unity自己,你必须添加一个守护进程,即“TouchInput”系列。 (他们有时会自动添加,有时他们会忘记,你必须这样做。)

很简单,自动化的“追逐”是愚蠢的,忘了它。 你必须添加一个守护进程。

2.你必须从IPointerDownHandler / etcinheritance,因为很简单,Unity搞砸了,你不能在StandAloneInputModule中正确inheritance。 复制和粘贴不是编程。

很简单,由于Unity搞砸了,所以沿着inheritanceStandAloneInputModule的路径走上去并不是一件好事。 您只需在新守护进程中使用IPointerDownHandler / etc。 关于此的更多讨论。

下面我举例说明“单触”和“捏”。 这些都是生产就绪的。 您可以编写自己的其他情况,例如四触式等。因此,使用pinch-daemon(字面意思只是将其放在有问题的对象上),然后它非常容易处理pinches:

 public void OnPinchZoom (float delta) { _processPinch(delta); } 

很难看到它更容易。

所以这样做,直到Unity记得它的产品“在手机上使用”并且他们添加了捏等的电话。


制作一个立方体,将自己的脚本放在FingerMove

制作剧本,比如移动相机LR,UD。 (或者其他 – 只需调试。记录更改。)

粘贴在此处理程序脚本中…

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 .. } } 

就像我说的,

太可笑了!

现在让我们考虑两个手指的情况,一个缩放缩放/解除缩放。

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中编程即可。 在特定示例中,我希望它“保持”拖动,而/如果用户正在缩放,那么上面的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项目,该模块是一个带有EventSystemStandAloneInputModule的游戏对象

实际上,您可以“从头开始编写”类似SingleFingerInputModule.cs或PinchInputModule.cs的内容, 这样它就可以“像Unity”的StandAloneInputModule一样工作

虽然很难做到这一点,但请注意本答案中评论中的链接。

但是有一个具体的, SingleFingerInputModule.cs 问题 :荒谬的是,你不能使用OO原则:在我上面的SingleFingerInputModule.cs代码中,我们非常明智 – 当然 – 使用Unity已经完成的现有惊人的IPointerDownHandler等function而我们(基本上)是“子类”,增加了一点逻辑。 这正是你应该,真的必须做的。 相反:如果你决定“制作像StandAloneInputModule一样的东西”,那就是惨败 – 你必须重新开始,可能会复制并粘贴Unity的源代码(对于IPointerDownHandler等),并根据你的方式修改它,当然是你永远不应该做软件工程的一个确切的例子。

但你必须“记得添加守护进程”吗?

请注意,如果你去“制作像StandAloneInputModule一样的东西”的路线,事实上,你仍然必须这样做! 这有点奇怪; 没有优势。

你必须给所有订户打电话?

如果你去“制作像StandAloneInputModule一样工作的东西”的路线,Unity就会有一个“执行……”调用……就这样做。 它仅仅是一个用于“呼叫订阅者”的宏(我们每天都在每个脚本中执行除了最微不足道的事情之外); 没有优势。

事实上:我个人认为订阅电话实际上要好得多, 正如Everts在这里建议的那样 ,只需将其作为接口调用之一。 我只是认为这比试图“变得像Unity”的魔术召唤系统(它实际上根本不起作用)更好的工程 – 你必须“记住附加”StandAloneInputModule无论如何)。

综上所述,

我开始相信了

(1)建立在IPointerDownHandler,IDragHandler,IPointerUpHandler上,实际上肯定是正确的方法

无可争议的是,开始重写代码以“制作像StandAloneInputModule一样的东西”是一个坏主意。

(2)必须添加一个守护进程没有任何问题

如果你试图“制作像StandAloneInputModule一样的东西”……你必须“记得加上它”,为了善良。

(3)找到消费者或者更好的是有订阅呼叫没有任何错误

如果你试图“制作像StandAloneInputModule一样的东西”,那么Unity给你的“执行…”调用几乎不存在优势,这是一行代码而不是你的代码(更短,更清晰,更快)代码“呼叫用户”。 再简单地进行订阅调用就更加明显和明确,你可以说任何非Unity程序员都会这样做。

所以对我来说,今天Unity中最好的方法就是编写模块/接口,例如SingleFingerInputModule.cs,PinchInputModule.cs,FourCornerInputModule.cs,将它放在游戏对象上,你想拥有那些消费者 – 你就是完成。 “就这么简单。”

 public class Zoom:MonoBehaviour, ISingleFingerHandler, IPinchHandler { public void OnPinchZoom (float delta) { ... 

实现这一点并不复杂。

每次有OnPointerDown事件时,使用List并存储OnPointerDown然后递增touchCount变量。 如果它已存在于List ,则不要将pointerId存储在pointerId上。

调用OnPointerUp或有释放时,检查pointerId存在。 如果是,则减少touchCount变量。 如果List存在,则不减少任何内容。

1非常简单的实施

 public class FingerMove : MonoBehaviour, IPointerDownHandler, IPointerUpHandler { public int touchCount; public List touchID = new List(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 counterChangedV1Delegate = delegate (IPointerCounterHandler handler, BaseEventData data) { //var casted = ExecuteEvents.ValidateEventData(data); handler.OnPointerCounterChanged(touchCount); }; //The callback with PointerCounterEventData parameter public static readonly ExecuteEvents.EventFunction counterChangedV2Delegate = delegate (IPointerCounterHandler handler, BaseEventData data) { var casted = ExecuteEvents.ValidateEventData(data); handler.OnPointerCounterChanged(casted); }; public static int touchCount = 0; public PointerCounterInfo touchCountData = new PointerCounterInfo(); public static List touchID = new List(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( target, eventParam1, PointerCounterEventData.counterChangedV1Delegate); //////////////////////Call the PointerCounterEventData parameter////////////////////// PointerCounterEventData eventParam2 = new PointerCounterEventData( eventSystem, eventData, pointerStat, target); ExecuteEvents.Execute( 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); function。

最后,在OnPointerDown函数中调用PointerCounterEventData.notifyPointerUp ,并在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 。 当使用多个手指时,这提供了极大的灵活性。 你可以做这样的手势。 例如,如果要执行锚定手势,则只需要f2f3手势,但f1也必须存在。 在这种情况下,你可以简单地忽略f1
  • 当移动手指时,创建新手势并且使用新创建的手势触发取决于该手指的所有组合的改变事件。

您通常还需要多点触控事件:

  • 手指的平均位置
  • 手指的集体旋转
  • 由手指表示的形状的集体大小,用于缩放和填充。 它可以是两个手指之间的矢量的大小,也可以是多边形的面积
  • 所有顶点的位置,如果你想做高级的东西
  • 所有上述变化(delta)

提前代码长:

 using UnityEngine; using UnityEngine.EventSystems; using System.Linq; using System.Collections.Generic; public class MultitouchHandler : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler { public List Fingers = new List(); public List FingerCombinations = new List(); 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 IDs = new List(); public List Fingers; public List PreviousData; public List 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 Fingers = new List(); void Start() { var h = GetComponent(); 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的格式,一个好的答案太长了。

The unity input system is not yet perfect. You have to track the touches yourself using the low level system for now. See an example here .