OpenXR开发实战项目之XR_Interactable_Main_Examples

一、框架视图

二、关键代码

XRInteractableAffordanceStateProvider

using System;
using System.Collections;
using Unity.XR.CoreUtils;
using Unity.XR.CoreUtils.Datums;
using UnityEngine.XR.Interaction.Toolkit.Utilities.Internal;


    /// <summary>
    /// State Machine component that derives an interaction affordance state from an associated <see cref="IXRInteractable"/>.
    /// </summary>
    [AddComponentMenu("Affordance System/XR Interactable Affordance State Provider", 11)]
    [HelpURL(XRHelpURLConstants.k_XRInteractableAffordanceStateProvider)]
    [DisallowMultipleComponent]
    public class XRInteractableAffordanceStateProvider : BaseAffordanceStateProvider
    {
        /// <summary>
        /// Animation mode options used for select state callbacks.
        /// </summary>
        public enum SelectClickAnimationMode
        {
            /// <summary>
            /// No click animation override for select events.
            /// </summary>
            None,

            /// <summary>
            /// Use click animation on select entered event.
            /// </summary>
            SelectEntered,


            /// <summary>
            /// Use click animation on select exited event.
            /// </summary>
            SelectExited,
        }

        /// <summary>
        /// Animation mode options used for activate state callbacks.
        /// </summary>
        public enum ActivateClickAnimationMode
        {
            /// <summary>
            /// No click animation override for activate events.
            /// </summary>
            None,

            /// <summary>
            /// Use click animation on activate event.
            /// </summary>
            Activated,


            /// <summary>
            /// Use click animation on deactivate event.
            /// </summary>
            Deactivated,
        }

        [SerializeField]
        [RequireInterface(typeof(IXRInteractable))]
        [Tooltip("The interactable component that drives the affordance states. If null, Unity will try and find an interactable component attached.")]
        Object m_InteractableSource;

        /// <summary>
        /// The interactable component that drives the affordance states.
        /// If <see langword="null"/>, Unity will try and find an interactable component attached.
        /// </summary>
        public Object interactableSource
        {
            get => m_InteractableSource;
            set
            {
                m_InteractableSource = value;
                if (Application.isPlaying && isActiveAndEnabled)
                    SetBoundInteractionReceiver(value as IXRInteractable);
            }
        }

        [Header("Event Constraints")]
        [SerializeField]
        [Tooltip("When hover events are registered and this is true, the state will fallback to idle or disabled.")]
        bool m_IgnoreHoverEvents;

        /// <summary>
        /// When hover events are registered and this is true, the state will fallback to idle or disabled.
        /// </summary>
        public bool ignoreHoverEvents
        {
            get => m_IgnoreHoverEvents;
            set => m_IgnoreHoverEvents = value;
        }

        [SerializeField]
        [Tooltip("When this is true, the state will fallback to hover if the later is not ignored. When this is false, this provider will check " +
                 "if the Interactable Source has priority for selection when hovered, and update its state accordingly.")]
        bool m_IgnoreHoverPriorityEvents = true;

        /// <summary>
        /// When hover events are registered and this is true, the state will fallback to hover. When this is <see langword="false"/>, this
        /// provider will check if the Interactable Source has priority for selection when hovered, and update its state accordingly
        /// </summary>
        /// <remarks>When updating this value to <see langword="false"/> during runtime, previously hover events are ignored.</remarks>
        public bool ignoreHoverPriorityEvents
        {
            get => m_IgnoreHoverPriorityEvents;
            set
            {
                if (Application.isPlaying && isActiveAndEnabled && !m_IgnoreHoverPriorityEvents && value)
                {
                    StopHoveredPriorityRoutine();
                    RefreshState();
                }

                m_IgnoreHoverPriorityEvents = value;
            }
        }

        [SerializeField]
        [Tooltip("When select events are registered and this is true, the state will fallback to idle or disabled. " +
                 "Note this will not affect click animations which can be disabled separately.")]
        bool m_IgnoreSelectEvents;

        /// <summary>
        /// When select events are registered and this is true, the state will fallback to idle or disabled.
        /// </summary>
        public bool ignoreSelectEvents
        {
            get => m_IgnoreSelectEvents;
            set => m_IgnoreSelectEvents = value;
        }

        [SerializeField]
        [Tooltip("When activate events are registered and this is true, the state will fallback to idle or disabled." +
                 "Note this will not affect click animations which can be disabled separately.")]
        bool m_IgnoreActivateEvents;

        /// <summary>
        /// When activate events are registered and this is true, the state will fallback to idle or disabled.
        /// </summary>
        public bool ignoreActivateEvents
        {
            get => m_IgnoreActivateEvents;
            set => m_IgnoreActivateEvents = value;
        }

        [Header("Click Animation Config")]
        [SerializeField]
        [Tooltip("Condition to trigger click animation for Selected interaction events.")]
        SelectClickAnimationMode m_SelectClickAnimationMode = SelectClickAnimationMode.SelectEntered;

        /// <summary>
        /// Condition to trigger click animation for Selected interaction events.
        /// </summary>
        public SelectClickAnimationMode selectClickAnimationMode
        {
            get => m_SelectClickAnimationMode;
            set => m_SelectClickAnimationMode = value;
        }

        [SerializeField]
        [Tooltip("Condition to trigger click animation for activated interaction events.")]
        ActivateClickAnimationMode m_ActivateClickAnimationMode = ActivateClickAnimationMode.None;

        /// <summary>
        /// Condition to trigger click animation for activated interaction events.
        /// </summary>
        public ActivateClickAnimationMode activateClickAnimationMode
        {
            get => m_ActivateClickAnimationMode;
            set => m_ActivateClickAnimationMode = value;
        }

        [SerializeField]
        [Range(0f, 1f)]
        [Tooltip("Duration of click animations for selected and activated events.")]
        float m_ClickAnimationDuration = 0.25f;

        /// <summary>
        /// Duration of click animations for selected and activated events.
        /// </summary>
        public float clickAnimationDuration
        {
            get => m_ClickAnimationDuration;
            set => m_ClickAnimationDuration = value;
        }

        [SerializeField]
        [Tooltip("Animation curve reference for click animation events. Select the More menu (\u22ee) to choose between a direct reference and a reusable scriptable object animation curve datum.")]
        AnimationCurveDatumProperty m_ClickAnimationCurve = new AnimationCurveDatumProperty(AnimationCurve.EaseInOut(0f, 0f, 1f, 1f));

        /// <summary>
        /// Animation curve reference for click animation events.
        /// </summary>
        public AnimationCurveDatumProperty clickAnimationCurve
        {
            get => m_ClickAnimationCurve;
            set => m_ClickAnimationCurve = value;
        }

        /// <summary>
        /// Is attached interactable in a hovered state.
        /// </summary>
        protected virtual bool isHovered => m_HoverInteractable != null && m_HoverInteractable.isHovered;

        /// <summary>
        /// Is attached interactable in a selected state.
        /// </summary>
        protected virtual bool isSelected => m_SelectInteractable != null && m_SelectInteractable.isSelected;

        /// <summary>
        /// Is attached interactable in an activated state.
        /// </summary>
        protected virtual bool isActivated => m_IsActivated;

        /// <summary>
        /// Is attached interactable in a registered state.
        /// </summary>
        protected virtual bool isRegistered => m_IsRegistered;

        IXRInteractable m_Interactable;
        IXRHoverInteractable m_HoverInteractable;
        IXRSelectInteractable m_SelectInteractable;
        IXRActivateInteractable m_ActivateInteractable;
        IXRInteractionStrengthInteractable m_InteractionStrengthInteractable;

        Coroutine m_SelectedClickAnimation;
        Coroutine m_ActivatedClickAnimation;
        Coroutine m_HoveredPriorityRoutine;

        bool m_IsBoundToInteractionEvents;

        bool m_IsActivated;
        bool m_IsRegistered;
        bool m_IsHoveredPriority;
        bool m_HasInteractionStrengthInteractable;

        int m_HoveringPriorityInteractorCount;

        /// <summary>
        /// See <see cref="MonoBehaviour"/>.
        /// </summary>
        protected void Awake()
        {
            var receiver = m_InteractableSource != null && m_InteractableSource is IXRInteractable interactable
                ? interactable
                : GetComponentInParent<IXRInteractable>();
            if (!SetBoundInteractionReceiver(receiver))
            {
                XRLoggingUtils.LogWarning($"Could not find required interactable component on {gameObject}" +
                    " for which to provide affordance states.", this);
                enabled = false;
            }
        }

        /// <inheritdoc />
        protected override void OnValidate()
        {
            base.OnValidate();

            if (Application.isPlaying && isActiveAndEnabled && m_IgnoreHoverPriorityEvents)
            {
                StopHoveredPriorityRoutine();
                RefreshState();
            }
        }

        /// <summary>
        /// Bind affordance provider state to <see cref="IXRInteractable"/> state and events.
        /// </summary>
        /// <param name="receiver">Receiver to bind events from.</param>
        /// <returns>Whether binding was successful.</returns>
        public bool SetBoundInteractionReceiver(IXRInteractable receiver)
        {
            ClearBindings();

            var isInteractableValid = receiver is Object unityObject && unityObject != null;
            if (isInteractableValid)
            {
                m_Interactable = receiver;

                if (m_Interactable is IXRHoverInteractable hoverInteractable)
                    m_HoverInteractable = hoverInteractable;

                if (m_Interactable is IXRSelectInteractable selectInteractable)
                    m_SelectInteractable = selectInteractable;

                if (m_Interactable is IXRActivateInteractable activateInteractable)
                    m_ActivateInteractable = activateInteractable;

                if (m_Interactable is IXRInteractionStrengthInteractable interactionStrengthInteractable)
                    m_InteractionStrengthInteractable = interactionStrengthInteractable;
            }
            else
            {
                m_Interactable = null;
                m_HoverInteractable = null;
                m_SelectInteractable = null;
                m_ActivateInteractable = null;
                m_InteractionStrengthInteractable = null;
            }

            BindToProviders();
            return isInteractableValid;
        }

        /// <summary>
        /// Callback triggered when the interactable is registered with the <see cref="XRInteractionManager"/>.
        /// Sets the internal isRegistered flag to true and refreshes the affordance state.
        /// </summary>
        /// <param name="args"><see cref="InteractableRegisteredEventArgs"/> callback args.</param>
        protected virtual void OnRegistered(InteractableRegisteredEventArgs args)
        {
            m_IsRegistered = true;
            RefreshState();
        }

        /// <summary>
        /// Callback triggered when the interactable is unregistered with the <see cref="XRInteractionManager"/>.
        /// Sets the internal isRegistered flag to false and refreshes the affordance state.
        /// </summary>
        /// <param name="args"><see cref="InteractableUnregisteredEventArgs"/> callback args.</param>
        protected virtual void OnUnregistered(InteractableUnregisteredEventArgs args)
        {
            m_IsRegistered = false;
            RefreshState();
        }

        /// <summary>
        /// Callback triggered by <see cref="IXRHoverInteractable"/> when the first interactor begins hovering over this interactable.
        /// Refreshes the affordance state.
        /// </summary>
        /// <param name="args"><see cref="HoverEnterEventArgs"/> callback args.</param>
        /// <seealso cref="IXRHoverInteractable.firstHoverEntered"/>
        protected virtual void OnFirstHoverEntered(HoverEnterEventArgs args)
        {
            RefreshState();
        }

        /// <summary>
        /// Callback triggered by <see cref="IXRHoverInteractable"/> when the last interactor exits hovering over this interactable.
        /// Refreshes the affordance state.
        /// </summary>
        /// <param name="args"><see cref="HoverExitEventArgs"/> callback args.</param>
        /// <seealso cref="IXRHoverInteractable.lastHoverExited"/>
        protected virtual void OnLastHoverExited(HoverExitEventArgs args)
        {
            RefreshState();
        }

        /// <summary>
        /// Callback triggered by <see cref="IXRHoverInteractable"/> when an interactor begins hovering over this interactable.
        /// Refreshes the affordance state.
        /// </summary>
        /// <param name="args"><see cref="HoverEnterEventArgs"/> callback args.</param>
        /// <seealso cref="IXRHoverInteractable.hoverEntered"/>
        protected virtual void OnHoverEntered(HoverEnterEventArgs args)
        {
            if (m_IgnoreHoverPriorityEvents)
                return;

            if (args.interactorObject is IXRTargetPriorityInteractor priorityInteractor)
            {
                m_HoveringPriorityInteractorCount++;
                if (priorityInteractor.targetPriorityMode != TargetPriorityMode.None)
                    m_HoveredPriorityRoutine = m_HoveredPriorityRoutine ?? StartCoroutine(HoveredPriorityRoutine());
            }
        }

        /// <summary>
        /// Callback triggered by <see cref="IXRHoverInteractable"/> when an interactor exits hovering over this interactable.
        /// Refreshes the affordance state.
        /// </summary>
        /// <param name="args"><see cref="HoverExitEventArgs"/> callback args.</param>
        /// <seealso cref="IXRHoverInteractable.hoverExited"/>
        protected virtual void OnHoverExited(HoverExitEventArgs args)
        {
            if (m_IgnoreHoverPriorityEvents)
                return;

            if (args.interactorObject is IXRTargetPriorityInteractor)
            {
                m_HoveringPriorityInteractorCount--;
                if (m_HoveringPriorityInteractorCount > 0)
                    return;

                StopHoveredPriorityRoutine();
                RefreshState();
            }
        }

        void StopHoveredPriorityRoutine()
        {
            m_HoveringPriorityInteractorCount = 0;
            m_IsHoveredPriority = false;
            if (m_HoveredPriorityRoutine != null)
            {
                StopCoroutine(m_HoveredPriorityRoutine);
                m_HoveredPriorityRoutine = null;
            }
        }

        /// <summary>
        /// Callback triggered by <see cref="IXRSelectInteractor"/> when the first interactor begins selecting over this interactable.
        /// Refreshes the affordance state and triggers the <see cref="SelectedClickBehavior"/> animation coroutine if the select animation mode is set to SelectEntered.
        /// </summary>
        /// <param name="args"><see cref="SelectEnterEventArgs"/> callback args.</param>
        /// <seealso cref="IXRSelectInteractable.firstSelectEntered"/>
        protected virtual void OnFirstSelectEntered(SelectEnterEventArgs args)
        {
            if (m_IgnoreSelectEvents || m_SelectClickAnimationMode != SelectClickAnimationMode.SelectEntered || m_ClickAnimationDuration < Mathf.Epsilon)
            {
                RefreshState();
                return;
            }

            SelectedClickBehavior();
        }

        /// <summary>
        /// Callback triggered by <see cref="IXRSelectInteractor"/> when the last interactor exits selecting over this interactable.
        /// Refreshes the affordance state and triggers the <see cref="SelectedClickBehavior"/> animation coroutine if the select animation mode is set to SelectExited.
        /// </summary>
        /// <param name="args"><see cref="SelectExitEventArgs"/> callback args.</param>
        /// <seealso cref="IXRSelectInteractable.lastSelectExited"/>
        protected virtual void OnLastSelectExited(SelectExitEventArgs args)
        {
            if (m_IgnoreSelectEvents || m_SelectClickAnimationMode != SelectClickAnimationMode.SelectExited || m_ClickAnimationDuration < Mathf.Epsilon)
            {
                // If Select animation is playing and we are exiting, we need to wait for the animation to finish before refreshing state
                if(m_SelectedClickAnimation != null)
                    return;
                
                RefreshState();
                return;
            }

            SelectedClickBehavior();
        }

        /// <summary>
        /// Callback triggered by <see cref="IXRActivateInteractable"/> when the interactor triggers an activated event on the interactable.
        /// Refreshes the affordance state and triggers the <see cref="ActivatedClickBehavior"/> animation coroutine if the activated animation mode is set to Activated.
        /// </summary>
        /// <param name="args"><see cref="ActivateEventArgs"/> callback args.</param>
        /// <seealso cref="IXRActivateInteractable.activated"/>
        protected virtual void OnActivatedEvent(ActivateEventArgs args)
        {
            m_IsActivated = true;
            if (m_IgnoreActivateEvents || (m_ActivateClickAnimationMode != ActivateClickAnimationMode.Activated) || m_ClickAnimationDuration < Mathf.Epsilon)
            {
                RefreshState();
                return;
            }

            ActivatedClickBehavior();
        }

        /// <summary>
        /// Callback triggered by <see cref="IXRActivateInteractable"/> when the interactor triggers an deactivated event on the interactable.
        /// Refreshes the affordance state and triggers the <see cref="ActivatedClickBehavior"/> animation coroutine if the activated animation mode is set to deactivated.
        /// </summary>
        /// <param name="args"><see cref="DeactivateEventArgs"/> callback args.</param>
        /// <seealso cref="IXRActivateInteractable.deactivated"/>
        protected virtual void OnDeactivatedEvent(DeactivateEventArgs args)
        {
            m_IsActivated = false;
            if (m_IgnoreActivateEvents || (m_ActivateClickAnimationMode != ActivateClickAnimationMode.Deactivated) || m_ClickAnimationDuration < Mathf.Epsilon)
            {
                // If activate animation is playing and we are exiting, we need to wait for the animation to finish before refreshing state
                if (m_ActivatedClickAnimation != null)
                    return;
                
                RefreshState();
                return;
            }

            ActivatedClickBehavior();
        }

        /// <summary>
        /// Callback triggered by <see cref="IXRInteractionStrengthInteractable"/> when the largest interaction strength of the interactable changes.
        /// Refreshes the affordance state.
        /// </summary>
        /// <param name="value">The new largest interaction strength value of all interactors hovering or selecting the interactable.</param>
        protected virtual void OnLargestInteractionStrengthChanged(float value)
        {
            // If currently executing animation, do not update interaction strength state.
            if(m_SelectedClickAnimation != null || m_ActivatedClickAnimation != null)
                return;
            RefreshState();
        }

        /// <summary>
        /// Handles starting the selected click animation coroutine. Stops any previously started coroutine.
        /// </summary>
        protected virtual void SelectedClickBehavior()
        {
            StopAllActivateAnimations();
            m_SelectedClickAnimation = StartCoroutine(ClickAnimation(AffordanceStateShortcuts.selected, m_ClickAnimationDuration, () => m_SelectedClickAnimation = null));
        }

        /// <summary>
        /// Handles starting the activated click animation coroutine. Stops any previously started coroutine.
        /// </summary>
        protected virtual void ActivatedClickBehavior()
        {
            StopAllActivateAnimations();
            m_ActivatedClickAnimation = StartCoroutine(ClickAnimation(AffordanceStateShortcuts.activated, m_ClickAnimationDuration, () => m_ActivatedClickAnimation = null));
        }

        void StopActivatedCoroutine()
        {
            if (m_ActivatedClickAnimation == null)
                return;
            StopCoroutine(m_ActivatedClickAnimation);
            m_ActivatedClickAnimation = null;
        }

        void StopSelectedCoroutine()
        {
            if (m_SelectedClickAnimation == null)
                return;
            StopCoroutine(m_SelectedClickAnimation);
            m_SelectedClickAnimation = null;
        }
        
        void StopAllActivateAnimations()
        {
            StopActivatedCoroutine();
            StopSelectedCoroutine();
        }

        /// <summary>
        /// Click animation coroutine that plays over a set period of time, transitioning between the lower and upper bounds of a given affordance state.
        /// </summary>
        /// <param name="targetStateIndex">Target animation state with bounds which to transition between.</param>
        /// <param name="duration">Duration of the animation.</param>
        /// <param name="onComplete">OnComplete callback action.</param>
        /// <returns>Enumerator used to play as a coroutine.</returns>
        protected virtual IEnumerator ClickAnimation(byte targetStateIndex, float duration, Action onComplete = null)
        {
            var elapsedTime = 0f;
            while (elapsedTime < duration)
            {
                var animValue = Mathf.Clamp01(elapsedTime / duration);
                var curveAdjustedAnimValue = m_ClickAnimationCurve.Value.Evaluate(animValue);
                var newAnimationState = new AffordanceStateData(targetStateIndex, curveAdjustedAnimValue);
                UpdateAffordanceState(newAnimationState);

                yield return null;
                elapsedTime += Time.deltaTime;
            }

            yield return null;

            RefreshState();
            onComplete?.Invoke();
        }

        /// <summary>
        /// Evaluates the state of the current interactable to generate a corresponding <see cref="AffordanceStateData"/>.
        /// </summary>
        /// <returns>Newly generated affordance state corresponding to the interactable state.</returns>
        protected virtual AffordanceStateData GenerateNewAffordanceState()
        {
            if (!m_IsBoundToInteractionEvents)
            {
                return currentAffordanceStateData.Value;
            }

            if (m_IsActivated && !m_IgnoreActivateEvents)
            {
                return AffordanceStateShortcuts.activatedState;
            }
            
            if (isSelected && !m_IgnoreSelectEvents)
            {
                var transitionAmount = m_HasInteractionStrengthInteractable ? m_InteractionStrengthInteractable.largestInteractionStrength.Value : 1f;
                return new AffordanceStateData(AffordanceStateShortcuts.selected, transitionAmount);
            }

            if (isHovered && !m_IgnoreHoverEvents)
            {
                var stateIndex = m_IsHoveredPriority ? AffordanceStateShortcuts.hoveredPriority : AffordanceStateShortcuts.hovered;
                var transitionAmount = m_HasInteractionStrengthInteractable ? m_InteractionStrengthInteractable.largestInteractionStrength.Value : 0f;
                return new AffordanceStateData(stateIndex, transitionAmount);
            }

            return m_IsRegistered ? AffordanceStateShortcuts.idleState : AffordanceStateShortcuts.disabledState;
        }

        IEnumerator HoveredPriorityRoutine()
        {
            do
            {
                if (m_HoverInteractable is XRBaseInteractable baseInteractable &&
                    baseInteractable.interactionManager != null &&
                    baseInteractable.interactionManager.IsHighestPriorityTarget(baseInteractable) != m_IsHoveredPriority)
                {
                    m_IsHoveredPriority = !m_IsHoveredPriority;
                    RefreshState();
                }

                yield return null;
            } while (m_HoveringPriorityInteractorCount > 0);

            m_HoveredPriorityRoutine = null;
        }

        /// <inheritdoc/>
        protected override void BindToProviders()
        {
            base.BindToProviders();

            m_IsBoundToInteractionEvents = m_Interactable is Object unityObject && unityObject != null;
            if (m_IsBoundToInteractionEvents)
            {
                m_Interactable.registered += OnRegistered;
                m_Interactable.unregistered += OnUnregistered;

                if (m_HoverInteractable != null)
                {
                    m_HoverInteractable.firstHoverEntered.AddListener(OnFirstHoverEntered);
                    m_HoverInteractable.lastHoverExited.AddListener(OnLastHoverExited);
                    m_HoverInteractable.hoverEntered.AddListener(OnHoverEntered);
                    m_HoverInteractable.hoverExited.AddListener(OnHoverExited);
                }

                if (m_SelectInteractable != null)
                {
                    m_SelectInteractable.firstSelectEntered.AddListener(OnFirstSelectEntered);
                    m_SelectInteractable.lastSelectExited.AddListener(OnLastSelectExited);
                }

                if (m_ActivateInteractable != null)
                {
                    m_ActivateInteractable.activated.AddListener(OnActivatedEvent);
                    m_ActivateInteractable.deactivated.AddListener(OnDeactivatedEvent);
                }

                if (m_InteractionStrengthInteractable != null)
                {
                    AddBinding(m_InteractionStrengthInteractable.largestInteractionStrength.Subscribe(OnLargestInteractionStrengthChanged));
                    m_HasInteractionStrengthInteractable = true;
                }
                else
                {
                    m_HasInteractionStrengthInteractable = false;
                }

                m_IsActivated = false;

                // Initialize field for whether the interactable is registered to distinguish between Idle and Disabled affordance states.
                // The registration status is not yet part of the base interactable interface, so we use a reasonable assumption here.
                if (m_Interactable is XRBaseInteractable baseInteractable)
                {
                    m_IsRegistered = baseInteractable.interactionManager != null && baseInteractable.interactionManager.IsRegistered(m_Interactable);
                }
                else if (m_Interactable is Behaviour behavior)
                {
                    m_IsRegistered = behavior.isActiveAndEnabled;
                }
                else
                {
                    m_IsRegistered = true;
                }
            }

            RefreshState();
        }

        /// <summary>
        /// Re-evaluates the current affordance state and triggers events for receivers if it changed.
        /// </summary>
        public void RefreshState()
        {
            var newState = GenerateNewAffordanceState();

            // If leaving the selected state, we have to terminate select animation coroutines.
            if (newState.stateIndex != AffordanceStateShortcuts.selected)
                StopSelectedCoroutine();

            // If Leaving the activated state, we have to terminate activated animation coroutines.
            if(newState.stateIndex == AffordanceStateShortcuts.activated)
                StopActivatedCoroutine();

            UpdateAffordanceState(GenerateNewAffordanceState());
        }

        /// <inheritdoc/>
        protected override void ClearBindings()
        {
            base.ClearBindings();

            if (m_IsBoundToInteractionEvents)
            {
                m_Interactable.registered -= OnRegistered;
                m_Interactable.unregistered -= OnUnregistered;

                if (m_HoverInteractable != null)
                {
                    m_HoverInteractable.firstHoverEntered.RemoveListener(OnFirstHoverEntered);
                    m_HoverInteractable.lastHoverExited.RemoveListener(OnLastHoverExited);
                    m_HoverInteractable.hoverEntered.RemoveListener(OnHoverEntered);
                    m_HoverInteractable.hoverExited.RemoveListener(OnHoverExited);
                }

                if (m_SelectInteractable != null)
                {
                    m_SelectInteractable.firstSelectEntered.RemoveListener(OnFirstSelectEntered);
                    m_SelectInteractable.lastSelectExited.RemoveListener(OnLastSelectExited);
                }

                if (m_ActivateInteractable != null)
                {
                    m_ActivateInteractable.activated.RemoveListener(OnActivatedEvent);
                    m_ActivateInteractable.deactivated.RemoveListener(OnDeactivatedEvent);
                }

                // No need to unsubscribe from interaction strength here since it was added to the binding group
                // and would have been cleared upon calling the base method.
            }

            m_IsBoundToInteractionEvents = false;
        }
    }


