123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- 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)
- {
- 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;
- }
- }
-
- [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_LastTouchId;
- [NonSerialized] private int m_PrimaryTouchIndex = -1;
- [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();
- }
-
- #endif
-
- ////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)
- {
- }
- }
- }
|