This commit is contained in:
2025-01-17 13:10:42 +01:00
commit 4536213c91
15115 changed files with 1442174 additions and 0 deletions

View File

@@ -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 &lt; 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()");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3cf42e49077e442fd8b9dc29c328d345
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 71f3c0e630f0148b496f565843b5b272
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6261dd2a83757498188548c57e2492bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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()
{
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4cc0c6a7fa1c14a518fec64497f4937f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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)
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e31057ef324a04478bb4f7d469b3cdc0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: