2023-03-28 13:24:16 -04:00
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?
/// <summary>
/// State layout for a single touch.
/// </summary>
/// <remarks>
/// 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 <see cref="Touchscreen"/>, <see cref="StateEvent"/>s containing
/// TouchStates are sent to the screen.
/// </remarks>
/// <seealso cref="TouchControl"/>
/// <seealso cref="Touchscreen"/>
// IMPORTANT: Must match TouchInputState in native code.
[StructLayout(LayoutKind.Explicit, Size = kSizeInBytes)]
public struct TouchState : IInputStateTypeInfo
{
internal const int kSizeInBytes = 56 ;
/// <summary>
/// Memory format tag for TouchState.
/// </summary>
/// <value>Returns "TOUC".</value>
/// <seealso cref="InputStateBlock.format"/>
public static FourCC Format = > new FourCC ( 'T' , 'O' , 'U' , 'C' ) ;
////REVIEW: this should really be a uint
/// <summary>
/// Numeric ID of the touch.
/// </summary>
/// <value>Numeric ID of the touch.</value>
/// <remarks>
/// While a touch is ongoing, it must have a non-zero ID different from
/// all other ongoing touches. Starting with <see cref="TouchPhase.Began"/>
/// and ending with <see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/>,
/// 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.
/// </remarks>
/// <seealso cref="TouchControl.touchId"/>
[InputControl(displayName = "Touch ID", layout = "Integer", synthetic = true, dontReset = true)]
[FieldOffset(0)]
public int touchId ;
/// <summary>
/// Screen-space position of the touch in pixels.
/// </summary>
/// <value>Screen-space position of the touch.</value>
/// <seealso cref="TouchControl.position"/>
[InputControl(displayName = "Position", dontReset = true)]
[FieldOffset(4)]
public Vector2 position ;
/// <summary>
/// Screen-space motion delta of the touch in pixels.
/// </summary>
/// <value>Screen-space movement delta.</value>
/// <seealso cref="TouchControl.delta"/>
[InputControl(displayName = "Delta", layout = "Delta")]
[FieldOffset(12)]
public Vector2 delta ;
/// <summary>
/// Pressure-level of the touch against the touchscreen.
/// </summary>
/// <value>Pressure of touch.</value>
/// <remarks>
/// 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.
/// </remarks>
/// <seealso cref="TouchControl.pressure"/>
[InputControl(displayName = "Pressure", layout = "Axis")]
[FieldOffset(20)]
public float pressure ;
/// <summary>
/// Radius of the touch print on the surface.
/// </summary>
/// <value>Touch extents horizontally and vertically.</value>
/// <remarks>
/// 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 <c>default</c> for an otherwise perfectly valid touch.
/// </remarks>
/// <seealso cref="TouchControl.radius"/>
[InputControl(displayName = "Radius")]
[FieldOffset(24)]
public Vector2 radius ;
/// <summary>
/// <see cref="TouchPhase"/> value of the touch.
/// </summary>
/// <value>Current <see cref="TouchPhase"/>.</value>
/// <seealso cref="phase"/>
[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 ;
2023-05-07 18:43:11 -04:00
/// <summary>
/// The index of the display that was touched.
/// </summary>
[InputControl(name = "displayIndex", displayName = "Display Index", layout = "Integer")]
2023-03-28 13:24:16 -04:00
[FieldOffset(34)]
2023-05-07 18:43:11 -04:00
public byte displayIndex ;
2023-03-28 13:24:16 -04:00
[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.
/// <summary>
/// Time that the touch was started. Relative to <c>Time.realTimeSinceStartup</c>.
/// </summary>
/// <value>Time that the touch was started.</value>
/// <remarks>
/// This is set automatically by <see cref="Touchscreen"/> and does not need to be provided
/// by events sent to the touchscreen.
/// </remarks>
/// <seealso cref="InputEvent.time"/>
/// <seealso cref="TouchControl.startTime"/>
[InputControl(displayName = "Start Time", layout = "Double", synthetic = true)]
[FieldOffset(40)]
public double startTime ; // In *external* time, i.e. currentTimeOffsetToRealtimeSinceStartup baked in.
/// <summary>
/// The position where the touch started.
/// </summary>
/// <value>Screen-space start position of the touch.</value>
/// <remarks>
/// This is set automatically by <see cref="Touchscreen"/> and does not need to be provided
/// by events sent to the touchscreen.
/// </remarks>
/// <seealso cref="TouchControl.startPosition"/>
[InputControl(displayName = "Start Position", synthetic = true)]
[FieldOffset(48)]
public Vector2 startPosition ;
/// <summary>
/// Get or set the phase of the touch.
/// </summary>
/// <value>Phase of the touch.</value>
/// <seealso cref="TouchControl.phase"/>
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 ;
/// <summary>
/// Whether, after not having any touch contacts, this is part of the first touch contact that started.
/// </summary>
/// <remarks>
/// This flag will be set internally by <see cref="Touchscreen"/>. Generally, it is
/// not necessary to set this bit manually when feeding data to Touchscreens.
/// </remarks>
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 ;
}
}
/// <inheritdoc/>
public FourCC format = > Format ;
/// <summary>
/// Return a string representation of the state useful for debugging.
/// </summary>
/// <returns>A string representation of the touch state.</returns>
public override string ToString ( )
{
return $"{{ id={touchId} phase={phase} pos={position} delta={delta} pressure={pressure} radius={radius} primary={isPrimaryTouch} }}" ;
}
}
/// <summary>
/// Default state layout for touch devices.
/// </summary>
/// <remarks>
/// 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 (<see cref="TouchState"/>)
/// and leaves state management for a touchscreen as a whole to the managed part of the system.
/// </remarks>
[StructLayout(LayoutKind.Explicit, Size = MaxTouches * TouchState.kSizeInBytes)]
internal unsafe struct TouchscreenState : IInputStateTypeInfo
{
/// <summary>
/// Memory format tag for TouchscreenState.
/// </summary>
/// <value>Returns "TSCR".</value>
/// <seealso cref="InputStateBlock.format"/>
public static FourCC Format = > new FourCC ( 'T' , 'S' , 'C' , 'R' ) ;
/// <summary>
/// Maximum number of touches that can be tracked at the same time.
/// </summary>
/// <value>Maximum number of concurrent touches.</value>
public const int MaxTouches = 10 ;
/// <summary>
/// Data for the touch that is deemed the "primary" touch at the moment.
/// </summary>
/// <remarks>
/// 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 <see cref="touchData"/> 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 <see cref="touchData"/>.
/// </remarks>
[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
{
/// <summary>
/// Indicates where in its lifecycle a given touch is.
/// </summary>
public enum TouchPhase
{
////REVIEW: Why have a separate None instead of just making this equivalent to either Ended or Canceled?
/// <summary>
/// No activity has been registered on the touch yet.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
None ,
/// <summary>
/// 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.
/// </summary>
Began ,
/// <summary>
/// An ongoing touch has changed position.
/// </summary>
Moved ,
/// <summary>
/// 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.
/// </summary>
Ended ,
/// <summary>
/// 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.
/// </summary>
Canceled ,
/// <summary>
/// An ongoing touch has not been moved (not received any input) in a frame.
/// </summary>
/// <remarks>
/// This phase is not used by <see cref="Touchscreen"/>. This means that <see cref="TouchControl"/> will not generally
/// return this value for <see cref="TouchControl.phase"/>. It is, however, used by <see cref="UnityEngine.InputSystem.EnhancedTouch.Touch"/>.
/// </remarks>
Stationary ,
}
/// <summary>
/// A multi-touch surface.
/// </summary>
/// <remarks>
/// 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 <see cref="TouchState"/> each. This is unusual as <see cref="TouchState"/>
/// uses a memory format different from <see cref="TouchState.Format"/>. However, when a <c>Touchscreen</c>
/// sees an event containing a <see cref="TouchState"/>, it will handle that event on a special code path.
///
/// This allows <c>Touchscreen</c> to decide on its own which control in <see cref="touches"/> to store
/// a touch at and to perform things such as tap detection (see <see cref="TouchControl.tap"/> and
/// <see cref="TouchControl.tapCount"/>) and primary touch handling (see <see cref="primaryTouch"/>).
///
/// <example>
/// <code>
/// // 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.
/// });
/// </code>
/// </example>
///
/// 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 <see cref="EnhancedTouch.Touch"/> API instead.
/// </remarks>
[InputControlLayout(stateType = typeof(TouchscreenState), isGenericTypeOfDevice = true)]
public class Touchscreen : Pointer , IInputStateCallbackReceiver , IEventMerger , ICustomDeviceReset
{
/// <summary>
/// Synthetic control that has the data for the touch that is deemed the "primary" touch at the moment.
/// </summary>
/// <value>Control tracking the screen's primary touch.</value>
/// <remarks>
/// 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, <see cref="TouchControl.phase"/>
/// of <c>primaryTouch</c> will only transition to <see cref="TouchPhase.Ended"/> once the last finger
/// has been lifted off the screen.
/// </remarks>
public TouchControl primaryTouch { get ; protected set ; }
/// <summary>
/// Array of all <see cref="TouchControl"/>s on the device.
/// </summary>
/// <value>All <see cref="TouchControl"/>s on the screen.</value>
/// <remarks>
/// 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.
/// </remarks>
public ReadOnlyArray < TouchControl > touches { get ; protected set ; }
protected TouchControl [ ] touchControlArray
{
get = > touches . m_Array ;
set = > touches = new ReadOnlyArray < TouchControl > ( value ) ;
}
/// <summary>
/// The touchscreen that was added or updated last or null if there is no
/// touchscreen connected to the system.
/// </summary>
/// <value>Current touch screen.</value>
public new static Touchscreen current { get ; internal set ; }
/// <inheritdoc />
public override void MakeCurrent ( )
{
base . MakeCurrent ( ) ;
current = this ;
}
/// <inheritdoc />
protected override void OnRemoved ( )
{
base . OnRemoved ( ) ;
if ( current = = this )
current = null ;
}
/// <inheritdoc />
protected override void FinishSetup ( )
{
base . FinishSetup ( ) ;
primaryTouch = GetChildControl < TouchControl > ( "primaryTouch" ) ;
2023-05-07 18:43:11 -04:00
displayIndex = primaryTouch . displayIndex ;
2023-03-28 13:24:16 -04:00
// 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 < TouchControl > ( 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 "<Pointer>/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 ( ) ;
}
/// <summary>
/// Called whenever a new state event is received.
/// </summary>
/// <param name="eventPtr"></param>
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 < StateEvent > ( ) )
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 = & currentTouchState [ 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 < TouchState > ( ) ) ;
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 < TouchControl > ( ) ;
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 < byte > ( StateEvent . GetEventSizeWithPayload < TouchState > ( ) , 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 > ( ) ) ;
( ( 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 > ( ) ) ;
( ( 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 ;
}
}