using System; using System.Collections.Generic; using UnityEngine.Events; using UnityEngine.XR.Interaction.Toolkit; namespace UnityEngine.XR.Content.Walkthrough { /// /// Contains information needed to process one step of a walkthrough. /// public class WalkthroughStep : MonoBehaviour { /// Local method use only -- created here to reduce garbage collection. Collections must be cleared before use static readonly List s_TriggersToRemove = new List(); [SerializeField] [Tooltip("Camera target to reposition user.")] GameObject m_CameraTarget; [SerializeField] [Tooltip("The Teleportation Provider used to reposition the user. Usually a component on the XR Origin.")] TeleportationProvider m_TeleportationProvider; [SerializeField] [Tooltip("Optional audio source for voiceover")] AudioSource m_AudioSource; [SerializeField] [Tooltip("Objects to enable when this step is active.")] List m_Visuals = new List(); #pragma warning disable 649 [SerializeField] [Tooltip("Actions to call when the step starts.")] UnityEvent m_OnStepBegin; [SerializeField] [Tooltip("Actions to call when the step completes.")] UnityEvent m_OnStepComplete; [SerializeField] [Tooltip("The purpose of this step.")] string m_Description; #pragma warning restore 649 [SerializeField] [Tooltip("If true, this step cannot be skipped until completed at least once.")] bool m_BlockUntilComplete; [SerializeField] [Tooltip("If true, this step will automatically progress when complete - unless explicitly skipped to.")] bool m_AutoProgressOnComplete = true; bool m_Started; bool m_AutoProgressEnabled = true; bool m_StepInvoked; Action m_OnComplete; GameObject m_Waypoint; GameObject m_Link; List m_Triggers = new List(); List m_RemainingTriggers = new List(); /// /// The purpose of this step. Appends a (Complete) if complete and normally has triggers. /// public string description => $"{m_Description}{(completed && m_Triggers.Count > 0 ? " (Complete)" : "") }"; public GameObject waypoint { get => m_Waypoint; set => m_Waypoint = value; } public GameObject link { get => m_Link; set => m_Link = value; } /// /// Ensures the step visuals are hidden until active and that all triggers are accounted for /// public void Initialize() { if (!m_Started) SetVisualsState(false); GetComponents(m_Triggers); } /// /// Returns true if this step does not currently have any triggers remaining to fire /// public bool canProgress => (!m_BlockUntilComplete || (m_RemainingTriggers.Count == 0)); /// /// Returns true if this step does not block, or has been completed at least once. /// public bool canSkip => (!m_BlockUntilComplete || completed); /// /// True if this step's triggers have been activated at least once /// public bool completed { get; private set; } /// /// Makes this step and its triggers the active focus of a walkthrough /// /// Callback to fire when this step's triggers are complete /// If this step is allow to auto-progress during this activation public void StartStep(Action onComplete, bool allowAutoProgress = true) { if (m_Started) return; // Autoprogression is enabled only if the step AND walkthrough allow it m_AutoProgressEnabled = allowAutoProgress && m_AutoProgressOnComplete; SetVisualsState(true); SetAudioSource(true); if (m_Waypoint != null) { m_Waypoint.SetActive(false); } if (m_CameraTarget != null && m_TeleportationProvider != null) { SetCameraPosition(); } m_OnComplete = onComplete; m_Started = true; if (m_Triggers.Count == 0 && m_AutoProgressEnabled) { CompleteStep(); return; } foreach (var currentTrigger in m_Triggers) { if (currentTrigger.ResetTrigger()) m_RemainingTriggers.Add(currentTrigger); } if (m_RemainingTriggers.Count == 0) { CompleteStep(); return; } if (m_RemainingTriggers.Count > 0) m_AutoProgressEnabled = m_AutoProgressOnComplete; } /// /// Ends this step being the focus of the current walkthrough /// public void CancelStep() { SetVisualsState(false); SetAudioSource(false); if (m_Waypoint != null) { m_Waypoint.SetActive(true); } if (!m_Started) return; m_OnComplete = null; m_Started = false; m_RemainingTriggers.Clear(); } void CompleteStep() { if (!m_Started) return; completed = true; m_OnComplete?.Invoke(m_AutoProgressEnabled); m_OnComplete = null; m_Started = false; // We disable visuals if the the next step is being activated if (m_AutoProgressEnabled) { SetVisualsState(false); SetAudioSource(false); if (m_Waypoint != null) { m_Waypoint.SetActive(true); } } m_RemainingTriggers.Clear(); } void Update() { // If this step is running, check remaining triggers. Any triggers that are now met get removed. // If there are no triggers left, then the step is complete. if (!m_Started) return; if (m_RemainingTriggers.Count == 0) return; s_TriggersToRemove.Clear(); foreach (var currentTrigger in m_RemainingTriggers) { if (currentTrigger.Check()) s_TriggersToRemove.Add(currentTrigger); } foreach (var toRemove in s_TriggersToRemove) { m_RemainingTriggers.Remove(toRemove); } s_TriggersToRemove.Clear(); if (m_RemainingTriggers.Count == 0) { CompleteStep(); return; } } void SetVisualsState(bool enabled) { if (m_Visuals != null) { foreach (var currentVisual in m_Visuals) { if (currentVisual != null) currentVisual.SetActive(enabled); } } if (m_StepInvoked == enabled) return; m_StepInvoked = enabled; if (enabled && m_OnStepBegin != null) m_OnStepBegin.Invoke(); if (!enabled && m_OnStepComplete != null) m_OnStepComplete.Invoke(); } void SetAudioSource(bool enabled) { if (m_AudioSource != null) { if (enabled) { m_AudioSource.Play(); } else { m_AudioSource.Stop(); } } } void SetCameraPosition() { TeleportRequest request = new TeleportRequest() { requestTime = Time.time, matchOrientation = MatchOrientation.TargetUpAndForward, destinationPosition = m_CameraTarget.transform.position, destinationRotation = m_CameraTarget.transform.rotation }; m_TeleportationProvider.QueueTeleportRequest(request); } } }