using System.Collections.Generic; using UnityEngine.XR.Interaction.Toolkit; namespace UnityEngine.XR.Content.Rendering { /// /// All-in-one controller for animated object highlights in different states - hovered, selected, and activated /// public class InteractableVisualsController : MonoBehaviour { const float k_ShineTime = 0.2f; enum PriorityHighlightingState { Unknown, Highlighted, Unhighlighted, } static List s_InteractorList = new List(); #pragma warning disable 649 [Header("Audio")] [SerializeField] [Tooltip("The hover audio source.")] AudioSource m_AudioHover; [SerializeField] [Tooltip("The click audio source.")] AudioSource m_AudioClick; [Header("Visual")] [SerializeField] [Tooltip("Material capture settings.")] HighlightController m_HighlightController = new HighlightController(); [SerializeField] [Tooltip("The outline highlight for selection.")] OutlineHighlight m_OutlineHighlight; [SerializeField] [Tooltip("The material highlight for hover.")] MaterialHighlight m_MaterialHighlight; [SerializeField] [Tooltip("The outline hover color.")] Color m_HoverColor = new Color(0.25f, 0.7f, 0.9f, 1f); [SerializeField] [Tooltip("The outline hover color when the Interactable has the highest priority for selection.")] Color m_HoverPriorityColor = new Color(0.09411765f, 0.4392157f, 0.7137255f, 1f); [SerializeField] [Tooltip("The outline selection color.")] Color m_SelectionColor = new Color(1f, 0.4f, 0f, 1f); [SerializeField] [Tooltip("To play material activate anim.")] bool m_PlayMaterialActivateAnim; [SerializeField] [Tooltip("To play outline activate anim.")] bool m_PlayOutlineActivateAnim; [SerializeField] [Tooltip("If true, the highlight state will be on during hover.")] bool m_HighlightOnHover = true; [SerializeField] [Tooltip("If true, the highlight state will be on during hover when the Interactable has the highest priority for selection.")] bool m_HighlightOnHoverPriority = true; [SerializeField] [Tooltip("If true, the highlight state will be on during select.")] bool m_HighlightOnSelect = true; [SerializeField] [Tooltip("If true, the highlight state will be on during activate.")] bool m_HighlightOnActivate = true; #pragma warning restore 649 XRBaseInteractable m_Interactable; Material m_PulseMaterial; float m_StartingAlpha; float m_StartingWidth; int m_SelectedCount; int m_HoveredCount; bool m_Highlighting; PriorityHighlightingState m_PriorityHighlightingState; bool m_PlayShine; float m_ShineTimer; bool isActivated { get; set; } bool isSelected => m_SelectedCount > 0; bool isHovered => m_HoveredCount > 0; /// /// See . /// protected void Awake() { // Find the grab interactable m_Interactable = GetComponentInParent(); // Hook up to events if (m_Interactable is IXRHoverInteractable hoverInteractable) { hoverInteractable.hoverEntered.AddListener(OnHoverEntered); hoverInteractable.hoverExited.AddListener(OnHoverExited); } if (m_Interactable is IXRSelectInteractable selectInteractable) { selectInteractable.selectEntered.AddListener(OnSelectEntered); selectInteractable.selectExited.AddListener(OnSelectExited); } if (m_Interactable is IXRActivateInteractable activateInteractable) { activateInteractable.activated.AddListener(OnActivated); activateInteractable.deactivated.AddListener(OnDeactivated); } // Cache materials for highlighting m_HighlightController.rendererSource = m_Interactable.transform; // Tell the highlight objects to get renderers starting at the grab interactable down if (m_MaterialHighlight != null) { m_HighlightController.RegisterCacheUser(m_MaterialHighlight); m_PulseMaterial = m_MaterialHighlight.highlightMaterial; if (m_PulseMaterial != null) m_StartingAlpha = m_PulseMaterial.GetFloat("_PulseMinAlpha"); } if (m_OutlineHighlight != null) m_HighlightController.RegisterCacheUser(m_OutlineHighlight); m_HighlightController.Initialize(); m_StartingWidth = m_OutlineHighlight.outlineScale; } /// /// See . /// protected void Update() { UpdatePriorityHighlightingState(); m_HighlightController.Update(); if (m_MaterialHighlight != null) { // Do timer count up/count down if (m_PlayShine) { m_ShineTimer += Time.deltaTime; var shinePercent = Mathf.Clamp01(m_ShineTimer / k_ShineTime); var shineValue = Mathf.PingPong(shinePercent, 0.5f) * 2.0f; m_PulseMaterial.SetFloat("_PulseMinAlpha", Mathf.Lerp(m_StartingAlpha, 1f, shineValue)); if (shinePercent >= 1.0f) { m_PlayShine = false; m_ShineTimer = 0.0f; } } } } void UpdateHighlightState() { var shouldHighlight = false; if (isActivated) shouldHighlight = m_HighlightOnActivate; else { if (isSelected) shouldHighlight = m_HighlightOnSelect; else if (isHovered) shouldHighlight = m_HighlightOnHover || (m_HighlightOnHoverPriority && m_PriorityHighlightingState == PriorityHighlightingState.Highlighted); } if (shouldHighlight == m_Highlighting) return; m_Highlighting = shouldHighlight; if (m_Highlighting) m_HighlightController.Highlight(); else m_HighlightController.Unhighlight(); } void OnHoverEntered(HoverEnterEventArgs args) { if (args.interactorObject is XRSocketInteractor) return; m_HoveredCount++; if (isSelected) return; if (m_AudioHover != null) m_AudioHover.Play(); if (m_MaterialHighlight != null) m_PulseMaterial.color = m_HoverColor; if (m_OutlineHighlight != null) m_OutlineHighlight.outlineColor = m_HoverColor; m_PriorityHighlightingState = PriorityHighlightingState.Unknown; UpdateHighlightState(); } void OnHoverExited(HoverExitEventArgs args) { if (args.interactorObject is XRSocketInteractor) return; m_HoveredCount--; m_PriorityHighlightingState = PriorityHighlightingState.Unknown; UpdateHighlightState(); } bool HasValidInteractor(List interactors) { foreach (var interactor in interactors) { if (!(interactor is XRSocketInteractor)) return true; } return false; } void UpdatePriorityHighlightingState() { if (!m_HighlightOnHoverPriority || !isHovered || isSelected) return; var manager = m_Interactable.interactionManager; if (manager == null) return; var highestPriorityForSelection = manager.IsHighestPriorityTarget(m_Interactable, s_InteractorList); if (!HasValidInteractor(s_InteractorList)) return; if (highestPriorityForSelection && m_PriorityHighlightingState != PriorityHighlightingState.Highlighted) { m_PriorityHighlightingState = PriorityHighlightingState.Highlighted; if (m_PulseMaterial != null) m_PulseMaterial.color = m_HoverPriorityColor; if (m_OutlineHighlight != null) m_OutlineHighlight.outlineColor = m_HoverPriorityColor; UpdateHighlightState(); } if (!highestPriorityForSelection && m_PriorityHighlightingState != PriorityHighlightingState.Unhighlighted) { m_PriorityHighlightingState = PriorityHighlightingState.Unhighlighted; if (m_PulseMaterial != null) m_PulseMaterial.color = m_HoverColor; if (m_OutlineHighlight != null) m_OutlineHighlight.outlineColor = m_HoverColor; UpdateHighlightState(); } } void OnSelectEntered(SelectEnterEventArgs args) { if (args.interactorObject is XRSocketInteractor) return; if (m_AudioClick != null) m_AudioClick.Play(); if (m_OutlineHighlight != null) { m_OutlineHighlight.outlineColor = m_SelectionColor; m_OutlineHighlight.PlayPulseAnimation(); } if (m_MaterialHighlight != null) m_PulseMaterial.color = m_SelectionColor; m_SelectedCount++; UpdateHighlightState(); } void OnSelectExited(SelectExitEventArgs args) { if (args.interactorObject is XRSocketInteractor) return; if (m_OutlineHighlight != null) m_OutlineHighlight.outlineColor = m_HoverColor; if (m_MaterialHighlight != null) m_PulseMaterial.color = m_HoverColor; m_OutlineHighlight.PlayPulseAnimation(); // In case the Interactable is dropped while activated. isActivated = false; m_SelectedCount--; m_PriorityHighlightingState = PriorityHighlightingState.Unknown; UpdateHighlightState(); } void OnActivated(ActivateEventArgs args) { if (args.interactorObject is XRSocketInteractor) return; if (m_OutlineHighlight != null) { if (m_PlayMaterialActivateAnim) m_PlayShine = true; if (m_PlayOutlineActivateAnim) { m_OutlineHighlight.outlineScale = 1f; m_OutlineHighlight.PlayPulseAnimation(); } } isActivated = true; UpdateHighlightState(); } void OnDeactivated(DeactivateEventArgs args) { if (args.interactorObject is XRSocketInteractor) return; if (m_OutlineHighlight != null) { if (m_PlayOutlineActivateAnim) { m_OutlineHighlight.outlineScale = m_StartingWidth; m_OutlineHighlight.PlayPulseAnimation(); } } isActivated = false; UpdateHighlightState(); } } }