OnSelectInteractable

using UnityEngine.XR.Interaction.Toolkit;

namespace UnityEngine.XR.Content.Interaction
{
    /// <summary>
    /// Triggers a Unity Event once when an interactable is selected by an interactor.
    /// </summary>
    public class OnSelectInteractable : MonoBehaviour
    {
        [SerializeField]
        [Tooltip("The interactable that is checked for selection.")]
        XRBaseInteractable m_TargetInteractable;

        [SerializeField]
        [Tooltip("The function to call when the interactable is selected.")]
        SelectEnterEvent m_OnSelected;

        void Start()
        {
            if (m_TargetInteractable != null)
                m_TargetInteractable.selectEntered.AddListener(OnSelected);
        }

        void OnSelected(SelectEnterEventArgs args)
        {
            m_OnSelected.Invoke(args);

            if (m_TargetInteractable != null)
                m_TargetInteractable.selectEntered.RemoveListener(OnSelected);
        }
    }
}

XRDualGrabFreeTransformer

using Unity.XR.CoreUtils;
using UnityEngine.Assertions;

namespace UnityEngine.XR.Interaction.Toolkit.Transformers
{
    /// <summary>
    /// Grab transformer which supports moving and rotating unconstrained with multiple Interactors.
    /// Maintains the offset from the attachment points used for each Interactor and points in the
    /// direction made by each grab.
    /// This is the default grab transformer used for multiple selections.
    /// </summary>
    /// <remarks>
    /// When there is a single Interactor, this has identical behavior to <see cref="XRSingleGrabFreeTransformer"/>.
    /// </remarks>
    /// <seealso cref="XRGrabInteractable"/>
    [AddComponentMenu("XR/Transformers/XR Dual Grab Free Transformer", 11)]
    [HelpURL(XRHelpURLConstants.k_XRDualGrabFreeTransformer)]
    public class XRDualGrabFreeTransformer : XRBaseGrabTransformer
    {
        /// <summary>
        /// Describes which combination of interactors influences a pose.
        /// </summary>
        public enum PoseContributor
        {
            /// <summary>
            /// Use the first interactor's data.
            /// </summary>
            First,

