267 lines
11 KiB
C#
267 lines
11 KiB
C#
using Unity.Collections.LowLevel.Unsafe;
|
|
using UnityEngine.InputSystem.LowLevel;
|
|
using UnityEngine.InputSystem.Utilities;
|
|
|
|
namespace UnityEngine.InputSystem.EnhancedTouch
|
|
{
|
|
/// <summary>
|
|
/// A source of touches (<see cref="Touch"/>).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Each <see cref="Touchscreen"/> has a limited number of fingers it supports corresponding to the total number of concurrent
|
|
/// touches supported by the screen. Unlike a <see cref="Touch"/>, a <see cref="Finger"/> will stay the same and valid for the
|
|
/// lifetime of its <see cref="Touchscreen"/>.
|
|
///
|
|
/// Note that a Finger does not represent an actual physical finger in the world. That is, the same Finger instance might be used,
|
|
/// for example, for a touch from the index finger at one point and then for a touch from the ring finger. Each Finger simply
|
|
/// corresponds to the Nth touch on the given screen.
|
|
/// </remarks>
|
|
/// <seealso cref="Touch"/>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
|
|
Justification = "Holds on to internally managed memory which should not be disposed by the user.")]
|
|
public class Finger
|
|
{
|
|
// This class stores pretty much all the data that is kept by the enhanced touch system. All
|
|
// the finger and history tracking is found here.
|
|
|
|
/// <summary>
|
|
/// The screen that the finger is associated with.
|
|
/// </summary>
|
|
/// <value>Touchscreen associated with the touch.</value>
|
|
public Touchscreen screen { get; }
|
|
|
|
/// <summary>
|
|
/// Index of the finger on <see cref="screen"/>. Each finger corresponds to the Nth touch on a screen.
|
|
/// </summary>
|
|
public int index { get; }
|
|
|
|
/// <summary>
|
|
/// Whether the finger is currently touching the screen.
|
|
/// </summary>
|
|
public bool isActive => currentTouch.valid;
|
|
|
|
/// <summary>
|
|
/// The current position of the finger on the screen or <c>default(Vector2)</c> if there is no
|
|
/// ongoing touch.
|
|
/// </summary>
|
|
public Vector2 screenPosition
|
|
{
|
|
get
|
|
{
|
|
////REVIEW: should this work off of currentTouch instead of lastTouch?
|
|
var touch = lastTouch;
|
|
if (!touch.valid)
|
|
return default;
|
|
return touch.screenPosition;
|
|
}
|
|
}
|
|
|
|
////REVIEW: should lastTouch and currentTouch have accumulated deltas? would that be confusing?
|
|
|
|
/// <summary>
|
|
/// The last touch that happened on the finger or <c>default(Touch)</c> (with <see cref="Touch.valid"/> being
|
|
/// false) if no touch has been registered on the finger yet.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A given touch will be returned from this property for as long as no new touch has been started. As soon as a
|
|
/// new touch is registered on the finger, the property switches to the new touch.
|
|
/// </remarks>
|
|
public Touch lastTouch
|
|
{
|
|
get
|
|
{
|
|
var count = m_StateHistory.Count;
|
|
if (count == 0)
|
|
return default;
|
|
return new Touch(this, m_StateHistory[count - 1]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The currently ongoing touch for the finger or <c>default(Touch)</c> (with <see cref="Touch.valid"/> being false)
|
|
/// if no touch is currently in progress on the finger.
|
|
/// </summary>
|
|
public Touch currentTouch
|
|
{
|
|
get
|
|
{
|
|
var touch = lastTouch;
|
|
if (!touch.valid)
|
|
return default;
|
|
if (touch.isInProgress)
|
|
return touch;
|
|
// Ended touches stay current in the frame they ended in.
|
|
if (touch.updateStepCount == InputUpdate.s_UpdateStepCount)
|
|
return touch;
|
|
return default;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The full touch history of the finger.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The history is capped at <see cref="Touch.maxHistoryLengthPerFinger"/>. Once full, newer touch records will start
|
|
/// overwriting older entries. Note that this means that a given touch will not trace all the way back to its beginning
|
|
/// if it runs past the max history size.
|
|
/// </remarks>
|
|
public TouchHistory touchHistory => new TouchHistory(this, m_StateHistory);
|
|
|
|
internal readonly InputStateHistory<TouchState> m_StateHistory;
|
|
|
|
internal Finger(Touchscreen screen, int index, InputUpdateType updateMask)
|
|
{
|
|
this.screen = screen;
|
|
this.index = index;
|
|
|
|
// Set up history recording.
|
|
m_StateHistory = new InputStateHistory<TouchState>(screen.touches[index])
|
|
{
|
|
historyDepth = Touch.maxHistoryLengthPerFinger,
|
|
extraMemoryPerRecord = UnsafeUtility.SizeOf<Touch.ExtraDataPerTouchState>(),
|
|
onRecordAdded = OnTouchRecorded,
|
|
onShouldRecordStateChange = ShouldRecordTouch,
|
|
updateMask = updateMask,
|
|
};
|
|
m_StateHistory.StartRecording();
|
|
|
|
// record the current state if touch is already in progress
|
|
if (screen.touches[index].isInProgress)
|
|
m_StateHistory.RecordStateChange(screen.touches[index], screen.touches[index].value);
|
|
}
|
|
|
|
private static unsafe bool ShouldRecordTouch(InputControl control, double time, InputEventPtr eventPtr)
|
|
{
|
|
// We only want to record changes that come from events. We ignore internal state
|
|
// changes that Touchscreen itself generates. This includes the resetting of deltas.
|
|
if (!eventPtr.valid)
|
|
return false;
|
|
var eventType = eventPtr.type;
|
|
if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type)
|
|
return false;
|
|
|
|
// Direct memory access for speed.
|
|
var currentTouchState = (TouchState*)((byte*)control.currentStatePtr + control.stateBlock.byteOffset);
|
|
|
|
// Touchscreen will record a button down and button up on a TouchControl when a tap occurs.
|
|
// We only want to record the button down, not the button up.
|
|
if (currentTouchState->isTapRelease)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
private unsafe void OnTouchRecorded(InputStateHistory.Record record)
|
|
{
|
|
var recordIndex = record.recordIndex;
|
|
var touchHeader = m_StateHistory.GetRecordUnchecked(recordIndex);
|
|
var touchState = (TouchState*)touchHeader->statePtrWithoutControlIndex; // m_StateHistory is bound to a single TouchControl.
|
|
touchState->updateStepCount = InputUpdate.s_UpdateStepCount;
|
|
|
|
// Invalidate activeTouches.
|
|
Touch.s_GlobalState.playerState.haveBuiltActiveTouches = false;
|
|
|
|
// Record the extra data we maintain for each touch.
|
|
var extraData = (Touch.ExtraDataPerTouchState*)((byte*)touchHeader + m_StateHistory.bytesPerRecord -
|
|
UnsafeUtility.SizeOf<Touch.ExtraDataPerTouchState>());
|
|
extraData->uniqueId = ++Touch.s_GlobalState.playerState.lastId;
|
|
|
|
// We get accumulated deltas from Touchscreen. Store the accumulated
|
|
// value and "unaccumulate" the value we store on delta.
|
|
extraData->accumulatedDelta = touchState->delta;
|
|
if (touchState->phase != TouchPhase.Began)
|
|
{
|
|
// Inlined (instead of just using record.previous) for speed. Bypassing
|
|
// the safety checks here.
|
|
if (recordIndex != m_StateHistory.m_HeadIndex)
|
|
{
|
|
var previousRecordIndex = recordIndex == 0 ? m_StateHistory.historyDepth - 1 : recordIndex - 1;
|
|
var previousTouchHeader = m_StateHistory.GetRecordUnchecked(previousRecordIndex);
|
|
var previousTouchState = (TouchState*)previousTouchHeader->statePtrWithoutControlIndex;
|
|
touchState->delta -= previousTouchState->delta;
|
|
touchState->beganInSameFrame = previousTouchState->beganInSameFrame &&
|
|
previousTouchState->updateStepCount == touchState->updateStepCount;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
touchState->beganInSameFrame = true;
|
|
}
|
|
|
|
// Trigger callback.
|
|
switch (touchState->phase)
|
|
{
|
|
case TouchPhase.Began:
|
|
DelegateHelpers.InvokeCallbacksSafe(ref Touch.s_GlobalState.onFingerDown, this, "Touch.onFingerDown");
|
|
break;
|
|
case TouchPhase.Moved:
|
|
DelegateHelpers.InvokeCallbacksSafe(ref Touch.s_GlobalState.onFingerMove, this, "Touch.onFingerMove");
|
|
break;
|
|
case TouchPhase.Ended:
|
|
case TouchPhase.Canceled:
|
|
DelegateHelpers.InvokeCallbacksSafe(ref Touch.s_GlobalState.onFingerUp, this, "Touch.onFingerUp");
|
|
break;
|
|
}
|
|
}
|
|
|
|
private unsafe Touch FindTouch(uint uniqueId)
|
|
{
|
|
Debug.Assert(uniqueId != default, "0 is not a valid ID");
|
|
foreach (var record in m_StateHistory)
|
|
{
|
|
if (((Touch.ExtraDataPerTouchState*)record.GetUnsafeExtraMemoryPtrUnchecked())->uniqueId == uniqueId)
|
|
return new Touch(this, record);
|
|
}
|
|
|
|
return default;
|
|
}
|
|
|
|
internal unsafe TouchHistory GetTouchHistory(Touch touch)
|
|
{
|
|
Debug.Assert(touch.finger == this);
|
|
|
|
// If the touch is not pointing to our history, it's probably a touch we copied for
|
|
// activeTouches. We know the unique ID of the touch so go and try to find the touch
|
|
// in our history.
|
|
var touchRecord = touch.m_TouchRecord;
|
|
if (touchRecord.owner != m_StateHistory)
|
|
{
|
|
touch = FindTouch(touch.uniqueId);
|
|
if (!touch.valid)
|
|
return default;
|
|
}
|
|
|
|
var touchId = touch.touchId;
|
|
var startIndex = touch.m_TouchRecord.index;
|
|
|
|
// If the current touch isn't the beginning of the touch, search back through the
|
|
// history for all touches belonging to the same contact.
|
|
var count = 0;
|
|
if (touch.phase != TouchPhase.Began)
|
|
{
|
|
for (var previousRecord = touch.m_TouchRecord.previous; previousRecord.valid; previousRecord = previousRecord.previous)
|
|
{
|
|
var touchState = (TouchState*)previousRecord.GetUnsafeMemoryPtr();
|
|
|
|
// Stop if the touch doesn't belong to the same contact.
|
|
if (touchState->touchId != touchId)
|
|
break;
|
|
++count;
|
|
|
|
// Stop if we've found the beginning of the touch.
|
|
if (touchState->phase == TouchPhase.Began)
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (count == 0)
|
|
return default;
|
|
|
|
// We don't want to include the touch we started with.
|
|
--startIndex;
|
|
|
|
return new TouchHistory(this, m_StateHistory, startIndex, count);
|
|
}
|
|
}
|
|
}
|