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

193 lines
6.9 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using VRC.Udon.Common;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// System responsible for listening to Input Events and sending them to UdonBehaviours.
/// </summary>
/// <remarks>
/// Listens to Events:
/// - ClientSimMenuStateChangedEvent
/// Listens to Input Events:
/// - Jump
/// - Grab
/// - Use
/// - Drop
/// </remarks>
public class ClientSimUdonInput : IDisposable
{
private const float MINIMUM_MOVE_INPUT_EPS = 1e-3f;
private const float MINIMUM_LOOK_INPUT_EPS = 1e-5f;
private readonly IClientSimInput _input;
private readonly IClientSimEventDispatcher _eventDispatcher;
/// <summary>
/// A wrapper for sending events to all UdonBehaviours to create unit tests not dependent on UdonManager.
/// </summary>
private readonly IClientSimUdonInputEventSender _udonInputEventSender;
private Vector2 _prevInput;
private Vector2 _prevLookInput = Vector2.zero;
private Vector2 _prevMoveAxes = Vector2.zero;
private bool _isMenuOpen;
private int _lastMenuUpdateFrame; // TODO update based on processing tick instead of time to allow for better testing.
// All button based events are queued until process time. This is done to ensure that all udon input events
// happen at the same time in the frame and not mixed between when unity's update for the new Input Manager
// sends events. Without this, input events would happen before UdonBehaviour.Update, causing strange out of
// order issues.
private readonly Queue<Action> _queuedEvents = new Queue<Action>();
public ClientSimUdonInput(
IClientSimEventDispatcher eventDispatcher,
IClientSimInput input,
IClientSimUdonInputEventSender udonInputEventSender)
{
_input = input;
_eventDispatcher = eventDispatcher;
_udonInputEventSender = udonInputEventSender;
_eventDispatcher.Subscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
// Input will be null with incorrect Unity input project settings.
_input?.SubscribeJump(JumpInput);
_input?.SubscribeUse(UseInput);
_input?.SubscribeGrab(GrabInput);
_input?.SubscribeDrop(DropInput);
_input?.SubscribeInputChangedEvent(SendInputChangedEvent);
}
public void Dispose()
{
_eventDispatcher?.Unsubscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
_input?.UnsubscribeJump(JumpInput);
_input?.UnsubscribeUse(UseInput);
_input?.UnsubscribeGrab(GrabInput);
_input?.UnsubscribeDrop(DropInput);
_input?.UnsubscribeInputChangedEvent(SendInputChangedEvent);
}
#region ClientSim Events
private void SetMenuOpen(ClientSimMenuStateChangedEvent stateChangedEvent)
{
_lastMenuUpdateFrame = Time.frameCount;
_isMenuOpen = stateChangedEvent.isMenuOpen;
}
#endregion
#region ClientSim Input
private void JumpInput(bool value, HandType hand)
{
QueueButtonInputEvent(value, hand, UdonManager.UDON_INPUT_JUMP);
}
private void UseInput(bool value, HandType hand)
{
QueueButtonInputEvent(value, hand, UdonManager.UDON_INPUT_USE);
}
private void GrabInput(bool value, HandType hand)
{
QueueButtonInputEvent(value, hand, UdonManager.UDON_INPUT_GRAB);
}
private void DropInput(bool value, HandType hand)
{
QueueButtonInputEvent(value, hand, UdonManager.UDON_INPUT_DROP);
}
private void SendInputChangedEvent(VRCInputMethod inputMethod)
{
_queuedEvents.Enqueue(() =>
{
UdonManager.Instance.RunEvent(UdonManager.UDON_EVENT_ONINPUTMETHODCHANGED, ("inputMethod", inputMethod));
});
}
#endregion
private void SendUdonInputBoolEvent(bool value, HandType hand, string eventName)
{
var args = new UdonInputEventArgs(value, hand);
_udonInputEventSender.RunInputAction(eventName, args);
}
private void SendUdonInputFloatEvent(float value, HandType hand, string eventName)
{
var args = new UdonInputEventArgs(value, hand);
_udonInputEventSender.RunInputAction(eventName, args);
}
private void QueueButtonInputEvent(bool value, HandType hand, string eventName)
{
// Do not queue event if the menu is open.
if (IsMenuOpen())
{
return;
}
_queuedEvents.Enqueue(() =>
{
SendUdonInputBoolEvent(value, hand, eventName);
});
}
public void ProcessInputEvents()
{
// If the menu is open or the menu was updated on this frame, skip sending input events.
if (IsMenuOpen())
{
_queuedEvents.Clear();
return;
}
// Ensure that all udon input events happen at the same time in the frame and not mixed between when unity's
// update for the new Input Manager sends events. Without this, input events would happen before
// UdonBehaviour.Update, causing strange out of order issues.
while (_queuedEvents.Count > 0)
{
Action inputEvent = _queuedEvents.Dequeue();
inputEvent?.Invoke();
}
Vector2 lookAxes = _input.GetLookAxes();
if (Mathf.Abs(lookAxes.x - _prevLookInput.x) > MINIMUM_LOOK_INPUT_EPS)
{
SendUdonInputFloatEvent(lookAxes.x, HandType.RIGHT, UdonManager.UDON_LOOK_HORIZONTAL);
}
if (Mathf.Abs(lookAxes.y - _prevLookInput.y) > MINIMUM_LOOK_INPUT_EPS)
{
SendUdonInputFloatEvent(lookAxes.y, HandType.RIGHT, UdonManager.UDON_LOOK_VERTICAL);
}
_prevLookInput = lookAxes;
Vector2 moveAxes = _input.GetMovementAxes();
if (Mathf.Abs(_prevMoveAxes.x - moveAxes.x) > MINIMUM_MOVE_INPUT_EPS)
{
SendUdonInputFloatEvent(moveAxes.x, HandType.LEFT, UdonManager.UDON_MOVE_HORIZONTAL);
}
if (Mathf.Abs(_prevMoveAxes.y - moveAxes.y) > MINIMUM_MOVE_INPUT_EPS)
{
SendUdonInputFloatEvent(moveAxes.y, HandType.LEFT, UdonManager.UDON_MOVE_VERTICAL);
}
_prevMoveAxes = moveAxes;
}
private bool IsMenuOpen()
{
return _isMenuOpen || _lastMenuUpdateFrame == Time.frameCount;
}
}
}