UI 자동화를 위해 바인딩 기능 구현
- 유니티 에셋 인증 오류로 meta 재생성
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
////REVIEW: this *really* should be renamed to TouchPolling or something like that
|
||||
|
||||
////REVIEW: Should this auto-enable itself when the API is used? Problem with this is that it means the first touch inputs will get missed
|
||||
//// as by the time the API is polled, we're already into the first frame.
|
||||
|
||||
////TODO: gesture support
|
||||
////TODO: high-frequency touch support
|
||||
|
||||
////REVIEW: have TouchTap, TouchSwipe, etc. wrapper MonoBehaviours like LeanTouch?
|
||||
|
||||
////TODO: as soon as we can break the API, remove the EnhancedTouchSupport class altogether and rename UnityEngine.InputSystem.EnhancedTouch to TouchPolling
|
||||
|
||||
////FIXME: does not survive domain reloads
|
||||
|
||||
namespace UnityEngine.InputSystem.EnhancedTouch
|
||||
{
|
||||
/// <summary>
|
||||
/// API to control enhanced touch facilities like <see cref="Touch"/> that are not
|
||||
/// enabled by default.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Enhanced touch support provides automatic finger tracking and touch history recording.
|
||||
/// It is an API designed for polling, i.e. for querying touch state directly in methods
|
||||
/// such as <c>MonoBehaviour.Update</c>. Enhanced touch support cannot be used in combination
|
||||
/// with <see cref="InputAction"/>s though both can be used side-by-side.
|
||||
///
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// public class MyBehavior : MonoBehaviour
|
||||
/// {
|
||||
/// protected void OnEnable()
|
||||
/// {
|
||||
/// EnhancedTouchSupport.Enable();
|
||||
/// }
|
||||
///
|
||||
/// protected void OnDisable()
|
||||
/// {
|
||||
/// EnhancedTouchSupport.Disable();
|
||||
/// }
|
||||
///
|
||||
/// protected void Update()
|
||||
/// {
|
||||
/// var activeTouches = Touch.activeTouches;
|
||||
/// for (var i = 0; i < activeTouches.Count; ++i)
|
||||
/// Debug.Log("Active touch: " + activeTouches[i]);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </remarks>
|
||||
/// <seealso cref="Touch"/>
|
||||
/// <seealso cref="Finger"/>
|
||||
public static class EnhancedTouchSupport
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether enhanced touch support is currently enabled.
|
||||
/// </summary>
|
||||
/// <value>True if EnhancedTouch support has been enabled.</value>
|
||||
public static bool enabled => s_Enabled > 0;
|
||||
|
||||
private static int s_Enabled;
|
||||
private static InputSettings.UpdateMode s_UpdateMode;
|
||||
|
||||
/// <summary>
|
||||
/// Enable enhanced touch support.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Calling this method is necessary to enable the functionality provided
|
||||
/// by <see cref="Touch"/> and <see cref="Finger"/>. These APIs add extra
|
||||
/// processing to touches and are thus disabled by default.
|
||||
///
|
||||
/// Calls to <c>Enable</c> and <see cref="Disable"/> balance each other out.
|
||||
/// If <c>Enable</c> is called repeatedly, it will take as many calls to
|
||||
/// <see cref="Disable"/> to disable the system again.
|
||||
/// </remarks>
|
||||
public static void Enable()
|
||||
{
|
||||
++s_Enabled;
|
||||
if (s_Enabled > 1)
|
||||
return;
|
||||
|
||||
InputSystem.onDeviceChange += OnDeviceChange;
|
||||
InputSystem.onBeforeUpdate += Touch.BeginUpdate;
|
||||
InputSystem.onSettingsChange += OnSettingsChange;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeDomainReload;
|
||||
#endif
|
||||
|
||||
SetUpState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable enhanced touch support.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method only undoes a single call to <see cref="Enable"/>.
|
||||
/// </remarks>
|
||||
public static void Disable()
|
||||
{
|
||||
if (!enabled)
|
||||
return;
|
||||
--s_Enabled;
|
||||
if (s_Enabled > 0)
|
||||
return;
|
||||
|
||||
InputSystem.onDeviceChange -= OnDeviceChange;
|
||||
InputSystem.onBeforeUpdate -= Touch.BeginUpdate;
|
||||
InputSystem.onSettingsChange -= OnSettingsChange;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeDomainReload;
|
||||
#endif
|
||||
|
||||
TearDownState();
|
||||
}
|
||||
|
||||
internal static void Reset()
|
||||
{
|
||||
Touch.s_GlobalState.touchscreens = default;
|
||||
Touch.s_GlobalState.playerState.Destroy();
|
||||
Touch.s_GlobalState.playerState = default;
|
||||
#if UNITY_EDITOR
|
||||
Touch.s_GlobalState.editorState.Destroy();
|
||||
Touch.s_GlobalState.editorState = default;
|
||||
#endif
|
||||
s_Enabled = 0;
|
||||
}
|
||||
|
||||
private static void SetUpState()
|
||||
{
|
||||
Touch.s_GlobalState.playerState.updateMask = InputUpdateType.Dynamic | InputUpdateType.Manual | InputUpdateType.Fixed;
|
||||
#if UNITY_EDITOR
|
||||
Touch.s_GlobalState.editorState.updateMask = InputUpdateType.Editor;
|
||||
#endif
|
||||
|
||||
s_UpdateMode = InputSystem.settings.updateMode;
|
||||
|
||||
foreach (var device in InputSystem.devices)
|
||||
OnDeviceChange(device, InputDeviceChange.Added);
|
||||
}
|
||||
|
||||
internal static void TearDownState()
|
||||
{
|
||||
foreach (var device in InputSystem.devices)
|
||||
OnDeviceChange(device, InputDeviceChange.Removed);
|
||||
|
||||
Touch.s_GlobalState.playerState.Destroy();
|
||||
#if UNITY_EDITOR
|
||||
Touch.s_GlobalState.editorState.Destroy();
|
||||
#endif
|
||||
|
||||
Touch.s_GlobalState.playerState = default;
|
||||
#if UNITY_EDITOR
|
||||
Touch.s_GlobalState.editorState = default;
|
||||
#endif
|
||||
}
|
||||
|
||||
private static void OnDeviceChange(InputDevice device, InputDeviceChange change)
|
||||
{
|
||||
switch (change)
|
||||
{
|
||||
case InputDeviceChange.Added:
|
||||
{
|
||||
if (device is Touchscreen touchscreen)
|
||||
Touch.AddTouchscreen(touchscreen);
|
||||
break;
|
||||
}
|
||||
|
||||
case InputDeviceChange.Removed:
|
||||
{
|
||||
if (device is Touchscreen touchscreen)
|
||||
Touch.RemoveTouchscreen(touchscreen);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnSettingsChange()
|
||||
{
|
||||
var currentUpdateMode = InputSystem.settings.updateMode;
|
||||
if (s_UpdateMode == currentUpdateMode)
|
||||
return;
|
||||
TearDownState();
|
||||
SetUpState();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private static void OnBeforeDomainReload()
|
||||
{
|
||||
// We need to release NativeArrays we're holding before losing track of them during domain reloads.
|
||||
Touch.s_GlobalState.playerState.Destroy();
|
||||
Touch.s_GlobalState.editorState.Destroy();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
[Conditional("DEVELOPMENT_BUILD")]
|
||||
[Conditional("UNITY_EDITOR")]
|
||||
internal static void CheckEnabled()
|
||||
{
|
||||
if (!enabled)
|
||||
throw new InvalidOperationException("EnhancedTouch API is not enabled; call EnhancedTouchSupport.Enable()");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3cf42e49077e442fd8b9dc29c328d345
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,266 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71f3c0e630f0148b496f565843b5b272
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,999 @@
|
||||
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?
|
||||
|
||||
////REVIEW: do we need to keep old touches around on activeTouches like the old UnityEngine touch API?
|
||||
|
||||
namespace UnityEngine.InputSystem.EnhancedTouch
|
||||
{
|
||||
/// <summary>
|
||||
/// A high-level representation of a touch which automatically keeps track of a touch
|
||||
/// over time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This API obsoletes the need for manually keeping tracking of touch IDs (<see cref="TouchControl.touchId"/>)
|
||||
/// and touch phases (<see cref="TouchControl.phase"/>) 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,
|
||||
/// <see cref="Touchscreen"/> may overwrite it with a new touch coming 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, that is, a
|
||||
/// finger beginning to touch the screen (<see cref="TouchPhase.Began"/>), moving on the screen (<see cref="TouchPhase.Moved"/>),
|
||||
/// or being lifted off the screen (<see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/>).
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// using UnityEngine;
|
||||
/// using UnityEngine.InputSystem.EnhancedTouch;
|
||||
///
|
||||
/// // Alias EnhancedTouch.Touch to "Touch" for less typing.
|
||||
/// using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;
|
||||
/// using TouchPhase = UnityEngine.InputSystem.TouchPhase;
|
||||
///
|
||||
/// public class Example : MonoBehaviour
|
||||
/// {
|
||||
/// void Awake()
|
||||
/// {
|
||||
/// // Note that enhanced touch support needs to be explicitly enabled.
|
||||
/// EnhancedTouchSupport.Enable();
|
||||
/// }
|
||||
///
|
||||
/// void Update()
|
||||
/// {
|
||||
/// // Illustrates how to examine all active touches once per frame and show their last recorded position
|
||||
/// // in the associated screen-space.
|
||||
/// foreach (var touch in Touch.activeTouches)
|
||||
/// {
|
||||
/// switch (touch.phase)
|
||||
/// {
|
||||
/// case TouchPhase.Began:
|
||||
/// Debug.Log($"Frame {Time.frameCount}: Touch {touch} started this frame at ({touch.screenPosition.x}, {touch.screenPosition.y})");
|
||||
/// break;
|
||||
/// case TouchPhase.Ended:
|
||||
/// Debug.Log($"Frame {Time.frameCount}:Touch {touch} ended this frame at ({touch.screenPosition.x}, {touch.screenPosition.y})");
|
||||
/// break;
|
||||
/// case TouchPhase.Moved:
|
||||
/// Debug.Log($"Frame {Time.frameCount}: Touch {touch} moved this frame to ({touch.screenPosition.x}, {touch.screenPosition.y})");
|
||||
/// break;
|
||||
/// case TouchPhase.Canceled:
|
||||
/// Debug.Log($"Frame {Time.frameCount}: Touch {touch} was canceled this frame");
|
||||
/// break;
|
||||
/// case TouchPhase.Stationary:
|
||||
/// Debug.Log($"Frame {Time.frameCount}: ouch {touch} was not updated this frame");
|
||||
/// break;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces")]
|
||||
public struct Touch : IEquatable<Touch>
|
||||
{
|
||||
// The way this works is that at the core, it simply attaches one InputStateHistory per "<Touchscreen>/touch*"
|
||||
// control and then presents a public API that crawls over the recorded touch history in various ways.
|
||||
|
||||
/// <summary>
|
||||
/// Whether this touch record holds valid data.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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 <c>Touch</c> instances referring to the record become invalid.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This property can be used to determine whether the record held on to by the <c>Touch</c>
|
||||
/// instance is still valid.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This property will be <c>false</c> for default-initialized <c>Touch</c> instances.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Note that accessing most of the other properties on this struct when the touch is
|
||||
/// invalid will trigger <see cref="InvalidOperationException"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool valid => m_TouchRecord.valid;
|
||||
|
||||
/// <summary>
|
||||
/// The finger used for the touch contact.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Note that this is only <c>null</c> for default-initialized instances of the struct.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// See <see cref="activeFingers"/> for how to access all active fingers.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public Finger finger => m_Finger;
|
||||
|
||||
/// <summary>
|
||||
/// The current touch phase of the touch indicating its current state in the phase cycle.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Every touch goes through a predefined cycle that starts with <see cref="TouchPhase.Began"/>,
|
||||
/// then potentially <see cref="TouchPhase.Moved"/> and/or <see cref="TouchPhase.Stationary"/>,
|
||||
/// and finally concludes with either <see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This property indicates where in the cycle the touch is and is based on <see cref="TouchControl.phase"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Use <see cref="isInProgress"/> to more conveniently evaluate whether this touch is currently active or not.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public TouchPhase phase => state.phase;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the touch has begun this frame, i.e. whether <see cref="phase"/> is <see cref="TouchPhase.Began"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use <see cref="isInProgress"/> to more conveniently evaluate whether this touch is currently active or not.
|
||||
/// </remarks>
|
||||
public bool began => phase == TouchPhase.Began;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the touch is currently in progress, i.e. whether <see cref="phase"/> is either
|
||||
/// <see cref="TouchPhase.Moved"/>, <see cref="TouchPhase.Stationary"/>, or <see cref="TouchPhase.Began"/>.
|
||||
/// </summary>
|
||||
public bool inProgress => phase == TouchPhase.Moved || phase == TouchPhase.Stationary || phase == TouchPhase.Began;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the touch has ended this frame, i.e. whether <see cref="phase"/> is either
|
||||
/// <see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use <see cref="isInProgress"/> to more conveniently evaluate whether this touch is currently active or not.
|
||||
/// </remarks>
|
||||
public bool ended => phase == TouchPhase.Ended || phase == TouchPhase.Canceled;
|
||||
|
||||
/// <summary>
|
||||
/// Unique ID of the touch as (usually) assigned by the platform.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Each touch contact that is made with the screen receives its own unique, non-zero ID which is
|
||||
/// normally assigned by the underlying platform via <see cref="TouchControl.touchId"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 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 <see cref="activeTouches"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In particular, all touches in <see cref="history"/> will have the same ID whereas
|
||||
/// touches in the finger's <see cref="Finger.touchHistory"/> may end up having the same
|
||||
/// touch ID even though constituting different physical touch contacts.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public int touchId => state.touchId;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized pressure of the touch against the touch surface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Not all touchscreens are pressure-sensitive. If unsupported, this property will
|
||||
/// always return 0.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In general, touch pressure is supported on mobile platforms only.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Touch pressure may also be retrieved directly from the device control via <see cref="TouchControl.pressure"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public float pressure => state.pressure;
|
||||
|
||||
/// <summary>
|
||||
/// Screen-space radius of the touch which define its horizontal and vertical extents.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If supported by the underlying device, this reports the size of the touch contact based on its
|
||||
/// <see cref="screenPosition"/> center point. If not supported, this will be <c>default(Vector2)</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Touch radius may also be retrieved directly from the device control via <see cref="TouchControl.radius"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public Vector2 radius => state.radius;
|
||||
|
||||
/// <summary>
|
||||
/// Start time of the touch in seconds on the same timeline as <c>Time.realTimeSinceStartup</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the value of <see cref="InputEvent.time"/> when the touch started with
|
||||
/// <see cref="phase"/> <see cref="TouchPhase.Began"/>. Note that start time may also be retrieved directly
|
||||
/// from the device control via <see cref="TouchControl.startTime"/>.
|
||||
/// </remarks>
|
||||
public double startTime => state.startTime;
|
||||
|
||||
/// <summary>
|
||||
/// Time the touch record was reported on the same timeline as <see cref="UnityEngine.Time.realtimeSinceStartup"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the value <see cref="InputEvent.time"/> of the event that signaled the current state
|
||||
/// change for the touch.
|
||||
/// </remarks>
|
||||
public double time => m_TouchRecord.time;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Touchscreen"/> associated with the touch contact.
|
||||
/// </summary>
|
||||
public Touchscreen screen => finger.screen;
|
||||
|
||||
/// <summary>
|
||||
/// Screen-space position of the touch.
|
||||
/// </summary>
|
||||
/// <remarks>Also see <see cref="TouchControl.position"/> for retrieving position directly from a device
|
||||
/// control.</remarks>
|
||||
public Vector2 screenPosition => state.position;
|
||||
|
||||
/// <summary>
|
||||
/// Screen-space position where the touch started.
|
||||
/// </summary>
|
||||
/// <remarks>Also see <see cref="TouchControl.startPosition"/> for retrieving start position directly from
|
||||
/// a device control.</remarks>
|
||||
public Vector2 startScreenPosition => state.startPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Screen-space motion delta of the touch.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Note that deltas have behaviors attached to them different from most other
|
||||
/// controls. See <see cref="Pointer.delta"/> for details.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Also see <see cref="TouchControl.delta"/> for retrieving delta directly from a device control.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public Vector2 delta => state.delta;
|
||||
|
||||
/// <summary>
|
||||
/// Number of times that the touch has been tapped in succession.
|
||||
/// </summary>
|
||||
/// <value>Indicates how many taps have been performed one after the other.</value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Successive taps have to come within <see cref="InputSettings.multiTapDelayTime"/> for them
|
||||
/// to increase the tap count. I.e. if a new tap finishes within that time after <see cref="startTime"/>
|
||||
/// of the previous touch, the tap count is increased by one. If more than <see cref="InputSettings.multiTapDelayTime"/>
|
||||
/// passes after a tap with no successive tap, the tap count is reset to zero.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Also see <see cref="TouchControl.tapCount"/> for retrieving tap count directly from a device control.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public int tapCount => state.tapCount;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the touch has performed a tap.
|
||||
/// </summary>
|
||||
/// <value>Indicates whether the touch has tapped the screen.</value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A tap is defined as a touch that begins and ends within <see cref="InputSettings.defaultTapTime"/> and
|
||||
/// stays within <see cref="InputSettings.tapRadius"/> of its <see cref="startScreenPosition"/>. If this
|
||||
/// is the case for a touch, this button is set to 1 at the time the touch goes to <see cref="phase"/>
|
||||
/// <see cref="TouchPhase.Ended"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Resets to 0 only when another touch is started on the control or when the control is reset.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Use <see cref="tapCount"/> to determine if there were multiple taps occurring during the frame.
|
||||
/// Also note that <see cref="TouchControl.tap"/> may be used to determine whether there was a tap.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool isTap => state.isTap;
|
||||
|
||||
/// <summary>
|
||||
/// The index of the display containing the touch.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A zero based number representing the display index of the <see cref="Display"/> that contains the touch.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Also see <see cref="TouchControl.displayIndex"/> for retrieving display index directly from a device
|
||||
/// control.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public int displayIndex => state.displayIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the touch is currently in progress.
|
||||
/// </summary>
|
||||
/// <remarks>This is effectively equivalent to checking if <see cref="phase"/> is equal to either of:
|
||||
/// <see cref="TouchPhase.Began"/>, <see cref="TouchPhase.Moved"/>, or <see cref="TouchPhase.Stationary"/>.
|
||||
/// </remarks>
|
||||
public bool isInProgress
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (phase)
|
||||
{
|
||||
case TouchPhase.Began:
|
||||
case TouchPhase.Moved:
|
||||
case TouchPhase.Stationary:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal uint updateStepCount => state.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();
|
||||
|
||||
/// <summary>
|
||||
/// History touch readings for this specific touch contact.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Unlike <see cref="Finger.touchHistory"/>, this gives the history of this touch only.
|
||||
/// </remarks>
|
||||
public TouchHistory history
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!valid)
|
||||
throw new InvalidOperationException("Touch is invalid");
|
||||
return finger.GetTouchHistory(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All touches that are either ongoing as of the current frame or have ended in the current frame.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A touch that begins in a frame will always have its phase set to <see cref="TouchPhase.Began"/> even
|
||||
/// if there was also movement (or even an end/cancellation) for the touch in the same frame.
|
||||
///
|
||||
/// A touch that begins and ends in the same frame will have its <see cref="TouchPhase.Began"/> surface
|
||||
/// in that frame and then another entry with <see cref="TouchPhase.Ended"/> surface in the
|
||||
/// <em>next</em> frame. This logic implies that there can be more active touches than concurrent touches
|
||||
/// supported by the hardware/platform.
|
||||
///
|
||||
/// A touch that begins and moves in the same frame will have its <see cref="TouchPhase.Began"/> surface
|
||||
/// in that frame and then another entry with <see cref="TouchPhase.Moved"/> and the screen motion
|
||||
/// surface in the <em>next</em> frame <em>except</em> if the touch also ended in the frame (in which
|
||||
/// case <see cref="phase"/> will be <see cref="TouchPhase.Ended"/> instead of <see cref="TouchPhase.Moved"/>).
|
||||
///
|
||||
/// Note that the touches reported by this API do <em>not</em> necessarily have to match the contents of
|
||||
/// <a href="https://docs.unity3d.com/ScriptReference/Input-touches.html">UnityEngine.Input.touches</a>.
|
||||
/// The reason for this is that the <c>UnityEngine.Input</c> API and the Input System API flush their input
|
||||
/// queues at different points in time and may thus have a different view on available input. In particular,
|
||||
/// the Input System event queue is flushed <em>later</em> in the frame than inputs for <c>UnityEngine.Input</c>
|
||||
/// and may thus have newer inputs available. On Android, for example, touch input is gathered from a separate
|
||||
/// UI thread and fed into the input system via a "background" event queue that can gather input asynchronously.
|
||||
/// Due to this setup, touch events that will reach <c>UnityEngine.Input</c> only in the next frame may have
|
||||
/// already reached the Input System.
|
||||
///
|
||||
/// In order to evaluate all active touches on a per-frame basis see <see cref="activeFingers"/>.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// using UnityEngine;
|
||||
/// using UnityEngine.InputSystem.EnhancedTouch;
|
||||
///
|
||||
/// // Alias EnhancedTouch.Touch to "Touch" for less typing.
|
||||
/// using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;
|
||||
/// using TouchPhase = UnityEngine.InputSystem.TouchPhase;
|
||||
///
|
||||
/// public class Example : MonoBehaviour
|
||||
/// {
|
||||
/// void Awake()
|
||||
/// {
|
||||
/// // Note that enhanced touch support needs to be explicitly enabled.
|
||||
/// EnhancedTouchSupport.Enable();
|
||||
/// }
|
||||
///
|
||||
/// void Update()
|
||||
/// {
|
||||
/// // Illustrates how to examine all active touches once per frame and show their last recorded position
|
||||
/// // in the associated screen-space.
|
||||
/// foreach (var touch in Touch.activeTouches)
|
||||
/// {
|
||||
/// switch (touch.phase)
|
||||
/// {
|
||||
/// case TouchPhase.Began:
|
||||
/// Debug.Log($"Frame {Time.frameCount}: Touch {touch} started this frame at ({touch.screenPosition.x}, {touch.screenPosition.y})");
|
||||
/// break;
|
||||
/// case TouchPhase.Ended:
|
||||
/// Debug.Log($"Frame {Time.frameCount}:Touch {touch} ended this frame at ({touch.screenPosition.x}, {touch.screenPosition.y})");
|
||||
/// break;
|
||||
/// case TouchPhase.Moved:
|
||||
/// Debug.Log($"Frame {Time.frameCount}: Touch {touch} moved this frame to ({touch.screenPosition.x}, {touch.screenPosition.y})");
|
||||
/// break;
|
||||
/// case TouchPhase.Canceled:
|
||||
/// Debug.Log($"Frame {Time.frameCount}: Touch {touch} was canceled this frame");
|
||||
/// break;
|
||||
/// case TouchPhase.Stationary:
|
||||
/// Debug.Log($"Frame {Time.frameCount}: ouch {touch} was not updated this frame");
|
||||
/// break;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
|
||||
public static ReadOnlyArray<Touch> activeTouches
|
||||
{
|
||||
get
|
||||
{
|
||||
EnhancedTouchSupport.CheckEnabled();
|
||||
// We lazily construct the array of active touches.
|
||||
s_GlobalState.playerState.UpdateActiveTouches();
|
||||
return new ReadOnlyArray<Touch>(s_GlobalState.playerState.activeTouches, 0, s_GlobalState.playerState.activeTouchCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An array of all possible concurrent touch contacts, i.e. all concurrent touch contacts regardless of whether
|
||||
/// they are currently active or not.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For querying only active fingers, use <see cref="activeFingers"/>.
|
||||
/// For querying only active touches, use <see cref="activeTouches"/>.
|
||||
///
|
||||
/// The length of this array will always correspond to the maximum number of concurrent touches supported by the system.
|
||||
/// Note that the actual number of physically supported concurrent touches as determined by the current hardware and
|
||||
/// operating system may be lower than this number.
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
|
||||
public static ReadOnlyArray<Finger> fingers
|
||||
{
|
||||
get
|
||||
{
|
||||
EnhancedTouchSupport.CheckEnabled();
|
||||
return new ReadOnlyArray<Finger>(s_GlobalState.playerState.fingers, 0, s_GlobalState.playerState.totalFingerCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set of currently active fingers, i.e. touch contacts that currently have an active touch (as defined by <see cref="activeTouches"/>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To instead get a collection of all fingers (not only currently active) use <see cref="fingers"/>.
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
|
||||
public static ReadOnlyArray<Finger> activeFingers
|
||||
{
|
||||
get
|
||||
{
|
||||
EnhancedTouchSupport.CheckEnabled();
|
||||
// We lazily construct the array of active fingers.
|
||||
s_GlobalState.playerState.UpdateActiveFingers();
|
||||
return new ReadOnlyArray<Finger>(s_GlobalState.playerState.activeFingers, 0, s_GlobalState.playerState.activeFingerCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the set of <see cref="Touchscreen"/>s on which touch input is monitored.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
|
||||
public static IEnumerable<Touchscreen> screens
|
||||
{
|
||||
get
|
||||
{
|
||||
EnhancedTouchSupport.CheckEnabled();
|
||||
return s_GlobalState.touchscreens;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event that is invoked when a finger touches a <see cref="Touchscreen"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In order to react to a finger being moved or released, see <see cref="onFingerMove"/> and
|
||||
/// <see cref="onFingerUp"/> respectively.
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
|
||||
public static event Action<Finger> onFingerDown
|
||||
{
|
||||
add
|
||||
{
|
||||
EnhancedTouchSupport.CheckEnabled();
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
s_GlobalState.onFingerDown.AddCallback(value);
|
||||
}
|
||||
remove
|
||||
{
|
||||
EnhancedTouchSupport.CheckEnabled();
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
s_GlobalState.onFingerDown.RemoveCallback(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event that is invoked when a finger stops touching a <see cref="Touchscreen"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In order to react to a finger that touches a <see cref="Touchscreen"/> or a finger that is being moved
|
||||
/// use <see cref="onFingerDown"/> and <see cref="onFingerMove"/> respectively.
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
|
||||
public static event Action<Finger> onFingerUp
|
||||
{
|
||||
add
|
||||
{
|
||||
EnhancedTouchSupport.CheckEnabled();
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
s_GlobalState.onFingerUp.AddCallback(value);
|
||||
}
|
||||
remove
|
||||
{
|
||||
EnhancedTouchSupport.CheckEnabled();
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
s_GlobalState.onFingerUp.RemoveCallback(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event that is invoked when a finger that is in contact with a <see cref="Touchscreen"/> moves
|
||||
/// on the screen.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In order to react to a finger that touches a <see cref="Touchscreen"/> or a finger that stops touching
|
||||
/// a <see cref="Touchscreen"/>, use <see cref="onFingerDown"/> and <see cref="onFingerUp"/> respectively.
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
|
||||
public static event Action<Finger> onFingerMove
|
||||
{
|
||||
add
|
||||
{
|
||||
EnhancedTouchSupport.CheckEnabled();
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
s_GlobalState.onFingerMove.AddCallback(value);
|
||||
}
|
||||
remove
|
||||
{
|
||||
EnhancedTouchSupport.CheckEnabled();
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
s_GlobalState.onFingerMove.RemoveCallback(value);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
public static Action<Finger> onFingerTap
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
set { throw new NotImplementedException(); }
|
||||
}
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// The amount of history kept for each single touch.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default, this is zero meaning that no history information is kept for
|
||||
/// touches. Setting this to <c>Int32.maxValue</c> will cause all history from
|
||||
/// the beginning to the end of a touch being kept.
|
||||
/// </remarks>
|
||||
public static int maxHistoryLengthPerFinger
|
||||
{
|
||||
get => s_GlobalState.historyLengthPerFinger;
|
||||
|
||||
////TODO
|
||||
/*set { throw new NotImplementedException(); }*/
|
||||
}
|
||||
|
||||
internal Touch(Finger finger, InputStateHistory<TouchState>.Record touchRecord)
|
||||
{
|
||||
m_Finger = finger;
|
||||
m_TouchRecord = touchRecord;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
if (!valid)
|
||||
return "<None>";
|
||||
|
||||
return $"{{id={touchId} finger={finger.index} phase={phase} position={screenPosition} delta={delta} time={time}}}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares this touch for equality with another instance <paramref name="other"/>.
|
||||
/// </summary>
|
||||
/// <param name="other">The other instance to compare with.</param>
|
||||
/// <returns><c>true</c> if this touch and <paramref name="other"/> represents the same finger and maps to the
|
||||
/// same touch record, otherwise <c>false</c></returns>
|
||||
public bool Equals(Touch other)
|
||||
{
|
||||
return Equals(m_Finger, other.m_Finger) && m_TouchRecord.Equals(other.m_TouchRecord);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is Touch other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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_GlobalState.touchscreens.ContainsReference(screen), "Already added touchscreen");
|
||||
s_GlobalState.touchscreens.AppendWithCapacity(screen, capacityIncrement: 5);
|
||||
|
||||
// Add finger tracking to states.
|
||||
s_GlobalState.playerState.AddFingers(screen);
|
||||
#if UNITY_EDITOR
|
||||
s_GlobalState.editorState.AddFingers(screen);
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static void RemoveTouchscreen(Touchscreen screen)
|
||||
{
|
||||
Debug.Assert(s_GlobalState.touchscreens.ContainsReference(screen), "Did not add touchscreen");
|
||||
|
||||
// Remove from list.
|
||||
var index = s_GlobalState.touchscreens.IndexOfReference(screen);
|
||||
s_GlobalState.touchscreens.RemoveAtWithCapacity(index);
|
||||
|
||||
// Remove fingers from states.
|
||||
s_GlobalState.playerState.RemoveFingers(screen);
|
||||
#if UNITY_EDITOR
|
||||
s_GlobalState.editorState.RemoveFingers(screen);
|
||||
#endif
|
||||
}
|
||||
|
||||
////TODO: only have this hooked when we actually need it
|
||||
internal static void BeginUpdate()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if ((InputState.currentUpdateType == InputUpdateType.Editor && s_GlobalState.playerState.updateMask != InputUpdateType.Editor) ||
|
||||
(InputState.currentUpdateType != InputUpdateType.Editor && s_GlobalState.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_GlobalState.playerState, ref s_GlobalState.editorState);
|
||||
}
|
||||
#endif
|
||||
|
||||
// If we have any touches in activeTouches that are ended or canceled,
|
||||
// we need to clear them in the next frame.
|
||||
if (s_GlobalState.playerState.haveActiveTouchesNeedingRefreshNextUpdate)
|
||||
s_GlobalState.playerState.haveBuiltActiveTouches = false;
|
||||
}
|
||||
|
||||
private readonly Finger m_Finger;
|
||||
internal InputStateHistory<TouchState>.Record m_TouchRecord;
|
||||
|
||||
/// <summary>
|
||||
/// Holds global (static) touch state.
|
||||
/// </summary>
|
||||
internal struct GlobalState
|
||||
{
|
||||
internal InlinedArray<Touchscreen> touchscreens;
|
||||
internal int historyLengthPerFinger;
|
||||
internal CallbackArray<Action<Finger>> onFingerDown;
|
||||
internal CallbackArray<Action<Finger>> onFingerMove;
|
||||
internal CallbackArray<Action<Finger>> onFingerUp;
|
||||
|
||||
internal FingerAndTouchState playerState;
|
||||
#if UNITY_EDITOR
|
||||
internal FingerAndTouchState editorState;
|
||||
#endif
|
||||
}
|
||||
|
||||
private static GlobalState CreateGlobalState()
|
||||
{ // Convenient method since parameterized construction is default
|
||||
return new GlobalState { historyLengthPerFinger = 64 };
|
||||
}
|
||||
|
||||
internal static GlobalState s_GlobalState = CreateGlobalState();
|
||||
|
||||
internal static ISavedState SaveAndResetState()
|
||||
{
|
||||
// Save current state
|
||||
var savedState = new SavedStructState<GlobalState>(
|
||||
ref s_GlobalState,
|
||||
(ref GlobalState state) => s_GlobalState = state,
|
||||
() => { /* currently nothing to dispose */ });
|
||||
|
||||
// Reset global state
|
||||
s_GlobalState = CreateGlobalState();
|
||||
|
||||
return savedState;
|
||||
}
|
||||
|
||||
// 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 Finger[] fingers;
|
||||
public Finger[] activeFingers;
|
||||
public Touch[] activeTouches;
|
||||
public int activeFingerCount;
|
||||
public int activeTouchCount;
|
||||
public int totalFingerCount;
|
||||
public uint lastId;
|
||||
public bool haveBuiltActiveTouches;
|
||||
public bool haveActiveTouchesNeedingRefreshNextUpdate;
|
||||
|
||||
// `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<TouchState> 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<TouchState>
|
||||
{
|
||||
extraMemoryPerRecord = UnsafeUtility.SizeOf<ExtraDataPerTouchState>()
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
activeTouchState.Clear();
|
||||
activeTouchState.m_ControlCount = 0;
|
||||
activeTouchState.m_Controls.Clear();
|
||||
}
|
||||
activeTouchCount = 0;
|
||||
haveActiveTouchesNeedingRefreshNextUpdate = false;
|
||||
var currentUpdateStepCount = InputUpdate.s_UpdateStepCount;
|
||||
|
||||
////OPTIMIZE: Handle touchscreens that have no activity more efficiently
|
||||
////FIXME: This is sensitive to history size; we probably need to ensure that the Begans and Endeds/Canceleds of touches are always available to us
|
||||
//// (instead of rebuild activeTouches from scratch each time, may be more useful to update it)
|
||||
|
||||
// Go through fingers and for each one, get the touches that were active this update.
|
||||
for (var i = 0; i < totalFingerCount; ++i)
|
||||
{
|
||||
ref var finger = ref fingers[i];
|
||||
|
||||
// NOTE: Many of the operations here are inlined in order to not perform the same
|
||||
// checks/computations repeatedly.
|
||||
|
||||
var history = finger.m_StateHistory;
|
||||
var touchRecordCount = history.Count;
|
||||
if (touchRecordCount == 0)
|
||||
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 touches that have ended in the frame while in the same
|
||||
// frame, a new touch was started.
|
||||
var currentTouchId = 0;
|
||||
var currentTouchState = default(TouchState*);
|
||||
var touchRecordIndex = history.UserIndexToRecordIndex(touchRecordCount - 1); // Start with last record.
|
||||
var touchRecordHeader = history.GetRecordUnchecked(touchRecordIndex);
|
||||
var touchRecordSize = history.bytesPerRecord;
|
||||
var extraMemoryOffset = touchRecordSize - history.extraMemoryPerRecord;
|
||||
for (var n = 0; n < touchRecordCount; ++n)
|
||||
{
|
||||
if (n != 0)
|
||||
{
|
||||
--touchRecordIndex;
|
||||
if (touchRecordIndex < 0)
|
||||
{
|
||||
// We're wrapping around so buffer must be full. Go to last record in buffer.
|
||||
//touchRecordIndex = history.historyDepth - history.m_HeadIndex - 1;
|
||||
touchRecordIndex = history.historyDepth - 1;
|
||||
touchRecordHeader = history.GetRecordUnchecked(touchRecordIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
touchRecordHeader = (InputStateHistory.RecordHeader*)((byte*)touchRecordHeader - touchRecordSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if part of an ongoing touch we've already recorded.
|
||||
var touchState = (TouchState*)touchRecordHeader->statePtrWithoutControlIndex; // History is tied to a single TouchControl.
|
||||
var wasUpdatedThisFrame = touchState->updateStepCount == currentUpdateStepCount;
|
||||
if (touchState->touchId == currentTouchId && !touchState->phase.IsEndedOrCanceled())
|
||||
{
|
||||
// If this is the Began record for the touch and that one happened in
|
||||
// the current frame, we force the touch phase to Began.
|
||||
if (wasUpdatedThisFrame && touchState->phase == TouchPhase.Began)
|
||||
{
|
||||
Debug.Assert(currentTouchState != null, "Must have current touch record at this point");
|
||||
|
||||
currentTouchState->phase = TouchPhase.Began;
|
||||
currentTouchState->position = touchState->position;
|
||||
currentTouchState->delta = default;
|
||||
|
||||
haveActiveTouchesNeedingRefreshNextUpdate = true;
|
||||
}
|
||||
|
||||
// Need to continue here as there may still be Ended touches that need to
|
||||
// be taken into account (as in, there may actually be multiple active touches
|
||||
// for the same finger due to how the polling API works).
|
||||
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.
|
||||
if (touchState->phase.IsEndedOrCanceled())
|
||||
{
|
||||
// An exception are touches that both began *and* ended in the previous frame.
|
||||
// For these, we surface the Began in the previous update and the Ended in the
|
||||
// current frame.
|
||||
if (!(touchState->beganInSameFrame && touchState->updateStepCount == currentUpdateStepCount - 1) &&
|
||||
!wasUpdatedThisFrame)
|
||||
break;
|
||||
}
|
||||
|
||||
// Make a copy of the touch so that we can modify data like deltas and phase.
|
||||
// NOTE: Again, not using AddRecord() for speed.
|
||||
// NOTE: Unlike `history`, `activeTouchState` stores control indices as each active touch
|
||||
// will correspond to a different TouchControl.
|
||||
var touchExtraState = (ExtraDataPerTouchState*)((byte*)touchRecordHeader + extraMemoryOffset);
|
||||
var newRecordHeader = activeTouchState.AllocateRecord(out var newRecordIndex);
|
||||
var newRecordState = (TouchState*)newRecordHeader->statePtrWithControlIndex;
|
||||
var newRecordExtraState = (ExtraDataPerTouchState*)((byte*)newRecordHeader + activeTouchState.bytesPerRecord - UnsafeUtility.SizeOf<ExtraDataPerTouchState>());
|
||||
newRecordHeader->time = touchRecordHeader->time;
|
||||
newRecordHeader->controlIndex = ArrayHelpers.AppendWithCapacity(ref activeTouchState.m_Controls,
|
||||
ref activeTouchState.m_ControlCount, finger.m_StateHistory.controls[0]);
|
||||
|
||||
UnsafeUtility.MemCpy(newRecordState, touchState, UnsafeUtility.SizeOf<TouchState>());
|
||||
UnsafeUtility.MemCpy(newRecordExtraState, touchExtraState, UnsafeUtility.SizeOf<ExtraDataPerTouchState>());
|
||||
|
||||
// If the touch hasn't moved this frame, mark it stationary.
|
||||
// EXCEPT: If we are looked at a Moved touch that also began in the same frame and that
|
||||
// frame is the one immediately preceding us. In that case, we want to surface the Moved
|
||||
// as if it happened this frame.
|
||||
var phase = touchState->phase;
|
||||
if ((phase == TouchPhase.Moved || phase == TouchPhase.Began) &&
|
||||
!wasUpdatedThisFrame && !(phase == TouchPhase.Moved && touchState->beganInSameFrame && touchState->updateStepCount == currentUpdateStepCount - 1))
|
||||
{
|
||||
newRecordState->phase = TouchPhase.Stationary;
|
||||
newRecordState->delta = default;
|
||||
}
|
||||
// If the touch wasn't updated this frame, zero out its delta.
|
||||
else if (!wasUpdatedThisFrame && !touchState->beganInSameFrame)
|
||||
{
|
||||
newRecordState->delta = default;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We want accumulated deltas only on activeTouches.
|
||||
newRecordState->delta = newRecordExtraState->accumulatedDelta;
|
||||
}
|
||||
|
||||
var newRecord = new InputStateHistory<TouchState>.Record(activeTouchState, newRecordIndex, newRecordHeader);
|
||||
var newTouch = new Touch(finger, newRecord);
|
||||
|
||||
ArrayHelpers.InsertAtWithCapacity(ref activeTouches, ref activeTouchCount, insertAt, newTouch);
|
||||
|
||||
currentTouchId = touchState->touchId;
|
||||
currentTouchState = newRecordState;
|
||||
|
||||
// For anything but stationary touches on the activeTouches list, we need a subsequent
|
||||
// update in the next frame.
|
||||
if (newTouch.phase != TouchPhase.Stationary)
|
||||
haveActiveTouchesNeedingRefreshNextUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
haveBuiltActiveTouches = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal struct ExtraDataPerTouchState
|
||||
{
|
||||
public Vector2 accumulatedDelta;
|
||||
|
||||
public uint uniqueId; // Unique ID for touch *record* (i.e. multiple TouchStates having the same touchId will still each have a unique ID).
|
||||
|
||||
////TODO
|
||||
//public uint tapCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6261dd2a83757498188548c57e2492bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
|
||||
namespace UnityEngine.InputSystem.EnhancedTouch
|
||||
{
|
||||
/// <summary>
|
||||
/// A fixed-size buffer of <see cref="Touch"/> records used to trace the history of touches.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct provides access to a recorded list of touches.
|
||||
/// </remarks>
|
||||
public struct TouchHistory : IReadOnlyList<Touch>
|
||||
{
|
||||
private readonly InputStateHistory<TouchState> m_History;
|
||||
private readonly Finger m_Finger;
|
||||
private readonly int m_Count;
|
||||
private readonly int m_StartIndex;
|
||||
private readonly uint m_Version;
|
||||
|
||||
internal TouchHistory(Finger finger, InputStateHistory<TouchState> history, int startIndex = -1, int count = -1)
|
||||
{
|
||||
m_Finger = finger;
|
||||
m_History = history;
|
||||
m_Version = history.version;
|
||||
m_Count = count >= 0 ? count : m_History.Count;
|
||||
m_StartIndex = startIndex >= 0 ? startIndex : m_History.Count - 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate touches in the history. Goes from newest records to oldest.
|
||||
/// </summary>
|
||||
/// <returns>Enumerator over the touches in the history.</returns>
|
||||
public IEnumerator<Touch> GetEnumerator()
|
||||
{
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Number of history records available.
|
||||
/// </summary>
|
||||
public int Count => m_Count;
|
||||
|
||||
/// <summary>
|
||||
/// Return a history record by index. Indexing starts at 0 == newest to <see cref="Count"/> - 1 == oldest.
|
||||
/// </summary>
|
||||
/// <param name="index">Index of history record.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than 0 or >= <see cref="Count"/>.</exception>
|
||||
public Touch this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckValid();
|
||||
if (index < 0 || index >= Count)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
$"Index {index} is out of range for history with {Count} entries", nameof(index));
|
||||
|
||||
// History records oldest-first but we index newest-first.
|
||||
return new Touch(m_Finger, m_History[m_StartIndex - index]);
|
||||
}
|
||||
}
|
||||
|
||||
internal void CheckValid()
|
||||
{
|
||||
if (m_Finger == null || m_History == null)
|
||||
throw new InvalidOperationException("Touch history not initialized");
|
||||
if (m_History.version != m_Version)
|
||||
throw new InvalidOperationException(
|
||||
"Touch history is no longer valid; the recorded history has been changed");
|
||||
}
|
||||
|
||||
private class Enumerator : IEnumerator<Touch>
|
||||
{
|
||||
private readonly TouchHistory m_Owner;
|
||||
private int m_Index;
|
||||
|
||||
internal Enumerator(TouchHistory owner)
|
||||
{
|
||||
m_Owner = owner;
|
||||
m_Index = -1;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (m_Index >= m_Owner.Count - 1)
|
||||
return false;
|
||||
++m_Index;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
m_Index = -1;
|
||||
}
|
||||
|
||||
public Touch Current => m_Owner[m_Index];
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cc0c6a7fa1c14a518fec64497f4937f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,411 @@
|
||||
using System;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using UnityEngine.InputSystem.Controls;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem.Editor;
|
||||
#endif
|
||||
|
||||
////TODO: add pressure support
|
||||
|
||||
////REVIEW: extend this beyond simulating from Pointers only? theoretically, we could simulate from any means of generating positions and presses
|
||||
|
||||
////REVIEW: I think this is a workable first attempt but overall, not a sufficient take on input simulation. ATM this uses InputState.Change
|
||||
//// to shove input directly into Touchscreen. Also, it uses state change notifications to set off the simulation. The latter leads
|
||||
//// to touch input potentially changing multiple times in response to a single pointer event. And the former leads to the simulated
|
||||
//// touch input not being visible at the event level -- which leaves Touch and Finger slightly unhappy, for example.
|
||||
//// I think being able to cycle simulated input fully through the event loop would result in a setup that is both simpler and more robust.
|
||||
//// Also, it would allow *disabling* the source devices as long as we don't disable them in the backend, too.
|
||||
//// Finally, the fact that we spin off input *from* events here and feed that into InputState.Change() by passing the event along
|
||||
//// means that places that make sure we process input only once (e.g. binding composites which will remember the event ID they have
|
||||
//// been triggered from) may reject the simulated input when they have already seen the non-simulated input (which may be okay
|
||||
//// behavior).
|
||||
|
||||
namespace UnityEngine.InputSystem.EnhancedTouch
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a <see cref="Touchscreen"/> with input simulated from other types of <see cref="Pointer"/> devices (e.g. <see cref="Mouse"/>
|
||||
/// or <see cref="Pen"/>).
|
||||
/// </summary>
|
||||
[AddComponentMenu("Input/Debug/Touch Simulation")]
|
||||
[ExecuteInEditMode]
|
||||
[HelpURL(InputSystem.kDocUrl + "/manual/Touch.html#touch-simulation")]
|
||||
#if UNITY_EDITOR
|
||||
[InitializeOnLoad]
|
||||
#endif
|
||||
public class TouchSimulation : MonoBehaviour, IInputStateChangeMonitor
|
||||
{
|
||||
public Touchscreen simulatedTouchscreen { get; private set; }
|
||||
|
||||
public static TouchSimulation instance => s_Instance;
|
||||
|
||||
public static void Enable()
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
////TODO: find instance
|
||||
var hiddenGO = new GameObject();
|
||||
hiddenGO.SetActive(false);
|
||||
hiddenGO.hideFlags = HideFlags.HideAndDontSave;
|
||||
s_Instance = hiddenGO.AddComponent<TouchSimulation>();
|
||||
instance.gameObject.SetActive(true);
|
||||
}
|
||||
instance.enabled = true;
|
||||
}
|
||||
|
||||
public static void Disable()
|
||||
{
|
||||
if (instance != null)
|
||||
instance.enabled = false;
|
||||
}
|
||||
|
||||
public static void Destroy()
|
||||
{
|
||||
Disable();
|
||||
|
||||
if (s_Instance != null)
|
||||
{
|
||||
Destroy(s_Instance.gameObject);
|
||||
s_Instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void AddPointer(Pointer pointer)
|
||||
{
|
||||
if (pointer == null)
|
||||
throw new ArgumentNullException(nameof(pointer));
|
||||
|
||||
// Ignore if already added.
|
||||
if (m_Pointers.ContainsReference(m_NumPointers, pointer))
|
||||
return;
|
||||
|
||||
// Add to list.
|
||||
ArrayHelpers.AppendWithCapacity(ref m_Pointers, ref m_NumPointers, pointer);
|
||||
ArrayHelpers.Append(ref m_CurrentPositions, default(Vector2));
|
||||
ArrayHelpers.Append(ref m_CurrentDisplayIndices, default(int));
|
||||
|
||||
InputSystem.DisableDevice(pointer, keepSendingEvents: true);
|
||||
}
|
||||
|
||||
protected void RemovePointer(Pointer pointer)
|
||||
{
|
||||
if (pointer == null)
|
||||
throw new ArgumentNullException(nameof(pointer));
|
||||
|
||||
// Ignore if not added.
|
||||
var pointerIndex = m_Pointers.IndexOfReference(pointer, m_NumPointers);
|
||||
if (pointerIndex == -1)
|
||||
return;
|
||||
|
||||
// Cancel all ongoing touches from the pointer.
|
||||
for (var i = 0; i < m_Touches.Length; ++i)
|
||||
{
|
||||
var button = m_Touches[i];
|
||||
if (button != null && button.device != pointer)
|
||||
continue;
|
||||
|
||||
UpdateTouch(i, pointerIndex, TouchPhase.Canceled);
|
||||
}
|
||||
|
||||
// Remove from list.
|
||||
m_Pointers.EraseAtWithCapacity(ref m_NumPointers, pointerIndex);
|
||||
ArrayHelpers.EraseAt(ref m_CurrentPositions, pointerIndex);
|
||||
ArrayHelpers.EraseAt(ref m_CurrentDisplayIndices, pointerIndex);
|
||||
|
||||
// Re-enable the device (only in case it's still added to the system).
|
||||
if (pointer.added)
|
||||
InputSystem.EnableDevice(pointer);
|
||||
}
|
||||
|
||||
private unsafe void OnEvent(InputEventPtr eventPtr, InputDevice device)
|
||||
{
|
||||
if (device == simulatedTouchscreen)
|
||||
{
|
||||
// Avoid processing events queued by this simulation device
|
||||
return;
|
||||
}
|
||||
|
||||
var pointerIndex = m_Pointers.IndexOfReference(device, m_NumPointers);
|
||||
if (pointerIndex < 0)
|
||||
return;
|
||||
|
||||
var eventType = eventPtr.type;
|
||||
if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type)
|
||||
return;
|
||||
|
||||
////REVIEW: should we have specialized paths for MouseState and PenState here? (probably can only use for StateEvents)
|
||||
|
||||
Pointer pointer = m_Pointers[pointerIndex];
|
||||
|
||||
// Read pointer position.
|
||||
var positionControl = pointer.position;
|
||||
var positionStatePtr = positionControl.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
|
||||
if (positionStatePtr != null)
|
||||
m_CurrentPositions[pointerIndex] = positionControl.ReadValueFromState(positionStatePtr);
|
||||
|
||||
// Read display index.
|
||||
var displayIndexControl = pointer.displayIndex;
|
||||
var displayIndexStatePtr = displayIndexControl.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
|
||||
if (displayIndexStatePtr != null)
|
||||
m_CurrentDisplayIndices[pointerIndex] = displayIndexControl.ReadValueFromState(displayIndexStatePtr);
|
||||
|
||||
// End touches for which buttons are no longer pressed.
|
||||
////REVIEW: There must be a better way to do this
|
||||
for (var i = 0; i < m_Touches.Length; ++i)
|
||||
{
|
||||
var button = m_Touches[i];
|
||||
if (button == null || button.device != device)
|
||||
continue;
|
||||
|
||||
var buttonStatePtr = button.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
|
||||
if (buttonStatePtr == null)
|
||||
{
|
||||
// Button is not contained in event. If we do have a position update, issue
|
||||
// a move on the button's corresponding touch. This makes us deal with delta
|
||||
// events that only update pointer positions.
|
||||
if (positionStatePtr != null)
|
||||
UpdateTouch(i, pointerIndex, TouchPhase.Moved, eventPtr);
|
||||
}
|
||||
else if (button.ReadValueFromState(buttonStatePtr) < (ButtonControl.s_GlobalDefaultButtonPressPoint * ButtonControl.s_GlobalDefaultButtonReleaseThreshold))
|
||||
UpdateTouch(i, pointerIndex, TouchPhase.Ended, eventPtr);
|
||||
}
|
||||
|
||||
// Add/update touches for buttons that are pressed.
|
||||
foreach (var control in eventPtr.EnumerateControls(InputControlExtensions.Enumerate.IgnoreControlsInDefaultState, device))
|
||||
{
|
||||
if (!control.isButton)
|
||||
continue;
|
||||
|
||||
// Check if it's pressed.
|
||||
var buttonStatePtr = control.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
|
||||
Debug.Assert(buttonStatePtr != null, "Button returned from EnumerateControls() must be found in event");
|
||||
var value = 0f;
|
||||
control.ReadValueFromStateIntoBuffer(buttonStatePtr, UnsafeUtility.AddressOf(ref value), 4);
|
||||
if (value <= ButtonControl.s_GlobalDefaultButtonPressPoint)
|
||||
continue; // Not in default state but also not pressed.
|
||||
|
||||
// See if we have an ongoing touch for the button.
|
||||
var touchIndex = m_Touches.IndexOfReference(control);
|
||||
if (touchIndex < 0)
|
||||
{
|
||||
// No, so add it.
|
||||
touchIndex = m_Touches.IndexOfReference((ButtonControl)null);
|
||||
if (touchIndex >= 0) // If negative, we're at max touch count and can't add more.
|
||||
{
|
||||
m_Touches[touchIndex] = (ButtonControl)control;
|
||||
UpdateTouch(touchIndex, pointerIndex, TouchPhase.Began, eventPtr);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Yes, so update it.
|
||||
UpdateTouch(touchIndex, pointerIndex, TouchPhase.Moved, eventPtr);
|
||||
}
|
||||
}
|
||||
|
||||
eventPtr.handled = true;
|
||||
}
|
||||
|
||||
private void OnDeviceChange(InputDevice device, InputDeviceChange change)
|
||||
{
|
||||
// If someone removed our simulated touchscreen, disable touch simulation.
|
||||
if (device == simulatedTouchscreen && change == InputDeviceChange.Removed)
|
||||
{
|
||||
Disable();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (change)
|
||||
{
|
||||
case InputDeviceChange.Added:
|
||||
{
|
||||
if (device is Pointer pointer)
|
||||
{
|
||||
if (device is Touchscreen)
|
||||
return; ////TODO: decide what to do
|
||||
|
||||
AddPointer(pointer);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case InputDeviceChange.Removed:
|
||||
{
|
||||
if (device is Pointer pointer)
|
||||
RemovePointer(pointer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnEnable()
|
||||
{
|
||||
if (simulatedTouchscreen != null)
|
||||
{
|
||||
if (!simulatedTouchscreen.added)
|
||||
InputSystem.AddDevice(simulatedTouchscreen);
|
||||
}
|
||||
else
|
||||
{
|
||||
simulatedTouchscreen = InputSystem.GetDevice("Simulated Touchscreen") as Touchscreen;
|
||||
if (simulatedTouchscreen == null)
|
||||
simulatedTouchscreen = InputSystem.AddDevice<Touchscreen>("Simulated Touchscreen");
|
||||
}
|
||||
|
||||
if (m_Touches == null)
|
||||
m_Touches = new ButtonControl[simulatedTouchscreen.touches.Count];
|
||||
|
||||
if (m_TouchIds == null)
|
||||
m_TouchIds = new int[simulatedTouchscreen.touches.Count];
|
||||
|
||||
foreach (var device in InputSystem.devices)
|
||||
OnDeviceChange(device, InputDeviceChange.Added);
|
||||
|
||||
if (m_OnDeviceChange == null)
|
||||
m_OnDeviceChange = OnDeviceChange;
|
||||
if (m_OnEvent == null)
|
||||
m_OnEvent = OnEvent;
|
||||
|
||||
InputSystem.onDeviceChange += m_OnDeviceChange;
|
||||
InputSystem.onEvent += m_OnEvent;
|
||||
}
|
||||
|
||||
protected void OnDisable()
|
||||
{
|
||||
if (simulatedTouchscreen != null && simulatedTouchscreen.added)
|
||||
InputSystem.RemoveDevice(simulatedTouchscreen);
|
||||
|
||||
// Re-enable all pointers we disabled.
|
||||
for (var i = 0; i < m_NumPointers; ++i)
|
||||
InputSystem.EnableDevice(m_Pointers[i]);
|
||||
|
||||
m_Pointers.Clear(m_NumPointers);
|
||||
m_Touches.Clear();
|
||||
|
||||
m_NumPointers = 0;
|
||||
m_LastTouchId = 0;
|
||||
|
||||
InputSystem.onDeviceChange -= m_OnDeviceChange;
|
||||
InputSystem.onEvent -= m_OnEvent;
|
||||
}
|
||||
|
||||
private unsafe void UpdateTouch(int touchIndex, int pointerIndex, TouchPhase phase, InputEventPtr eventPtr = default)
|
||||
{
|
||||
Vector2 position = m_CurrentPositions[pointerIndex];
|
||||
Debug.Assert(m_CurrentDisplayIndices[pointerIndex] <= byte.MaxValue, "Display index was larger than expected");
|
||||
byte displayIndex = (byte)m_CurrentDisplayIndices[pointerIndex];
|
||||
|
||||
// We need to partially set TouchState in a similar way that the Native side would do, but deriving that
|
||||
// data from the Pointer events.
|
||||
// The handling of the remaining fields is done by the Touchscreen.OnStateEvent() callback.
|
||||
var touch = new TouchState
|
||||
{
|
||||
phase = phase,
|
||||
position = position,
|
||||
displayIndex = displayIndex
|
||||
};
|
||||
|
||||
if (phase == TouchPhase.Began)
|
||||
{
|
||||
touch.startTime = eventPtr.valid ? eventPtr.time : InputState.currentTime;
|
||||
touch.startPosition = position;
|
||||
touch.touchId = ++m_LastTouchId;
|
||||
m_TouchIds[touchIndex] = m_LastTouchId;
|
||||
}
|
||||
else
|
||||
{
|
||||
touch.touchId = m_TouchIds[touchIndex];
|
||||
}
|
||||
|
||||
//NOTE: Processing these events still happen in the current frame.
|
||||
InputSystem.QueueStateEvent(simulatedTouchscreen, touch);
|
||||
|
||||
if (phase.IsEndedOrCanceled())
|
||||
{
|
||||
m_Touches[touchIndex] = null;
|
||||
}
|
||||
}
|
||||
|
||||
[NonSerialized] private int m_NumPointers;
|
||||
[NonSerialized] private Pointer[] m_Pointers;
|
||||
[NonSerialized] private Vector2[] m_CurrentPositions;
|
||||
[NonSerialized] private int[] m_CurrentDisplayIndices;
|
||||
[NonSerialized] private ButtonControl[] m_Touches;
|
||||
[NonSerialized] private int[] m_TouchIds;
|
||||
|
||||
[NonSerialized] private int m_LastTouchId;
|
||||
[NonSerialized] private Action<InputDevice, InputDeviceChange> m_OnDeviceChange;
|
||||
[NonSerialized] private Action<InputEventPtr, InputDevice> m_OnEvent;
|
||||
|
||||
internal static TouchSimulation s_Instance;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
static TouchSimulation()
|
||||
{
|
||||
// We're a MonoBehaviour so our cctor may get called as part of the MonoBehaviour being
|
||||
// created. We don't want to trigger InputSystem initialization from there so delay-execute
|
||||
// the code here.
|
||||
EditorApplication.delayCall +=
|
||||
() =>
|
||||
{
|
||||
InputSystem.onSettingsChange += OnSettingsChanged;
|
||||
InputSystem.onBeforeUpdate += ReEnableAfterDomainReload;
|
||||
};
|
||||
}
|
||||
|
||||
private static void ReEnableAfterDomainReload()
|
||||
{
|
||||
OnSettingsChanged();
|
||||
InputSystem.onBeforeUpdate -= ReEnableAfterDomainReload;
|
||||
}
|
||||
|
||||
private static void OnSettingsChanged()
|
||||
{
|
||||
if (InputEditorUserSettings.simulateTouch)
|
||||
Enable();
|
||||
else
|
||||
Disable();
|
||||
}
|
||||
|
||||
[CustomEditor(typeof(TouchSimulation))]
|
||||
private class TouchSimulationEditor : UnityEditor.Editor
|
||||
{
|
||||
public void OnDisable()
|
||||
{
|
||||
new InputComponentEditorAnalytic(InputSystemComponent.TouchSimulation).Send();
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
||||
|
||||
////TODO: Remove IInputStateChangeMonitor from this class when we can break the API
|
||||
void IInputStateChangeMonitor.NotifyControlStateChanged(InputControl control, double time, InputEventPtr eventPtr, long monitorIndex)
|
||||
{
|
||||
}
|
||||
|
||||
void IInputStateChangeMonitor.NotifyTimerExpired(InputControl control, double time, long monitorIndex, int timerIndex)
|
||||
{
|
||||
}
|
||||
|
||||
// Disable warnings about unused parameters.
|
||||
#pragma warning disable CA1801
|
||||
|
||||
////TODO: [Obsolete]
|
||||
protected void InstallStateChangeMonitors(int startIndex = 0)
|
||||
{
|
||||
}
|
||||
|
||||
////TODO: [Obsolete]
|
||||
protected void OnSourceControlChangedValue(InputControl control, double time, InputEventPtr eventPtr,
|
||||
long sourceDeviceAndButtonIndex)
|
||||
{
|
||||
}
|
||||
|
||||
////TODO: [Obsolete]
|
||||
protected void UninstallStateChangeMonitors(int startIndex = 0)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e31057ef324a04478bb4f7d469b3cdc0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user