            /// <summary>
            /// Use the second interactor's data.
            /// </summary>
            Second,

            /// <summary>
            /// Use an average of the first and second interactor's data.
            /// </summary>
            Average,
        }

        [SerializeField]
        PoseContributor m_MultiSelectPosition = PoseContributor.First;

        /// <summary>
        /// Controls how multiple interactors combine to drive this interactable's position
        /// </summary>
        /// <seealso cref="PoseContributor"/>
        public PoseContributor multiSelectPosition
        {
            get => m_MultiSelectPosition;
            set => m_MultiSelectPosition = value;
        }

        [SerializeField]
        PoseContributor m_MultiSelectRotation = PoseContributor.Average;

        /// <summary>
        /// Controls how multiple interactors combine to drive this interactable's rotation
        /// </summary>
        /// <seealso cref="PoseContributor"/>
        public PoseContributor multiSelectRotation
        {
            get => m_MultiSelectRotation;
            set => m_MultiSelectRotation = value;
        }

        /// <inheritdoc />
        protected override RegistrationMode registrationMode => RegistrationMode.Multiple;

        // For Gizmo
        internal Pose lastInteractorAttachPose { get; private set; }

        Vector3 m_LastUp;

        /// <summary>
        /// See <see cref="MonoBehaviour"/>.
        /// </summary>
        // ReSharper disable once Unity.RedundantEventFunction -- See comment in method
        protected virtual void OnDrawGizmosSelected()
        {
            // Empty method, but needed to allow the user to toggle it by folding the component in the Inspector window
            // and making it visible in the Gizmos dropdown in the Scene view.
        }

