// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Input; using Microsoft.MixedReality.Toolkit.Utilities; using System; using System.Collections; using UnityEngine; using UnityEngine.Serialization; namespace Microsoft.MixedReality.Toolkit.UI.HandCoach { /// /// This class provides wrapper functionality for triggering animations and fades for the hand rig. /// public class HandInteractionHint : MonoBehaviour { public GameObject VisualsRoot { get; set; } [Tooltip("React to hand tracking state to hide visuals when hands are being tracked. If false, only the customShouldHideHands function will be evaluated.")] [SerializeField] private bool hideIfHandTracked = false; /// /// React to hand tracking state to hide visuals when hands are being tracked. /// If false, only the customShouldHideHands function will be evaluated. /// public bool HideIfHandTracked { get { return hideIfHandTracked; } set { hideIfHandTracked = value; } } [Tooltip("When the user's hands are not in view, the visuals will appear after HintDisplayDelay seconds.")] [SerializeField] [FormerlySerializedAs("minDelay")] private float hintDisplayDelay = 5f; /// /// When the user's hands are not in view, the visuals will appear after HintDisplayDelay seconds. /// public float HintDisplayDelay { get { return hintDisplayDelay; } set { hintDisplayDelay = value; } } [Tooltip("When the user's hands are in view, the visuals will appear after TrackedHandHintDisplayDelay seconds.")] [SerializeField] [FormerlySerializedAs("maxDelay")] private float trackedHandHintDisplayDelay = 10f; /// /// When the user's hands are in view, the visuals will appear after TrackedHandHintDisplayDelay seconds." /// public float TrackedHandHintDisplayDelay { get { return trackedHandHintDisplayDelay; } set { trackedHandHintDisplayDelay = value; } } [Tooltip("Number of times to repeat the hint before fading out and waiting for timer again.")] [SerializeField] private int repeats = 2; /// /// Number of times to repeat the hint before fading out and waiting for timer again. /// public int Repeats { get { return repeats; } set { repeats = value; } } [Tooltip("If true, logic runs whenever this component is active. If false, you must manually start the logic with StartShowTimer.")] [SerializeField] private bool autoActivate = true; /// /// If true, logic runs whenever this component is active. If false, you must manually start the logic with StartShowTimer. /// public bool AutoActivate { get { return autoActivate; } set { autoActivate = value; } } [Tooltip("Name of animation to play during loop.")] [SerializeField] private string animationState = ""; /// /// Name of animation to play during loop. /// public string AnimationState { get { return animationState; } set { animationState = value; } } [Tooltip("Time to wait between repeats in seconds.")] [SerializeField] private float repeatDelay = 1f; /// /// Time to wait between repeats in seconds. /// public float RepeatDelay { get { return repeatDelay; } set { repeatDelay = value; } } private string fadeInAnimationState = "Fade_In"; private string fadeOutAnimationState = "Fade_Out"; private float animationHideTime = 3f; private float animationHideDuration = 0.5f; /// /// Custom function to determine visibility of visuals. /// Return true to hide visuals and reset min timer (max timer will still be in effect), return false when user is doing nothing and needs a hint. /// public Func CustomShouldHideVisuals = delegate { return false; }; private Animator animator; private bool animatingOut = false; private bool loopRunning = false; private void Awake() { if (VisualsRoot == null) { if (transform.childCount > 0) { VisualsRoot = transform.GetChild(0).gameObject; } else { Debug.LogError("Incorrect hand rig setup. Disabling gameObject"); gameObject.SetActive(false); } } // store the root's animator animator = VisualsRoot.GetComponent(); if (animator == null) { Debug.LogError("Hand rig does not have an animator. Disabling gameObject"); gameObject.SetActive(false); } // hide visuals by default if (VisualsRoot != null) { VisualsRoot.SetActive(false); } } private void OnEnable() { // When component is enabled, start up the timer logic if auto activate is specified if (AutoActivate) { StartHintLoop(); } } private void OnDisable() { // Stop all logic when the component is disabled, even if not using auto activate if (loopRunning) { StopAllCoroutines(); loopRunning = false; } // Also turn off the visuals SetActive(VisualsRoot, false); } /// /// Starts the hint loop logic. /// public void StartHintLoop() { if (!loopRunning && VisualsRoot != null) { loopRunning = true; animationHideDuration = GetAnimationDuration(fadeOutAnimationState); animationHideTime = GetAnimationDuration(AnimationState) - animationHideDuration; if (animationHideTime < 0) { animationHideTime = 0; } StartCoroutine(HintLoopSequence(AnimationState)); } } /// /// Fades out the hint and stops the hint loop logic /// public void StopHintLoop() { if (loopRunning && !animatingOut) { StopAllCoroutines(); StartCoroutine(FadeOutHint()); } loopRunning = false; } /// /// Stops the hint with appropriate fade. /// private IEnumerator FadeOutHint() { animatingOut = true; if (animationHideDuration > 0) { // Tell the animator to play the animation if (animator != null) { animator.Play(fadeOutAnimationState); } yield return new WaitForSeconds(animationHideDuration); } SetActive(VisualsRoot, false); animatingOut = false; } /// /// The main timer logic coroutine. Pass the min/max delay to use and the function to evaluate the desired state. /// private IEnumerator HintLoopSequence(string stateToPlay) { // loop until the gameObject has been turned off while (VisualsRoot != null && loopRunning) { // First wait for the min timer, resetting it whenever ShouldHide is true. Also // wait for the max timer, never resetting it. float timer = 0; float displayDelay = IsHandTracked() ? TrackedHandHintDisplayDelay : HintDisplayDelay; while (timer < displayDelay) { if (ShouldHideVisuals()) { timer = 0; } else { timer += Time.deltaTime; } displayDelay = IsHandTracked() ? TrackedHandHintDisplayDelay : HintDisplayDelay; yield return null; } // show the root SetActive(VisualsRoot, true); if (animator != null) { animator.Play(stateToPlay); } float visibleTime = Time.time; int playCount = 0; // loop as long as visuals are active and we haven't repeated the number of times desired while (VisualsRoot.activeSelf && playCount < Repeats) { // hide if hand is present and HideIfHandTracked is true bool shouldHide = ShouldHideVisuals(); bool fadeOut = Time.time - visibleTime >= animationHideTime; if (shouldHide || fadeOut) { // Yield while deactivate anim is playing (or instant deactivate if not animating) yield return FadeOutHint(); // if fade out was caused by user interacting, we've reached the repeat limit, or we've stopped the loop, break out if (shouldHide || playCount == Repeats - 1 || !loopRunning) { break; } // If we auto-hid, then reappear if hands are not tracked else { yield return new WaitForSeconds(RepeatDelay); SetActive(VisualsRoot, true); if (animator != null) { animator.Play(stateToPlay); } visibleTime = Time.time; playCount++; } } yield return null; } } } private void SetActive(GameObject root, bool show) { if (root != null) { root.SetActive(show); if (show && animator != null) { animator.Play(fadeInAnimationState); } } } /// /// Gets the duration of the animation name passed in, or 0 if the state name is not found. /// public float GetAnimationDuration(string animationStateName) { if (animator != null) { RuntimeAnimatorController ac = animator.runtimeAnimatorController; for (int i = 0; i < ac.animationClips.Length; i++) { if (ac.animationClips[i].name.StartsWith(animationStateName)) { return ac.animationClips[i].length; } } } // the specified state is not found return 0; } /// /// Check if the user is making an attempt to proceed, according to the hint. /// Return true if the user is attempting to interact properly. Visuals will hide until the max timer expires. /// Return false if the user is doing nothing. Visuals will show according to the min timer. /// private bool ShouldHideVisuals() { // Check hand tracking, if configured to do so bool shouldHide = HideIfHandTracked && IsHandTracked(); // Check the custom show visuals function if (!shouldHide) { shouldHide |= CustomShouldHideVisuals(); } return shouldHide; } /// /// Return true if either of the user's hands are being tracked. /// Return false if neither of the user's hands are being tracked. /// private bool IsHandTracked() { return HandJointUtils.FindHand(Handedness.Right) != null || HandJointUtils.FindHand(Handedness.Left) != null; } } }