diff options
author | pryazha <pryadeiniv@mail.ru> | 2025-07-02 08:46:23 -0700 |
---|---|---|
committer | pryazha <pryadeiniv@mail.ru> | 2025-07-02 08:46:23 -0700 |
commit | 8263edd59284aba390aca011d25b79efecef4c48 (patch) | |
tree | 6346e2afaaabd32156601cafaf20d4ee813befaf /Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerInputActionManager.cs |
Diffstat (limited to 'Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerInputActionManager.cs')
-rw-r--r-- | Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerInputActionManager.cs | 530 |
1 files changed, 530 insertions, 0 deletions
diff --git a/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerInputActionManager.cs b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerInputActionManager.cs new file mode 100644 index 0000000..84a0c0f --- /dev/null +++ b/Assets/Samples/XR Interaction Toolkit/3.1.2/Starter Assets/Scripts/ControllerInputActionManager.cs @@ -0,0 +1,530 @@ +using System.Collections.Generic; +using Unity.XR.CoreUtils.Bindings; +using UnityEngine.Events; +using UnityEngine.InputSystem; +using UnityEngine.Serialization; +using UnityEngine.XR.Interaction.Toolkit.Attachment; +using UnityEngine.XR.Interaction.Toolkit.Inputs.Readers; +using UnityEngine.XR.Interaction.Toolkit.Interactors; +using UnityEngine.XR.Interaction.Toolkit.UI; + +namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets +{ + /// <summary> + /// Use this class to mediate the interactors for a controller under different interaction states + /// and the input actions used by them. + /// </summary> + /// <remarks> + /// If the teleport ray input is engaged, the Ray Interactor used for distant manipulation is disabled + /// and the Ray Interactor used for teleportation is enabled. If the Ray Interactor is selecting and it + /// is configured to allow for attach transform manipulation, all locomotion input actions are disabled + /// (teleport ray, move, and turn controls) to prevent input collision with the manipulation inputs used + /// by the ray interactor. + /// <br /> + /// A typical hierarchy also includes an XR Interaction Group component to mediate between interactors. + /// The interaction group ensures that the Direct and Ray Interactors cannot interact at the same time, + /// with the Direct Interactor taking priority over the Ray Interactor. + /// </remarks> + [AddComponentMenu("XR/Controller Input Action Manager")] + public class ControllerInputActionManager : MonoBehaviour + { + [Space] + [Header("Interactors")] + + [SerializeField] + [Tooltip("The interactor used for distant/ray manipulation. Use this or Near-Far Interactor, not both.")] + XRRayInteractor m_RayInteractor; + + [SerializeField] + [Tooltip("Near-Far Interactor used for distant/ray manipulation. Use this or Ray Interactor, not both.")] + NearFarInteractor m_NearFarInteractor; + + [SerializeField] + [Tooltip("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.")] + [FormerlySerializedAs("m_TeleportModeActivate")] + InputActionReference m_TeleportMode; + + [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 be 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; + + [SerializeField] + [Tooltip("With the Near-Far Interactor, if true, teleport will be enabled during near interaction. If false, teleport will be disabled during near interaction.")] + bool m_NearFarEnableTeleportDuringNearInteraction = true; + + [Space] + [Header("UI Settings")] + + [SerializeField] + [Tooltip("If true, UI scrolling will be enabled. Locomotion will be disabled when pointing at UI to allow it to be scrolled.")] + bool m_UIScrollingEnabled = true; + + [Space] + [Header("Mediation Events")] + + [SerializeField] + [Tooltip("Event fired when the active ray interactor changes between interaction and teleport.")] + UnityEvent<IXRRayProvider> 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_StartCalled; + bool m_PostponedDeactivateTeleport; + bool m_PostponedNearRegionLocomotion; + bool m_HoveringScrollableUI; + + readonly HashSet<InputAction> m_LocomotionUsers = new HashSet<InputAction>(); + readonly BindingsGroup m_BindingsGroup = new BindingsGroup(); + + void SetupInteractorEvents() + { + if (m_NearFarInteractor != null) + { + m_NearFarInteractor.uiHoverEntered.AddListener(OnUIHoverEntered); + m_NearFarInteractor.uiHoverExited.AddListener(OnUIHoverExited); + m_BindingsGroup.AddBinding(m_NearFarInteractor.selectionRegion.Subscribe(OnNearFarSelectionRegionChanged)); + } + + 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 teleportModeAction = GetInputAction(m_TeleportMode); + if (teleportModeAction != null) + { + teleportModeAction.performed += OnStartTeleport; + teleportModeAction.performed += OnStartLocomotion; + teleportModeAction.canceled += OnCancelTeleport; + teleportModeAction.canceled += OnStopLocomotion; + } + + var teleportModeCancelAction = GetInputAction(m_TeleportModeCancel); + if (teleportModeCancelAction != null) + { + teleportModeCancelAction.performed += OnCancelTeleport; + } + + var moveAction = GetInputAction(m_Move); + if (moveAction != null) + { + moveAction.started += OnStartLocomotion; + moveAction.canceled += OnStopLocomotion; + } + + var turnAction = GetInputAction(m_Turn); + if (turnAction != null) + { + turnAction.started += OnStartLocomotion; + turnAction.canceled += OnStopLocomotion; + } + + var snapTurnAction = GetInputAction(m_SnapTurn); + if (snapTurnAction != null) + { + snapTurnAction.started += OnStartLocomotion; + snapTurnAction.canceled += OnStopLocomotion; + } + } + + void TeardownInteractorEvents() + { + m_BindingsGroup.Clear(); + + if (m_NearFarInteractor != null) + { + m_NearFarInteractor.uiHoverEntered.RemoveListener(OnUIHoverEntered); + m_NearFarInteractor.uiHoverExited.RemoveListener(OnUIHoverExited); + } + + if (m_RayInteractor != null) + { + m_RayInteractor.selectEntered.RemoveListener(OnRaySelectEntered); + m_RayInteractor.selectExited.RemoveListener(OnRaySelectExited); + m_RayInteractor.uiHoverEntered.RemoveListener(OnUIHoverEntered); + m_RayInteractor.uiHoverExited.RemoveListener(OnUIHoverExited); + } + + var teleportModeAction = GetInputAction(m_TeleportMode); + if (teleportModeAction != null) + { + teleportModeAction.performed -= OnStartTeleport; + teleportModeAction.performed -= OnStartLocomotion; + teleportModeAction.canceled -= OnCancelTeleport; + teleportModeAction.canceled -= OnStopLocomotion; + } + + var teleportModeCancelAction = GetInputAction(m_TeleportModeCancel); + if (teleportModeCancelAction != null) + { + teleportModeCancelAction.performed -= OnCancelTeleport; + } + + var moveAction = GetInputAction(m_Move); + if (moveAction != null) + { + moveAction.started -= OnStartLocomotion; + moveAction.canceled -= OnStopLocomotion; + } + + var turnAction = GetInputAction(m_Turn); + if (turnAction != null) + { + turnAction.started -= OnStartLocomotion; + turnAction.canceled -= OnStopLocomotion; + } + + var snapTurnAction = GetInputAction(m_SnapTurn); + if (snapTurnAction != null) + { + snapTurnAction.started -= OnStartLocomotion; + snapTurnAction.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); + + if (m_NearFarInteractor != null && m_NearFarInteractor.selectionRegion.Value != NearFarInteractor.Region.Near) + m_NearFarInteractor.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); + + if (m_NearFarInteractor != null) + m_NearFarInteractor.gameObject.SetActive(true); + + m_RayInteractorChanged?.Invoke(m_RayInteractor); + } + + void OnStartLocomotion(InputAction.CallbackContext context) + { + m_LocomotionUsers.Add(context.action); + } + + void OnStopLocomotion(InputAction.CallbackContext context) + { + m_LocomotionUsers.Remove(context.action); + + if (m_LocomotionUsers.Count == 0 && m_HoveringScrollableUI) + { + DisableAllLocomotionActions(); + UpdateUIActions(); + } + } + + void OnNearFarSelectionRegionChanged(NearFarInteractor.Region selectionRegion) + { + m_PostponedNearRegionLocomotion = false; + + if (selectionRegion == NearFarInteractor.Region.None) + { + UpdateLocomotionActions(); + return; + } + + var manipulateAttachTransform = false; + var attachController = m_NearFarInteractor.interactionAttachController as InteractionAttachController; + if (attachController != null) + { + manipulateAttachTransform = attachController.useManipulationInput && + (attachController.manipulationInput.inputSourceMode == XRInputValueReader.InputSourceMode.InputActionReference && attachController.manipulationInput.inputActionReference != null) || + (attachController.manipulationInput.inputSourceMode != XRInputValueReader.InputSourceMode.InputActionReference && attachController.manipulationInput.inputSourceMode != XRInputValueReader.InputSourceMode.Unused); + } + + if (selectionRegion == NearFarInteractor.Region.Far) + { + if (manipulateAttachTransform) + DisableAllLocomotionActions(); + else + DisableTeleportActions(); + } + else if (selectionRegion == NearFarInteractor.Region.Near) + { + // Determine if the user entered the near region due to pulling back on the thumbstick. + // If so, postpone enabling locomotion until the user releases the thumbstick + // in order to avoid an immediate snap turn around from triggering on region change. + var hasStickInput = manipulateAttachTransform && HasStickInput(attachController); + if (hasStickInput) + { + m_PostponedNearRegionLocomotion = true; + DisableAllLocomotionActions(); + } + else + { + UpdateLocomotionActions(); + if (!m_NearFarEnableTeleportDuringNearInteraction) + DisableTeleportActions(); + } + } + } + + void OnRaySelectEntered(SelectEnterEventArgs args) + { + if (m_RayInteractor.manipulateAttachTransform) + { + // Disable locomotion and turn actions + DisableAllLocomotionActions(); + } + } + + void OnRaySelectExited(SelectExitEventArgs args) + { + if (m_RayInteractor.manipulateAttachTransform) + { + // Re-enable the locomotion and turn actions + UpdateLocomotionActions(); + } + } + + void OnUIHoverEntered(UIHoverEventArgs args) + { + m_HoveringScrollableUI = m_UIScrollingEnabled && args.deviceModel.isScrollable; + UpdateUIActions(); + + // If locomotion is occurring, wait + if (m_HoveringScrollableUI && m_LocomotionUsers.Count == 0) + { + // Disable locomotion and turn actions + DisableAllLocomotionActions(); + } + } + + void OnUIHoverExited(UIHoverEventArgs args) + { + m_HoveringScrollableUI = false; + UpdateUIActions(); + + // Re-enable the locomotion and turn actions + UpdateLocomotionActions(); + } + + protected void OnEnable() + { + if (m_RayInteractor != null && m_NearFarInteractor != null) + { + Debug.LogWarning("Both Ray Interactor and Near-Far Interactor are assigned. Only one should be assigned, not both. Clearing Ray Interactor.", this); + m_RayInteractor = null; + } + + if (m_TeleportInteractor != null) + m_TeleportInteractor.gameObject.SetActive(false); + + // Allow the actions to be refreshed when this component is re-enabled. + // See comments in Start for why we wait until Start to enable/disable actions. + if (m_StartCalled) + { + UpdateLocomotionActions(); + UpdateUIActions(); + } + + SetupInteractorEvents(); + } + + protected void OnDisable() + { + TeardownInteractorEvents(); + } + + protected void Start() + { + m_StartCalled = true; + + // 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(); + } + + protected void Update() + { + // Since this behavior has the default execution order, it runs after the XRInteractionManager, + // so selection events have been finished by now this frame. This means that the teleport interactor + // has had a chance to process its select interaction event and teleport if needed. + if (m_PostponedDeactivateTeleport) + { + if (m_TeleportInteractor != null) + m_TeleportInteractor.gameObject.SetActive(false); + + m_PostponedDeactivateTeleport = false; + } + + // If stick input caused the near region to be entered, + // wait until the stick is released before enabling locomotion. + if (m_PostponedNearRegionLocomotion) + { + var hasStickInput = false; + if (m_NearFarInteractor != null && + m_NearFarInteractor.interactionAttachController is InteractionAttachController attachController + && attachController != null) + { + hasStickInput = HasStickInput(attachController); + } + + if (!hasStickInput) + { + m_PostponedNearRegionLocomotion = false; + + UpdateLocomotionActions(); + if (!m_NearFarEnableTeleportDuringNearInteraction) + DisableTeleportActions(); + } + } + } + + void UpdateLocomotionActions() + { + // Disable/enable Teleport and Turn when Move is enabled/disabled. + SetEnabled(m_Move, m_SmoothMotionEnabled); + SetEnabled(m_TeleportMode, !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 DisableTeleportActions() + { + DisableAction(m_TeleportMode); + DisableAction(m_TeleportModeCancel); + } + + void DisableMoveAndTurnActions() + { + DisableAction(m_Move); + DisableAction(m_Turn); + DisableAction(m_SnapTurn); + } + + void DisableAllLocomotionActions() + { + DisableTeleportActions(); + DisableMoveAndTurnActions(); + } + + void UpdateUIActions() + { + SetEnabled(m_UIScroll, m_UIScrollingEnabled && m_HoveringScrollableUI && m_LocomotionUsers.Count == 0); + } + + static bool HasStickInput(InteractionAttachController attachController) + { + // 75% of default 0.5 press threshold + const float sqrStickReleaseThreshold = 0.375f * 0.375f; + + return attachController.manipulationInput.TryReadValue(out var stickInput) && + stickInput.sqrMagnitude > sqrStickReleaseThreshold; + } + + static void SetEnabled(InputActionReference actionReference, bool enabled) + { + if (enabled) + EnableAction(actionReference); + else + DisableAction(actionReference); + } + + static void EnableAction(InputActionReference actionReference) + { + var action = GetInputAction(actionReference); + action?.Enable(); + } + + static void DisableAction(InputActionReference actionReference) + { + var action = GetInputAction(actionReference); + 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 + } + } +} |