        /// <inheritdoc />
        public override void OnGrabCountChanged(XRGrabInteractable grabInteractable, Pose targetPose, Vector3 localScale)
        {
            base.OnGrabCountChanged(grabInteractable, targetPose, localScale);
            if (grabInteractable.interactorsSelecting.Count == 2)
                m_LastUp = grabInteractable.transform.up;
        }

        /// <inheritdoc />
        public override void Process(XRGrabInteractable grabInteractable, XRInteractionUpdateOrder.UpdatePhase updatePhase, ref Pose targetPose, ref Vector3 localScale)
        {
            switch (updatePhase)
            {
                case XRInteractionUpdateOrder.UpdatePhase.Dynamic:
                case XRInteractionUpdateOrder.UpdatePhase.OnBeforeRender:
                {
                    UpdateTarget(grabInteractable, ref targetPose);

                    break;
                }
            }
        }

        void UpdateTarget(XRGrabInteractable grabInteractable, ref Pose targetPose)
        {
            if (grabInteractable.interactorsSelecting.Count == 1)
                XRSingleGrabFreeTransformer.UpdateTarget(grabInteractable, ref targetPose);
            else
                UpdateTargetMulti(grabInteractable, ref targetPose);
        }

        void UpdateTargetMulti(XRGrabInteractable grabInteractable, ref Pose targetPose)
        {
            Debug.Assert(grabInteractable.interactorsSelecting.Count > 1, this);

            var primaryAttachPose = grabInteractable.interactorsSelecting[0].GetAttachTransform(grabInteractable).GetWorldPose();
            var secondaryAttachPose = grabInteractable.interactorsSelecting[1].GetAttachTransform(grabInteractable).GetWorldPose();

            // When multi-selecting, adjust the effective interactorAttachPose with our default 2-hand algorithm.
            // Default to the primary interactor.
            var interactorAttachPose = primaryAttachPose;

            switch (m_MultiSelectPosition)
            {
                case PoseContributor.First:
                    interactorAttachPose.position = primaryAttachPose.position;
                    break;
                case PoseContributor.Second:
                    interactorAttachPose.position = secondaryAttachPose.position;
                    break;
                case PoseContributor.Average:
                    interactorAttachPose.position = (primaryAttachPose.position + secondaryAttachPose.position) * 0.5f;
                    break;
                default:
                    Assert.IsTrue(false, $"Unhandled {nameof(PoseContributor)}={m_MultiSelectPosition}.");
                    goto case PoseContributor.First;
            }

            // For rotation, we match the anchor's forward to the vector made by the two interactor positions - imagine a hammer handle.
            // We use the interactor's up as the base of the combined multi-select up, unless it is too similar to the forward vector
            // In that case, we will gradually fall back to the right vector and calculate the final 'up' from that
            var forward = (secondaryAttachPose.position - primaryAttachPose.position).normalized;

            Vector3 up;
            Vector3 right;
            switch (m_MultiSelectRotation)
            {
                case PoseContributor.First:
                    up = primaryAttachPose.up;
                    right = primaryAttachPose.right;
                    if (forward == Vector3.zero)
                        forward = primaryAttachPose.forward;
                    break;
                case PoseContributor.Second:
                    up = secondaryAttachPose.up;
                    right = secondaryAttachPose.right;
                    if (forward == Vector3.zero)
                        forward = secondaryAttachPose.forward;
                    break;
                case PoseContributor.Average:
                    up = Vector3.Slerp(primaryAttachPose.up, secondaryAttachPose.up, 0.5f);
                    right = Vector3.Slerp(primaryAttachPose.right, secondaryAttachPose.right, 0.5f);
                    if (forward == Vector3.zero)
                        forward = primaryAttachPose.forward;
                    break;
                default:
                    Assert.IsTrue(false, $"Unhandled {nameof(PoseContributor)}={m_MultiSelectRotation}.");
                    goto case PoseContributor.First;
            }

            var crossUp = Vector3.Cross(forward, right);

            var angleDiff = Mathf.PingPong(Vector3.Angle(up, forward), 90f);
            up = Vector3.Slerp(crossUp, up, angleDiff / 90f);

            var crossRight = Vector3.Cross(up, forward);
            up = Vector3.Cross(forward, crossRight);

            // We also keep track of whether the up vector was pointing up or down previously, to allow for objects to be flipped through a series of rotations
            // Such as a 180 degree rotation on the y, followed by a 180 degree rotation on the x
            if (Vector3.Dot(up, m_LastUp) <= 0f)
            {
                up = -up;
            }

            m_LastUp = up;

            interactorAttachPose.rotation = Quaternion.LookRotation(forward, up);

            lastInteractorAttachPose = interactorAttachPose;

            if (!grabInteractable.trackRotation)
            {
                // When not using the rotation of the Interactor we apply the position without an offset
                targetPose.position = interactorAttachPose.position;
                return;
            }

            // Compute the new target world pose
            if (m_MultiSelectRotation == PoseContributor.First || m_MultiSelectRotation == PoseContributor.Second)
            {
                var controllerIndex = m_MultiSelectRotation == PoseContributor.First ? 0 : 1;
                var thisAttachTransform = grabInteractable.GetAttachTransform(grabInteractable.interactorsSelecting[controllerIndex]);
                var thisTransformPose = grabInteractable.transform.GetWorldPose();

                // Calculate offset of the grab interactable's position relative to its attach transform.
                // Transform that offset direction from world space to local space of the transform it's relative to.
                // It will be applied to the interactor's attach position using the orientation of the Interactor's attach transform.
                var attachOffset = thisTransformPose.position - thisAttachTransform.position;
                var positionOffset = thisAttachTransform.InverseTransformDirection(attachOffset);
                targetPose.position = (interactorAttachPose.rotation * positionOffset) + interactorAttachPose.position;
            }
            else if (m_MultiSelectRotation == PoseContributor.Average)
            {
                // Average rotation does not use offset and keeps objects between two attach points (controllers).
                targetPose.position = interactorAttachPose.position;
            }
            else
            {
                Assert.IsTrue(false, $"Unhandled {nameof(PoseContributor)}={m_MultiSelectRotation}.");
            }

            targetPose.rotation = interactorAttachPose.rotation;
        }
    }
}

