using System; using System.Collections.Generic; using UnityEngine; using VRC.SDKBase; using VRC.Udon; using VRC.Udon.Common; namespace VRC.SDK3.ClientSim { /// /// System responsible for listening to Input Events and sending them to UdonBehaviours. /// /// /// Listens to Events: /// - ClientSimMenuStateChangedEvent /// Listens to Input Events: /// - Jump /// - Grab /// - Use /// - Drop /// 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; /// /// A wrapper for sending events to all UdonBehaviours to create unit tests not dependent on UdonManager. /// 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 _queuedEvents = new Queue(); public ClientSimUdonInput( IClientSimEventDispatcher eventDispatcher, IClientSimInput input, IClientSimUdonInputEventSender udonInputEventSender) { _input = input; _eventDispatcher = eventDispatcher; _udonInputEventSender = udonInputEventSender; _eventDispatcher.Subscribe(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(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; } } }