using System; using System.Collections.Generic; using UnityEngine.UI; namespace UnityEngine.XR.Interaction.Toolkit.Examples { /// /// Use this class to present locomotion control schemes and configuration preferences, /// and respond to player input in the UI to set them. /// /// public class LocomotionConfigurationMenu : MonoBehaviour { class EnumDropdownCache where T : Enum { public List options { get; } = new List(); readonly List m_Values = new List(); public EnumDropdownCache() { foreach (var name in Enum.GetNames(typeof(T))) { options.Add(new Dropdown.OptionData(name)); } foreach (var value in Enum.GetValues(typeof(T))) { m_Values.Add((T)value); } } public T GetValue(int index) { return m_Values[index]; } public int FindIndex(T value) { // This will not distinguish between enums with duplicate values, // and will always select the first equal value. var comparer = EqualityComparer.Default; for (var index = 0; index < m_Values.Count; ++index) { if (comparer.Equals(m_Values[index], value)) { return index; } } return -1; } } [SerializeField] [Tooltip("Stores the Toggle used to enable or disable continuous movement locomotion.")] Toggle m_ContinuousMoveToggle; /// /// Stores the used to enable or disable continuous movement locomotion. /// public Toggle continuousMoveToggle { get => m_ContinuousMoveToggle; set { UnsubscribeContinuousMove(m_ContinuousMoveToggle); m_ContinuousMoveToggle = value; SubscribeContinuousMove(m_ContinuousMoveToggle); } } [SerializeField] [Tooltip("Stores the Slider used to set the move speed of continuous movement.")] Slider m_MoveSpeedSlider; /// /// Stores the used to set the move speed of continuous movement. /// public Slider moveSpeedSlider { get => m_MoveSpeedSlider; set { UnsubscribeMoveSpeed(m_MoveSpeedSlider); m_MoveSpeedSlider = value; SubscribeMoveSpeed(m_MoveSpeedSlider, m_MoveSpeedValueText); RefreshMoveDependentInteractable(); } } [SerializeField] [Tooltip("Stores the Text used to display the current move speed of continuous movement.")] Text m_MoveSpeedValueText; /// /// Stores the used to display the current move speed of continuous movement. /// public Text moveSpeedValueText { get => m_MoveSpeedValueText; set { UnsubscribeMoveSpeed(m_MoveSpeedSlider); m_MoveSpeedValueText = value; SubscribeMoveSpeed(m_MoveSpeedSlider, m_MoveSpeedValueText); } } [SerializeField] [Tooltip("Stores the Toggle used to enable or disable strafing (sideways movement) of continuous movement.")] Toggle m_EnableStrafeToggle; /// /// Stores the used to enable or disable strafing (sideways movement) of continuous movement. /// public Toggle enableStrafeToggle { get => m_EnableStrafeToggle; set { UnsubscribeEnableStrafe(m_EnableStrafeToggle); m_EnableStrafeToggle = value; SubscribeEnableStrafe(m_EnableStrafeToggle); RefreshMoveDependentInteractable(); } } [SerializeField] [Tooltip("Stores the Toggle used to enable or disable gravity on continuous movement.")] Toggle m_UseGravityToggle; /// /// Stores the used to enable or disable gravity on continuous movement. /// public Toggle useGravityToggle { get => m_UseGravityToggle; set { UnsubscribeUseGravity(m_UseGravityToggle); m_UseGravityToggle = value; SubscribeUseGravity(m_UseGravityToggle); RefreshMoveDependentInteractable(); } } [SerializeField] [Tooltip("Stores the Dropdown used to select when gravity is applied with continuous movement.")] Dropdown m_GravityApplicationModeDropdown; /// /// Stores the used to select when gravity is applied with continuous movement. /// public Dropdown gravityApplicationModeDropdown { get => m_GravityApplicationModeDropdown; set { UnsubscribeGravityApplicationMode(m_GravityApplicationModeDropdown); m_GravityApplicationModeDropdown = value; SubscribeGravityApplicationMode(m_GravityApplicationModeDropdown); RefreshMoveDependentInteractable(); } } [SerializeField] [Tooltip("Stores the Dropdown used to select the source Transform to define the forward direction of continuous movement.")] Dropdown m_ForwardSourceDropdown; /// /// Stores the used to select the source to define the forward direction of continuous movement. /// public Dropdown forwardSourceDropdown { get => m_ForwardSourceDropdown; set { UnsubscribeForwardSource(m_ForwardSourceDropdown); m_ForwardSourceDropdown = value; SubscribeForwardSource(m_ForwardSourceDropdown); RefreshMoveDependentInteractable(); } } [SerializeField] [Tooltip("Stores the Toggle used to enable or disable continuous turn locomotion.")] Toggle m_ContinuousTurnToggle; /// /// Stores the used to enable or disable continuous turn locomotion. /// public Toggle continuousTurnToggle { get => m_ContinuousTurnToggle; set { UnsubscribeContinuousTurn(m_ContinuousTurnToggle); m_ContinuousTurnToggle = value; SubscribeContinuousTurn(m_ContinuousTurnToggle); } } [SerializeField] [Tooltip("Stores the Slider used to set the turn speed of continuous turning.")] Slider m_TurnSpeedSlider; /// /// Stores the used to set the turn speed of continuous turning. /// public Slider turnSpeedSlider { get => m_TurnSpeedSlider; set { UnsubscribeTurnSpeed(m_TurnSpeedSlider); m_TurnSpeedSlider = value; SubscribeTurnSpeed(m_TurnSpeedSlider, m_TurnSpeedValueText); RefreshTurnDependentInteractable(); } } [SerializeField] [Tooltip("Stores the Text used to display the current turn speed of continuous turning.")] Text m_TurnSpeedValueText; /// /// Stores the used to display the current turn speed of continuous turning. /// public Text turnSpeedValueText { get => m_TurnSpeedValueText; set { UnsubscribeTurnSpeed(m_TurnSpeedSlider); m_TurnSpeedValueText = value; SubscribeTurnSpeed(m_TurnSpeedSlider, m_TurnSpeedValueText); } } [SerializeField] [Tooltip("Stores the Dropdown used to select the number of degrees to rotate for snap turning.")] Dropdown m_SnapTurnAmountDropdown; /// /// Stores the used to select the number of degrees to rotate for snap turning. /// public Dropdown snapTurnAmountDropdown { get => m_SnapTurnAmountDropdown; set { UnsubscribeSnapTurnAmount(m_SnapTurnAmountDropdown); m_SnapTurnAmountDropdown = value; SubscribeSnapTurnAmount(m_SnapTurnAmountDropdown); RefreshTurnDependentInteractable(); } } [SerializeField] [Tooltip("Stores the Toggle used to enable or disable 180° snap turns.")] Toggle m_EnableTurnAroundToggle; /// /// Stores the used to enable or disable 180° snap turns. /// public Toggle enableTurnAroundToggle { get => m_EnableTurnAroundToggle; set { UnsubscribeEnableTurnAround(m_EnableTurnAroundToggle); m_EnableTurnAroundToggle = value; SubscribeEnableTurnAround(m_EnableTurnAroundToggle); } } [SerializeField] [Tooltip("Stores the behavior that will be used to configure locomotion control schemes and configuration preferences.")] LocomotionSchemeManager m_Manager; /// /// Stores the behavior that will be used to configure locomotion control schemes and configuration preferences. /// public LocomotionSchemeManager manager { get => m_Manager; set { UnsubscribeAll(); m_Manager = value; if (m_Manager != null) { SubscribeAll(); RefreshInteractable(); } } } static readonly List k_SnapTurnAmounts = new List { 15f, 30f, 45f, 60f, 75f, 90f }; static readonly List k_SnapTurnAmountOptions = new List(k_SnapTurnAmounts.Count); static EnumDropdownCache s_GravityApplicationModeDropdownCache; static EnumDropdownCache s_ForwardSourceDropdownCache; protected void Awake() { // Initialize Dropdown options for Snap Turn Amount if (k_SnapTurnAmountOptions.Count != k_SnapTurnAmounts.Count) { foreach (var value in k_SnapTurnAmounts) { k_SnapTurnAmountOptions.Add(new Dropdown.OptionData($"{value}°")); } } // Initialize Dropdown options for Gravity Application Mode if (s_GravityApplicationModeDropdownCache == null) { s_GravityApplicationModeDropdownCache = new EnumDropdownCache(); } // Initialize Dropdown options for Forward Source if (s_ForwardSourceDropdownCache == null) { s_ForwardSourceDropdownCache = new EnumDropdownCache(); } } protected void OnEnable() { if (!ValidateManager()) return; SubscribeAll(); RefreshInteractable(); } protected void OnDisable() { UnsubscribeAll(); } void SubscribeAll() { SubscribeContinuousMove(m_ContinuousMoveToggle); SubscribeMoveSpeed(m_MoveSpeedSlider, m_MoveSpeedValueText); SubscribeEnableStrafe(m_EnableStrafeToggle); SubscribeUseGravity(m_UseGravityToggle); SubscribeGravityApplicationMode(m_GravityApplicationModeDropdown); SubscribeForwardSource(m_ForwardSourceDropdown); SubscribeContinuousTurn(m_ContinuousTurnToggle); SubscribeTurnSpeed(m_TurnSpeedSlider, m_TurnSpeedValueText); SubscribeSnapTurnAmount(m_SnapTurnAmountDropdown); SubscribeEnableTurnAround(m_EnableTurnAroundToggle); } void UnsubscribeAll() { UnsubscribeContinuousMove(m_ContinuousMoveToggle); UnsubscribeMoveSpeed(m_MoveSpeedSlider); UnsubscribeEnableStrafe(m_EnableStrafeToggle); UnsubscribeUseGravity(m_UseGravityToggle); UnsubscribeGravityApplicationMode(m_GravityApplicationModeDropdown); UnsubscribeForwardSource(m_ForwardSourceDropdown); UnsubscribeContinuousTurn(m_ContinuousTurnToggle); UnsubscribeTurnSpeed(m_TurnSpeedSlider); UnsubscribeSnapTurnAmount(m_SnapTurnAmountDropdown); UnsubscribeEnableTurnAround(m_EnableTurnAroundToggle); } /// /// Grey out input options that don't apply to the current control scheme. /// void RefreshInteractable() { if (!ValidateManager()) return; RefreshMoveDependentInteractable(); RefreshTurnDependentInteractable(); } void RefreshMoveDependentInteractable() { if (!ValidateManager()) return; var continuousMove = m_Manager.moveScheme == LocomotionSchemeManager.MoveScheme.Continuous; RefreshMoveDependentInteractable(continuousMove); } void RefreshMoveDependentInteractable(bool continuous) { if (m_MoveSpeedSlider != null) m_MoveSpeedSlider.interactable = continuous; if (m_EnableStrafeToggle != null) m_EnableStrafeToggle.interactable = continuous; if (m_UseGravityToggle != null) m_UseGravityToggle.interactable = continuous; if (m_GravityApplicationModeDropdown != null) m_GravityApplicationModeDropdown.interactable = continuous; if (m_ForwardSourceDropdown != null) m_ForwardSourceDropdown.interactable = continuous; } void RefreshTurnDependentInteractable() { if (!ValidateManager()) return; var continuousTurn = m_Manager.turnStyle == LocomotionSchemeManager.TurnStyle.Continuous; RefreshTurnDependentInteractable(continuousTurn); } void RefreshTurnDependentInteractable(bool continuous) { if (m_TurnSpeedSlider != null) m_TurnSpeedSlider.interactable = continuous; if (m_SnapTurnAmountDropdown != null) m_SnapTurnAmountDropdown.interactable = !continuous; } bool ValidateManager() { if (m_Manager == null) { Debug.LogError($"Reference to the {nameof(LocomotionSchemeManager)} is not set or the object has been destroyed," + " configuring locomotion settings from the menu will not be possible." + " Ensure the value has been set in the Inspector.", this); return false; } if (m_Manager.continuousMoveProvider == null) { Debug.LogError($"Reference to the {nameof(ContinuousMoveProviderBase)} is not set or the object has been destroyed," + " configuring locomotion settings from the menu will not be possible." + $" Ensure the value has been set in the Inspector on {m_Manager}.", this); return false; } if (m_Manager.continuousTurnProvider == null) { Debug.LogError($"Reference to the {nameof(ContinuousTurnProviderBase)} is not set or the object has been destroyed," + " configuring locomotion settings from the menu will not be possible." + $" Ensure the value has been set in the Inspector on {m_Manager}.", this); return false; } if (m_Manager.snapTurnProvider == null) { Debug.LogError($"Reference to the {nameof(SnapTurnProviderBase)} is not set or the object has been destroyed," + " configuring locomotion settings from the menu will not be possible." + $" Ensure the value has been set in the Inspector on {m_Manager}.", this); return false; } return true; } void SubscribeContinuousMove(Toggle toggle) { if (toggle == null) return; if (!ValidateManager()) return; var continuousMove = m_Manager.moveScheme == LocomotionSchemeManager.MoveScheme.Continuous; toggle.isOn = continuousMove; toggle.onValueChanged.AddListener(OnContinuousMoveToggleValueChanged); } void SubscribeMoveSpeed(Slider slider, Text valueText) { if (slider == null) return; if (!ValidateManager()) return; var currentMoveSpeed = m_Manager.continuousMoveProvider.moveSpeed; if (currentMoveSpeed < slider.minValue || currentMoveSpeed > slider.maxValue) { Debug.LogError($"Move speed {currentMoveSpeed} is outside [{slider.minValue}, {slider.maxValue}] range of slider."); } slider.value = currentMoveSpeed; if (valueText != null) { valueText.text = $"{currentMoveSpeed:F2}"; } slider.onValueChanged.AddListener(OnMoveSpeedSliderValueChanged); } void SubscribeEnableStrafe(Toggle toggle) { if (toggle == null) return; if (!ValidateManager()) return; toggle.isOn = m_Manager.continuousMoveProvider.enableStrafe; toggle.onValueChanged.AddListener(OnEnableStrafeToggleValueChanged); } void SubscribeUseGravity(Toggle toggle) { if (toggle == null) return; if (!ValidateManager()) return; toggle.isOn = m_Manager.continuousMoveProvider.useGravity; toggle.onValueChanged.AddListener(OnUseGravityToggleValueChanged); } void SubscribeGravityApplicationMode(Dropdown dropdown) { if (dropdown == null) return; if (!ValidateManager()) return; dropdown.options = s_GravityApplicationModeDropdownCache.options; dropdown.value = s_GravityApplicationModeDropdownCache.FindIndex(m_Manager.continuousMoveProvider.gravityApplicationMode); dropdown.onValueChanged.AddListener(OnGravityApplicationModeDropdownValueChanged); } void SubscribeForwardSource(Dropdown dropdown) { if (dropdown == null) return; if (!ValidateManager()) return; dropdown.options = s_ForwardSourceDropdownCache.options; dropdown.value = s_ForwardSourceDropdownCache.FindIndex(m_Manager.moveForwardSource); dropdown.onValueChanged.AddListener(OnForwardSourceDropdownValueChanged); } void SubscribeContinuousTurn(Toggle toggle) { if (toggle == null) return; if (!ValidateManager()) return; var continuousTurn = m_Manager.turnStyle == LocomotionSchemeManager.TurnStyle.Continuous; toggle.isOn = continuousTurn; toggle.onValueChanged.AddListener(OnContinuousTurnToggleValueChanged); } void SubscribeTurnSpeed(Slider slider, Text valueText) { if (slider == null) return; if (!ValidateManager()) return; var currentTurnSpeed = m_Manager.continuousTurnProvider.turnSpeed; if (currentTurnSpeed < slider.minValue || currentTurnSpeed > slider.maxValue) { Debug.LogError($"Turn speed {currentTurnSpeed} is outside [{slider.minValue}, {slider.maxValue}] range of slider."); } slider.value = currentTurnSpeed; if (valueText != null) { valueText.text = currentTurnSpeed.ToString(); } slider.onValueChanged.AddListener(OnTurnSpeedSliderValueChanged); } void SubscribeSnapTurnAmount(Dropdown dropdown) { if (dropdown == null) return; if (!ValidateManager()) return; dropdown.options = k_SnapTurnAmountOptions; var currentTurnAmount = m_Manager.snapTurnProvider.turnAmount; // Find the index of the current turn amount within the options list of the Dropdown var snapTurnIndex = -1; for (var index = 0; index < k_SnapTurnAmounts.Count; ++index) { var value = k_SnapTurnAmounts[index]; if (Mathf.Approximately(value, currentTurnAmount)) { snapTurnIndex = index; break; } } if (snapTurnIndex < 0) { Debug.LogError($"Turn amount {currentTurnAmount} is not contained within options list {{{string.Join(", ", k_SnapTurnAmounts)}}}", this); } else { dropdown.value = snapTurnIndex; } dropdown.onValueChanged.AddListener(OnSnapTurnAmountDropdownValueChanged); } void SubscribeEnableTurnAround(Toggle toggle) { if (toggle == null) return; if (!ValidateManager()) return; toggle.isOn = m_Manager.snapTurnProvider.enableTurnAround; toggle.onValueChanged.AddListener(OnEnableTurnAroundToggleValueChanged); } void UnsubscribeContinuousMove(Toggle toggle) { if (toggle != null) toggle.onValueChanged.RemoveListener(OnContinuousMoveToggleValueChanged); } void UnsubscribeMoveSpeed(Slider slider) { if (slider != null) slider.onValueChanged.RemoveListener(OnMoveSpeedSliderValueChanged); } void UnsubscribeEnableStrafe(Toggle toggle) { if (toggle != null) toggle.onValueChanged.RemoveListener(OnEnableStrafeToggleValueChanged); } void UnsubscribeUseGravity(Toggle toggle) { if (toggle != null) toggle.onValueChanged.RemoveListener(OnUseGravityToggleValueChanged); } void UnsubscribeGravityApplicationMode(Dropdown dropdown) { if (dropdown != null) dropdown.onValueChanged.RemoveListener(OnGravityApplicationModeDropdownValueChanged); } void UnsubscribeForwardSource(Dropdown dropdown) { if (dropdown != null) dropdown.onValueChanged.RemoveListener(OnForwardSourceDropdownValueChanged); } void UnsubscribeContinuousTurn(Toggle toggle) { if (toggle != null) toggle.onValueChanged.RemoveListener(OnContinuousTurnToggleValueChanged); } void UnsubscribeTurnSpeed(Slider slider) { if (slider != null) slider.onValueChanged.RemoveListener(OnTurnSpeedSliderValueChanged); } void UnsubscribeSnapTurnAmount(Dropdown dropdown) { if (dropdown != null) dropdown.onValueChanged.RemoveListener(OnSnapTurnAmountDropdownValueChanged); } void UnsubscribeEnableTurnAround(Toggle toggle) { if (toggle != null) toggle.onValueChanged.RemoveListener(OnEnableTurnAroundToggleValueChanged); } void OnContinuousMoveToggleValueChanged(bool value) { m_Manager.moveScheme = value ? LocomotionSchemeManager.MoveScheme.Continuous : LocomotionSchemeManager.MoveScheme.Noncontinuous; RefreshMoveDependentInteractable(value); } void OnMoveSpeedSliderValueChanged(float value) { m_Manager.continuousMoveProvider.moveSpeed = value; m_MoveSpeedValueText.text = $"{value:F2}"; } void OnEnableStrafeToggleValueChanged(bool value) { m_Manager.continuousMoveProvider.enableStrafe = value; } void OnUseGravityToggleValueChanged(bool value) { m_Manager.continuousMoveProvider.useGravity = value; } void OnGravityApplicationModeDropdownValueChanged(int index) { m_Manager.continuousMoveProvider.gravityApplicationMode = s_GravityApplicationModeDropdownCache.GetValue(index); } void OnForwardSourceDropdownValueChanged(int index) { m_Manager.moveForwardSource = s_ForwardSourceDropdownCache.GetValue(index); } void OnContinuousTurnToggleValueChanged(bool value) { m_Manager.turnStyle = value ? LocomotionSchemeManager.TurnStyle.Continuous : LocomotionSchemeManager.TurnStyle.Snap; RefreshTurnDependentInteractable(value); } void OnTurnSpeedSliderValueChanged(float value) { m_Manager.continuousTurnProvider.turnSpeed = value; m_TurnSpeedValueText.text = value.ToString(); } void OnSnapTurnAmountDropdownValueChanged(int index) { var turnAmount = k_SnapTurnAmounts[index]; m_Manager.snapTurnProvider.turnAmount = turnAmount; } void OnEnableTurnAroundToggleValueChanged(bool value) { m_Manager.snapTurnProvider.enableTurnAround = value; } } }