576 lines
19 KiB
C#
576 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using JetBrains.Annotations;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using VRC.SDKBase;
|
|
using VRC.Udon.Common;
|
|
|
|
namespace VRC.SDK3.ClientSim
|
|
{
|
|
/// <summary>
|
|
/// System responsible for displaying the menu to the players and updating settings.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Sends Events:
|
|
/// - ClientSimMenuStateChangedEvent
|
|
/// - ClientSimMenuRespawnClickedEvent
|
|
/// - ClientSimOnPlayerHeightUpdateEvent
|
|
/// Listens to Events:
|
|
/// - ClientSimReadyEvent
|
|
/// - ClientSimOnPlayerMovedEvent
|
|
/// - ClientSimOnNewMasterEvent
|
|
/// - ClientSimOnPlayerJoinedEvent
|
|
/// Listens to Input Events:
|
|
/// - ToggleMenu
|
|
/// </remarks>
|
|
[AddComponentMenu("")]
|
|
public class ClientSimMenu : ClientSimBehaviour, IDisposable
|
|
{
|
|
// Property name on UI shaders to set the ZTest mode. Used to make the menu appear on top of everything.
|
|
private const string GUI_ZTEST_MODE_PROPERTY_NAME = "unity_GUIZTestMode";
|
|
|
|
private const string HAS_USER_ACCEPTED_WARNING = "accepted_warning";
|
|
|
|
public enum ClientSimDisplayedPage
|
|
{
|
|
PAUSE_MENU,
|
|
WARNING_PAGE,
|
|
INVALID_SETTINGS_PAGE,
|
|
DELAYED_START_PAGE,
|
|
}
|
|
|
|
// The method to open the settings window is set from Editor context.
|
|
// This hook is set on playmode start in ClientSimEditorRuntimeLinker.cs
|
|
internal static Action openSettingsHook;
|
|
// This method allows the menu to check the editor only method if ClientSim has all settings properly set
|
|
internal static Func<bool> checkValidSettingsHook;
|
|
|
|
[SerializeField]
|
|
private GameObject menu;
|
|
|
|
public float menuScaleFactor = 0.0035f;
|
|
|
|
[SerializeField]
|
|
private GameObject pauseMenu;
|
|
[SerializeField]
|
|
private GameObject warningsPage;
|
|
[SerializeField]
|
|
private GameObject invalidSettingsPage;
|
|
[SerializeField]
|
|
private GameObject delayStartPage;
|
|
|
|
|
|
[SerializeField]
|
|
private Toggle tooltipsToggle;
|
|
[SerializeField]
|
|
private Toggle reticleToggle;
|
|
[SerializeField]
|
|
private Toggle invertMouseToggle;
|
|
[SerializeField]
|
|
private Toggle consoleLoggingToggle;
|
|
[SerializeField]
|
|
private Slider playerHeightSlider;
|
|
[SerializeField]
|
|
private Text playerHeightText;
|
|
|
|
[SerializeField]
|
|
private Text playerNameText;
|
|
[SerializeField]
|
|
private Text playerIdText;
|
|
[SerializeField]
|
|
private Toggle isMasterToggle;
|
|
[SerializeField]
|
|
private Toggle isInstanceOwnerToggle;
|
|
[SerializeField]
|
|
private Toggle isVRCPlusToggle;
|
|
|
|
|
|
private IClientSimEventDispatcher _eventDispatcher;
|
|
private IClientSimInput _input;
|
|
private ClientSimSettings _settings;
|
|
private IClientSimSessionState _sessionState;
|
|
private IClientSimPlayerHeightManager _heightManager;
|
|
|
|
private ClientSimDisplayedPage _displayedPage = ClientSimDisplayedPage.WARNING_PAGE;
|
|
private bool _menuIsActive;
|
|
private bool _stackedCameraReady = false;
|
|
|
|
private float _playerHeightOriginalMaxvalue;
|
|
|
|
private Canvas _menuCanvas;
|
|
|
|
public void SetCanvasCamera(Camera cam)
|
|
{
|
|
_menuCanvas.worldCamera = cam;
|
|
}
|
|
|
|
protected override void Awake()
|
|
{
|
|
base.Awake();
|
|
|
|
_menuCanvas = menu.GetComponent<Canvas>();
|
|
_playerHeightOriginalMaxvalue = playerHeightSlider.maxValue;
|
|
|
|
}
|
|
|
|
public void Initialize(
|
|
IClientSimEventDispatcher eventDispatcher,
|
|
IClientSimInput input,
|
|
ClientSimSettings settings,
|
|
IClientSimSessionState sessionState,
|
|
IClientSimPlayerHeightManager heightManager)
|
|
{
|
|
_eventDispatcher = eventDispatcher;
|
|
_input = input;
|
|
_settings = settings;
|
|
_sessionState = sessionState;
|
|
_heightManager = heightManager;
|
|
|
|
// Input will be null with incorrect Unity input project settings.
|
|
_input?.SubscribeToggleMenu(HandleInputMenuToggle);
|
|
_eventDispatcher.Subscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
|
|
_eventDispatcher.Subscribe<ClientSimOnNewMasterEvent>(OnMasterChange);
|
|
_eventDispatcher.Subscribe<ClientSimReadyEvent>(OnReady);
|
|
_eventDispatcher.Subscribe<ClientSimOnPlayerHeightUpdateEvent>(OnPlayerHeightUpdate);
|
|
_eventDispatcher.Subscribe<ClientSimOnToggleManualScalingEvent>(OnManualScalingToggled);
|
|
_eventDispatcher.Subscribe<ClientSimStackedCameraReadyEvent>(OnStackedCameraReady);
|
|
|
|
playerNameText.text = "";
|
|
playerIdText.text = "";
|
|
isMasterToggle.isOn = false;
|
|
isInstanceOwnerToggle.isOn = settings.isInstanceOwner;
|
|
isVRCPlusToggle.isOn = settings.isVRCPlus;
|
|
isVRCPlusToggle.onValueChanged.AddListener(OnVRCPlusToggleChanged);
|
|
|
|
UpdateValuesFromSettings();
|
|
#if UNITY_EDITOR
|
|
UnityEditor.SceneVisibilityManager.instance.Hide(gameObject, true);
|
|
#endif
|
|
}
|
|
|
|
public ClientSimDisplayedPage GetDisplayedPage()
|
|
{
|
|
return _displayedPage;
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
SetUIOverlayMaterial();
|
|
|
|
bool shouldShowMenu = true;
|
|
if (checkValidSettingsHook?.Invoke() == false)
|
|
{
|
|
// Force open the ClientSim settings window.
|
|
// When first importing ClientSim and disabling legacy input, all UI menus fail to react to the mouse,
|
|
// preventing users from clicking the settings window button.
|
|
OpenSettings();
|
|
SetDisplayedPage(ClientSimDisplayedPage.INVALID_SETTINGS_PAGE);
|
|
}
|
|
else if (_settings.initializationDelay > 0)
|
|
{
|
|
SetDisplayedPage(ClientSimDisplayedPage.DELAYED_START_PAGE);
|
|
}
|
|
else if (_settings.spawnPlayer)
|
|
{
|
|
DisplayInitialPageForPlayer();
|
|
}
|
|
else
|
|
{
|
|
shouldShowMenu = false;
|
|
}
|
|
|
|
ToggleMenu(shouldShowMenu);
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
Dispose();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_input?.UnsubscribeToggleMenu(HandleInputMenuToggle);
|
|
|
|
_eventDispatcher?.Unsubscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
|
|
_eventDispatcher?.Unsubscribe<ClientSimOnNewMasterEvent>(OnMasterChange);
|
|
_eventDispatcher?.Unsubscribe<ClientSimReadyEvent>(OnReady);
|
|
_eventDispatcher?.Unsubscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
|
|
_eventDispatcher?.Unsubscribe<ClientSimOnToggleManualScalingEvent>(OnManualScalingToggled);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
// Only update the menu settings while the menu is displayed.
|
|
if (_menuIsActive)
|
|
{
|
|
UpdateValuesFromSettings();
|
|
}
|
|
}
|
|
|
|
private void SetUIOverlayMaterial()
|
|
{
|
|
int propertyId = Shader.PropertyToID(GUI_ZTEST_MODE_PROPERTY_NAME);
|
|
|
|
// Solution provided from Unity forums:
|
|
// https://answers.unity.com/questions/878667/world-space-canvas-on-top-of-everything.html
|
|
Graphic[] graphics = GetComponentsInChildren<Graphic>(true);
|
|
Dictionary<Material, Material> newMaterialMapping = new Dictionary<Material, Material>();
|
|
foreach (var graphic in graphics)
|
|
{
|
|
Material mat = graphic.materialForRendering;
|
|
if (mat == null)
|
|
{
|
|
continue;
|
|
}
|
|
if (!newMaterialMapping.TryGetValue(mat, out Material updatedMaterial))
|
|
{
|
|
updatedMaterial = new Material(mat);
|
|
newMaterialMapping.Add(mat, updatedMaterial);
|
|
}
|
|
updatedMaterial.SetInt(propertyId, (int)UnityEngine.Rendering.CompareFunction.Always);
|
|
graphic.material = updatedMaterial;
|
|
}
|
|
}
|
|
|
|
private void SetDisplayedPage(ClientSimDisplayedPage page)
|
|
{
|
|
_displayedPage = page;
|
|
|
|
pauseMenu.SetActive(_displayedPage == ClientSimDisplayedPage.PAUSE_MENU);
|
|
warningsPage.SetActive(_displayedPage == ClientSimDisplayedPage.WARNING_PAGE);
|
|
delayStartPage.SetActive(_displayedPage == ClientSimDisplayedPage.DELAYED_START_PAGE);
|
|
invalidSettingsPage.SetActive(_displayedPage == ClientSimDisplayedPage.INVALID_SETTINGS_PAGE);
|
|
}
|
|
|
|
private void DisplayInitialPageForPlayer()
|
|
{
|
|
SetDisplayedPage(_sessionState.GetBool(HAS_USER_ACCEPTED_WARNING)
|
|
? ClientSimDisplayedPage.PAUSE_MENU
|
|
: ClientSimDisplayedPage.WARNING_PAGE);
|
|
}
|
|
|
|
private void UpdateValuesFromSettings()
|
|
{
|
|
tooltipsToggle.isOn = _settings.showTooltips;
|
|
reticleToggle.isOn = _settings.showDesktopReticle;
|
|
invertMouseToggle.isOn = _settings.invertMouseLook;
|
|
consoleLoggingToggle.isOn = _settings.displayLogs;
|
|
|
|
float playerHeight = _heightManager.GetAvatarEyeHeightAsMetersClamped();
|
|
if (!Mathf.Approximately(playerHeight, playerHeightSlider.value))
|
|
{
|
|
// the player height slider obeys manual scale restrictions as if through the radial menu
|
|
ClampPlayerHeightSliderBounds();
|
|
|
|
playerHeightSlider.value = playerHeight;
|
|
playerHeightText.text = playerHeight.ToString("F2");
|
|
|
|
_eventDispatcher.SendEvent(new ClientSimOnPlayerHeightUpdateEvent { playerHeight = playerHeight });
|
|
}
|
|
}
|
|
|
|
private void ClampPlayerHeightSliderBounds()
|
|
{
|
|
playerHeightSlider.minValue = _heightManager.GetAvatarEyeHeightMinimumAsMeters();
|
|
playerHeightSlider.maxValue = _heightManager.GetAvatarEyeHeightMaximumAsMeters();
|
|
}
|
|
|
|
private void SaveSettings()
|
|
{
|
|
#if UNITY_EDITOR
|
|
ClientSimSettings.SaveSettings(_settings);
|
|
#endif
|
|
}
|
|
|
|
private void ToggleMenu(bool isActive)
|
|
{
|
|
// Don't allow the menu to be closed if the stacked camera system is not ready.
|
|
if(!isActive && !_stackedCameraReady)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_menuIsActive = isActive;
|
|
menu.SetActive(isActive);
|
|
|
|
// toggle internal UI camera stack to improve menu performance
|
|
if (_menuCanvas.worldCamera != null)
|
|
_menuCanvas.worldCamera.enabled = isActive;
|
|
|
|
_eventDispatcher.SendEvent(new ClientSimMenuStateChangedEvent { isMenuOpen = _menuIsActive });
|
|
|
|
if (_menuIsActive)
|
|
{
|
|
_eventDispatcher.Subscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
|
|
UpdateCanvasLocation();
|
|
|
|
// If the user sets the player height value too large through the Settings window,
|
|
// toggling the menu will clamp the max range to make it more usable again.
|
|
ClampPlayerHeightSliderBounds();
|
|
}
|
|
else
|
|
{
|
|
_eventDispatcher.Unsubscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
|
|
}
|
|
}
|
|
|
|
// TODO update position based on tracking type. Desktop should always be in front of the camera and VR
|
|
// should be stationary relative to the playspace position.
|
|
private void UpdateCanvasLocation()
|
|
{
|
|
Camera cam = _menuCanvas.worldCamera;
|
|
|
|
// Always use main camera to position the menu if the camera is missing or not enabled.
|
|
if (cam == null || !cam.enabled || !cam.gameObject.activeInHierarchy)
|
|
{
|
|
cam = Camera.main;
|
|
}
|
|
|
|
// TODO handle missing camera better
|
|
if (cam == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Transform camTransform = cam.transform;
|
|
menu.transform.localScale = camTransform.lossyScale * menuScaleFactor;
|
|
|
|
Vector3 position = camTransform.TransformPoint(Vector3.forward * 2);
|
|
menu.transform.SetPositionAndRotation(position, camTransform.rotation);
|
|
}
|
|
|
|
#region ClientSim Input
|
|
|
|
private void HandleInputMenuToggle(bool value, HandType hand)
|
|
{
|
|
// Only handle menu input down, and not on release.
|
|
if (!value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Users can only change the enabled state when the current page is the normal pause menu.
|
|
if (_displayedPage != ClientSimDisplayedPage.PAUSE_MENU)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Player is not active, do not allow changing the state of the menu.
|
|
if (!_settings.spawnPlayer)
|
|
{
|
|
return;
|
|
}
|
|
|
|
#if ENABLE_INPUT_SYSTEM
|
|
// Ignore pressing escape to toggle the menu off. Due to Unity using the escape key as a special key to
|
|
// remove focus from the game window, it is impossible to recapture it.
|
|
if (_menuIsActive && (UnityEngine.InputSystem.Keyboard.current?.escapeKey.wasPressedThisFrame ?? false))
|
|
{
|
|
return;
|
|
}
|
|
#endif
|
|
ToggleMenu(!_menuIsActive);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ClientSim Events
|
|
|
|
private void OnReady(ClientSimReadyEvent readyEvent)
|
|
{
|
|
// Disable the menu if the player is not spawned, and the displayed page isn't invalid.
|
|
if (!_settings.spawnPlayer && _displayedPage != ClientSimDisplayedPage.INVALID_SETTINGS_PAGE)
|
|
{
|
|
ToggleMenu(false);
|
|
return;
|
|
}
|
|
|
|
if (_displayedPage == ClientSimDisplayedPage.DELAYED_START_PAGE)
|
|
{
|
|
DisplayInitialPageForPlayer();
|
|
UpdateCanvasLocation();
|
|
}
|
|
}
|
|
|
|
private void OnStackedCameraReady(ClientSimStackedCameraReadyEvent readyEvent)
|
|
{
|
|
_stackedCameraReady = true;
|
|
_eventDispatcher.Unsubscribe<ClientSimStackedCameraReadyEvent>(OnStackedCameraReady);
|
|
if(_settings.hideMenuOnLaunch && _sessionState.GetBool(HAS_USER_ACCEPTED_WARNING))
|
|
ToggleMenu(false);
|
|
|
|
}
|
|
|
|
private void OnPlayerJoined(ClientSimOnPlayerJoinedEvent joinEvent)
|
|
{
|
|
VRCPlayerApi player = joinEvent.player;
|
|
if (!player.isLocal)
|
|
{
|
|
return;
|
|
}
|
|
|
|
isMasterToggle.isOn = player.isMaster;
|
|
playerNameText.text = player.displayName;
|
|
playerIdText.text = player.playerId.ToString();
|
|
}
|
|
|
|
private void OnMasterChange(ClientSimOnNewMasterEvent masterEvent)
|
|
{
|
|
isMasterToggle.isOn = Networking.IsMaster;
|
|
}
|
|
|
|
private void OnVRCPlusToggleChanged(bool value)
|
|
{
|
|
var localPlayer = Networking.LocalPlayer;
|
|
if (localPlayer != null)
|
|
{
|
|
localPlayer.GetClientSimPlayer().isVRCPlus = value;
|
|
}
|
|
}
|
|
|
|
private void OnPlayerMoved(ClientSimOnPlayerMovedEvent movedEvent)
|
|
{
|
|
UpdateCanvasLocation();
|
|
}
|
|
|
|
private void OnPlayerHeightUpdate(ClientSimOnPlayerHeightUpdateEvent heightEvent)
|
|
{
|
|
// if height was set programatically and exceeds a manual scaling limit, set slider to the exceeded limit
|
|
if (heightEvent.exceedsManualScalingMinimum)
|
|
playerHeightSlider.SetValueWithoutNotify(_heightManager.GetAvatarEyeHeightMinimumAsMeters());
|
|
|
|
else if (heightEvent.exceedsManualScalingMaximum)
|
|
playerHeightSlider.SetValueWithoutNotify(_heightManager.GetAvatarEyeHeightMaximumAsMeters());
|
|
|
|
else
|
|
playerHeightSlider.SetValueWithoutNotify(heightEvent.playerHeight);
|
|
|
|
playerHeightText.text = heightEvent.playerHeight.ToString("F2");
|
|
ClientSimSettings.Instance.SetInitialPlayerHeight(heightEvent.playerHeight);
|
|
}
|
|
|
|
private void OnManualScalingToggled(ClientSimOnToggleManualScalingEvent toggleEvent)
|
|
{
|
|
playerHeightSlider.interactable = toggleEvent.manualScalingAllowed;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region UI Hooks
|
|
|
|
[PublicAPI]
|
|
public void WarningAccepted()
|
|
{
|
|
CloseMenu();
|
|
SetDisplayedPage(ClientSimDisplayedPage.PAUSE_MENU);
|
|
_sessionState.SetBool(HAS_USER_ACCEPTED_WARNING, true);
|
|
}
|
|
|
|
[PublicAPI]
|
|
public void OpenMenu()
|
|
{
|
|
ToggleMenu(true);
|
|
}
|
|
|
|
[PublicAPI]
|
|
public void CloseMenu()
|
|
{
|
|
ToggleMenu(false);
|
|
}
|
|
|
|
[PublicAPI]
|
|
public void Respawn()
|
|
{
|
|
CloseMenu();
|
|
_eventDispatcher.SendEvent(new ClientSimMenuRespawnClickedEvent());
|
|
}
|
|
|
|
[PublicAPI]
|
|
public void OpenSettings()
|
|
{
|
|
openSettingsHook?.Invoke();
|
|
}
|
|
|
|
[PublicAPI]
|
|
public void ExitPlaymode()
|
|
{
|
|
#if UNITY_EDITOR
|
|
UnityEditor.EditorApplication.ExitPlaymode();
|
|
#endif
|
|
}
|
|
|
|
[PublicAPI]
|
|
public void SpawnRemotePlayer()
|
|
{
|
|
ClientSimMain.SpawnRemotePlayer();
|
|
}
|
|
|
|
[PublicAPI]
|
|
public void UpdatePlayerHeight(float playerHeight)
|
|
{
|
|
if (!_heightManager.GetManualAvatarScalingAllowed())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Mathf.Approximately(_heightManager.GetAvatarEyeHeightAsMetersClamped(), playerHeight))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_heightManager.SetAvatarEyeHeightByMeters(playerHeight, true);
|
|
SaveSettings();
|
|
}
|
|
|
|
[PublicAPI]
|
|
public void UpdateShowTooltips(bool showTooltips)
|
|
{
|
|
if (_settings.showTooltips == showTooltips)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_settings.showTooltips = showTooltips;
|
|
SaveSettings();
|
|
}
|
|
|
|
[PublicAPI]
|
|
public void UpdateShowReticle(bool showReticle)
|
|
{
|
|
if (_settings.showDesktopReticle == showReticle)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_settings.showDesktopReticle = showReticle;
|
|
SaveSettings();
|
|
}
|
|
|
|
[PublicAPI]
|
|
public void UpdateInvertMouseLook(bool invertMouseLook)
|
|
{
|
|
if (_settings.invertMouseLook == invertMouseLook)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_settings.invertMouseLook = invertMouseLook;
|
|
SaveSettings();
|
|
}
|
|
|
|
[PublicAPI]
|
|
public void UpdateConsoleLogging(bool consoleLogging)
|
|
{
|
|
if (_settings.displayLogs == consoleLogging)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_settings.displayLogs = consoleLogging;
|
|
SaveSettings();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
} |