/* * @author Valentin Simonov / http://va.lent.in/ */ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using TouchScript.Hit; using TouchScript.Utils; using TouchScript.Utils.Attributes; using TouchScript.Pointers; using UnityEngine; using UnityEngine.Events; using TouchScript.Core; namespace TouchScript.Gestures { /// /// Base class for all gestures. /// public abstract class Gesture : DebuggableMonoBehaviour { #region Constants /// /// Unity event for gesture state changes. /// [Serializable] public class GestureEvent : UnityEvent {} /// /// Message sent when gesture changes state if SendMessage is used. /// public const string STATE_CHANGE_MESSAGE = "OnGestureStateChange"; /// /// Message sent when gesture is cancelled if SendMessage is used. /// public const string CANCEL_MESSAGE = "OnGestureCancel"; /// /// Possible states of a gesture. /// public enum GestureState { /// /// Gesture is idle. /// Idle, /// /// Gesture started looking for the patern. /// Possible, /// /// Continuous gesture has just begun. /// Began, /// /// Started continuous gesture is updated. /// Changed, /// /// Continuous gesture is ended. /// Ended, /// /// Gesture is cancelled. /// Cancelled, /// /// Gesture is failed by itself or by another recognized gesture. /// Failed, /// /// Gesture is recognized. /// Recognized = Ended } /// /// Current state of the number of pointers. /// protected enum PointersNumState { /// /// The number of pointers is between min and max thresholds. /// InRange, /// /// The number of pointers is less than min threshold. /// TooFew, /// /// The number of pointers is greater than max threshold. /// TooMany, /// /// The number of pointers passed min threshold this frame and is now in range. /// PassedMinThreshold, /// /// The number of pointers passed max threshold this frame and is now in range. /// PassedMaxThreshold, /// /// The number of pointers passed both min and max thresholds. /// PassedMinMaxThreshold } #endregion #region Events /// /// Occurs when gesture changes state. /// public event EventHandler StateChanged { add { stateChangedInvoker += value; } remove { stateChangedInvoker -= value; } } /// /// Occurs when gesture is cancelled. /// public event EventHandler Cancelled { add { cancelledInvoker += value; } remove { cancelledInvoker -= value; } } // Needed to overcome iOS AOT limitations private EventHandler stateChangedInvoker; private EventHandler cancelledInvoker; /// /// Occurs when gesture changes state. /// public GestureEvent OnStateChange = new GestureEvent(); #endregion #region Public properties /// /// Gets or sets minimum number of pointers this gesture reacts to. /// The gesture will not be recognized if it has less than pointers. /// /// Minimum number of pointers. public int MinPointers { get { return minPointers; } set { if (value < 0) return; minPointers = value; } } /// /// Gets or sets maximum number of pointers this gesture reacts to. /// The gesture will not be recognized if it has more than pointers. /// /// Maximum number of pointers. public int MaxPointers { get { return maxPointers; } set { if (value < 0) return; maxPointers = value; } } /// /// Gets or sets another gesture which must fail before this gesture can be recognized. /// /// The gesture which must fail before this gesture can be recognized. public Gesture RequireGestureToFail { get { return requireGestureToFail; } set { if (!Application.isPlaying) return; if (requireGestureToFail != null) requireGestureToFail.StateChanged -= requiredToFailGestureStateChangedHandler; requireGestureToFail = value; if (requireGestureToFail != null) requireGestureToFail.StateChanged += requiredToFailGestureStateChangedHandler; } } /// /// Gets or sets whether gesture should use Unity's SendMessage in addition to C# events. /// /// true if gesture uses SendMessage; otherwise, false. public bool UseSendMessage { get { return useSendMessage; } set { useSendMessage = value; } } /// /// Gets or sets a value indicating whether state change events are broadcasted if is true. /// /// true if state change events should be broadcaster; otherwise, false. public bool SendStateChangeMessages { get { return sendStateChangeMessages; } set { sendStateChangeMessages = value; } } /// /// Gets or sets the target of Unity messages sent from this gesture. /// /// The target of Unity messages. public GameObject SendMessageTarget { get { return sendMessageTarget; } set { sendMessageTarget = value; if (value == null) sendMessageTarget = gameObject; } } /// /// Gets or sets whether gesture should use Unity Events in addition to C# events. /// /// true if gesture uses Unity Events; otherwise, false. public bool UseUnityEvents { get { return useUnityEvents; } set { useUnityEvents = value; } } /// /// Gets or sets a value indicating whether state change events are broadcasted if is true. /// /// true if state change events should be broadcaster; otherwise, false. public bool SendStateChangeEvents { get { return sendStateChangeEvents; } set { sendStateChangeEvents = value; } } /// /// Gets current gesture state. /// /// Current state of the gesture. public GestureState State { get { return state; } private set { PreviousState = state; state = value; switch (value) { case GestureState.Idle: onIdle(); break; case GestureState.Possible: onPossible(); break; case GestureState.Began: retainPointers(); onBegan(); break; case GestureState.Changed: onChanged(); break; case GestureState.Recognized: // Only retain/release pointers for continuos gestures if (PreviousState == GestureState.Changed || PreviousState == GestureState.Began) releasePointers(true); onRecognized(); break; case GestureState.Failed: onFailed(); break; case GestureState.Cancelled: if (PreviousState == GestureState.Changed || PreviousState == GestureState.Began) releasePointers(false); onCancelled(); break; } if (stateChangedInvoker != null) stateChangedInvoker.InvokeHandleExceptions(this, GestureStateChangeEventArgs.GetCachedEventArgs(state, PreviousState)); if (useSendMessage && sendStateChangeMessages && SendMessageTarget != null) sendMessageTarget.SendMessage(STATE_CHANGE_MESSAGE, this, SendMessageOptions.DontRequireReceiver); if (useUnityEvents && sendStateChangeEvents) OnStateChange.Invoke(this); } } /// /// Gets previous gesture state. /// /// Previous state of the gesture. public GestureState PreviousState { get; private set; } /// /// Gets current screen position. /// /// Gesture's position in screen coordinates. public virtual Vector2 ScreenPosition { get { if (NumPointers == 0) { if (!TouchManager.IsInvalidPosition(cachedScreenPosition)) return cachedScreenPosition; return TouchManager.INVALID_POSITION; } return activePointers[0].Position; } } /// /// Gets previous screen position. /// /// Gesture's previous position in screen coordinates. public virtual Vector2 PreviousScreenPosition { get { if (NumPointers == 0) { if (!TouchManager.IsInvalidPosition(cachedPreviousScreenPosition)) return cachedPreviousScreenPosition; return TouchManager.INVALID_POSITION; } return activePointers[0].PreviousPosition; } } /// /// Gets normalized screen position. /// /// Gesture's position in normalized screen coordinates. public Vector2 NormalizedScreenPosition { get { var position = ScreenPosition; if (TouchManager.IsInvalidPosition(position)) return TouchManager.INVALID_POSITION; return new Vector2(position.x / Screen.width, position.y / Screen.height); } } /// /// Gets previous screen position. /// /// Gesture's previous position in normalized screen coordinates. public Vector2 PreviousNormalizedScreenPosition { get { var position = PreviousScreenPosition; if (TouchManager.IsInvalidPosition(position)) return TouchManager.INVALID_POSITION; return new Vector2(position.x / Screen.width, position.y / Screen.height); } } /// /// Gets list of gesture's active pointers. /// /// The list of pointers owned by this gesture. public IList ActivePointers { get { if (readonlyActivePointers == null) readonlyActivePointers = new ReadOnlyCollection(activePointers); return readonlyActivePointers; } } /// /// Gets the number of active pointerss. /// /// The number of pointers owned by this gesture. public int NumPointers { get { return numPointers; } } /// /// Gets or sets an object implementing to be asked for gesture specific actions. /// /// The delegate. public IGestureDelegate Delegate { get; set; } #endregion #region Private variables /// /// Reference to global GestureManager. /// protected IGestureManager gestureManager { // implemented as a property because it returns IGestureManager but we need to reference GestureManagerInstance to access internal methods get { return gestureManagerInstance; } } /// /// Reference to global TouchManager. /// protected TouchManagerInstance touchManager { get; private set; } /// /// The state of min/max number of pointers. /// protected PointersNumState pointersNumState { get; private set; } /// /// Pointers the gesture currently owns and works with. /// protected List activePointers = new List(10); /// /// Cached transform of the parent object. /// protected Transform cachedTransform; /// [SerializeField] [HideInInspector] protected bool basicEditor = true; [SerializeField] [HideInInspector] private bool generalProps; // Used in the custom inspector [SerializeField] [HideInInspector] private bool limitsProps; // Used in the custom inspector [SerializeField] [HideInInspector] private bool advancedProps; // Used in the custom inspector [SerializeField] private int minPointers = 0; [SerializeField] private int maxPointers = 0; [SerializeField] [ToggleLeft] private bool useSendMessage = false; [SerializeField] [ToggleLeft] private bool sendStateChangeMessages = false; [SerializeField] private GameObject sendMessageTarget; [SerializeField] private bool useUnityEvents = false; [SerializeField] [ToggleLeft] private bool sendStateChangeEvents = false; [SerializeField] [NullToggle] private Gesture requireGestureToFail; [SerializeField] // Serialized list of gestures for Unity IDE. private List friendlyGestures = new List(); private int numPointers; private ReadOnlyCollection readonlyActivePointers; private GestureManagerInstance gestureManagerInstance; private GestureState delayedStateChange = GestureState.Idle; private bool requiredGestureFailed = false; private FakePointer fakePointer = new FakePointer(); private GestureState state = GestureState.Idle; /// /// Cached screen position. /// Used to keep tap's position which can't be calculated from pointers when the gesture is recognized since all pointers are gone. /// protected Vector2 cachedScreenPosition; /// /// Cached previous screen position. /// Used to keep tap's position which can't be calculated from pointers when the gesture is recognized since all pointers are gone. /// protected Vector2 cachedPreviousScreenPosition; #endregion #region Public methods /// /// Adds a friendly gesture. /// /// The gesture. public void AddFriendlyGesture(Gesture gesture) { if (gesture == null || gesture == this) return; registerFriendlyGesture(gesture); gesture.registerFriendlyGesture(this); } /// /// Checks if a gesture is friendly with this gesture. /// /// A gesture to check. /// true if gestures are friendly; false otherwise. public bool IsFriendly(Gesture gesture) { return friendlyGestures.Contains(gesture); } /// /// Determines whether gesture controls a pointer. /// /// The pointer. /// true if gesture controls the pointer point; false otherwise. public bool HasPointer(Pointer pointer) { return activePointers.Contains(pointer); } /// /// Determines whether this instance can prevent the specified gesture. /// /// The gesture. /// true if this instance can prevent the specified gesture; false otherwise. public virtual bool CanPreventGesture(Gesture gesture) { if (Delegate == null) { if (gesture.CanBePreventedByGesture(this)) return !IsFriendly(gesture); return false; } return !Delegate.ShouldRecognizeSimultaneously(this, gesture); } /// /// Determines whether this instance can be prevented by specified gesture. /// /// The gesture. /// true if this instance can be prevented by specified gesture; false otherwise. public virtual bool CanBePreventedByGesture(Gesture gesture) { if (Delegate == null) return !IsFriendly(gesture); return !Delegate.ShouldRecognizeSimultaneously(this, gesture); } /// /// Specifies if gesture can receive this specific pointer point. /// /// The pointer. /// true if this pointer should be received by the gesture; false otherwise. public virtual bool ShouldReceivePointer(Pointer pointer) { if (Delegate == null) return true; return Delegate.ShouldReceivePointer(this, pointer); } /// /// Specifies if gesture can begin or recognize. /// /// true if gesture should begin; false otherwise. public virtual bool ShouldBegin() { if (Delegate == null) return true; return Delegate.ShouldBegin(this); } /// /// Cancels this gesture. /// /// if set to true also implicitly cancels all pointers owned by the gesture. /// if set to true redispatched all canceled pointers. public void Cancel(bool cancelPointers, bool returnPointers) { switch (state) { case GestureState.Cancelled: case GestureState.Failed: return; } setState(GestureState.Cancelled); if (!cancelPointers) return; for (var i = 0; i < numPointers; i++) touchManager.CancelPointer(activePointers[i].Id, returnPointers); } /// /// Cancels this gesture. /// public void Cancel() { Cancel(false, false); } /// /// Returns for gesture's , i.e. what is right beneath it. /// public virtual HitData GetScreenPositionHitData() { HitData hit; fakePointer.Position = ScreenPosition; LayerManager.Instance.GetHitTarget(fakePointer, out hit); return hit; } #endregion #region Unity methods /// protected virtual void Awake() { cachedTransform = transform; var count = friendlyGestures.Count; for (var i = 0; i < count; i++) { AddFriendlyGesture(friendlyGestures[i]); } RequireGestureToFail = requireGestureToFail; } /// /// Unity Start handler. /// protected virtual void OnEnable() { // TouchManager might be different in another scene touchManager = TouchManager.Instance as TouchManagerInstance; gestureManagerInstance = GestureManager.Instance as GestureManagerInstance; if (touchManager == null) Debug.LogError("No TouchManager found! Please add an instance of TouchManager to the scene!"); if (gestureManagerInstance == null) Debug.LogError("No GesturehManager found! Please add an instance of GesturehManager to the scene!"); if (sendMessageTarget == null) sendMessageTarget = gameObject; INTERNAL_Reset(); } /// /// Unity OnDisable handler. /// protected virtual void OnDisable() { setState(GestureState.Cancelled); } /// /// Unity OnDestroy handler. /// protected virtual void OnDestroy() { var copy = new List(friendlyGestures); var count = copy.Count; for (var i = 0; i < count; i++) { INTERNAL_RemoveFriendlyGesture(copy[i]); } RequireGestureToFail = null; } #endregion #region Internal functions internal void INTERNAL_SetState(GestureState value) { setState(value); } internal void INTERNAL_Reset() { activePointers.Clear(); numPointers = 0; delayedStateChange = GestureState.Idle; pointersNumState = PointersNumState.TooFew; requiredGestureFailed = false; reset(); } internal void INTERNAL_PointersPressed(IList pointers) { var count = pointers.Count; var total = numPointers + count; pointersNumState = PointersNumState.InRange; if (minPointers <= 0) { // MinPointers is not set and we got our first pointers if (numPointers == 0) pointersNumState = PointersNumState.PassedMinThreshold; } else { if (numPointers < minPointers) { // had < MinPointers, got >= MinPointers if (total >= minPointers) pointersNumState = PointersNumState.PassedMinThreshold; else pointersNumState = PointersNumState.TooFew; } } if (maxPointers > 0) { if (numPointers <= maxPointers) { if (total > maxPointers) { // this event we crossed both MinPointers and MaxPointers if (pointersNumState == PointersNumState.PassedMinThreshold) pointersNumState = PointersNumState.PassedMinMaxThreshold; // this event we crossed MaxPointers else pointersNumState = PointersNumState.PassedMaxThreshold; } } // last event we already were over MaxPointers else pointersNumState = PointersNumState.TooMany; } if (state == GestureState.Began || state == GestureState.Changed) { for (var i = 0; i < count; i++) pointers[i].INTERNAL_Retain(); } activePointers.AddRange(pointers); numPointers = total; pointersPressed(pointers); } internal void INTERNAL_PointersUpdated(IList pointers) { pointersNumState = PointersNumState.InRange; if (minPointers > 0 && numPointers < minPointers) pointersNumState = PointersNumState.TooFew; if (maxPointers > 0 && pointersNumState == PointersNumState.InRange && numPointers > maxPointers) pointersNumState = PointersNumState.TooMany; pointersUpdated(pointers); } internal void INTERNAL_PointersReleased(IList pointers) { var count = pointers.Count; var total = numPointers - count; pointersNumState = PointersNumState.InRange; if (minPointers <= 0) { // have no pointers if (total == 0) pointersNumState = PointersNumState.PassedMinThreshold; } else { if (numPointers >= minPointers) { // had >= MinPointers, got < MinPointers if (total < minPointers) pointersNumState = PointersNumState.PassedMinThreshold; } // last event we already were under MinPointers else pointersNumState = PointersNumState.TooFew; } if (maxPointers > 0) { if (numPointers > maxPointers) { if (total <= maxPointers) { // this event we crossed both MinPointers and MaxPointers if (pointersNumState == PointersNumState.PassedMinThreshold) pointersNumState = PointersNumState.PassedMinMaxThreshold; // this event we crossed MaxPointers else pointersNumState = PointersNumState.PassedMaxThreshold; } // last event we already were over MaxPointers else pointersNumState = PointersNumState.TooMany; } } for (var i = 0; i < count; i++) activePointers.Remove(pointers[i]); numPointers = total; if (NumPointers == 0) { var lastPoint = pointers[count - 1]; if (shouldCachePointerPosition(lastPoint)) { cachedScreenPosition = lastPoint.Position; cachedPreviousScreenPosition = lastPoint.PreviousPosition; } else { cachedScreenPosition = TouchManager.INVALID_POSITION; cachedPreviousScreenPosition = TouchManager.INVALID_POSITION; } } pointersReleased(pointers); } internal void INTERNAL_PointersCancelled(IList pointers) { var count = pointers.Count; var total = numPointers - count; pointersNumState = PointersNumState.InRange; if (minPointers <= 0) { // have no pointers if (total == 0) pointersNumState = PointersNumState.PassedMinThreshold; } else { if (numPointers >= minPointers) { // had >= MinPointers, got < MinPointers if (total < minPointers) pointersNumState = PointersNumState.PassedMinThreshold; } // last event we already were under MinPointers else pointersNumState = PointersNumState.TooFew; } if (maxPointers > 0) { if (numPointers > maxPointers) { if (total <= maxPointers) { // this event we crossed both MinPointers and MaxPointers if (pointersNumState == PointersNumState.PassedMinThreshold) pointersNumState = PointersNumState.PassedMinMaxThreshold; // this event we crossed MaxPointers else pointersNumState = PointersNumState.PassedMaxThreshold; } // last event we already were over MaxPointers else pointersNumState = PointersNumState.TooMany; } } for (var i = 0; i < count; i++) activePointers.Remove(pointers[i]); numPointers = total; pointersCancelled(pointers); } internal virtual void INTERNAL_RemoveFriendlyGesture(Gesture gesture) { if (gesture == null || gesture == this) return; unregisterFriendlyGesture(gesture); gesture.unregisterFriendlyGesture(this); } #endregion #region Protected methods /// /// Should the gesture cache this pointers to use it later in calculation of . /// /// Pointer to cache. /// true if pointers should be cached; false otherwise. protected virtual bool shouldCachePointerPosition(Pointer value) { return true; } /// /// Tries to change gesture state. /// /// New state. /// true if state was changed; otherwise, false. protected bool setState(GestureState value) { if (gestureManagerInstance == null) return false; if (requireGestureToFail != null) { switch (value) { case GestureState.Recognized: case GestureState.Began: if (!requiredGestureFailed) { delayedStateChange = value; return false; } break; case GestureState.Idle: case GestureState.Possible: case GestureState.Failed: case GestureState.Cancelled: delayedStateChange = GestureState.Idle; break; } } var newState = gestureManagerInstance.INTERNAL_GestureChangeState(this, value); State = newState; return value == newState; } #endregion #region Callbacks /// /// Called when new pointers appear. /// /// The pointers. protected virtual void pointersPressed(IList pointers) {} /// /// Called for moved pointers. /// /// The pointers. protected virtual void pointersUpdated(IList pointers) {} /// /// Called if pointers are removed. /// /// The pointers. protected virtual void pointersReleased(IList pointers) {} /// /// Called when pointers are cancelled. /// /// The pointers. protected virtual void pointersCancelled(IList pointers) { if (pointersNumState == PointersNumState.PassedMinThreshold) { // moved below the threshold switch (state) { case GestureState.Began: case GestureState.Changed: // cancel started gestures setState(GestureState.Cancelled); break; } } } /// /// Called to reset gesture state after it fails or recognizes. /// protected virtual void reset() { cachedScreenPosition = TouchManager.INVALID_POSITION; cachedPreviousScreenPosition = TouchManager.INVALID_POSITION; } /// /// Called when state is changed to Idle. /// protected virtual void onIdle() {} /// /// Called when state is changed to Possible. /// protected virtual void onPossible() {} /// /// Called when state is changed to Began. /// protected virtual void onBegan() {} /// /// Called when state is changed to Changed. /// protected virtual void onChanged() {} /// /// Called when state is changed to Recognized. /// protected virtual void onRecognized() {} /// /// Called when state is changed to Failed. /// protected virtual void onFailed() {} /// /// Called when state is changed to Cancelled. /// protected virtual void onCancelled() { if (cancelledInvoker != null) cancelledInvoker.InvokeHandleExceptions(this, EventArgs.Empty); if (useSendMessage && SendMessageTarget != null) sendMessageTarget.SendMessage(CANCEL_MESSAGE, this, SendMessageOptions.DontRequireReceiver); } #endregion #region Private functions private void retainPointers() { var total = NumPointers; for (var i = 0; i < total; i++) activePointers[i].INTERNAL_Retain(); } private void releasePointers(bool cancel) { var total = NumPointers; for (var i = 0; i < total; i++) { var pointer = activePointers[i]; if (pointer.INTERNAL_Release() == 0 && cancel) touchManager.CancelPointer(pointer.Id, true); } } private void registerFriendlyGesture(Gesture gesture) { if (gesture == null || gesture == this) return; if (!friendlyGestures.Contains(gesture)) friendlyGestures.Add(gesture); } private void unregisterFriendlyGesture(Gesture gesture) { if (gesture == null || gesture == this) return; friendlyGestures.Remove(gesture); } #endregion #region Event handlers private void requiredToFailGestureStateChangedHandler(object sender, GestureStateChangeEventArgs e) { if ((sender as Gesture) != requireGestureToFail) return; switch (e.State) { case GestureState.Failed: requiredGestureFailed = true; if (delayedStateChange != GestureState.Idle) { setState(delayedStateChange); } break; case GestureState.Began: case GestureState.Recognized: case GestureState.Cancelled: if (state != GestureState.Failed) setState(GestureState.Failed); break; } } #endregion } /// /// Event arguments for Gesture state change events. /// public class GestureStateChangeEventArgs : EventArgs { /// /// Previous gesture state. /// public Gesture.GestureState PreviousState { get; private set; } /// /// Current gesture state. /// public Gesture.GestureState State { get; private set; } private static GestureStateChangeEventArgs instance; /// /// Initializes a new instance of the class. /// public GestureStateChangeEventArgs() {} /// /// Returns cached instance of EventArgs. /// This cached EventArgs is reused throughout the library not to alocate new ones on every call. /// /// Current gesture state. /// Previous gesture state. /// Cached EventArgs object. public static GestureStateChangeEventArgs GetCachedEventArgs(Gesture.GestureState state, Gesture.GestureState previousState) { if (instance == null) instance = new GestureStateChangeEventArgs(); instance.State = state; instance.PreviousState = previousState; return instance; } } }