XRControllerRecorder

namespace UnityEngine.XR.Interaction.Toolkit
{
    /// <summary>
    /// <see cref="MonoBehaviour"/> that controls interaction recording and playback (via <see cref="XRControllerRecording"/> assets).
    /// </summary>
    [AddComponentMenu("XR/Debug/XR Controller Recorder", 11)]
    [DisallowMultipleComponent]
    [DefaultExecutionOrder(XRInteractionUpdateOrder.k_ControllerRecorder)]
    [HelpURL(XRHelpURLConstants.k_XRControllerRecorder)]
    public class XRControllerRecorder : MonoBehaviour
    {
        [Header("Input Recording/Playback")]

        [SerializeField, Tooltip("Controls whether this recording will start playing when the component's Awake() method is called.")]
        bool m_PlayOnStart;

        /// <summary>
        /// Controls whether this recording will start playing when the component's <see cref="Awake"/> method is called.
        /// </summary>
        public bool playOnStart
        {
            get => m_PlayOnStart;
            set => m_PlayOnStart = value;
        }

        [SerializeField, Tooltip("Controller Recording asset for recording and playback of controller events.")]
        XRControllerRecording m_Recording;

        /// <summary>
        /// Controller Recording asset for recording and playback of controller events.
        /// </summary>
        public XRControllerRecording recording
        {
            get => m_Recording;
            set => m_Recording = value;
        }

