A game about forced loneliness, made by TACStudios
1using System;
2using Unity.Collections.LowLevel.Unsafe;
3using UnityEngine.InputSystem.Controls;
4using UnityEngine.InputSystem.LowLevel;
5using UnityEngine.InputSystem.Utilities;
6#if UNITY_EDITOR
7using UnityEditor;
8using UnityEngine.InputSystem.Editor;
9#endif
10
11////TODO: add pressure support
12
13////REVIEW: extend this beyond simulating from Pointers only? theoretically, we could simulate from any means of generating positions and presses
14
15////REVIEW: I think this is a workable first attempt but overall, not a sufficient take on input simulation. ATM this uses InputState.Change
16//// to shove input directly into Touchscreen. Also, it uses state change notifications to set off the simulation. The latter leads
17//// to touch input potentially changing multiple times in response to a single pointer event. And the former leads to the simulated
18//// touch input not being visible at the event level -- which leaves Touch and Finger slightly unhappy, for example.
19//// 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.
20//// Also, it would allow *disabling* the source devices as long as we don't disable them in the backend, too.
21//// Finally, the fact that we spin off input *from* events here and feed that into InputState.Change() by passing the event along
22//// means that places that make sure we process input only once (e.g. binding composites which will remember the event ID they have
23//// been triggered from) may reject the simulated input when they have already seen the non-simulated input (which may be okay
24//// behavior).
25
26namespace UnityEngine.InputSystem.EnhancedTouch
27{
28 /// <summary>
29 /// Adds a <see cref="Touchscreen"/> with input simulated from other types of <see cref="Pointer"/> devices (e.g. <see cref="Mouse"/>
30 /// or <see cref="Pen"/>).
31 /// </summary>
32 [AddComponentMenu("Input/Debug/Touch Simulation")]
33 [ExecuteInEditMode]
34 [HelpURL(InputSystem.kDocUrl + "/manual/Touch.html#touch-simulation")]
35 #if UNITY_EDITOR
36 [InitializeOnLoad]
37 #endif
38 public class TouchSimulation : MonoBehaviour, IInputStateChangeMonitor
39 {
40 public Touchscreen simulatedTouchscreen { get; private set; }
41
42 public static TouchSimulation instance => s_Instance;
43
44 public static void Enable()
45 {
46 if (instance == null)
47 {
48 ////TODO: find instance
49 var hiddenGO = new GameObject();
50 hiddenGO.SetActive(false);
51 hiddenGO.hideFlags = HideFlags.HideAndDontSave;
52 s_Instance = hiddenGO.AddComponent<TouchSimulation>();
53 instance.gameObject.SetActive(true);
54 }
55 instance.enabled = true;
56 }
57
58 public static void Disable()
59 {
60 if (instance != null)
61 instance.enabled = false;
62 }
63
64 public static void Destroy()
65 {
66 Disable();
67
68 if (s_Instance != null)
69 {
70 Destroy(s_Instance.gameObject);
71 s_Instance = null;
72 }
73 }
74
75 protected void AddPointer(Pointer pointer)
76 {
77 if (pointer == null)
78 throw new ArgumentNullException(nameof(pointer));
79
80 // Ignore if already added.
81 if (m_Pointers.ContainsReference(m_NumPointers, pointer))
82 return;
83
84 // Add to list.
85 ArrayHelpers.AppendWithCapacity(ref m_Pointers, ref m_NumPointers, pointer);
86 ArrayHelpers.Append(ref m_CurrentPositions, default(Vector2));
87 ArrayHelpers.Append(ref m_CurrentDisplayIndices, default(int));
88
89 InputSystem.DisableDevice(pointer, keepSendingEvents: true);
90 }
91
92 protected void RemovePointer(Pointer pointer)
93 {
94 if (pointer == null)
95 throw new ArgumentNullException(nameof(pointer));
96
97 // Ignore if not added.
98 var pointerIndex = m_Pointers.IndexOfReference(pointer, m_NumPointers);
99 if (pointerIndex == -1)
100 return;
101
102 // Cancel all ongoing touches from the pointer.
103 for (var i = 0; i < m_Touches.Length; ++i)
104 {
105 var button = m_Touches[i];
106 if (button != null && button.device != pointer)
107 continue;
108
109 UpdateTouch(i, pointerIndex, TouchPhase.Canceled);
110 }
111
112 // Remove from list.
113 m_Pointers.EraseAtWithCapacity(ref m_NumPointers, pointerIndex);
114 ArrayHelpers.EraseAt(ref m_CurrentPositions, pointerIndex);
115 ArrayHelpers.EraseAt(ref m_CurrentDisplayIndices, pointerIndex);
116
117 // Re-enable the device (only in case it's still added to the system).
118 if (pointer.added)
119 InputSystem.EnableDevice(pointer);
120 }
121
122 private unsafe void OnEvent(InputEventPtr eventPtr, InputDevice device)
123 {
124 if (device == simulatedTouchscreen)
125 {
126 // Avoid processing events queued by this simulation device
127 return;
128 }
129
130 var pointerIndex = m_Pointers.IndexOfReference(device, m_NumPointers);
131 if (pointerIndex < 0)
132 return;
133
134 var eventType = eventPtr.type;
135 if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type)
136 return;
137
138 ////REVIEW: should we have specialized paths for MouseState and PenState here? (probably can only use for StateEvents)
139
140 Pointer pointer = m_Pointers[pointerIndex];
141
142 // Read pointer position.
143 var positionControl = pointer.position;
144 var positionStatePtr = positionControl.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
145 if (positionStatePtr != null)
146 m_CurrentPositions[pointerIndex] = positionControl.ReadValueFromState(positionStatePtr);
147
148 // Read display index.
149 var displayIndexControl = pointer.displayIndex;
150 var displayIndexStatePtr = displayIndexControl.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
151 if (displayIndexStatePtr != null)
152 m_CurrentDisplayIndices[pointerIndex] = displayIndexControl.ReadValueFromState(displayIndexStatePtr);
153
154 // End touches for which buttons are no longer pressed.
155 ////REVIEW: There must be a better way to do this
156 for (var i = 0; i < m_Touches.Length; ++i)
157 {
158 var button = m_Touches[i];
159 if (button == null || button.device != device)
160 continue;
161
162 var buttonStatePtr = button.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
163 if (buttonStatePtr == null)
164 {
165 // Button is not contained in event. If we do have a position update, issue
166 // a move on the button's corresponding touch. This makes us deal with delta
167 // events that only update pointer positions.
168 if (positionStatePtr != null)
169 UpdateTouch(i, pointerIndex, TouchPhase.Moved, eventPtr);
170 }
171 else if (button.ReadValueFromState(buttonStatePtr) < (ButtonControl.s_GlobalDefaultButtonPressPoint * ButtonControl.s_GlobalDefaultButtonReleaseThreshold))
172 UpdateTouch(i, pointerIndex, TouchPhase.Ended, eventPtr);
173 }
174
175 // Add/update touches for buttons that are pressed.
176 foreach (var control in eventPtr.EnumerateControls(InputControlExtensions.Enumerate.IgnoreControlsInDefaultState, device))
177 {
178 if (!control.isButton)
179 continue;
180
181 // Check if it's pressed.
182 var buttonStatePtr = control.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
183 Debug.Assert(buttonStatePtr != null, "Button returned from EnumerateControls() must be found in event");
184 var value = 0f;
185 control.ReadValueFromStateIntoBuffer(buttonStatePtr, UnsafeUtility.AddressOf(ref value), 4);
186 if (value <= ButtonControl.s_GlobalDefaultButtonPressPoint)
187 continue; // Not in default state but also not pressed.
188
189 // See if we have an ongoing touch for the button.
190 var touchIndex = m_Touches.IndexOfReference(control);
191 if (touchIndex < 0)
192 {
193 // No, so add it.
194 touchIndex = m_Touches.IndexOfReference((ButtonControl)null);
195 if (touchIndex >= 0) // If negative, we're at max touch count and can't add more.
196 {
197 m_Touches[touchIndex] = (ButtonControl)control;
198 UpdateTouch(touchIndex, pointerIndex, TouchPhase.Began, eventPtr);
199 }
200 }
201 else
202 {
203 // Yes, so update it.
204 UpdateTouch(touchIndex, pointerIndex, TouchPhase.Moved, eventPtr);
205 }
206 }
207
208 eventPtr.handled = true;
209 }
210
211 private void OnDeviceChange(InputDevice device, InputDeviceChange change)
212 {
213 // If someone removed our simulated touchscreen, disable touch simulation.
214 if (device == simulatedTouchscreen && change == InputDeviceChange.Removed)
215 {
216 Disable();
217 return;
218 }
219
220 switch (change)
221 {
222 case InputDeviceChange.Added:
223 {
224 if (device is Pointer pointer)
225 {
226 if (device is Touchscreen)
227 return; ////TODO: decide what to do
228
229 AddPointer(pointer);
230 }
231 break;
232 }
233
234 case InputDeviceChange.Removed:
235 {
236 if (device is Pointer pointer)
237 RemovePointer(pointer);
238 break;
239 }
240 }
241 }
242
243 protected void OnEnable()
244 {
245 if (simulatedTouchscreen != null)
246 {
247 if (!simulatedTouchscreen.added)
248 InputSystem.AddDevice(simulatedTouchscreen);
249 }
250 else
251 {
252 simulatedTouchscreen = InputSystem.GetDevice("Simulated Touchscreen") as Touchscreen;
253 if (simulatedTouchscreen == null)
254 simulatedTouchscreen = InputSystem.AddDevice<Touchscreen>("Simulated Touchscreen");
255 }
256
257 if (m_Touches == null)
258 m_Touches = new ButtonControl[simulatedTouchscreen.touches.Count];
259
260 if (m_TouchIds == null)
261 m_TouchIds = new int[simulatedTouchscreen.touches.Count];
262
263 foreach (var device in InputSystem.devices)
264 OnDeviceChange(device, InputDeviceChange.Added);
265
266 if (m_OnDeviceChange == null)
267 m_OnDeviceChange = OnDeviceChange;
268 if (m_OnEvent == null)
269 m_OnEvent = OnEvent;
270
271 InputSystem.onDeviceChange += m_OnDeviceChange;
272 InputSystem.onEvent += m_OnEvent;
273 }
274
275 protected void OnDisable()
276 {
277 if (simulatedTouchscreen != null && simulatedTouchscreen.added)
278 InputSystem.RemoveDevice(simulatedTouchscreen);
279
280 // Re-enable all pointers we disabled.
281 for (var i = 0; i < m_NumPointers; ++i)
282 InputSystem.EnableDevice(m_Pointers[i]);
283
284 m_Pointers.Clear(m_NumPointers);
285 m_Touches.Clear();
286
287 m_NumPointers = 0;
288 m_LastTouchId = 0;
289
290 InputSystem.onDeviceChange -= m_OnDeviceChange;
291 InputSystem.onEvent -= m_OnEvent;
292 }
293
294 private unsafe void UpdateTouch(int touchIndex, int pointerIndex, TouchPhase phase, InputEventPtr eventPtr = default)
295 {
296 Vector2 position = m_CurrentPositions[pointerIndex];
297 Debug.Assert(m_CurrentDisplayIndices[pointerIndex] <= byte.MaxValue, "Display index was larger than expected");
298 byte displayIndex = (byte)m_CurrentDisplayIndices[pointerIndex];
299
300 // We need to partially set TouchState in a similar way that the Native side would do, but deriving that
301 // data from the Pointer events.
302 // The handling of the remaining fields is done by the Touchscreen.OnStateEvent() callback.
303 var touch = new TouchState
304 {
305 phase = phase,
306 position = position,
307 displayIndex = displayIndex
308 };
309
310 if (phase == TouchPhase.Began)
311 {
312 touch.startTime = eventPtr.valid ? eventPtr.time : InputState.currentTime;
313 touch.startPosition = position;
314 touch.touchId = ++m_LastTouchId;
315 m_TouchIds[touchIndex] = m_LastTouchId;
316 }
317 else
318 {
319 touch.touchId = m_TouchIds[touchIndex];
320 }
321
322 //NOTE: Processing these events still happen in the current frame.
323 InputSystem.QueueStateEvent(simulatedTouchscreen, touch);
324
325 if (phase.IsEndedOrCanceled())
326 {
327 m_Touches[touchIndex] = null;
328 }
329 }
330
331 [NonSerialized] private int m_NumPointers;
332 [NonSerialized] private Pointer[] m_Pointers;
333 [NonSerialized] private Vector2[] m_CurrentPositions;
334 [NonSerialized] private int[] m_CurrentDisplayIndices;
335 [NonSerialized] private ButtonControl[] m_Touches;
336 [NonSerialized] private int[] m_TouchIds;
337
338 [NonSerialized] private int m_LastTouchId;
339 [NonSerialized] private Action<InputDevice, InputDeviceChange> m_OnDeviceChange;
340 [NonSerialized] private Action<InputEventPtr, InputDevice> m_OnEvent;
341
342 internal static TouchSimulation s_Instance;
343
344 #if UNITY_EDITOR
345 static TouchSimulation()
346 {
347 // We're a MonoBehaviour so our cctor may get called as part of the MonoBehaviour being
348 // created. We don't want to trigger InputSystem initialization from there so delay-execute
349 // the code here.
350 EditorApplication.delayCall +=
351 () =>
352 {
353 InputSystem.onSettingsChange += OnSettingsChanged;
354 InputSystem.onBeforeUpdate += ReEnableAfterDomainReload;
355 };
356 }
357
358 private static void ReEnableAfterDomainReload()
359 {
360 OnSettingsChanged();
361 InputSystem.onBeforeUpdate -= ReEnableAfterDomainReload;
362 }
363
364 private static void OnSettingsChanged()
365 {
366 if (InputEditorUserSettings.simulateTouch)
367 Enable();
368 else
369 Disable();
370 }
371
372 [CustomEditor(typeof(TouchSimulation))]
373 private class TouchSimulationEditor : UnityEditor.Editor
374 {
375 public void OnDisable()
376 {
377 new InputComponentEditorAnalytic(InputSystemComponent.TouchSimulation).Send();
378 }
379 }
380
381 #endif // UNITY_EDITOR
382
383 ////TODO: Remove IInputStateChangeMonitor from this class when we can break the API
384 void IInputStateChangeMonitor.NotifyControlStateChanged(InputControl control, double time, InputEventPtr eventPtr, long monitorIndex)
385 {
386 }
387
388 void IInputStateChangeMonitor.NotifyTimerExpired(InputControl control, double time, long monitorIndex, int timerIndex)
389 {
390 }
391
392 // Disable warnings about unused parameters.
393 #pragma warning disable CA1801
394
395 ////TODO: [Obsolete]
396 protected void InstallStateChangeMonitors(int startIndex = 0)
397 {
398 }
399
400 ////TODO: [Obsolete]
401 protected void OnSourceControlChangedValue(InputControl control, double time, InputEventPtr eventPtr,
402 long sourceDeviceAndButtonIndex)
403 {
404 }
405
406 ////TODO: [Obsolete]
407 protected void UninstallStateChangeMonitors(int startIndex = 0)
408 {
409 }
410 }
411}