Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /InputSystem /Plugins /EnhancedTouch /TouchSimulation.cs
| using System; | |
| using Unity.Collections.LowLevel.Unsafe; | |
| using UnityEngine.InputSystem.Controls; | |
| using UnityEngine.InputSystem.LowLevel; | |
| using UnityEngine.InputSystem.Utilities; | |
| using UnityEditor; | |
| using UnityEngine.InputSystem.Editor; | |
| ////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> | |
| [] | |
| [] | |
| [] | |
| [] | |
| 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) | |
| { | |
| var pointerIndex = m_Pointers.IndexOfReference(device, m_NumPointers); | |
| if (pointerIndex < 0) | |
| return; | |
| var eventType = eventPtr.type; | |
| if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type) | |
| return; | |
| ////TODO: this can be simplified if we use events instead of InputState.Change() but doing so requires work on buffering events while processing; also | |
| //// needs extra handling to not lag into the next frame | |
| ////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]; | |
| 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; | |
| m_PrimaryTouchIndex = -1; | |
| 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]; | |
| var touch = new TouchState | |
| { | |
| phase = phase, | |
| position = position, | |
| displayIndex = displayIndex | |
| }; | |
| var time = eventPtr.valid ? eventPtr.time : InputState.currentTime; | |
| var oldTouchState = (TouchState*)((byte*)simulatedTouchscreen.currentStatePtr + | |
| simulatedTouchscreen.touches[touchIndex].stateBlock.byteOffset); | |
| if (phase == TouchPhase.Began) | |
| { | |
| touch.isPrimaryTouch = m_PrimaryTouchIndex < 0; | |
| touch.startTime = time; | |
| touch.startPosition = position; | |
| touch.touchId = ++m_LastTouchId; | |
| touch.tapCount = oldTouchState->tapCount; // Get reset automatically by Touchscreen. | |
| if (touch.isPrimaryTouch) | |
| m_PrimaryTouchIndex = touchIndex; | |
| } | |
| else | |
| { | |
| touch.touchId = oldTouchState->touchId; | |
| touch.isPrimaryTouch = m_PrimaryTouchIndex == touchIndex; | |
| touch.delta = position - oldTouchState->position; | |
| touch.startPosition = oldTouchState->startPosition; | |
| touch.startTime = oldTouchState->startTime; | |
| touch.tapCount = oldTouchState->tapCount; | |
| if (phase == TouchPhase.Ended) | |
| { | |
| touch.isTap = time - oldTouchState->startTime <= Touchscreen.s_TapTime && | |
| (position - oldTouchState->startPosition).sqrMagnitude <= Touchscreen.s_TapRadiusSquared; | |
| if (touch.isTap) | |
| ++touch.tapCount; | |
| } | |
| } | |
| if (touch.isPrimaryTouch) | |
| InputState.Change(simulatedTouchscreen.primaryTouch, touch, eventPtr: eventPtr); | |
| InputState.Change(simulatedTouchscreen.touches[touchIndex], touch, eventPtr: eventPtr); | |
| if (phase.IsEndedOrCanceled()) | |
| { | |
| m_Touches[touchIndex] = null; | |
| if (m_PrimaryTouchIndex == touchIndex) | |
| m_PrimaryTouchIndex = -1; | |
| } | |
| } | |
| [] private int m_NumPointers; | |
| [] private Pointer[] m_Pointers; | |
| [] private Vector2[] m_CurrentPositions; | |
| [] private int[] m_CurrentDisplayIndices; | |
| [] private ButtonControl[] m_Touches; | |
| [] private int m_LastTouchId; | |
| [] private int m_PrimaryTouchIndex = -1; | |
| [] private Action<InputDevice, InputDeviceChange> m_OnDeviceChange; | |
| [] private Action<InputEventPtr, InputDevice> m_OnEvent; | |
| internal static TouchSimulation s_Instance; | |
| 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(); | |
| } | |
| ////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. | |
| ////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) | |
| { | |
| } | |
| } | |
| } | |