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