VirtualMouseInput.cs (25520B)
1 using System; 2 using UnityEngine.InputSystem.LowLevel; 3 using UnityEngine.UI; 4 5 ////TODO: investigate how driving the HW cursor behaves when FPS drops low 6 //// (also, maybe we can add support where we turn the gamepad mouse on and off automatically based on whether the system mouse is used) 7 8 ////TODO: add support for acceleration 9 10 ////TODO: automatically scale mouse speed to resolution such that it stays constant regardless of resolution 11 12 ////TODO: make it work with PlayerInput such that it will automatically look up actions in the actual PlayerInput instance it is used with (based on the action IDs it has) 13 14 ////REVIEW: consider this for inclusion directly in the input system 15 16 namespace UnityEngine.InputSystem.UI 17 { 18 /// <summary> 19 /// A component that creates a virtual <see cref="Mouse"/> device and drives its input from gamepad-style inputs. This effectively 20 /// adds a software mouse cursor. 21 /// </summary> 22 /// <remarks> 23 /// This component can be used with UIs that are designed for mouse input, i.e. need to be operated with a cursor. 24 /// By hooking up the <see cref="InputAction"/>s of this component to gamepad input and directing <see cref="cursorTransform"/> 25 /// to the UI transform of the cursor, you can use this component to drive an on-screen cursor. 26 /// 27 /// Note that this component does not actually trigger UI input itself. Instead, it creates a virtual <see cref="Mouse"/> 28 /// device which can then be picked up elsewhere (such as by <see cref="InputSystemUIInputModule"/>) where mouse/pointer input 29 /// is expected. 30 /// 31 /// Also note that if there is a <see cref="Mouse"/> added by the platform, it is not impacted by this component. More specifically, 32 /// the system mouse cursor will not be moved or otherwise used by this component. 33 /// </remarks> 34 /// <seealso cref="Gamepad"/> 35 /// <seealso cref="Mouse"/> 36 [AddComponentMenu("Input/Virtual Mouse")] 37 public class VirtualMouseInput : MonoBehaviour 38 { 39 /// <summary> 40 /// Optional transform that will be updated to correspond to the current mouse position. 41 /// </summary> 42 /// <value>Transform to update with mouse position.</value> 43 /// <remarks> 44 /// This is useful for having a UI object that directly represents the mouse cursor. Simply add both the 45 /// <c>VirtualMouseInput</c> component and an <a href="https://docs.unity3d.com/Manual/script-Image.html">Image</a> 46 /// component and hook the <a href="https://docs.unity3d.com/ScriptReference/RectTransform.html">RectTransform</a> 47 /// component for the UI object into here. The object as a whole will then follow the generated mouse cursor 48 /// motion. 49 /// </remarks> 50 public RectTransform cursorTransform 51 { 52 get => m_CursorTransform; 53 set => m_CursorTransform = value; 54 } 55 56 /// <summary> 57 /// How many pixels per second the cursor travels in one axis when the respective axis from 58 /// <see cref="stickAction"/> is 1. 59 /// </summary> 60 /// <value>Mouse speed in pixels per second.</value> 61 public float cursorSpeed 62 { 63 get => m_CursorSpeed; 64 set => m_CursorSpeed = value; 65 } 66 67 /// <summary> 68 /// Determines which cursor representation to use. If this is set to <see cref="CursorMode.SoftwareCursor"/> 69 /// (the default), then <see cref="cursorGraphic"/> and <see cref="cursorTransform"/> define a software cursor 70 /// that is made to correspond to the position of <see cref="virtualMouse"/>. If this is set to <see 71 /// cref="CursorMode.HardwareCursorIfAvailable"/> and there is a native <see cref="Mouse"/> device present, 72 /// the component will take over that mouse device and disable it (so as for it to not also generate position 73 /// updates). It will then use <see cref="Mouse.WarpCursorPosition"/> to move the system mouse cursor to 74 /// correspond to the position of the <see cref="virtualMouse"/>. In this case, <see cref="cursorGraphic"/> 75 /// will be disabled and <see cref="cursorTransform"/> will not be updated. 76 /// </summary> 77 /// <value>Whether the system mouse cursor (if present) should be made to correspond with the virtual mouse position.</value> 78 /// <remarks> 79 /// Note that regardless of which mode is used for the cursor, mouse input is expected to be picked up from <see cref="virtualMouse"/>. 80 /// 81 /// Note that if <see cref="CursorMode.HardwareCursorIfAvailable"/> is used, the software cursor is still used 82 /// if no native <see cref="Mouse"/> device is present. 83 /// </remarks> 84 public CursorMode cursorMode 85 { 86 get => m_CursorMode; 87 set 88 { 89 if (m_CursorMode == value) 90 return; 91 92 // If we're turning it off, make sure we re-enable the system mouse. 93 if (m_CursorMode == CursorMode.HardwareCursorIfAvailable && m_SystemMouse != null) 94 { 95 InputSystem.EnableDevice(m_SystemMouse); 96 m_SystemMouse = null; 97 } 98 99 m_CursorMode = value; 100 101 if (m_CursorMode == CursorMode.HardwareCursorIfAvailable) 102 TryEnableHardwareCursor(); 103 else if (m_CursorGraphic != null) 104 m_CursorGraphic.enabled = true; 105 } 106 } 107 108 /// <summary> 109 /// The UI graphic element that represents the mouse cursor. 110 /// </summary> 111 /// <value>Graphic element for the software mouse cursor.</value> 112 /// <remarks> 113 /// If <see cref="cursorMode"/> is set to <see cref="CursorMode.HardwareCursorIfAvailable"/>, this graphic will 114 /// be disabled. 115 /// 116 /// Also, this UI component implicitly determines the <c>Canvas</c> that defines the screen area for the cursor. 117 /// The canvas that this graphic is on will be looked up using <c>GetComponentInParent</c> and then the <c>Canvas.pixelRect</c> 118 /// of the canvas is used as the bounds for the cursor motion range. 119 /// </remarks> 120 /// <seealso cref="CursorMode.SoftwareCursor"/> 121 public Graphic cursorGraphic 122 { 123 get => m_CursorGraphic; 124 set 125 { 126 m_CursorGraphic = value; 127 TryFindCanvas(); 128 } 129 } 130 131 /// <summary> 132 /// Multiplier for values received from <see cref="scrollWheelAction"/>. 133 /// </summary> 134 /// <value>Multiplier for scroll values.</value> 135 public float scrollSpeed 136 { 137 get => m_ScrollSpeed; 138 set => m_ScrollSpeed = value; 139 } 140 141 /// <summary> 142 /// The virtual mouse device that the component feeds with input. 143 /// </summary> 144 /// <value>Instance of virtual mouse or <c>null</c>.</value> 145 /// <remarks> 146 /// This is only initialized after the component has been enabled for the first time. Note that 147 /// when subsequently disabling the component, the property will continue to return the mouse device 148 /// but the device will not be added to the system while the component is not enabled. 149 /// </remarks> 150 public Mouse virtualMouse => m_VirtualMouse; 151 152 /// <summary> 153 /// The Vector2 stick input that drives the mouse cursor, i.e. <see cref="Mouse.position"/> on 154 /// <see cref="virtualMouse"/> and the <a 155 /// href="https://docs.unity3d.com/ScriptReference/RectTransform-anchoredPosition.html">anchoredPosition</a> 156 /// on <see cref="cursorTransform"/> (if set). 157 /// </summary> 158 /// <value>Stick input that drives cursor position.</value> 159 /// <remarks> 160 /// This should normally be bound to controls such as <see cref="Gamepad.leftStick"/> and/or 161 /// <see cref="Gamepad.rightStick"/>. 162 /// </remarks> 163 public InputActionProperty stickAction 164 { 165 get => m_StickAction; 166 set => SetAction(ref m_StickAction, value); 167 } 168 169 /// <summary> 170 /// Optional button input that determines when <see cref="Mouse.leftButton"/> is pressed on 171 /// <see cref="virtualMouse"/>. 172 /// </summary> 173 /// <value>Input for <see cref="Mouse.leftButton"/>.</value> 174 public InputActionProperty leftButtonAction 175 { 176 get => m_LeftButtonAction; 177 set 178 { 179 if (m_ButtonActionTriggeredDelegate != null) 180 SetActionCallback(m_LeftButtonAction, m_ButtonActionTriggeredDelegate, false); 181 SetAction(ref m_LeftButtonAction, value); 182 if (m_ButtonActionTriggeredDelegate != null) 183 SetActionCallback(m_LeftButtonAction, m_ButtonActionTriggeredDelegate, true); 184 } 185 } 186 187 /// <summary> 188 /// Optional button input that determines when <see cref="Mouse.rightButton"/> is pressed on 189 /// <see cref="virtualMouse"/>. 190 /// </summary> 191 /// <value>Input for <see cref="Mouse.rightButton"/>.</value> 192 public InputActionProperty rightButtonAction 193 { 194 get => m_RightButtonAction; 195 set 196 { 197 if (m_ButtonActionTriggeredDelegate != null) 198 SetActionCallback(m_RightButtonAction, m_ButtonActionTriggeredDelegate, false); 199 SetAction(ref m_RightButtonAction, value); 200 if (m_ButtonActionTriggeredDelegate != null) 201 SetActionCallback(m_RightButtonAction, m_ButtonActionTriggeredDelegate, true); 202 } 203 } 204 205 /// <summary> 206 /// Optional button input that determines when <see cref="Mouse.middleButton"/> is pressed on 207 /// <see cref="virtualMouse"/>. 208 /// </summary> 209 /// <value>Input for <see cref="Mouse.middleButton"/>.</value> 210 public InputActionProperty middleButtonAction 211 { 212 get => m_MiddleButtonAction; 213 set 214 { 215 if (m_ButtonActionTriggeredDelegate != null) 216 SetActionCallback(m_MiddleButtonAction, m_ButtonActionTriggeredDelegate, false); 217 SetAction(ref m_MiddleButtonAction, value); 218 if (m_ButtonActionTriggeredDelegate != null) 219 SetActionCallback(m_MiddleButtonAction, m_ButtonActionTriggeredDelegate, true); 220 } 221 } 222 223 /// <summary> 224 /// Optional button input that determines when <see cref="Mouse.forwardButton"/> is pressed on 225 /// <see cref="virtualMouse"/>. 226 /// </summary> 227 /// <value>Input for <see cref="Mouse.forwardButton"/>.</value> 228 public InputActionProperty forwardButtonAction 229 { 230 get => m_ForwardButtonAction; 231 set 232 { 233 if (m_ButtonActionTriggeredDelegate != null) 234 SetActionCallback(m_ForwardButtonAction, m_ButtonActionTriggeredDelegate, false); 235 SetAction(ref m_ForwardButtonAction, value); 236 if (m_ButtonActionTriggeredDelegate != null) 237 SetActionCallback(m_ForwardButtonAction, m_ButtonActionTriggeredDelegate, true); 238 } 239 } 240 241 /// <summary> 242 /// Optional button input that determines when <see cref="Mouse.forwardButton"/> is pressed on 243 /// <see cref="virtualMouse"/>. 244 /// </summary> 245 /// <value>Input for <see cref="Mouse.forwardButton"/>.</value> 246 public InputActionProperty backButtonAction 247 { 248 get => m_BackButtonAction; 249 set 250 { 251 if (m_ButtonActionTriggeredDelegate != null) 252 SetActionCallback(m_BackButtonAction, m_ButtonActionTriggeredDelegate, false); 253 SetAction(ref m_BackButtonAction, value); 254 if (m_ButtonActionTriggeredDelegate != null) 255 SetActionCallback(m_BackButtonAction, m_ButtonActionTriggeredDelegate, true); 256 } 257 } 258 259 /// <summary> 260 /// Optional Vector2 value input that determines the value of <see cref="Mouse.scroll"/> on 261 /// <see cref="virtualMouse"/>. 262 /// </summary> 263 /// <value>Input for <see cref="Mouse.scroll"/>.</value> 264 /// <remarks> 265 /// In case you want to only bind vertical scrolling, simply have a <see cref="Composites.Vector2Composite"/> 266 /// with only <c>Up</c> and <c>Down</c> bound and <c>Left</c> and <c>Right</c> deleted or bound to nothing. 267 /// </remarks> 268 public InputActionProperty scrollWheelAction 269 { 270 get => m_ScrollWheelAction; 271 set => SetAction(ref m_ScrollWheelAction, value); 272 } 273 274 protected void OnEnable() 275 { 276 // Hijack system mouse, if enabled. 277 if (m_CursorMode == CursorMode.HardwareCursorIfAvailable) 278 TryEnableHardwareCursor(); 279 280 // Add mouse device. 281 if (m_VirtualMouse == null) 282 m_VirtualMouse = (Mouse)InputSystem.AddDevice("VirtualMouse"); 283 else if (!m_VirtualMouse.added) 284 InputSystem.AddDevice(m_VirtualMouse); 285 286 // Set initial cursor position. 287 if (m_CursorTransform != null) 288 { 289 var position = m_CursorTransform.anchoredPosition; 290 InputState.Change(m_VirtualMouse.position, position); 291 m_SystemMouse?.WarpCursorPosition(position); 292 } 293 294 // Hook into input update. 295 if (m_AfterInputUpdateDelegate == null) 296 m_AfterInputUpdateDelegate = OnAfterInputUpdate; 297 InputSystem.onAfterUpdate += m_AfterInputUpdateDelegate; 298 299 // Hook into actions. 300 if (m_ButtonActionTriggeredDelegate == null) 301 m_ButtonActionTriggeredDelegate = OnButtonActionTriggered; 302 SetActionCallback(m_LeftButtonAction, m_ButtonActionTriggeredDelegate, true); 303 SetActionCallback(m_RightButtonAction, m_ButtonActionTriggeredDelegate, true); 304 SetActionCallback(m_MiddleButtonAction, m_ButtonActionTriggeredDelegate, true); 305 SetActionCallback(m_ForwardButtonAction, m_ButtonActionTriggeredDelegate, true); 306 SetActionCallback(m_BackButtonAction, m_ButtonActionTriggeredDelegate, true); 307 308 // Enable actions. 309 m_StickAction.action?.Enable(); 310 m_LeftButtonAction.action?.Enable(); 311 m_RightButtonAction.action?.Enable(); 312 m_MiddleButtonAction.action?.Enable(); 313 m_ForwardButtonAction.action?.Enable(); 314 m_BackButtonAction.action?.Enable(); 315 m_ScrollWheelAction.action?.Enable(); 316 } 317 318 protected void OnDisable() 319 { 320 // Remove mouse device. 321 if (m_VirtualMouse != null && m_VirtualMouse.added) 322 InputSystem.RemoveDevice(m_VirtualMouse); 323 324 // Let go of system mouse. 325 if (m_SystemMouse != null) 326 { 327 InputSystem.EnableDevice(m_SystemMouse); 328 m_SystemMouse = null; 329 } 330 331 // Remove ourselves from input update. 332 if (m_AfterInputUpdateDelegate != null) 333 InputSystem.onAfterUpdate -= m_AfterInputUpdateDelegate; 334 335 // Disable actions. 336 m_StickAction.action?.Disable(); 337 m_LeftButtonAction.action?.Disable(); 338 m_RightButtonAction.action?.Disable(); 339 m_MiddleButtonAction.action?.Disable(); 340 m_ForwardButtonAction.action?.Disable(); 341 m_BackButtonAction.action?.Disable(); 342 m_ScrollWheelAction.action?.Disable(); 343 344 // Unhock from actions. 345 if (m_ButtonActionTriggeredDelegate != null) 346 { 347 SetActionCallback(m_LeftButtonAction, m_ButtonActionTriggeredDelegate, false); 348 SetActionCallback(m_RightButtonAction, m_ButtonActionTriggeredDelegate, false); 349 SetActionCallback(m_MiddleButtonAction, m_ButtonActionTriggeredDelegate, false); 350 SetActionCallback(m_ForwardButtonAction, m_ButtonActionTriggeredDelegate, false); 351 SetActionCallback(m_BackButtonAction, m_ButtonActionTriggeredDelegate, false); 352 } 353 354 m_LastTime = default; 355 m_LastStickValue = default; 356 } 357 358 private void TryFindCanvas() 359 { 360 m_Canvas = m_CursorGraphic?.GetComponentInParent<Canvas>(); 361 } 362 363 private void TryEnableHardwareCursor() 364 { 365 var devices = InputSystem.devices; 366 for (var i = 0; i < devices.Count; ++i) 367 { 368 var device = devices[i]; 369 if (device.native && device is Mouse mouse) 370 { 371 m_SystemMouse = mouse; 372 break; 373 } 374 } 375 376 if (m_SystemMouse == null) 377 { 378 if (m_CursorGraphic != null) 379 m_CursorGraphic.enabled = true; 380 return; 381 } 382 383 InputSystem.DisableDevice(m_SystemMouse); 384 385 // Sync position. 386 if (m_VirtualMouse != null) 387 m_SystemMouse.WarpCursorPosition(m_VirtualMouse.position.ReadValue()); 388 389 // Turn off mouse cursor image. 390 if (m_CursorGraphic != null) 391 m_CursorGraphic.enabled = false; 392 } 393 394 private void UpdateMotion() 395 { 396 if (m_VirtualMouse == null) 397 return; 398 399 // Read current stick value. 400 var stickAction = m_StickAction.action; 401 if (stickAction == null) 402 return; 403 var stickValue = stickAction.ReadValue<Vector2>(); 404 if (Mathf.Approximately(0, stickValue.x) && Mathf.Approximately(0, stickValue.y)) 405 { 406 // Motion has stopped. 407 m_LastTime = default; 408 m_LastStickValue = default; 409 } 410 else 411 { 412 var currentTime = InputState.currentTime; 413 if (Mathf.Approximately(0, m_LastStickValue.x) && Mathf.Approximately(0, m_LastStickValue.y)) 414 { 415 // Motion has started. 416 m_LastTime = currentTime; 417 } 418 419 // Compute delta. 420 var deltaTime = (float)(currentTime - m_LastTime); 421 var delta = new Vector2(m_CursorSpeed * stickValue.x * deltaTime, m_CursorSpeed * stickValue.y * deltaTime); 422 423 // Update position. 424 var currentPosition = m_VirtualMouse.position.ReadValue(); 425 var newPosition = currentPosition + delta; 426 427 ////REVIEW: for the hardware cursor, clamp to something else? 428 // Clamp to canvas. 429 if (m_Canvas != null) 430 { 431 // Clamp to canvas. 432 var pixelRect = m_Canvas.pixelRect; 433 newPosition.x = Mathf.Clamp(newPosition.x, pixelRect.xMin, pixelRect.xMax); 434 newPosition.y = Mathf.Clamp(newPosition.y, pixelRect.yMin, pixelRect.yMax); 435 } 436 437 ////REVIEW: the fact we have no events on these means that actions won't have an event ID to go by; problem? 438 InputState.Change(m_VirtualMouse.position, newPosition); 439 InputState.Change(m_VirtualMouse.delta, delta); 440 441 // Update software cursor transform, if any. 442 if (m_CursorTransform != null && m_CursorMode == CursorMode.SoftwareCursor) 443 m_CursorTransform.anchoredPosition = newPosition; 444 445 m_LastStickValue = stickValue; 446 m_LastTime = currentTime; 447 448 // Update hardware cursor. 449 m_SystemMouse?.WarpCursorPosition(newPosition); 450 } 451 452 // Update scroll wheel. 453 var scrollAction = m_ScrollWheelAction.action; 454 if (scrollAction != null) 455 { 456 var scrollValue = scrollAction.ReadValue<Vector2>(); 457 scrollValue.x *= m_ScrollSpeed; 458 scrollValue.y *= m_ScrollSpeed; 459 460 InputState.Change(m_VirtualMouse.scroll, scrollValue); 461 } 462 } 463 464 [Header("Cursor")] 465 [SerializeField] private CursorMode m_CursorMode; 466 [SerializeField] private Graphic m_CursorGraphic; 467 [SerializeField] private RectTransform m_CursorTransform; 468 469 [Header("Motion")] 470 [SerializeField] private float m_CursorSpeed = 400; 471 [SerializeField] private float m_ScrollSpeed = 45; 472 473 [Space(10)] 474 [SerializeField] private InputActionProperty m_StickAction; 475 [SerializeField] private InputActionProperty m_LeftButtonAction; 476 [SerializeField] private InputActionProperty m_MiddleButtonAction; 477 [SerializeField] private InputActionProperty m_RightButtonAction; 478 [SerializeField] private InputActionProperty m_ForwardButtonAction; 479 [SerializeField] private InputActionProperty m_BackButtonAction; 480 [SerializeField] private InputActionProperty m_ScrollWheelAction; 481 482 private Canvas m_Canvas; // Canvas that gives the motion range for the software cursor. 483 private Mouse m_VirtualMouse; 484 private Mouse m_SystemMouse; 485 private Action m_AfterInputUpdateDelegate; 486 private Action<InputAction.CallbackContext> m_ButtonActionTriggeredDelegate; 487 private double m_LastTime; 488 private Vector2 m_LastStickValue; 489 490 private void OnButtonActionTriggered(InputAction.CallbackContext context) 491 { 492 if (m_VirtualMouse == null) 493 return; 494 495 // The button controls are bit controls. We can't (yet?) use InputState.Change to state 496 // the change of those controls as the state update machinery of InputManager only supports 497 // byte region updates. So we just grab the full state of our virtual mouse, then update 498 // the button in there and then simply overwrite the entire state. 499 500 var action = context.action; 501 MouseButton? button = null; 502 if (action == m_LeftButtonAction.action) 503 button = MouseButton.Left; 504 else if (action == m_RightButtonAction.action) 505 button = MouseButton.Right; 506 else if (action == m_MiddleButtonAction.action) 507 button = MouseButton.Middle; 508 else if (action == m_ForwardButtonAction.action) 509 button = MouseButton.Forward; 510 else if (action == m_BackButtonAction.action) 511 button = MouseButton.Back; 512 513 if (button != null) 514 { 515 var isPressed = context.control.IsPressed(); 516 m_VirtualMouse.CopyState<MouseState>(out var mouseState); 517 mouseState.WithButton(button.Value, isPressed); 518 519 InputState.Change(m_VirtualMouse, mouseState); 520 } 521 } 522 523 private static void SetActionCallback(InputActionProperty field, Action<InputAction.CallbackContext> callback, bool install = true) 524 { 525 var action = field.action; 526 if (action == null) 527 return; 528 529 // We don't need the performed callback as our mouse buttons are binary and thus 530 // we only care about started (1) and canceled (0). 531 532 if (install) 533 { 534 action.started += callback; 535 action.canceled += callback; 536 } 537 else 538 { 539 action.started -= callback; 540 action.canceled -= callback; 541 } 542 } 543 544 private static void SetAction(ref InputActionProperty field, InputActionProperty value) 545 { 546 var oldValue = field; 547 field = value; 548 549 if (oldValue.reference == null) 550 { 551 var oldAction = oldValue.action; 552 if (oldAction != null && oldAction.enabled) 553 { 554 oldAction.Disable(); 555 if (value.reference == null) 556 value.action?.Enable(); 557 } 558 } 559 } 560 561 private void OnAfterInputUpdate() 562 { 563 UpdateMotion(); 564 } 565 566 /// <summary> 567 /// Determines how the cursor for the virtual mouse is represented. 568 /// </summary> 569 /// <seealso cref="cursorMode"/> 570 public enum CursorMode 571 { 572 /// <summary> 573 /// The cursor is represented as a UI element. See <see cref="cursorGraphic"/>. 574 /// </summary> 575 SoftwareCursor, 576 577 /// <summary> 578 /// If a native <see cref="Mouse"/> device is present, its cursor will be used and driven 579 /// by the virtual mouse using <see cref="Mouse.WarpCursorPosition"/>. The software cursor 580 /// referenced by <see cref="cursorGraphic"/> will be disabled. 581 /// 582 /// Note that if no native <see cref="Mouse"/> is present, behavior will fall back to 583 /// <see cref="SoftwareCursor"/>. 584 /// </summary> 585 HardwareCursorIfAvailable, 586 } 587 } 588 }