Files
2026-06-07 16:58:24 +01:00

314 lines
10 KiB
C#

using System;
using UnityEngine;
using UnityEngine.EventSystems;
using VRC.SDKBase;
using VRC.Udon.Common;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// The system responsible for managing the mouse position for both ClientSim raycasting
/// and for Unity's EventSystem to know where to raycast for UI elements.
/// </summary>
/// <remarks>
/// Sends Events:
/// - ClientSimMouseReleasedEvent
/// - ClientSimCurrentHandEvent
/// Listens to Events:
/// - ClientSimMenuStateChangedEvent
/// - ClientSimRaycastHitResultsEvent
/// Listens to Input Events:
/// - Use
/// - ReleaseMouse
/// </remarks>
[AddComponentMenu("")]
// Update FrameTick at the end of frame so that Input from playmode and test can be processed in the same order.
[DefaultExecutionOrder(10000)]
class ClientSimBaseInput : BaseInput, IClientSimMousePositionProvider, IDisposable
{
private IClientSimEventDispatcher _eventDispatcher;
private IClientSimInput _input;
private ClientSimSettings _settings;
private bool _menuIsOpen;
private bool _mouseReleaseKeyIsDown;
private bool _prevMouseReleased;
// Used for interacting with UI
private int _frameTick = 0;
private bool _rightUseDown = false;
private int _rightUseChangeTick = -1;
private bool _leftUseDown = false;
private int _leftUseChangeTick = -1;
private HandType _lastHandUsed = HandType.RIGHT;
private Camera _playerCamera = null;
private bool _uiShapeHit = false;
private Vector3 _raycastMousePosition;
public static Vector2 GetScreenCenter()
{
return new Vector2(Screen.width, Screen.height) * 0.5f;
}
public Vector2 GetMousePosition()
{
if (IsMouseFree())
{
// Due to having multiple inputs enabled or disabled, this method ensures no errors are thrown even
// if setup is incorrect.
#if ENABLE_INPUT_SYSTEM
// TODO if gamepad input, emulate mouse position to allow clicking on menus.
return UnityEngine.InputSystem.Mouse.current?.position.ReadValue() ?? Vector2.zero;
#elif ENABLE_LEGACY_INPUT_MANAGER
return base.mousePosition;
#else
return Vector2.zero;
#endif
}
return GetScreenCenter();
}
protected override void Awake()
{
base.Awake();
this.PreventComponentFromSaving();
}
public void Initialize(
IClientSimEventDispatcher eventDispatcher,
IClientSimInput input,
ClientSimSettings settings)
{
// Do not lock mouse if the player is never spawned.
if (!settings.spawnPlayer)
{
enabled = false;
return;
}
_eventDispatcher = eventDispatcher;
_input = input;
_settings = settings;
_eventDispatcher.Subscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
_eventDispatcher.Subscribe<ClientSimRaycastHitResultsEvent>(OnRaycastHit);
// Input will be null with incorrect Unity input project settings.
_input?.SubscribeReleaseMouse(InputMouseReleased);
_input?.SubscribeUse(InputUse);
}
protected override void Start()
{
base.Start();
// TODO properly pass in the camera provider instead of using this method.
_playerCamera = VRC_UiShape.GetEventCamera?.Invoke(this.gameObject);
foreach (var canvas in FindObjectsByType<Canvas>(FindObjectsSortMode.None))
{
if ((canvas.renderMode == RenderMode.WorldSpace) && (canvas.worldCamera == null))
canvas.worldCamera = _playerCamera;
}
}
protected override void OnDestroy()
{
base.OnDestroy();
Dispose();
}
public void Dispose()
{
_eventDispatcher?.Unsubscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
_eventDispatcher?.Unsubscribe<ClientSimRaycastHitResultsEvent>(OnRaycastHit);
_input?.UnsubscribeReleaseMouse(InputMouseReleased);
_input?.UnsubscribeUse(InputUse);
}
private void Update()
{
// Update mouse lock every frame to ensure it is always locked when needed.
InternalLockUpdate();
// TODO Move this to input system and support checking when Use input is down or up on the current frame.
++_frameTick;
}
#region Overrides
// Use the screenspace value of the last raycast hit position as the current mouse position.
// Using the raycast position decouples Desktop and VR's input source, allowing both to interact with UI without
// knowing the source of the raycast (mouse vs controller position)
public override Vector2 mousePosition => _raycastMousePosition;
public override bool GetMouseButton(int button)
{
return _lastHandUsed == HandType.RIGHT ? _rightUseDown : _leftUseDown;
}
public override bool GetMouseButtonUp(int button)
{
return
_lastHandUsed == HandType.RIGHT
? (!_rightUseDown && _rightUseChangeTick == _frameTick)
: (!_leftUseDown && _leftUseChangeTick == _frameTick);
}
public override bool GetMouseButtonDown(int button)
{
return
_lastHandUsed == HandType.RIGHT
? (_rightUseDown && _rightUseChangeTick == _frameTick)
: (_leftUseDown && _leftUseChangeTick == _frameTick);
}
// Override mouse scroll method to prevent errors when input settings are incorrectly setup on first import.
public override Vector2 mouseScrollDelta
{
get
{
#if ENABLE_INPUT_SYSTEM
return UnityEngine.InputSystem.Mouse.current?.scroll.ReadValue() ?? Vector2.zero;
#elif ENABLE_LEGACY_INPUT_MANAGER
return base.mouseScrollDelta;
#else
return Vector2.zero;
#endif
}
}
// Override generic axis method to prevent errors when input settings are incorrectly setup on first import.
public override float GetAxisRaw(string axisName)
{
if (axisName == "Horizontal")
{
return _input?.GetMovementHorizontal() ?? 0;
}
if (axisName == "Vertical")
{
return _input?.GetMovementVertical() ?? 0;
}
return 0f;
}
// Override generic button method to prevent errors when input settings are incorrectly setup on first import.
public override bool GetButtonDown(string buttonName)
{
if (buttonName == "Horizontal")
{
return Mathf.Abs(_input?.GetMovementHorizontal() ?? 0) > 0.5;
}
if (buttonName == "Vertical")
{
return Mathf.Abs(_input?.GetMovementVertical() ?? 0) > 0.5;
}
return false;
}
#endregion
#region ClientSim Events
private void SetMenuOpen(ClientSimMenuStateChangedEvent stateChangedEvent)
{
_menuIsOpen = stateChangedEvent.isMenuOpen;
CheckMouseRelease();
}
private void OnRaycastHit(ClientSimRaycastHitResultsEvent hitEvent)
{
if (_lastHandUsed == hitEvent.handType)
{
var hitResults = hitEvent.raycastResults;
_uiShapeHit = hitResults != null && hitResults.uiShape != null;
_raycastMousePosition = GetScreenCenter();
// If there is a player camera and there was a hit point, convert the world point to screen space.
// Transforming it now instead of when requested to ensure that player position updates do not affect
// interacting with the menu.
if (hitResults != null && _playerCamera != null)
{
_raycastMousePosition = _playerCamera.WorldToScreenPoint(hitResults.hitPoint);
}
}
}
#endregion
#region ClientSim Input Events
private void InputMouseReleased(bool value)
{
_mouseReleaseKeyIsDown = value;
CheckMouseRelease();
}
private void InputUse(bool value, HandType handType)
{
if (value)
{
if (_lastHandUsed != handType)
{
_lastHandUsed = handType;
_eventDispatcher.SendEvent(new ClientSimCurrentHandEvent { currentUsedHand = _lastHandUsed });
}
}
if (handType == HandType.RIGHT)
{
_rightUseDown = value;
_rightUseChangeTick = _frameTick;
}
else
{
_leftUseDown = value;
_leftUseChangeTick = _frameTick;
}
}
#endregion
public bool HitUIShape()
{
return _uiShapeHit;
}
private bool IsMouseFree()
{
return _mouseReleaseKeyIsDown || _menuIsOpen;
}
private void CheckMouseRelease()
{
bool released = IsMouseFree();
if (released != _prevMouseReleased)
{
_prevMouseReleased = released;
_eventDispatcher.SendEvent(new ClientSimMouseReleasedEvent { isReleased = released });
}
InternalLockUpdate();
}
private void InternalLockUpdate()
{
// If the menu is open or the tab key is held down, do not lock the mouse and show the cursor.
// TODO Check if TrackingProvider is VR and do not lock mouse.
if (IsMouseFree() || ClientSimRuntimeLoader.IsInUnityTest())
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
// Else hide the cursor and lock the cursor to the center of the screen.
else
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
}
}