using System; using System.Runtime.InteropServices; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem.LowLevel; using UnityEngine.InputSystem.Utilities; using UnityEngine.Profiling; ////TODO: property that tells whether a Touchscreen is multi-touch capable ////TODO: property that tells whether a Touchscreen supports pressure ////TODO: add support for screen orientation ////TODO: touch is hardwired to certain memory layouts ATM; either allow flexibility or make sure the layouts cannot be changed ////TODO: startTimes are baked *external* times; reset touch when coming out of play mode ////TODO: detect and diagnose touchId=0 events ////REVIEW: where should we put handset vibration support? should that sit on the touchscreen class? be its own separate device? ////REVIEW: Given that Touchscreen is no use for polling, should we remove Touchscreen.current? ////REVIEW: Should Touchscreen reset individual TouchControls to default(TouchState) after a touch has ended? This would allow //// binding to a TouchControl as a whole and the action would correctly cancel if the touch ends namespace UnityEngine.InputSystem.LowLevel { [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1028:EnumStorageShouldBeInt32", Justification = "byte to correspond to TouchState layout.")] [Flags] internal enum TouchFlags : byte { IndirectTouch = 1 << 0, // NOTE: Leaving the first 3 bits for native. PrimaryTouch = 1 << 3, TapPress = 1 << 4, TapRelease = 1 << 5, // Indicates that the touch that established this primary touch has ended but that when // it did, there were still other touches going on. We end the primary touch when the // last touch leaves the screen. OrphanedPrimaryTouch = 1 << 6, // This is only used by EnhancedTouch to mark touch records that have begun in the same // frame as the current touch record. BeganInSameFrame = 1 << 7, } ////REVIEW: add timestamp directly to touch? /// /// State layout for a single touch. /// /// /// This is the low-level memory representation of a single touch, i.e the /// way touches are internally transmitted and stored in the system. To update /// touches on a , s containing /// TouchStates are sent to the screen. /// /// /// // IMPORTANT: Must match TouchInputState in native code. [StructLayout(LayoutKind.Explicit, Size = kSizeInBytes)] public struct TouchState : IInputStateTypeInfo { internal const int kSizeInBytes = 56; /// /// Memory format tag for TouchState. /// /// Returns "TOUC". /// public static FourCC Format => new FourCC('T', 'O', 'U', 'C'); ////REVIEW: this should really be a uint /// /// Numeric ID of the touch. /// /// Numeric ID of the touch. /// /// While a touch is ongoing, it must have a non-zero ID different from /// all other ongoing touches. Starting with /// and ending with or , /// a touch is identified by its ID, i.e. a TouchState with the same ID /// belongs to the same touch. /// /// After a touch has ended or been canceled, an ID can be reused. /// /// [InputControl(displayName = "Touch ID", layout = "Integer", synthetic = true, dontReset = true)] [FieldOffset(0)] public int touchId; /// /// Screen-space position of the touch in pixels. /// /// Screen-space position of the touch. /// [InputControl(displayName = "Position", dontReset = true)] [FieldOffset(4)] public Vector2 position; /// /// Screen-space motion delta of the touch in pixels. /// /// Screen-space movement delta. /// [InputControl(displayName = "Delta", layout = "Delta")] [FieldOffset(12)] public Vector2 delta; /// /// Pressure-level of the touch against the touchscreen. /// /// Pressure of touch. /// /// The core range for this value is [0..1] with 1 indicating maximum pressure. Note, however, /// that the actual value may go beyond 1 in practice. This is because the system will usually /// define "maximum pressure" to be less than the physical maximum limit the hardware is capable /// of reporting so that to achieve maximum pressure, one does not need to press as hard as /// possible. /// /// [InputControl(displayName = "Pressure", layout = "Axis")] [FieldOffset(20)] public float pressure; /// /// Radius of the touch print on the surface. /// /// Touch extents horizontally and vertically. /// /// The touch radius is given in screen-space pixel coordinates along X and Y centered in the middle /// of the touch. Note that not all screens and systems support radius detection on touches so this /// value may be at default for an otherwise perfectly valid touch. /// /// [InputControl(displayName = "Radius")] [FieldOffset(24)] public Vector2 radius; /// /// value of the touch. /// /// Current . /// [InputControl(name = "phase", displayName = "Touch Phase", layout = "TouchPhase", synthetic = true)] [InputControl(name = "press", displayName = "Touch Contact?", layout = "TouchPress", useStateFrom = "phase")] [FieldOffset(32)] public byte phaseId; [InputControl(name = "tapCount", displayName = "Tap Count", layout = "Integer")] [FieldOffset(33)] public byte tapCount; /// /// The index of the display that was touched. /// [InputControl(name = "displayIndex", displayName = "Display Index", layout = "Integer")] [FieldOffset(34)] public byte displayIndex; [InputControl(name = "indirectTouch", displayName = "Indirect Touch?", layout = "Button", bit = 0, synthetic = true)] [InputControl(name = "tap", displayName = "Tap", layout = "Button", bit = 4)] [FieldOffset(35)] public byte flags; // Need four bytes of alignment here for the startTime double. Using that for storing updateStepCounts. // They aren't needed directly by Touchscreen but are used by EnhancedTouch and since we have the four // bytes, may just as well use them instead of wasting them on padding. [FieldOffset(36)] internal uint updateStepCount; // NOTE: The following data is NOT sent by native but rather data we add on the managed side to each touch. /// /// Time that the touch was started. Relative to Time.realTimeSinceStartup. /// /// Time that the touch was started. /// /// This is set automatically by and does not need to be provided /// by events sent to the touchscreen. /// /// /// [InputControl(displayName = "Start Time", layout = "Double", synthetic = true)] [FieldOffset(40)] public double startTime; // In *external* time, i.e. currentTimeOffsetToRealtimeSinceStartup baked in. /// /// The position where the touch started. /// /// Screen-space start position of the touch. /// /// This is set automatically by and does not need to be provided /// by events sent to the touchscreen. /// /// [InputControl(displayName = "Start Position", synthetic = true)] [FieldOffset(48)] public Vector2 startPosition; /// /// Get or set the phase of the touch. /// /// Phase of the touch. /// public TouchPhase phase { get => (TouchPhase)phaseId; set => phaseId = (byte)value; } public bool isNoneEndedOrCanceled => phase == TouchPhase.None || phase == TouchPhase.Ended || phase == TouchPhase.Canceled; public bool isInProgress => phase == TouchPhase.Began || phase == TouchPhase.Moved || phase == TouchPhase.Stationary; /// /// Whether, after not having any touch contacts, this is part of the first touch contact that started. /// /// /// This flag will be set internally by . Generally, it is /// not necessary to set this bit manually when feeding data to Touchscreens. /// public bool isPrimaryTouch { get => (flags & (byte)TouchFlags.PrimaryTouch) != 0; set { if (value) flags |= (byte)TouchFlags.PrimaryTouch; else flags &= (byte)~TouchFlags.PrimaryTouch; } } internal bool isOrphanedPrimaryTouch { get => (flags & (byte)TouchFlags.OrphanedPrimaryTouch) != 0; set { if (value) flags |= (byte)TouchFlags.OrphanedPrimaryTouch; else flags &= (byte)~TouchFlags.OrphanedPrimaryTouch; } } public bool isIndirectTouch { get => (flags & (byte)TouchFlags.IndirectTouch) != 0; set { if (value) flags |= (byte)TouchFlags.IndirectTouch; else flags &= (byte)~TouchFlags.IndirectTouch; } } public bool isTap { get => isTapPress; set => isTapPress = value; } internal bool isTapPress { get => (flags & (byte)TouchFlags.TapPress) != 0; set { if (value) flags |= (byte)TouchFlags.TapPress; else flags &= (byte)~TouchFlags.TapPress; } } internal bool isTapRelease { get => (flags & (byte)TouchFlags.TapRelease) != 0; set { if (value) flags |= (byte)TouchFlags.TapRelease; else flags &= (byte)~TouchFlags.TapRelease; } } internal bool beganInSameFrame { get => (flags & (byte)TouchFlags.BeganInSameFrame) != 0; set { if (value) flags |= (byte)TouchFlags.BeganInSameFrame; else flags &= (byte)~TouchFlags.BeganInSameFrame; } } /// public FourCC format => Format; /// /// Return a string representation of the state useful for debugging. /// /// A string representation of the touch state. public override string ToString() { return $"{{ id={touchId} phase={phase} pos={position} delta={delta} pressure={pressure} radius={radius} primary={isPrimaryTouch} }}"; } } /// /// Default state layout for touch devices. /// /// /// Combines multiple pointers each corresponding to a single contact. /// /// Normally, TODO (sending state events) /// /// All touches combine to quite a bit of state; ideally send delta events that update /// only specific fingers. /// /// This is NOT used by native. Instead, the native runtime always sends individual touches () /// and leaves state management for a touchscreen as a whole to the managed part of the system. /// [StructLayout(LayoutKind.Explicit, Size = MaxTouches * TouchState.kSizeInBytes)] internal unsafe struct TouchscreenState : IInputStateTypeInfo { /// /// Memory format tag for TouchscreenState. /// /// Returns "TSCR". /// public static FourCC Format => new FourCC('T', 'S', 'C', 'R'); /// /// Maximum number of touches that can be tracked at the same time. /// /// Maximum number of concurrent touches. public const int MaxTouches = 10; /// /// Data for the touch that is deemed the "primary" touch at the moment. /// /// /// This touch duplicates touch data from whichever touch is deemed the primary touch at the moment. /// When going from no fingers down to any finger down, the first finger to touch the screen is /// deemed the "primary touch". It stays the primary touch until released. At that point, if any other /// finger is still down, the next finger in is /// /// Having this touch be its own separate state and own separate control allows actions to track the /// state of the primary touch even if the touch moves from one finger to another in . /// [InputControl(name = "primaryTouch", displayName = "Primary Touch", layout = "Touch", synthetic = true)] [InputControl(name = "primaryTouch/tap", usage = "PrimaryAction")] // Add controls compatible with what Pointer expects and redirect their // state to the state of touch0 so that this essentially becomes our // pointer control. // NOTE: Some controls from Pointer don't make sense for touch and we "park" // them by assigning them invalid offsets (thus having automatic state // layout put them at the end of our fixed state). [InputControl(name = "position", useStateFrom = "primaryTouch/position")] [InputControl(name = "delta", useStateFrom = "primaryTouch/delta", layout = "Delta")] [InputControl(name = "pressure", useStateFrom = "primaryTouch/pressure")] [InputControl(name = "radius", useStateFrom = "primaryTouch/radius")] [InputControl(name = "press", useStateFrom = "primaryTouch/phase", layout = "TouchPress", synthetic = true, usages = new string[0])] [FieldOffset(0)] public fixed byte primaryTouchData[TouchState.kSizeInBytes]; internal const int kTouchDataOffset = TouchState.kSizeInBytes; [InputControl(layout = "Touch", name = "touch", displayName = "Touch", arraySize = MaxTouches)] [FieldOffset(kTouchDataOffset)] public fixed byte touchData[MaxTouches * TouchState.kSizeInBytes]; public TouchState* primaryTouch { get { fixed(byte* ptr = primaryTouchData) return (TouchState*)ptr; } } public TouchState* touches { get { fixed(byte* ptr = touchData) return (TouchState*)ptr; } } public FourCC format => Format; } } namespace UnityEngine.InputSystem { /// /// Indicates where in its lifecycle a given touch is. /// public enum TouchPhase { ////REVIEW: Why have a separate None instead of just making this equivalent to either Ended or Canceled? /// /// No activity has been registered on the touch yet. /// /// /// A given touch state will generally not go back to None once there has been input for it. Meaning that /// it generally indicates a default-initialized touch record. /// None, /// /// A touch has just begun, i.e. a finger has touched the screen.. Only the first touch input in any given touch will have this phase. /// Began, /// /// An ongoing touch has changed position. /// Moved, /// /// An ongoing touch has just ended, i.e. the respective finger has been lifted off of the screen. Only the last touch input in a /// given touch will have this phase. /// Ended, /// /// An ongoing touch has been cancelled, i.e. ended in a way other than through user interaction. This happens, for example, if /// focus is moved away from the application while the touch is ongoing. /// Canceled, /// /// An ongoing touch has not been moved (not received any input) in a frame. /// /// /// This phase is not used by . This means that will not generally /// return this value for . It is, however, used by . /// Stationary, } /// /// A multi-touch surface. /// /// /// Touchscreen is somewhat different from most other device implementations in that it does not usually /// consume input in the form of a full device snapshot but rather consumes input sent to it in the form /// of events containing a each. This is unusual as /// uses a memory format different from . However, when a Touchscreen /// sees an event containing a , it will handle that event on a special code path. /// /// This allows Touchscreen to decide on its own which control in to store /// a touch at and to perform things such as tap detection (see and /// ) and primary touch handling (see ). /// /// /// /// // Create a touchscreen device. /// var touchscreen = InputSystem.AddDevice<Touchscreen>(); /// /// // Send a touch to the device. /// InputSystem.QueueStateEvent(touchscreen, /// new TouchState /// { /// phase = TouchPhase.Began, /// // Must have a valid, non-zero touch ID. Touchscreen will not operate /// // correctly if we don't set IDs properly. /// touchId = 1, /// position = new Vector2(123, 234), /// // Delta will be computed by Touchscreen automatically. /// }); /// /// /// /// Note that this class presents a fairly low-level touch API. When working with touch from script code, /// it is recommended to use the higher-level API instead. /// [InputControlLayout(stateType = typeof(TouchscreenState), isGenericTypeOfDevice = true)] public class Touchscreen : Pointer, IInputStateCallbackReceiver, IEventMerger, ICustomDeviceReset { /// /// Synthetic control that has the data for the touch that is deemed the "primary" touch at the moment. /// /// Control tracking the screen's primary touch. /// /// This touch duplicates touch data from whichever touch is deemed the primary touch at the moment. /// When going from no fingers down to any finger down, the first finger to touch the screen is /// deemed the "primary touch". It stays the primary touch until the last finger is released. /// /// Note that unlike the touch from which it originates, the primary touch will be kept ongoing for /// as long as there is still a finger on the screen. Put another way, /// of primaryTouch will only transition to once the last finger /// has been lifted off the screen. /// public TouchControl primaryTouch { get; protected set; } /// /// Array of all s on the device. /// /// All s on the screen. /// /// By default, a touchscreen will allocate 10 touch controls. This can be changed /// by modifying the "Touchscreen" layout itself or by derived layouts. In practice, /// this means that this array will usually have a fixed length of 10 entries but /// it may deviate from that. /// public ReadOnlyArray touches { get; protected set; } protected TouchControl[] touchControlArray { get => touches.m_Array; set => touches = new ReadOnlyArray(value); } /// /// The touchscreen that was added or updated last or null if there is no /// touchscreen connected to the system. /// /// Current touch screen. public new static Touchscreen current { get; internal set; } /// public override void MakeCurrent() { base.MakeCurrent(); current = this; } /// protected override void OnRemoved() { base.OnRemoved(); if (current == this) current = null; } /// protected override void FinishSetup() { base.FinishSetup(); primaryTouch = GetChildControl("primaryTouch"); displayIndex = primaryTouch.displayIndex; // Find out how many touch controls we have. var touchControlCount = 0; foreach (var child in children) if (child is TouchControl) ++touchControlCount; // Keep primaryTouch out of array. Debug.Assert(touchControlCount >= 1, "Should have found at least primaryTouch control"); if (touchControlCount >= 1) --touchControlCount; // Gather touch controls into array. var touchArray = new TouchControl[touchControlCount]; var touchIndex = 0; foreach (var child in children) { if (child == primaryTouch) continue; if (child is TouchControl control) touchArray[touchIndex++] = control; } touches = new ReadOnlyArray(touchArray); } // Touch has more involved state handling than most other devices. To not put touch allocation logic // in all the various platform backends (i.e. see a touch with a certain ID coming in from the system // and then having to decide *where* to store that inside of Touchscreen's state), we have backends // send us individual touches ('TOUC') instead of whole Touchscreen snapshots ('TSRC'). Using // IInputStateCallbackReceiver, Touchscreen then dynamically decides where to store the touch. // // Also, Touchscreen has bits of logic to automatically synthesize the state of controls it inherits // from Pointer (such as "/press"). // // NOTE: We do *NOT* make a effort here to prevent us from losing short-lived touches. This is different // from the old input system where individual touches were not reused until the next frame. This meant // that additional touches potentially had to be allocated in order to accommodate new touches coming // in from the system. // // The rationale for *NOT* doing this is that: // // a) Actions don't need it. They observe every single state change and thus will not lose data // even if it is short-lived (i.e. changes more than once in the same update). // b) The higher-level Touch (EnhancedTouchSupport) API is provided to // not only handle this scenario but also give a generally more flexible and useful touch API // than writing code directly against Touchscreen. protected new unsafe void OnNextUpdate() { Profiler.BeginSample("Touchscreen.OnNextUpdate"); ////TODO: early out and skip crawling through touches if we didn't change state in the last update //// (also obsoletes the need for the if() check below) var statePtr = currentStatePtr; var touchStatePtr = (TouchState*)((byte*)statePtr + stateBlock.byteOffset + TouchscreenState.kTouchDataOffset); for (var i = 0; i < touches.Count; ++i, ++touchStatePtr) { // Reset delta. if (touchStatePtr->delta != default) InputState.Change(touches[i].delta, Vector2.zero); // Reset tap count. // NOTE: We are basing this on startTime rather than adding on end time of the last touch. The reason is // that to do so we would have to add another record to keep track of timestamps for each touch. And // since we know the maximum time that a tap can take, we have a reasonable estimate for when a prior // tap must have ended. if (touchStatePtr->tapCount > 0 && InputState.currentTime >= touchStatePtr->startTime + s_TapTime + s_TapDelayTime) InputState.Change(touches[i].tapCount, (byte)0); } var primaryTouchState = (TouchState*)((byte*)statePtr + stateBlock.byteOffset); if (primaryTouchState->delta != default) InputState.Change(primaryTouch.delta, Vector2.zero); if (primaryTouchState->tapCount > 0 && InputState.currentTime >= primaryTouchState->startTime + s_TapTime + s_TapDelayTime) InputState.Change(primaryTouch.tapCount, (byte)0); Profiler.EndSample(); } /// /// Called whenever a new state event is received. /// /// protected new unsafe void OnStateEvent(InputEventPtr eventPtr) { var eventType = eventPtr.type; // We don't allow partial updates for TouchStates. if (eventType == DeltaStateEvent.Type) return; // If it's not a single touch, just take the event state as is (will have to be TouchscreenState). var stateEventPtr = StateEvent.FromUnchecked(eventPtr); if (stateEventPtr->stateFormat != TouchState.Format) { InputState.Change(this, eventPtr); return; } Profiler.BeginSample("TouchAllocate"); // For performance reasons, we read memory here directly rather than going through // ReadValue() of the individual TouchControl children. This means that Touchscreen, // unlike other devices, is hardwired to a single memory layout only. var statePtr = currentStatePtr; var currentTouchState = (TouchState*)((byte*)statePtr + touches[0].stateBlock.byteOffset); var primaryTouchState = (TouchState*)((byte*)statePtr + primaryTouch.stateBlock.byteOffset); var touchControlCount = touches.Count; // Native does not send a full TouchState as we define it here. We have added some fields // that we store internally. Make sure we don't read invalid memory here and copy only what // we got. TouchState newTouchState; if (stateEventPtr->stateSizeInBytes == TouchState.kSizeInBytes) { newTouchState = *(TouchState*)stateEventPtr->state; } else { newTouchState = default; UnsafeUtility.MemCpy(UnsafeUtility.AddressOf(ref newTouchState), stateEventPtr->state, stateEventPtr->stateSizeInBytes); } // Make sure we're not getting thrown off by noise on fields that we don't want to // pick up from input. newTouchState.tapCount = 0; newTouchState.isTapPress = false; newTouchState.isTapRelease = false; newTouchState.updateStepCount = InputUpdate.s_UpdateStepCount; ////REVIEW: The logic in here makes us inherently susceptible to the ordering of the touch events in the event //// stream. I believe we have platforms (Android?) that send us touch events finger-by-finger (or touch-by-touch?) //// rather than sorted by time. This will probably screw up the logic in here. // If it's an ongoing touch, try to find the TouchState we have allocated to the touch // previously. var phase = newTouchState.phase; if (phase != TouchPhase.Began) { var touchId = newTouchState.touchId; for (var i = 0; i < touchControlCount; ++i) { if (currentTouchState[i].touchId == touchId) { // Preserve primary touch state. var isPrimaryTouch = currentTouchState[i].isPrimaryTouch; newTouchState.isPrimaryTouch = isPrimaryTouch; // Compute delta if touch doesn't have one. if (newTouchState.delta == default) newTouchState.delta = newTouchState.position - currentTouchState[i].position; // Accumulate delta. newTouchState.delta += currentTouchState[i].delta; // Keep start time and position. newTouchState.startTime = currentTouchState[i].startTime; newTouchState.startPosition = currentTouchState[i].startPosition; // Detect taps. var isTap = newTouchState.isNoneEndedOrCanceled && (eventPtr.time - newTouchState.startTime) <= s_TapTime && ////REVIEW: this only takes the final delta to start position into account, not the delta over the lifetime of the //// touch; is this robust enough or do we need to make sure that we never move more than the tap radius //// over the entire lifetime of the touch? (newTouchState.position - newTouchState.startPosition).sqrMagnitude <= s_TapRadiusSquared; if (isTap) newTouchState.tapCount = (byte)(currentTouchState[i].tapCount + 1); else newTouchState.tapCount = currentTouchState[i].tapCount; // Preserve tap count; reset in OnCarryStateForward. // Update primary touch. if (isPrimaryTouch) { if (newTouchState.isNoneEndedOrCanceled) { ////REVIEW: also reset tapCounts here when tap delay time has expired on the touch? newTouchState.isPrimaryTouch = false; // Primary touch was ended. See if there are still other ongoing touches. var haveOngoingTouch = false; for (var n = 0; n < touchControlCount; ++n) { if (n == i) continue; if (currentTouchState[n].isInProgress) { haveOngoingTouch = true; break; } } if (!haveOngoingTouch) { // No, primary was the only ongoing touch. End it. if (isTap) TriggerTap(primaryTouch, ref newTouchState, eventPtr); else InputState.Change(primaryTouch, ref newTouchState, eventPtr: eventPtr); } else { // Yes, we have other touches going on. Make the primary touch an // orphan and wait until the other touches are released. var newPrimaryTouchState = newTouchState; newPrimaryTouchState.phase = TouchPhase.Moved; newPrimaryTouchState.isOrphanedPrimaryTouch = true; InputState.Change(primaryTouch, ref newPrimaryTouchState, eventPtr: eventPtr); } } else { // Primary touch was updated. InputState.Change(primaryTouch, ref newTouchState, eventPtr: eventPtr); } } else { // If it's not the primary touch but the touch has ended, see if we have an // orphaned primary touch. If so, end it now. if (newTouchState.isNoneEndedOrCanceled && primaryTouchState->isOrphanedPrimaryTouch) { var haveOngoingTouch = false; for (var n = 0; n < touchControlCount; ++n) { if (n == i) continue; if (currentTouchState[n].isInProgress) { haveOngoingTouch = true; break; } } if (!haveOngoingTouch) { primaryTouchState->isOrphanedPrimaryTouch = false; InputState.Change(primaryTouch.phase, (byte)TouchPhase.Ended); } } } if (isTap) { // Make tap button go down and up. // // NOTE: We do this here instead of right away up there when we detect the touch so // that the state change notifications go together. First those for the primary // touch, then the ones for the touch record itself. TriggerTap(touches[i], ref newTouchState, eventPtr); } else { InputState.Change(touches[i], ref newTouchState, eventPtr: eventPtr); } Profiler.EndSample(); return; } } // Couldn't find an entry. Either it was a touch that we previously ran out of available // entries for or it's an event sent out of sequence. Ignore the touch to be consistent. Profiler.EndSample(); return; } // It's a new touch. Try to find an unused TouchState. for (var i = 0; i < touchControlCount; ++i, ++currentTouchState) { // NOTE: We're overwriting any ended touch immediately here. This means we immediately overwrite even // if we still have other unused slots. What this gives us is a completely predictable touch #0..#N // sequence (i.e. touch #N is only ever used if there are indeed #N concurrently touches). However, // it does mean that we overwrite state aggressively. If you are not using actions or the higher-level // Touch API, be aware of this! if (currentTouchState->isNoneEndedOrCanceled) { newTouchState.delta = Vector2.zero; newTouchState.startTime = eventPtr.time; newTouchState.startPosition = newTouchState.position; // Make sure we're not picking up noise sent from native. newTouchState.isPrimaryTouch = false; newTouchState.isOrphanedPrimaryTouch = false; newTouchState.isTap = false; // Tap counts are preserved from prior touches on the same finger. newTouchState.tapCount = currentTouchState->tapCount; // Make primary touch, if there's none currently. if (primaryTouchState->isNoneEndedOrCanceled) { newTouchState.isPrimaryTouch = true; InputState.Change(primaryTouch, ref newTouchState, eventPtr: eventPtr); } InputState.Change(touches[i], ref newTouchState, eventPtr: eventPtr); Profiler.EndSample(); return; } } // We ran out of state and we don't want to stomp an existing ongoing touch. // Drop this touch entirely. // NOTE: Getting here means we're having fewer touch entries than the number of concurrent touches supported // by the backend (or someone is simply sending us nonsense data). Profiler.EndSample(); } void IInputStateCallbackReceiver.OnNextUpdate() { OnNextUpdate(); } void IInputStateCallbackReceiver.OnStateEvent(InputEventPtr eventPtr) { OnStateEvent(eventPtr); } unsafe bool IInputStateCallbackReceiver.GetStateOffsetForEvent(InputControl control, InputEventPtr eventPtr, ref uint offset) { // This code goes back to the trickery we perform in OnStateEvent. We consume events in TouchState format // instead of in TouchscreenState format. This means that the input system does not know how the state in those // events correlates to the controls we have. // // This method is used to give the input system an offset based on which the input system can compute relative // offsets into the state of eventPtr for controls that are part of the control hierarchy rooted at 'control'. if (!eventPtr.IsA()) return false; var stateEventPtr = StateEvent.FromUnchecked(eventPtr); if (stateEventPtr->stateFormat != TouchState.Format) return false; // If we get a null control and a TouchState event, all the system wants to know is what // state offset to use to make sense of the event. if (control == null) { // We can't say which specific touch this would go to (if any at all) without going through // the same logic that we run through in OnStateEvent. For the sake of just being able to read // out data from a touch event, it'd be enough to return the offset of *any* TouchControl here. // But for the sake of being able to compare the data in an event to that in the Touchscreen, // this would not be enough. Thus we make an attempt here at locating a touch record which *should* // be receiving the event if it were to be processed by OnStateEvent. var currentTouchState = (TouchState*)((byte*)currentStatePtr + touches[0].stateBlock.byteOffset); var eventTouchState = (TouchState*)stateEventPtr->state; var eventTouchId = eventTouchState->touchId; var eventTouchPhase = eventTouchState->phase; var touchControlCount = touches.Count; for (var i = 0; i < touchControlCount; ++i) { var touch = ¤tTouchState[i]; if (touch->touchId == eventTouchId || (!touch->isInProgress && eventTouchPhase.IsActive())) { offset = primaryTouch.m_StateBlock.byteOffset + primaryTouch.m_StateBlock.alignedSizeInBytes - m_StateBlock.byteOffset + (uint)(i * UnsafeUtility.SizeOf()); return true; } } return false; } // The only controls we can read out from a TouchState event are those that are part of TouchControl // (and part of this Touchscreen). var touchControl = control.FindInParentChain(); if (touchControl == null || touchControl.parent != this) return false; // We could allow *any* of the TouchControls on the Touchscreen here. We'd simply base the // offset on the TouchControl of the 'control' we get as an argument. // // However, doing that would mean that all the TouchControls would map into the same input event. // So when a piece of code like in InputUser goes and cycles through all controls to determine ones // that have changed in an event, it would find that instead of a single touch position value changing, // all of them would be changing from the same single event. // // For this reason, we lock things down to the primaryTouch control. if (touchControl != primaryTouch) return false; offset = touchControl.stateBlock.byteOffset - m_StateBlock.byteOffset; return true; } // Implement our own custom reset so that we can cancel touches instead of just wiping them // with default state. unsafe void ICustomDeviceReset.Reset() { var statePtr = currentStatePtr; //// https://jira.unity3d.com/browse/ISX-930 ////TODO: Figure out a proper way to distinguish the source / reason for a state change. //// What we're doing here is constructing an event solely for the purpose of Finger.ShouldRecordTouch() not //// ignoring the state change like it does for delta resets. using (var buffer = new NativeArray(StateEvent.GetEventSizeWithPayload(), Allocator.Temp)) { var eventPtr = (StateEvent*)buffer.GetUnsafePtr(); eventPtr->baseEvent = new InputEvent(StateEvent.Type, buffer.Length, deviceId); var primaryTouchState = (TouchState*)((byte*)statePtr + primaryTouch.stateBlock.byteOffset); if (primaryTouchState->phase.IsActive()) { UnsafeUtility.MemCpy(eventPtr->state, primaryTouchState, UnsafeUtility.SizeOf()); ((TouchState*)eventPtr->state)->phase = TouchPhase.Canceled; InputState.Change(primaryTouch.phase, TouchPhase.Canceled, eventPtr: new InputEventPtr((InputEvent*)eventPtr)); } var touchStates = (TouchState*)((byte*)statePtr + touches[0].stateBlock.byteOffset); var touchCount = touches.Count; for (var i = 0; i < touchCount; ++i) { if (touchStates[i].phase.IsActive()) { UnsafeUtility.MemCpy(eventPtr->state, &touchStates[i], UnsafeUtility.SizeOf()); ((TouchState*)eventPtr->state)->phase = TouchPhase.Canceled; InputState.Change(touches[i].phase, TouchPhase.Canceled, eventPtr: new InputEventPtr((InputEvent*)eventPtr)); } } } } internal static unsafe bool MergeForward(InputEventPtr currentEventPtr, InputEventPtr nextEventPtr) { if (currentEventPtr.type != StateEvent.Type || nextEventPtr.type != StateEvent.Type) return false; var currentEvent = StateEvent.FromUnchecked(currentEventPtr); var nextEvent = StateEvent.FromUnchecked(nextEventPtr); if (currentEvent->stateFormat != TouchState.Format || nextEvent->stateFormat != TouchState.Format) return false; var currentState = (TouchState*)currentEvent->state; var nextState = (TouchState*)nextEvent->state; if (currentState->touchId != nextState->touchId || currentState->phaseId != nextState->phaseId || currentState->flags != nextState->flags) return false; nextState->delta += currentState->delta; return true; } bool IEventMerger.MergeForward(InputEventPtr currentEventPtr, InputEventPtr nextEventPtr) { return MergeForward(currentEventPtr, nextEventPtr); } // We can only detect taps on touch *release*. At which point it acts like a button that triggers and releases // in one operation. private static void TriggerTap(TouchControl control, ref TouchState state, InputEventPtr eventPtr) { ////REVIEW: we're updating the entire TouchControl here; we could update just the tap state using a delta event; problem //// is that the tap *down* still needs a full update on the state // We don't increase tapCount here as we may be sending the tap from the same state to both the TouchControl // that got tapped and to primaryTouch. // Press. state.isTapPress = true; state.isTapRelease = false; InputState.Change(control, ref state, eventPtr: eventPtr); // Release. state.isTapPress = false; state.isTapRelease = true; InputState.Change(control, ref state, eventPtr: eventPtr); state.isTapRelease = false; } internal static float s_TapTime; internal static float s_TapDelayTime; internal static float s_TapRadiusSquared; } }