        [SerializeField, Tooltip("XR Controller who's output will be recorded and played back")]
        XRBaseController m_XRController;

        /// <summary>
        /// The controller that this recording uses for recording and playback.
        /// </summary>
        public XRBaseController xrController
        {
            get => m_XRController;
            set => m_XRController = value;
        }

        /// <summary>
        /// Whether the <see cref="XRControllerRecorder"/> is currently recording interaction state.
        /// </summary>
        public bool isRecording
        {
            get => m_IsRecording;
            set
            {
                if (m_IsRecording != value)
                {
                    recordingStartTime = Time.time;
                    isPlaying = false;
                    m_CurrentTime = 0d;
                    if (m_Recording)
                    {
                        if (value)
                            m_Recording.InitRecording();
                        else
                            m_Recording.SaveRecording();
                    }
                    m_IsRecording = value;
                }
            }
        }

        /// <summary>
        /// Whether the XRControllerRecorder is currently playing back interaction state.
        /// </summary>
        public bool isPlaying
        {
            get => m_IsPlaying;
            set
            {
                if (m_IsPlaying != value)
                {
                    isRecording = false;
                    if (m_Recording)
                        ResetPlayback();
                    m_CurrentTime = 0d;
                    m_IsPlaying = value;

                    // Cache the previous state of the XRController, or put it back
                    if (value && m_XRController != null)
                    {
                        m_PrevEnableInputActions = m_XRController.enableInputActions;
                        m_PrevEnableInputTracking = m_XRController.enableInputTracking;
                        m_XRController.enableInputActions = false;
                        m_XRController.enableInputTracking = false;
                    }
                    else if (m_XRController != null)
                    {
                        m_XRController.enableInputActions = m_PrevEnableInputActions;
                        m_XRController.enableInputTracking = m_PrevEnableInputTracking;
                    }
                }
            }
        }

