akako
5/18/2016 - 12:12 PM

Touch gesture detector for Unity.

Touch gesture detector for Unity.

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Events;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// Touch gesture detector for Unity.
/// </summary>
/// <author>KAKO Akihito</author>
/// <email>kako@qnote.co.jp</email>
/// <license>MIT License</license>
public class TouchGestureDetector : MonoBehaviour
{
    private const float FLICK_TIME_LIMIT = 0.3f;

    public enum Gesture
    {
        TouchBegin,
        TouchMove,
        TouchStationary,
        TouchEnd,
        Click,
        FlickTopToBottom,
        FlickBottomToTop,
        FlickLeftToRight,
        FlickRightToLeft,
    }

    public Camera shootingCamera;
    public bool hitCheck = true;
    public bool detectFlick = true;
    public GestureDetectorEvent onGestureDetected = new GestureDetectorEvent();
    private List<TouchInfo> touchInfos = new List<TouchInfo>();

    private void Awake()
    {
        if (null == shootingCamera)
        {
            shootingCamera = Camera.main;
        }
    }

    private void Update()
    {
        foreach (var touch in Input.touches)
        {
            switch (touch.phase)
            {
                case TouchPhase.Began:
                    OnTouchBegin(touch.fingerId, touch.position);
                    break;
                case TouchPhase.Moved:
                    OnTouchMove(touch.fingerId, touch.position);
                    break;
                case TouchPhase.Stationary:
                    OnTouchStationary(touch.fingerId);
                    break;
                case TouchPhase.Canceled:
                case TouchPhase.Ended:
                    OnTouchEnd(touch.fingerId, touch.position);
                    break;
            }
        }
        if (Input.touchCount == 0)
        {
            if (Input.GetMouseButtonDown(0))
            {
                OnTouchBegin(Int32.MaxValue, Input.mousePosition);
            }
            else if (Input.GetMouseButtonUp(0))
            {
                OnTouchEnd(Int32.MaxValue, Input.mousePosition);
            }
            else
            {
                OnTouchMove(Int32.MaxValue, Input.mousePosition);
            }
        }
    }

    private void OnTouchBegin(int fingerId, Vector2 position)
    {
        var touchInfo = new TouchInfo(fingerId, position);
        if (!hitCheck || touchInfo.IsHit(gameObject, shootingCamera))
        {
            touchInfos.Add(touchInfo);
            OnGestureDetected(Gesture.TouchBegin, touchInfo);
        }
    }

    private void OnTouchMove(int fingerId, Vector2 position)
    {
        var touchInfo = touchInfos.FirstOrDefault(x => x.fingerId == fingerId);
        if (null == touchInfo)
        {
            return;
        }
        touchInfo.AddPosition(position);
        OnGestureDetected(Gesture.TouchMove, touchInfo);
    }

    private void OnTouchStationary(int fingerId)
    {
        var touchInfo = touchInfos.FirstOrDefault(x => x.fingerId == fingerId);
        if (null == touchInfo)
        {
            return;
        }
        OnGestureDetected(Gesture.TouchStationary, touchInfo);
    }

    private void OnTouchEnd(int fingerId, Vector2 position)
    {
        var touchInfo = touchInfos.FirstOrDefault(x => x.fingerId == fingerId);
        if (null == touchInfo)
        {
            return;
        }
        touchInfo.AddPosition(position);
        OnGestureDetected(Gesture.TouchEnd, touchInfo);

        var diff = touchInfo.Diff;
        var flickDistanceLimit = (float)Math.Min(Screen.width, Screen.height) / 10;
        if (detectFlick && touchInfo.ElapsedTime < FLICK_TIME_LIMIT && (Mathf.Abs(diff.x) > flickDistanceLimit || Mathf.Abs(diff.y) > flickDistanceLimit))
        {
            if (Mathf.Abs(diff.x) > Mathf.Abs(diff.y))
            {
                if (diff.x < 0f)
                {
                    OnGestureDetected(Gesture.FlickRightToLeft, touchInfo);
                }
                else
                {
                    OnGestureDetected(Gesture.FlickLeftToRight, touchInfo);
                }
            }
            else
            {
                if (diff.y < 0f)
                {
                    OnGestureDetected(Gesture.FlickTopToBottom, touchInfo);
                }
                else
                {
                    OnGestureDetected(Gesture.FlickBottomToTop, touchInfo);
                }
            }
        }
        else if (hitCheck && touchInfo.IsHit(gameObject, shootingCamera))
        {
            OnGestureDetected(Gesture.Click, touchInfo);
        }

        touchInfos.RemoveAll(x => x.fingerId == fingerId);
    }

    private void OnGestureDetected(Gesture gesture, TouchInfo touchInfo)
    {
        onGestureDetected.Invoke(gesture, touchInfo);
    }

    public class TouchInfo
    {
        public Vector2 Diff
        {
            get
            {
                return new Vector2(positions.Last().x - positions.First().x, positions.Last().y - positions.First().y);
            }
        }

        public float ElapsedTime
        {
            get
            {
                return Time.realtimeSinceStartup - startTime;
            }
        }

        public readonly int fingerId;
        private float startTime;
        private List<Vector2> positions = new List<Vector2>();

        public TouchInfo(int fingerId, Vector2 position)
        {
            this.fingerId = fingerId;
            startTime = Time.realtimeSinceStartup;
            AddPosition(position);
        }

        public void AddPosition(Vector2 position)
        {
            positions.Add(position);
        }

        public bool IsHit(GameObject targetGameObject, Camera camera = null)
        {
            if (null == camera)
            {
                camera = Camera.main;
            }
            var lastTouchPosition = positions.Last();
            if (null == targetGameObject.GetComponent<RectTransform>())
            {
                var ray = camera.ScreenPointToRay(lastTouchPosition);
                var hit = new RaycastHit();
                return Physics.Raycast(ray, out hit) && hit.collider.gameObject == targetGameObject;
            }
            else
            {
                var pointerEventData = new PointerEventData(EventSystem.current);
                pointerEventData.position = lastTouchPosition;
                var raycastResults = new List<RaycastResult>();
                EventSystem.current.RaycastAll(pointerEventData, raycastResults);
                if (raycastResults.Count == 0)
                {
                    return false;
                }
                var resultGameObject = raycastResults.First().gameObject;
                while (null != resultGameObject)
                {
                    if (resultGameObject == targetGameObject)
                    {
                        return true;
                    }
                    var parent = resultGameObject.transform.parent;
                    resultGameObject = null != parent ? parent.gameObject : null;
                }
                return false;
            }
        }
    }

    public class GestureDetectorEvent : UnityEvent<Gesture, TouchInfo>
    {
        public GestureDetectorEvent()
        {
        }
    }
}