314 lines
10 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
} |