        double m_CurrentTime;

        /// <summary>
        /// (Read Only) The current recording/playback time.
        /// </summary>
        public double currentTime => m_CurrentTime;

        /// <summary>
        /// (Read Only) The total playback time (or 0 if no recording).
        /// </summary>
        public double duration => m_Recording != null ? m_Recording.duration : 0d;

        /// <summary>
        /// The <see cref="Time.time"/> when recording was last started.
        /// </summary>
        protected float recordingStartTime { get; set; }

        bool m_IsRecording;
        bool m_IsPlaying;
        double m_LastPlaybackTime;
        int m_LastFrameIdx;

        bool m_PrevEnableInputActions;
        bool m_PrevEnableInputTracking;

        /// <summary>
        /// See <see cref="MonoBehaviour"/>.
        /// </summary>
        protected void Awake()
        {
            if (m_XRController == null)
                m_XRController = GetComponent<XRBaseController>();

            m_CurrentTime = 0d;

            if (m_PlayOnStart)
                isPlaying = true;
        }

        /// <summary>
        /// See <see cref="MonoBehaviour"/>.
        /// </summary>
        protected virtual void Update()
        {
            if (isRecording && m_XRController != null)
            {
                var state = m_XRController.currentControllerState;

                state.time = Time.time - recordingStartTime;

                m_Recording.AddRecordingFrame(state);
            }
            else if (isPlaying)
            {
                UpdatePlaybackTime(m_CurrentTime);
            }

            if (isRecording || isPlaying)
                m_CurrentTime += Time.deltaTime;
            if (isPlaying && m_CurrentTime > m_Recording.duration)
                isPlaying = false;
        }

