using System.Collections; using System.Collections.Generic; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.XR.Interaction.Toolkit.UI; namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets { /// /// Use this class to mediate the controllers and their associated interactors and input actions under different interaction states. /// [AddComponentMenu("XR/Action Based Controller Manager")] [DefaultExecutionOrder(k_UpdateOrder)] public class ActionBasedControllerManager : MonoBehaviour { /// /// Order when instances of type are updated. /// /// /// Executes before controller components to ensure input processors can be attached /// to input actions and/or bindings before the controller component reads the current /// values of the input actions. /// public const int k_UpdateOrder = XRInteractionUpdateOrder.k_Controllers - 1; [Space] [Header("Interactors")] [SerializeField] [Tooltip("The GameObject containing the interaction group used for direct and distant manipulation.")] XRInteractionGroup m_ManipulationInteractionGroup; [SerializeField] [Tooltip("The GameObject containing the interactor used for direct manipulation.")] XRDirectInteractor m_DirectInteractor; [SerializeField] [Tooltip("The GameObject containing the interactor used for distant/ray manipulation.")] XRRayInteractor m_RayInteractor; [SerializeField] [Tooltip("The GameObject containing the interactor used for teleportation.")] XRRayInteractor m_TeleportInteractor; [Space] [Header("Controller Actions")] [SerializeField] [Tooltip("The reference to the action to start the teleport aiming mode for this controller.")] InputActionReference m_TeleportModeActivate; [SerializeField] [Tooltip("The reference to the action to cancel the teleport aiming mode for this controller.")] InputActionReference m_TeleportModeCancel; [SerializeField] [Tooltip("The reference to the action of continuous turning the XR Origin with this controller.")] InputActionReference m_Turn; [SerializeField] [Tooltip("The reference to the action of snap turning the XR Origin with this controller.")] InputActionReference m_SnapTurn; [SerializeField] [Tooltip("The reference to the action of moving the XR Origin with this controller.")] InputActionReference m_Move; [SerializeField] [Tooltip("The reference to the action of scrolling UI with this controller.")] InputActionReference m_UIScroll; [Space] [Header("Locomotion Settings")] [SerializeField] [Tooltip("If true, continuous movement will be enabled. If false, teleport will enabled.")] bool m_SmoothMotionEnabled; [SerializeField] [Tooltip("If true, continuous turn will be enabled. If false, snap turn will be enabled. Note: If smooth motion is enabled and enable strafe is enabled on the continuous move provider, turn will be overriden in favor of strafe.")] bool m_SmoothTurnEnabled; [Space] [Header("UI Settings")] [SerializeField] [Tooltip("If true, UI scrolling will be enabled.")] bool m_UIScrollingEnabled; [Space] [Header("Mediation Events")] [SerializeField] [Tooltip("Event fired when the active ray interactor changes between interaction and teleport.")] UnityEvent m_RayInteractorChanged; public bool smoothMotionEnabled { get => m_SmoothMotionEnabled; set { m_SmoothMotionEnabled = value; UpdateLocomotionActions(); } } public bool smoothTurnEnabled { get => m_SmoothTurnEnabled; set { m_SmoothTurnEnabled = value; UpdateLocomotionActions(); } } public bool uiScrollingEnabled { get => m_UIScrollingEnabled; set { m_UIScrollingEnabled = value; UpdateUIActions(); } } bool m_PostponedDeactivateTeleport; bool m_UIScrollModeActive = false; const int k_InteractorNotInGroup = -1; IEnumerator m_AfterInteractionEventsRoutine; HashSet m_LocomotionUsers = new HashSet(); /// /// Temporary scratch list to populate with the group members of the interaction group. /// static readonly List s_GroupMembers = new List(); // For our input mediation, we are enforcing a few rules between direct, ray, and teleportation interaction: // 1. If the Teleportation Ray is engaged, the Ray interactor is disabled // 2. The interaction group ensures that the Direct and Ray interactors cannot interact at the same time, with the Direct interactor taking priority // 3. If the Ray interactor is selecting, all locomotion controls are disabled (teleport ray, move, and turn controls) to prevent input collision void SetupInteractorEvents() { if (m_RayInteractor != null) { m_RayInteractor.selectEntered.AddListener(OnRaySelectEntered); m_RayInteractor.selectExited.AddListener(OnRaySelectExited); m_RayInteractor.uiHoverEntered.AddListener(OnUIHoverEntered); m_RayInteractor.uiHoverExited.AddListener(OnUIHoverExited); } var teleportModeActivateAction = GetInputAction(m_TeleportModeActivate); if (teleportModeActivateAction != null) { teleportModeActivateAction.performed += OnStartTeleport; teleportModeActivateAction.performed += OnStartLocomotion; teleportModeActivateAction.canceled += OnCancelTeleport; teleportModeActivateAction.canceled += OnStopLocomotion; } var teleportModeCancelAction = GetInputAction(m_TeleportModeCancel); if (teleportModeCancelAction != null) { teleportModeCancelAction.performed += OnCancelTeleport; teleportModeActivateAction.canceled += OnStopLocomotion; } var moveAction = GetInputAction(m_Move); if (moveAction != null) { moveAction.performed += OnStartLocomotion; moveAction.canceled += OnStopLocomotion; } var turnAction = GetInputAction(m_Turn); if (turnAction != null) { turnAction.performed += OnStartLocomotion; turnAction.canceled += OnStopLocomotion; } } void TeardownInteractorEvents() { if (m_RayInteractor != null) { m_RayInteractor.selectEntered.RemoveListener(OnRaySelectEntered); m_RayInteractor.selectExited.RemoveListener(OnRaySelectExited); } var teleportModeActivateAction = GetInputAction(m_TeleportModeActivate); if (teleportModeActivateAction != null) { teleportModeActivateAction.performed -= OnStartTeleport; teleportModeActivateAction.performed -= OnStartLocomotion; teleportModeActivateAction.canceled -= OnCancelTeleport; teleportModeActivateAction.canceled -= OnStopLocomotion; } var teleportModeCancelAction = GetInputAction(m_TeleportModeCancel); if (teleportModeCancelAction != null) { teleportModeCancelAction.performed -= OnCancelTeleport; teleportModeCancelAction.performed -= OnStopLocomotion; } var moveAction = GetInputAction(m_Move); if (moveAction != null) { moveAction.performed -= OnStartLocomotion; moveAction.canceled -= OnStopLocomotion; } var turnAction = GetInputAction(m_Turn); if (turnAction != null) { turnAction.performed -= OnStartLocomotion; turnAction.canceled -= OnStopLocomotion; } } void OnStartTeleport(InputAction.CallbackContext context) { m_PostponedDeactivateTeleport = false; if (m_TeleportInteractor != null) m_TeleportInteractor.gameObject.SetActive(true); if (m_RayInteractor != null) m_RayInteractor.gameObject.SetActive(false); m_RayInteractorChanged?.Invoke(m_TeleportInteractor); } void OnCancelTeleport(InputAction.CallbackContext context) { // Do not deactivate the teleport interactor in this callback. // We delay turning off the teleport interactor in this callback so that // the teleport interactor has a chance to complete the teleport if needed. // OnAfterInteractionEvents will handle deactivating its GameObject. m_PostponedDeactivateTeleport = true; if (m_RayInteractor != null) m_RayInteractor.gameObject.SetActive(true); m_RayInteractorChanged?.Invoke(m_RayInteractor); } void OnStartLocomotion(InputAction.CallbackContext context) { if (!context.started) return; m_LocomotionUsers.Add(context.action); } void OnStopLocomotion(InputAction.CallbackContext context) { m_LocomotionUsers.Remove(context.action); if (m_LocomotionUsers.Count == 0 && m_UIScrollModeActive) { DisableLocomotionActions(); } } void OnRaySelectEntered(SelectEnterEventArgs args) { // Disable locomotion and turn actions DisableLocomotionActions(); } void OnRaySelectExited(SelectExitEventArgs args) { // Re-enable the locomotion and turn actions UpdateLocomotionActions(); } void OnUIHoverEntered(UIHoverEventArgs args) { m_UIScrollModeActive = args.deviceModel.isScrollable && m_UIScrollingEnabled; if (!m_UIScrollModeActive) return; // If locomotion is occurring, wait if (m_LocomotionUsers.Count == 0) { // Disable locomotion and turn actions DisableLocomotionActions(); } } void OnUIHoverExited(UIHoverEventArgs args) { m_UIScrollModeActive = false; // Re-enable the locomotion and turn actions UpdateLocomotionActions(); } protected void Awake() { m_AfterInteractionEventsRoutine = OnAfterInteractionEvents(); } protected void OnEnable() { if (m_TeleportInteractor != null) m_TeleportInteractor.gameObject.SetActive(false); SetupInteractorEvents(); // Start the coroutine that executes code after the Update phase (during yield null). // Since this behavior has an execution order that runs before the XRInteractionManager, // we use the coroutine to run after the selection events StartCoroutine(m_AfterInteractionEventsRoutine); } protected void OnDisable() { TeardownInteractorEvents(); StopCoroutine(m_AfterInteractionEventsRoutine); } protected void Start() { // Ensure the enabled state of locomotion and turn actions are properly set up. // Called in Start so it is done after the InputActionManager enables all input actions earlier in OnEnable. UpdateLocomotionActions(); UpdateUIActions(); if (m_ManipulationInteractionGroup == null) { Debug.LogError("Missing required Manipulation Interaction Group reference. Use the Inspector window to assign the XR Interaction Group component reference.", this); return; } // Ensure interactors are properly set up in the interaction group by adding // them if necessary and ordering Direct before Ray interactor. var directInteractorIndex = k_InteractorNotInGroup; var rayInteractorIndex = k_InteractorNotInGroup; m_ManipulationInteractionGroup.GetGroupMembers(s_GroupMembers); for (var i = 0; i < s_GroupMembers.Count; ++i) { var groupMember = s_GroupMembers[i]; if (ReferenceEquals(groupMember, m_DirectInteractor)) directInteractorIndex = i; else if (ReferenceEquals(groupMember, m_RayInteractor)) rayInteractorIndex = i; } if (directInteractorIndex == k_InteractorNotInGroup) { // Must add Direct interactor to group, and make sure it is ordered before the Ray interactor if (rayInteractorIndex == k_InteractorNotInGroup) { // Must add Ray interactor to group if (m_DirectInteractor != null) m_ManipulationInteractionGroup.AddGroupMember(m_DirectInteractor); if (m_RayInteractor != null) m_ManipulationInteractionGroup.AddGroupMember(m_RayInteractor); } else if (m_DirectInteractor != null) { m_ManipulationInteractionGroup.MoveGroupMemberTo(m_DirectInteractor, rayInteractorIndex); } } else { if (rayInteractorIndex == k_InteractorNotInGroup) { // Must add Ray interactor to group if (m_RayInteractor != null) m_ManipulationInteractionGroup.AddGroupMember(m_RayInteractor); } else { // Must make sure Direct interactor is ordered before the Ray interactor if (rayInteractorIndex < directInteractorIndex) { m_ManipulationInteractionGroup.MoveGroupMemberTo(m_DirectInteractor, rayInteractorIndex); } } } } IEnumerator OnAfterInteractionEvents() { while (true) { // Yield so this coroutine is resumed after the teleport interactor // has a chance to process its select interaction event during Update. yield return null; if (m_PostponedDeactivateTeleport) { if (m_TeleportInteractor != null) m_TeleportInteractor.gameObject.SetActive(false); m_PostponedDeactivateTeleport = false; } } } void UpdateLocomotionActions() { // Disable/enable Teleport and Turn when Move is enabled/disabled. SetEnabled(m_Move, m_SmoothMotionEnabled); SetEnabled(m_TeleportModeActivate, !m_SmoothMotionEnabled); SetEnabled(m_TeleportModeCancel, !m_SmoothMotionEnabled); // Disable ability to turn when using continuous movement SetEnabled(m_Turn, !m_SmoothMotionEnabled && m_SmoothTurnEnabled); SetEnabled(m_SnapTurn, !m_SmoothMotionEnabled && !m_SmoothTurnEnabled); } void DisableLocomotionActions() { DisableAction(m_Move); DisableAction(m_TeleportModeActivate); DisableAction(m_TeleportModeCancel); DisableAction(m_Turn); DisableAction(m_SnapTurn); } void UpdateUIActions() { SetEnabled(m_UIScroll, m_UIScrollingEnabled); } static void SetEnabled(InputActionReference actionReference, bool enabled) { if (enabled) EnableAction(actionReference); else DisableAction(actionReference); } static void EnableAction(InputActionReference actionReference) { var action = GetInputAction(actionReference); if (action != null && !action.enabled) action.Enable(); } static void DisableAction(InputActionReference actionReference) { var action = GetInputAction(actionReference); if (action != null && action.enabled) action.Disable(); } static InputAction GetInputAction(InputActionReference actionReference) { #pragma warning disable IDE0031 // Use null propagation -- Do not use for UnityEngine.Object types return actionReference != null ? actionReference.action : null; #pragma warning restore IDE0031 } } }