Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pinch, and other multi-finger gestures, in modern Unity3D?

Tags:

c#

unity3d

touch

In modern Unity3D, we use the IPointerDownHandler family of calls.

Regarding the IPointerDownHandler family of calls,

public class FingerMove:MonoBehaviour, IPointerDownHandler...
    {
    public void OnPointerDown (PointerEventData data)
        {

Of course they are fantastic

for dealing with single touches.

But how do you deal with multiple touches in a serious way?

You can "do it all by hand" tracking the touches yourself, but it seems incredible Unity would want you to do that for something so absolutely basic. (I mean - it's a game engine. Sure, I could also write all my own rendering and physics!)

Here's an example of basically "cowboy programming", just doing it by hand with no software engineering. What's the real solution?

//
// 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;
        }
    
    }

(Note, please do not answer regarding the legacy "Touches" system which is unusable. This about normal modern Unity development.)

like image 444
Fattie Avatar asked Oct 31 '16 11:10

Fattie


3 Answers

I don't like to answer my own questions, but after much investigation and expert input, the following is the only way to do it.


Let's summarize:

1. You DO IN FACT have to add a daemon. So, for Pinch, just drop "PinchInputModule.cs" on to the consumer object - and you are done.

You might think "it sucks it doesn't work automagically without adding a daemon." BUT - in fact, with Unity's own you have to add a daemon, the "TouchInput" family. (Which they sometimes automatically add, sometimes they forget and you have to do it.)

So quite simply, the "chase" for automagic is silly, forget it. You have to add a daemon.

2. You DO have to inherit sideways from IPointerDownHandler/etc, because, quite simply, Unity messed-up and you can't inherit properly in StandAloneInputModule. Copying and pasting is not programming.

Quite simply, it's not good engineering to go down the path of subclassing StandAloneInputModule, since Unity messed-up. You simply use IPointerDownHandler/etc in your new daemons. More discussion on this below.

Below I give examples for "single touch" and for "pinch". These are production-ready. You can write your own for other situations such as four-touch etc. So, with the pinch-daemon (literally just drop it on the object in question), it's then insanely easy to handle pinches:

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

Difficult to see it being any easier.

So do this, until Unity remembers it's product is "used on phones" and they add calls for pinch, etc.


Make a cube, put your own script on it FingerMove.

Make the script, say move the camera LR, UD. (Or whatever - just Debug.Log the changes.)

Paste in this handler script...

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);
            }
        }
    
    }

Put that daemon onto the game object, with your consumer FingerMove, and forget about it. It is now

ridiculously easy

to handle dragging:

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

like I said,

ridiculously easy!

Now let's think about the two finger case, a pinch to 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;
        }
    
    }

Put that daemon ON TO the game object, where you have a consumer of the service. Note that there is absolutely no problem with "mixing and matching". In this example, let's have BOTH a drag and pinch gesture. It is now

just stupidly easy

to handle pinch:

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

Like I say,

stupidly easy! :)

To see how elegant this is consider issues such as this: when pinching, do you want that to "pause" dragging, or let both happen? The amazing thing is, you simply program that inside SingleFingerInputModule.cs. In the specific example, I wanted it to "hold" dragging, while/if the user is zooming, so SingleFingerInputModule.cs above is programmed that way. You can easily modify it to have keep-going dragging, change to the centroid, cancel dragging, or whatever you want. The amazing thing is that FingerMove.cs is not affected at all! Incredibly useful abstraction!

Note that for Gökhan's excellent four-corner example above, I would write it like this:

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
        }

Which is just a mind-bogglingly simple approach.

That is mind-bogglingly simple :O

Gökhan would encapsulate all the logic for the fingers inside FourCornerInputModule.cs which would have an interface IFourCornerHandler. Note that FourCornerInputModule would sensibly make all the philosophical decisions (example, must you have all four fingers down, what if you have one extra, etc etc).

Here are the issues arising:

1. Should we do "Event-system like" programming?

Look at your Unity project at the so-called "stand alone input module" which is a game object with a EventSystem and a StandAloneInputModule

You can in fact "write from scratch" something like SingleFingerInputModule.cs or PinchInputModule.cs, so that it will "work like" Unity's StandAloneInputModule.

While difficult this can be done, note the links in the comments in this answer.

But there is a specific, knock-down problem: ridiculously, you can't use OO principles: in my code above for SingleFingerInputModule.cs, we very sensibly - of course - use the existing amazing IPointerDownHandler etc. power which Unity has done already and we (essentially) "subclass that", adding a little more logic. Which is exactly what you should, really must, do. In contrast: if you do decide to "make something that works like StandAloneInputModule", it's a fiasco - you have to start again, likely copying and pasting Unity's source code (for IPointerDownHandler etc) and sort of modifying it a bit your way, which is of course an exact example of how you should never do software engineering.

