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}