using System;
using System.Collections.Generic;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Utilities;
////TODO: recorded times are baked *external* times; reset touch when coming out of play mode
////REVIEW: record velocity on touches? or add method to very easily get the data?
namespace UnityEngine.InputSystem.EnhancedTouch
{
///
/// A high-level representation of a touch which automatically keeps track of a touch
/// over time.
///
///
/// This API obsoletes the need for manually keeping tracking of touch IDs ()
/// and touch phases () in order to tell one touch apart from another.
///
/// Also, this class protects against losing touches. If a touch is shorter-lived than a single input update,
/// may overwrite it with a new touch coming in in the same update whereas this class
/// will retain all changes that happened on the touchscreen in any particular update.
///
/// The API makes a distinction between "fingers" and "touches". A touch refers to one contact state change event, i.e. a
/// finger beginning to touch the screen (), moving on the screen (),
/// or being lifted off the screen ( or ).
/// A finger, on the other hand, always refers to the Nth contact on the screen.
///
/// A Touch instance is a struct which only contains a reference to the actual data which is stored in unmanaged
/// memory.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces")]
public struct Touch : IEquatable
{
// The way this works is that at the core, it simply attaches one InputStateHistory per "/touch*"
// control and then presents a public API that crawls over the recorded touch history in various ways.
///
/// Whether this touch record holds valid data.
///
/// If true, the data contained in the touch is valid.
///
/// Touch data is stored in unmanaged memory as a circular input buffer. This means that when
/// the buffer runs out of capacity, older touch entries will get reused. When this happens,
/// existing Touch instances referring to the record become invalid.
///
/// This property can be used to determine whether the record held on to by the Touch
/// instance is still valid.
///
/// This property will be false for default-initialized Touch instances.
///
/// Note that accessing most of the other properties on this struct when the touch is
/// invalid will trigger InvalidOperationException.
///
public bool valid => m_TouchRecord.valid;
///
/// The finger used for the touch contact. Null only for default-initialized
/// instances of the struct.
///
/// Finger used for the touch contact.
///
public Finger finger => m_Finger;
///
/// Current phase of the touch.
///
/// Current phase of the touch.
///
/// Every touch goes through a predefined cycle that starts with ,
/// then potentially and/or ,
/// and finally concludes with either or .
///
/// This property indicates where in the cycle the touch is.
///
///
///
public TouchPhase phase => state.phase;
///
/// Unique ID of the touch as (usually) assigned by the platform.
///
/// Unique, non-zero ID of the touch.
///
/// Each touch contact that is made with the screen receives its own unique ID which is
/// normally assigned by the underlying platform.
///
/// Note a platform may reuse touch IDs after their respective touches have finished.
/// This means that the guarantee of uniqueness is only made with respect to .
///
/// In particular, all touches in will have the same ID whereas
/// touches in the a finger's may end up having the same
/// touch ID even though constituting different physical touch contacts.
///
///
public int touchId => state.touchId;
///
/// Normalized pressure of the touch against the touch surface.
///
/// Pressure level of the touch.
///
/// Not all touchscreens are pressure-sensitive. If unsupported, this property will
/// always return 0.
///
/// In general, touch pressure is supported on mobile platforms only.
///
/// Note that it is possible for the value to go above 1 even though it is considered normalized. The reason is
/// that calibration on the system can put the maximum pressure point below the physically supported maximum value.
///
///
public float pressure => state.pressure;
///
/// Screen-space radius of the touch.
///
/// Horizontal and vertical extents of the touch contact.
///
/// If supported by the underlying device, this reports the size of the touch contact based on its
/// center point. If not supported, this will be default(Vector2).
///
///
public Vector2 radius => state.radius;
///
/// Time in seconds on the same timeline as Time.realTimeSinceStartup when the touch began.
///
/// Start time of the touch.
///
/// This is the value of when the touch started with
/// .
///
///
public double startTime => state.startTime;
///
/// Time in seconds on the same timeline as Time.realTimeSinceStartup when the touch record was
/// reported.
///
/// Time the touch record was reported.
///
/// This is the value of the event that signaled the current state
/// change for the touch.
///
public double time => m_TouchRecord.time;
///
/// The touchscreen on which the touch occurred.
///
/// Touchscreen associated with the touch.
public Touchscreen screen => finger.screen;
///
/// Screen-space position of the touch.
///
/// Screen-space position of the touch.
///
public Vector2 screenPosition => state.position;
///
/// Screen-space position where the touch started.
///
/// Start position of the touch.
///
public Vector2 startScreenPosition => state.startPosition;
///
/// Screen-space motion delta of the touch.
///
/// Screen-space motion delta of the touch.
///
/// Note that deltas have behaviors attached to them different from most other
/// controls. See for details.
///
///
public Vector2 delta => state.delta;
///
/// Number of times that the touch has been tapped in succession.
///
/// Indicates how many taps have been performed one after the other.
///
/// Successive taps have to come within for them
/// to increase the tap count. I.e. if a new tap finishes within that time after
/// of the previous touch, the tap count is increased by one. If more than
/// passes after a tap with no successive tap, the tap count is reset to zero.
///
///
public int tapCount => state.tapCount;
///
/// Whether the touch has performed a tap.
///
/// Indicates whether the touch has tapped the screen.
///
/// A tap is defined as a touch that begins and ends within and
/// stays within of its . If this
/// is the case for a touch, this button is set to 1 at the time the touch goes to
/// .
///
/// Resets to 0 only when another touch is started on the control or when the control is reset.
///
///
///
///
public bool isTap => state.isTap;
///
/// Whether the touch is currently in progress, i.e. has a of
/// , , or .
///
/// Whether the touch is currently ongoing.
public bool isInProgress
{
get
{
switch (phase)
{
case TouchPhase.Began:
case TouchPhase.Moved:
case TouchPhase.Stationary:
return true;
}
return false;
}
}
internal uint updateStepCount => extraData.updateStepCount;
internal uint uniqueId => extraData.uniqueId;
private unsafe ref TouchState state => ref *(TouchState*)m_TouchRecord.GetUnsafeMemoryPtr();
private unsafe ref ExtraDataPerTouchState extraData =>
ref *(ExtraDataPerTouchState*)m_TouchRecord.GetUnsafeExtraMemoryPtr();
///
/// History for this specific touch.
///
///
/// Unlike , this gives the history of this touch only.
///
public TouchHistory history
{
get
{
if (!valid)
throw new InvalidOperationException("Touch is invalid");
return finger.GetTouchHistory(this);
}
}
///
/// All touches that are either on-going as of the current frame or have ended in the current frame.
///
///
/// Note that the fact that ended touches are being kept around until the end of a frame means that this
/// array may have more touches than the total number of concurrent touches supported by the system.
///
public static ReadOnlyArray activeTouches
{
get
{
// We lazily construct the array of active touches.
s_PlayerState.UpdateActiveTouches();
return new ReadOnlyArray(s_PlayerState.activeTouches, 0, s_PlayerState.activeTouchCount);
}
}
///
///
///
///
/// The length of this array will always correspond to the maximum number of concurrent touches supported by the system.
///
public static ReadOnlyArray fingers =>
new ReadOnlyArray(s_PlayerState.fingers, 0, s_PlayerState.totalFingerCount);
public static ReadOnlyArray activeFingers
{
get
{
// We lazily construct the array of active fingers.
s_PlayerState.UpdateActiveFingers();
return new ReadOnlyArray(s_PlayerState.activeFingers, 0, s_PlayerState.activeFingerCount);
}
}
public static IEnumerable screens => s_Touchscreens;
public static event Action onFingerDown
{
add
{
if (value == null)
throw new ArgumentNullException(nameof(value));
s_OnFingerDown.AppendWithCapacity(value);
}
remove
{
if (value == null)
throw new ArgumentNullException(nameof(value));
var index = s_OnFingerDown.IndexOf(value);
if (index != -1)
s_OnFingerDown.RemoveAtWithCapacity(index);
}
}
public static event Action onFingerUp
{
add
{
if (value == null)
throw new ArgumentNullException(nameof(value));
s_OnFingerUp.AppendWithCapacity(value);
}
remove
{
if (value == null)
throw new ArgumentNullException(nameof(value));
var index = s_OnFingerUp.IndexOf(value);
if (index != -1)
s_OnFingerUp.RemoveAtWithCapacity(index);
}
}
public static event Action onFingerMove
{
add
{
if (value == null)
throw new ArgumentNullException(nameof(value));
s_OnFingerMove.AppendWithCapacity(value);
}
remove
{
if (value == null)
throw new ArgumentNullException(nameof(value));
var index = s_OnFingerMove.IndexOf(value);
if (index != -1)
s_OnFingerMove.RemoveAtWithCapacity(index);
}
}
/*
public static Action onFingerTap
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
*/
///
/// The amount of history kept for each single touch.
///
///
/// By default, this is zero meaning that no history information is kept for
/// touches. Setting this to Int32.maxValue will cause all history from
/// the beginning to the end of a touch being kept.
///
public static int maxHistoryLengthPerFinger
{
get => s_HistoryLengthPerFinger;
////TODO
/*set { throw new NotImplementedException(); }*/
}
internal Touch(Finger finger, InputStateHistory.Record touchRecord)
{
m_Finger = finger;
m_TouchRecord = touchRecord;
}
public override string ToString()
{
if (!valid)
return "";
return $"{{finger={finger.index} touchId={touchId} phase={phase} position={screenPosition} time={time}}}";
}
public bool Equals(Touch other)
{
return Equals(m_Finger, other.m_Finger) && m_TouchRecord.Equals(other.m_TouchRecord);
}
public override bool Equals(object obj)
{
return obj is Touch other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
return ((m_Finger != null ? m_Finger.GetHashCode() : 0) * 397) ^ m_TouchRecord.GetHashCode();
}
}
internal static void AddTouchscreen(Touchscreen screen)
{
Debug.Assert(!s_Touchscreens.ContainsReference(screen), "Already added touchscreen");
s_Touchscreens.AppendWithCapacity(screen, capacityIncrement: 5);
// Add finger tracking to states.
s_PlayerState.AddFingers(screen);
#if UNITY_EDITOR
s_EditorState.AddFingers(screen);
#endif
}
internal static void RemoveTouchscreen(Touchscreen screen)
{
Debug.Assert(s_Touchscreens.ContainsReference(screen), "Did not add touchscreen");
// Remove from list.
var index = s_Touchscreens.IndexOfReference(screen);
s_Touchscreens.RemoveAtWithCapacity(index);
// Remove fingers from states.
s_PlayerState.RemoveFingers(screen);
#if UNITY_EDITOR
s_EditorState.RemoveFingers(screen);
#endif
}
//only have this hooked when we actually need it
internal static void BeginUpdate()
{
#if UNITY_EDITOR
if ((InputState.currentUpdateType == InputUpdateType.Editor && s_PlayerState.updateMask != InputUpdateType.Editor) ||
(InputState.currentUpdateType != InputUpdateType.Editor && s_PlayerState.updateMask == InputUpdateType.Editor))
{
// Either swap in editor state and retain currently active player state in s_EditorState
// or swap player state back in.
MemoryHelpers.Swap(ref s_PlayerState, ref s_EditorState);
}
#endif
++s_PlayerState.updateStepCount;
s_PlayerState.haveBuiltActiveTouches = false;
}
private readonly Finger m_Finger;
internal InputStateHistory.Record m_TouchRecord;
internal static InlinedArray s_Touchscreens;
internal static int s_HistoryLengthPerFinger = 64;
internal static InlinedArray> s_OnFingerDown;
internal static InlinedArray> s_OnFingerMove;
internal static InlinedArray> s_OnFingerUp;
internal static FingerAndTouchState s_PlayerState;
#if UNITY_EDITOR
internal static FingerAndTouchState s_EditorState;
#endif
// In scenarios where we have to support multiple different types of input updates (e.g. in editor or in
// player when both dynamic and fixed input updates are enabled), we need more than one copy of touch state.
// We encapsulate the state in this struct so that we can easily swap it.
//
// NOTE: Finger instances are per state. This means that you will actually see different Finger instances for
// the same finger in two different update types.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
Justification = "Managed internally")]
internal struct FingerAndTouchState
{
public InputUpdateType updateMask;
public uint updateStepCount;
public Finger[] fingers;
public Finger[] activeFingers;
public Touch[] activeTouches;
public int activeFingerCount;
public int activeTouchCount;
public int totalFingerCount;
public uint lastId;
public bool haveBuiltActiveTouches;
// `activeTouches` adds yet another view of input state that is different from "normal" recorded
// state history. In this view, touches become stationary in the next update and deltas reset
// between updates. We solve this by storing state separately for active touches. We *only* do
// so when `activeTouches` is actually queried meaning that `activeTouches` has no overhead if
// not used.
public InputStateHistory activeTouchState;
public void AddFingers(Touchscreen screen)
{
var touchCount = screen.touches.Count;
ArrayHelpers.EnsureCapacity(ref fingers, totalFingerCount, touchCount);
for (var i = 0; i < touchCount; ++i)
{
var finger = new Finger(screen, i, updateMask);
ArrayHelpers.AppendWithCapacity(ref fingers, ref totalFingerCount, finger);
}
}
public void RemoveFingers(Touchscreen screen)
{
var touchCount = screen.touches.Count;
for (var i = 0; i < fingers.Length; ++i)
{
if (fingers[i].screen != screen)
continue;
// Release unmanaged memory.
for (var n = 0; n < touchCount; ++n)
fingers[i + n].m_StateHistory.Dispose();
////REVIEW: leave Fingers in place and reuse the instances?
ArrayHelpers.EraseSliceWithCapacity(ref fingers, ref totalFingerCount, i, touchCount);
break;
}
// Force rebuilding of active touches.
haveBuiltActiveTouches = false;
}
public void Destroy()
{
for (var i = 0; i < totalFingerCount; ++i)
fingers[i].m_StateHistory.Dispose();
activeTouchState?.Dispose();
activeTouchState = null;
}
public void UpdateActiveFingers()
{
////TODO: do this only once per update per activeFingers getter
activeFingerCount = 0;
for (var i = 0; i < totalFingerCount; ++i)
{
var finger = fingers[i];
var lastTouch = finger.currentTouch;
if (lastTouch.valid)
ArrayHelpers.AppendWithCapacity(ref activeFingers, ref activeFingerCount, finger);
}
}
public unsafe void UpdateActiveTouches()
{
if (haveBuiltActiveTouches)
return;
// Clear activeTouches state.
if (activeTouchState == null)
{
activeTouchState = new InputStateHistory();
activeTouchState.extraMemoryPerRecord = UnsafeUtility.SizeOf();
}
else
activeTouchState.Clear();
activeTouchCount = 0;
// Go through fingers and for each one, get the touches that were active this update.
for (var i = 0; i < totalFingerCount; ++i)
{
var finger = fingers[i];
// Skip going through the finger's touches if the finger does not have an active touch.
// Avoids doing unnecessary work of sifting through the finger's history.
if (!finger.currentTouch.valid)
continue;
// We're walking newest-first through the touch history but want the resulting list of
// active touches to be oldest first (so that a record for an ended touch comes before
// a record of a new touch started on the same finger). To achieve that, we insert
// new touch entries for any finger always at the same index (i.e. we prepend rather
// than append).
var insertAt = activeTouchCount;
// Go back in time through the touch records on the finger and collect any touch
// active in the current frame. Note that this may yield *multiple* touches for the
// finger as there may be touched that have ended in the frame while in the same
// frame, a new touch was started.
var history = finger.m_StateHistory;
var touchRecordCount = history.Count;
var currentTouchId = 0;
for (var n = touchRecordCount - 1; n >= 0; --n)
{
var record = history[n];
var state = *(TouchState*)record.GetUnsafeMemoryPtr();
var extra = (ExtraDataPerTouchState*)record.GetUnsafeExtraMemoryPtr();
// Skip if part of an ongoing touch we've already recorded.
if (state.touchId == currentTouchId && !state.phase.IsEndedOrCanceled())
continue;
// If the touch is older than the current frame and it's a touch that has
// ended, we don't need to look further back into the history as anything
// coming before that will be equally outdated.
var wasUpdatedThisFrame = extra->updateStepCount == updateStepCount;
if (!wasUpdatedThisFrame && state.phase.IsEndedOrCanceled())
break;
// Make a copy of the touch so that we can modify data like deltas and phase.
var newRecord = activeTouchState.AddRecord(record);
var newTouch = new Touch(finger, newRecord);
// If the touch hasn't moved this frame, mark it stationary.
if ((state.phase == TouchPhase.Moved || state.phase == TouchPhase.Began) &&
!wasUpdatedThisFrame)
((TouchState*)newRecord.GetUnsafeMemoryPtr())->phase = TouchPhase.Stationary;
// If the touch is hasn't moved or ended this frame, zero out its delta.
if (!((state.phase == TouchPhase.Moved || state.phase == TouchPhase.Ended) &&
wasUpdatedThisFrame))
{
((TouchState*)newRecord.GetUnsafeMemoryPtr())->delta = new Vector2();
}
else
{
// We want accumulated deltas only on activeTouches.
((TouchState*)newRecord.GetUnsafeMemoryPtr())->delta =
((ExtraDataPerTouchState*)newRecord.GetUnsafeExtraMemoryPtr())->accumulatedDelta;
}
ArrayHelpers.InsertAtWithCapacity(ref activeTouches, ref activeTouchCount, insertAt, newTouch);
currentTouchId = state.touchId;
}
}
haveBuiltActiveTouches = true;
}
}
internal struct ExtraDataPerTouchState
{
public Vector2 accumulatedDelta;
public uint updateStepCount;
// We can't guarantee that the platform is not reusing touch IDs.
public uint uniqueId;
////TODO
//public uint tapCount;
}
}
}