Пинч и другие жесты с несколькими пальцами, в современном 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;
        }

    }

(Обратите внимание, пожалуйста, не отвечайте в отношении устаревшей системы "Прикосновения", которая непригодна для использования. Это касается нормального современного развития Unity.)

Ответ 1

Я не люблю отвечать на свои вопросы, но после большого расследования и экспертного ввода, единственный способ сделать это.


Обобщите:

1. В DOACT необходимо добавить демона. Итак, для Pinch просто снимите "PinchInputModule.cs" на потребительский объект - и все готово.

Вы можете подумать, что "это отстой, он не работает автоматически, не добавляя демона". НО - на самом деле, с Unity own вы должны добавить демона, семейства TouchInput. (Которые они иногда автоматически добавляют, иногда они забывают, и вы должны это делать.)

Так просто, "преследование" для автоматов глупо, забыть. Вы должны добавить демона.

2. Вы должны наследовать сторону от IPointerDownHandler/etc, потому что, просто, Unity испорчен, и вы не можете правильно наследовать в StandAloneInputModule. Копирование и вставка - это не программирование.

Проще говоря, это не очень хорошая инженерия, чтобы спуститься по пути подкласса StandAloneInputModule, поскольку Unity испортился. Вы просто используете IPointerDownHandler/etc в своих новых демонах. Подробнее об этом ниже.

Ниже приводятся примеры для "одного касания" и для "щепотки". Они готовы к производству. Вы можете написать свой собственный для других ситуаций, таких как четыре касания и т.д. Таким образом, с помощью пинч-демона (буквально просто бросайте его на объект, о котором идет речь), тогда он безумно легко обрабатывает пинки:

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

Сложно видеть, что это легче.

Так сделайте это, пока Unity не вспомнит, что продукт "используется на телефонах", и они добавляют призывы к щепотку и т.д.


Сделайте куб, положите на него script FingerMove.

Сделайте script, скажем, переместите камеру LR, UD. (Или что угодно - просто Debug.Log изменения.)

Вставить в этот обработчик 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 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 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 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 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 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 в так называемом "автономном модуле ввода", который является игровым объектом с EventSystem и StandAloneInputModule

На самом деле вы можете "написать с нуля" что-то вроде SingleFingerInputModule.cs или PinchInputModule.cs, , чтобы он "работал как Unity StandAloneInputModule.

Хотя это трудно сделать, обратите внимание на ссылки в комментариях в этом ответе.

Но существует конкретная проблема с нокаутом: смешно, вы не можете использовать принципы OO: в моем коде выше для SingleFingerInputModule.cs мы очень разумно, конечно, используем существующую удивительную IPointerDownHandler и т.д., которую Unity уже сделал, и мы (по существу) "подкласс", добавив немного больше логики. Это именно то, что вы должны, действительно должны делать. В противоположность этому: если вы решите "сделать что-то, что работает, как StandAloneInputModule", это фиаско - вам нужно начать снова, скорее всего, скопировать и вставить исходный код Unity (для IPointerDownHandler и т.д.) И немного изменить его путь, который конечно, является точным примером того, как вы никогда не должны заниматься разработкой программного обеспечения.

2. Но вы должны "не забыть добавить демона"?

Обратите внимание, что если вы идете на "сделать что-то, что работает, как StandAloneInputModule", на самом деле вам все равно придется это делать!!!!! Это несколько странно; есть нулевое преимущество.

3. Вам нужно позвонить всем подписчикам?

Если вы идете по пути "сделать что-то, что работает, как StandAloneInputModule", у Unity есть вызов "Execute....", который... делает именно это. Это немного больше, чем макрос для просто "вызова подписчиков" (который мы все делаем каждый день в каждом script, кроме самого тривиального); нет преимущества.

На самом деле: я лично считаю, что на самом деле гораздо лучше иметь подписку, как предлагает Everts, просто попросите ее вызовов интерфейса. Я просто думаю, что намного лучше, чем пытаться "походить" на Unity whacky magic-call system (которая действительно вообще не работает вообще), вы должны "не забудьте прикрепить" StandAloneInputModule в любом случае).