        /// <summary>
        /// See <see cref="MonoBehaviour"/>.
        /// </summary>
        protected void OnDestroy()
        {
            isRecording = false;
            isPlaying = false;
        }

        /// <summary>
        /// Resets the recorder to the start of the clip.
        /// </summary>
        public void ResetPlayback()
        {
            m_LastPlaybackTime = 0d;
            m_LastFrameIdx = 0;
        }

        void UpdatePlaybackTime(double playbackTime)
        {
            if (!m_Recording || m_Recording == null || m_Recording.frames.Count == 0 || m_LastFrameIdx >= m_Recording.frames.Count  )
                return;

            // Look for next frame in order (binary search would be faster but we are only searching from last cached frame index)
            var prevFrame = m_Recording.frames[m_LastFrameIdx];
            var frameIdx = m_LastFrameIdx;
            if (prevFrame.time < playbackTime)
            {
                for (; frameIdx < m_Recording.frames.Count &&
                    m_Recording.frames[frameIdx].time >= m_LastPlaybackTime &&
                    m_Recording.frames[frameIdx].time <= playbackTime;
                ++frameIdx) { }
            }

            // Past last frame or on the same frame, don't do anything
            if (frameIdx >= m_Recording.frames.Count)
                return;

            if (m_XRController != null)
            {
                var recordingFrame = m_Recording.frames[frameIdx];
                m_XRController.currentControllerState = recordingFrame;
            }

            m_LastFrameIdx = frameIdx;
            m_LastPlaybackTime = playbackTime;
        }

        /// <summary>
        /// Gets the state of the controller.
        /// </summary>
        /// <param name="controllerState">When this method returns, contains the <see cref="XRControllerState"/> object representing the state of the controller.</param>
        /// <returns>Returns <see langword="true"/> when playing or recording. Otherwise, returns <see langword="false"/>.</returns>
        public virtual bool GetControllerState(out XRControllerState controllerState)
        {
            if (isPlaying)
            {
                // Return the current frame we're playing back
                if (m_Recording.frames.Count > m_LastFrameIdx)
                {
                    controllerState = m_Recording.frames[m_LastFrameIdx];
                    return true;
                }
            }
            else if (isRecording)
            {
                // Relay the last frame we got
                if (m_Recording.frames.Count > 0)
                {
                    controllerState = m_Recording.frames[m_Recording.frames.Count - 1];
                    return true;
                }
            }
            else if (m_XRController != null)
            {
                // Pass through as we're not recording or playing
                controllerState = m_XRController.currentControllerState;
                return false;
            }

            controllerState = new XRControllerState();
            return false;
        }
    }
}

ObjectReset

using System.Collections.Generic;

namespace UnityEngine.XR.Content.Interaction
{
    /// <summary>
    /// Provides the ability to reset specified objects if they fall below a certain position - designated by this transform's height.
    /// </summary>
    public class ObjectReset : MonoBehaviour
    {
        [SerializeField]
        [Tooltip("Which objects to reset if falling out of range.")]
        List<Transform> m_ObjectsToReset = new List<Transform>();

        [SerializeField]
        [Tooltip("How often to check if objects should be reset.")]
        float m_CheckDuration = 2f;

        readonly List<Pose> m_OriginalPositions = new List<Pose>();

        float m_CheckTimer;

        /// <summary>
        /// See <see cref="MonoBehaviour"/>.
        /// </summary>
        protected void Start()
        {
            foreach (var currentTransform in m_ObjectsToReset)
            {
                if (currentTransform != null)
                {
                    m_OriginalPositions.Add(new Pose(currentTransform.position, currentTransform.rotation));
                }
                else
                {
                    Debug.LogWarning("Objects To Reset contained a null element. Update the reference or delete the array element of the missing object.", this);
                    m_OriginalPositions.Add(new Pose());
                }
            }
        }

        /// <summary>
        /// See <see cref="MonoBehaviour"/>.
        /// </summary>
        protected void Update()
        {
            m_CheckTimer -= Time.deltaTime;

            if (m_CheckTimer > 0)
                return;

            m_CheckTimer = m_CheckDuration;

            var resetPlane = transform.position.y;

            for (var transformIndex = 0; transformIndex < m_ObjectsToReset.Count; transformIndex++)
            {
                var currentTransform = m_ObjectsToReset[transformIndex];
                if (currentTransform == null)
                    continue;

                if (currentTransform.position.y < resetPlane)
                {
                    currentTransform.SetPositionAndRotation(m_OriginalPositions[transformIndex].position, m_OriginalPositions[transformIndex].rotation);

                    var rigidBody = currentTransform.GetComponentInChildren<Rigidbody>();
                    if (rigidBody != null)
                    {
                        rigidBody.velocity = Vector3.zero;
                        rigidBody.angularVelocity = Vector3.zero;
                    }
                }
            }
        }
    }
}

三、效果展示

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容