2. But you have to 'remember to add the daemon'?

Note that if you go the "make something that works like StandAloneInputModule" route, in fact, you still have to do that!!!!! Which is somewhat bizarre; there is zero advantage.

3. You have to call all the subscribers?

If you go the "make something that works like StandAloneInputModule" route, Unity have an "Execute...." call which ... does just that. It's little more than a macro for just "calling the subscribers" (which we all do every day in every script other than the most trivial); no advantage.

In fact: I personally believe it's actually far better to have a subscribe call, as Everts suggests here, just have it as one of the interface calls. I just think that's far better engineering than trying to "be like" Unity's whacky magic-call system (which really doesn't work at all anyway - you have to "remember to attach" a StandAloneInputModule anyway).

In summary,

I have come to believe that

(1) building on IPointerDownHandler, IDragHandler, IPointerUpHandler is, in fact, definitely the correct approach

It's unarguably a bad idea to start re-writing code to "make something that works like StandAloneInputModule"

(2) there's nothing wrong with having to add a daemon

If you try to "make something that works like StandAloneInputModule" ... you have to "remember to add it" anyway, for goodness sake.

(3) there's nothing at all wrong with finding consumers, or better, having a subscribe call

If you try to "make something that works like StandAloneInputModule" there's the almost non-existent advantage of the "Execute..." call which Unity gives you, which is one line of code versus your one (shorter, clearer, faster) line of code to "call the subscriber". Again it's just far more obvious and clear to simply have a subscribe call, you could say any non-Unity programmer would simply do that.

So for me, the best approach in Unity today is just write modules/interfaces such as SingleFingerInputModule.cs, PinchInputModule.cs, FourCornerInputModule.cs, drop it on the game object where you want to have a consumer of those - and you're done. "It's that simple."

public class Zoom:MonoBehaviour, ISingleFingerHandler, IPinchHandler
    {
    public void OnPinchZoom (float delta)
        {
        ...
like image 151
Fattie Avatar answered Nov 03 '22 01:11

Fattie


You have no predefined way to do this in Unity. All you can do is, again, going with a custom solution, in an object oriented approach. Best thing to do would be splitting event detecting and event handling.

The main question to ask is, how to represent the fingers, touch, gesture etc. in an OOP manner. I choose to do it like so:

  • Whenever a pointer down event occurs, a new finger is created.
  • When a new finger is created, its combination with all possible subset of existing fingers is also created. This means, when a third finger is added, if you label the fingers as f1, f2, f3, created finger combinatons are: f3, f1f3, f2f3, f1f2f3. This gives the ultimate flexibility when working with multiple fingers. You can do gestures like this. For example, if you want to do the anchor gesture, you only need gestures of f2f3 but f1 must also exists. You can simply ignore f1 in that case.
  • When a finger is moved, a new gesture is created and change event of all combinations depending on that finger is fired with new created gesture.

Also what you generally need from a multi-touch event:

  • Mean position of fingers
  • Collective rotation of fingers
  • Collective size of the shape represented by fingers, for scaling and stuff. It can be the magnitude of the vector between 2 fingers, or the area of the polygon
  • Position of all vertices, if you want to do advanced stuff
  • Change (delta) of all the above

Long code ahead:

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

Here is an example showing how it is used with 4 fingers.

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);
    }
}

And the result looks like:

Result

This is only a simple example though. A good answer would be too long for the format of SO.

like image 31
Gokhan Kurt Avatar answered Nov 03 '22 00:11

Gokhan Kurt


Implementing this is not really complicated.

Use a List and store the pointerId each time there is a OnPointerDown event then increment touchCount variable. Don't store pointerId on OnPointerDown if it already exist in the List.

When OnPointerUp is called or when there is a release, check if the pointerId exist. If it does, decrement the touchCount variable. If it does not exist in the List then don't decrement anything.

1.Very Simple Implementation:

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.The first example is very simple but it can be improved with our own interface.

This method uses two scripts:

IPointerCounterHandler.cs interface:

public interface IPointerCounterHandler : IEventSystemHandler
{
    void OnPointerCounterChanged(int touchCount);
    void OnPointerCounterChanged(PointerCounterEventData touchCountData);
}

PointerCounterEventData.cs script.

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

Usage:

Implement IPointerCounterHandler in your script then override the

void OnPointerCounterChanged(int touchCount); and

void OnPointerCounterChanged(PointerCounterEventData touchCountData); functions.

Finally, call PointerCounterEventData.notifyPointerDown in the OnPointerDown function and also call PointerCounterEventData.notifyPointerUp in the OnPointerUp function.

Test:

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);
    }
}
like image 2
Programmer Avatar answered Nov 03 '22 01:11

Programmer