Таким образом,

Я пришел к выводу, что

(1) построение на IPointerDownHandler, IDragHandler, IPointerUpHandler - это, безусловно, правильный подход

Это неоспоримо плохая идея, чтобы начать заново писать код "делать то, что работает, как StandAloneInputModule"

(2) нет ничего плохого в том, что нужно добавить демона

Если вы попытаетесь "сделать что-то, что работает, как StandAloneInputModule"... вы должны "не забудьте добавить его" в любом случае, ради всего святого.

(3) нет ничего плохого в поиске потребителей, или, лучше, с подпиской на звонок

Если вы пытаетесь "сделать что-то, что работает как StandAloneInputModule", там почти несуществующее преимущество вызова "Execute...", которое дает вам Unity, которое является одной строкой кода по сравнению с вашей (короче, яснее, быстрее) строка кода для "вызова абонента". Опять же, гораздо более очевидным и понятным просто иметь подписку на звонок, можно сказать, что любой программист, не относящийся к Unity, просто сделает это.

Итак, для меня лучший подход в Unity сегодня - это просто написать модули/интерфейсы, такие как SingleFingerInputModule.cs, PinchInputModule.cs, FourCornerInputModule.cs, отбросить его на игровом объекте, где вы хотите иметь своего потребителя, - и все готово. "Это так просто".

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

Ответ 2

Реализация этого не очень сложна.

Используйте List и сохраняйте pointerId каждый раз, когда есть событие OnPointerDown, а затем увеличивайте touchCount. Не храните pointerId в OnPointerDown, если он уже существует в List.

Когда вызывается OnPointerUp или когда есть релиз, проверьте, существует ли pointerId. Если это так, уменьшите значение переменной touchCount. Если он не существует в 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 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;
}

Использование

Внесите IPointerCounterHandler в script, затем переопределите

void OnPointerCounterChanged(int touchCount); и

void OnPointerCounterChanged(PointerCounterEventData touchCountData);.

Наконец, вызовите PointerCounterEventData.notifyPointerDown в функции OnPointerDown, а также вызовите PointerCounterEventData.notifyPointerUp в функции OnPointerUp.

Тест:

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

Ответ 3

Система ввода единства еще не идеальна. На данный момент вы должны отслеживать свои приказы, используя систему низкого уровня. См. пример здесь.

Ответ 4

У вас нет предопределенного способа сделать это в Unity. Все, что вы можете сделать, это снова использовать индивидуальное решение в объектно-ориентированном подходе. Лучше всего было бы разбивать обнаружение событий и обработку событий.

Основной вопрос: спросить, как изображать пальцы, касание, жест и т.д. в порядке ООП. Я решил сделать это так:

  • Всякий раз, когда происходит событие указателя вниз, создается новый палец.
  • Когда создается новый палец, его комбинация со всем возможным подмножеством существующих пальцев также создается. Это означает, что при добавлении третьего пальца, если вы обозначаете пальцы как f1, f2, f3, созданные комбинаты пальцев: f3, f1f3, f2f3, f1f2f3. Это дает максимальную гибкость при работе с несколькими пальцами. Вы можете делать жесты как это. Например, если вы хотите сделать жест привязки, вам нужны только жесты f2f3, но также должен существовать f1. В этом случае вы можете просто игнорировать f1.
  • При перемещении пальца создается новый жест и изменение события всех комбинаций в зависимости от того, что этот палец запускается с новым созданным жестом.

Также, что вам обычно нужно от события multi-touch:

  • Среднее положение пальцев
  • Коллективное вращение пальцев
  • Коллективный размер формы, представленной пальцами, для масштабирования и прочего. Это может быть величина вектора между двумя пальцами или площадь многоугольника
  • Расположение всех вершин, если вы хотите сделать продвинутый материал
  • Изменение (дельта) всех вышеперечисленных

Длинный код впереди:

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.