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
{
///
/// Adds a with input simulated from other types of devices (e.g.
/// or ).
///
[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();
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.
var numPointers = m_NumPointers;
ArrayHelpers.AppendWithCapacity(ref m_Pointers, ref m_NumPointers, pointer);
ArrayHelpers.AppendWithCapacity(ref m_CurrentPositions, ref numPointers, default);
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.
var numPointers = m_NumPointers;
m_Pointers.EraseAtWithCapacity(ref m_NumPointers, pointerIndex);
m_CurrentPositions.EraseAtWithCapacity(ref numPointers, 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)
// Read pointer position.
var positionControl = m_Pointers[pointerIndex].position;
var positionStatePtr = positionControl.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
if (positionStatePtr != null)
m_CurrentPositions[pointerIndex] = positionControl.ReadValueFromState(positionStatePtr);
// 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("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)
{
var position = m_CurrentPositions[pointerIndex];
var touch = new TouchState
{
phase = phase,
position = position
};
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 ButtonControl[] m_Touches;
[NonSerialized] private int m_LastTouchId;
[NonSerialized] private int m_PrimaryTouchIndex = -1;
[NonSerialized] private Action m_OnDeviceChange;
[NonSerialized] private Action 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)
{
}
}
}