using Unity.Collections.LowLevel.Unsafe; using UnityEngine.InputSystem.LowLevel; using UnityEngine.InputSystem.Utilities; namespace UnityEngine.InputSystem.EnhancedTouch { /// /// A source of touches (). /// /// /// Each has a limited number of fingers it supports corresponding to the total number of concurrent /// touches supported by the screen. Unlike a , a will stay the same and valid for the /// lifetime of its . /// /// 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. /// /// [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. /// /// The screen that the finger is associated with. /// /// Touchscreen associated with the touch. public Touchscreen screen { get; } /// /// Index of the finger on . Each finger corresponds to the Nth touch on a screen. /// public int index { get; } /// /// Whether the finger is currently touching the screen. /// public bool isActive => currentTouch.valid; /// /// The current position of the finger on the screen or default(Vector2) if there is no /// ongoing touch. /// 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? /// /// The last touch that happened on the finger or default(Touch) (with being /// false) if no touch has been registered on the finger yet. /// /// /// 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. /// public Touch lastTouch { get { var count = m_StateHistory.Count; if (count == 0) return default; return new Touch(this, m_StateHistory[count - 1]); } } /// /// The currently ongoing touch for the finger or default(Touch) (with being false) /// if no touch is currently in progress on the finger. /// 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; } } /// /// The full touch history of the finger. /// /// /// The history is capped at . 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. /// public TouchHistory touchHistory => new TouchHistory(this, m_StateHistory); internal readonly InputStateHistory m_StateHistory; internal Finger(Touchscreen screen, int index, InputUpdateType updateMask) { this.screen = screen; this.index = index; // Set up history recording. m_StateHistory = new InputStateHistory(screen.touches[index]) { historyDepth = Touch.maxHistoryLengthPerFinger, extraMemoryPerRecord = UnsafeUtility.SizeOf(), 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()); 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); } } }