413 lines
13 KiB
C#
413 lines
13 KiB
C#
// 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
|
|
{
|
|
/// <summary>
|
|
/// This class provides wrapper functionality for triggering animations and fades for the hand rig.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// React to hand tracking state to hide visuals when hands are being tracked.
|
|
/// If false, only the customShouldHideHands function will be evaluated.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// When the user's hands are not in view, the visuals will appear after HintDisplayDelay seconds.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// When the user's hands are in view, the visuals will appear after TrackedHandHintDisplayDelay seconds."
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Number of times to repeat the hint before fading out and waiting for timer again.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// If true, logic runs whenever this component is active. If false, you must manually start the logic with StartShowTimer.
|
|
/// </summary>
|
|
public bool AutoActivate
|
|
{
|
|
get
|
|
{
|
|
return autoActivate;
|
|
}
|
|
set
|
|
{
|
|
autoActivate = value;
|
|
}
|
|
}
|
|
|
|
[Tooltip("Name of animation to play during loop.")]
|
|
[SerializeField]
|
|
private string animationState = "";
|
|
|
|
/// <summary>
|
|
/// Name of animation to play during loop.
|
|
/// </summary>
|
|
public string AnimationState
|
|
{
|
|
get
|
|
{
|
|
return animationState;
|
|
}
|
|
set
|
|
{
|
|
animationState = value;
|
|
}
|
|
}
|
|
|
|
[Tooltip("Time to wait between repeats in seconds.")]
|
|
[SerializeField]
|
|
private float repeatDelay = 1f;
|
|
|
|
/// <summary>
|
|
/// Time to wait between repeats in seconds.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public Func<bool> 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<Animator>();
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the hint loop logic.
|
|
/// </summary>
|
|
public void StartHintLoop()
|
|
{
|
|
if (!loopRunning && VisualsRoot != null)
|
|
{
|
|
loopRunning = true;
|
|
animationHideDuration = GetAnimationDuration(fadeOutAnimationState);
|
|
animationHideTime = GetAnimationDuration(AnimationState) - animationHideDuration;
|
|
if (animationHideTime < 0)
|
|
{
|
|
animationHideTime = 0;
|
|
}
|
|
StartCoroutine(HintLoopSequence(AnimationState));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fades out the hint and stops the hint loop logic
|
|
/// </summary>
|
|
public void StopHintLoop()
|
|
{
|
|
if (loopRunning && !animatingOut)
|
|
{
|
|
StopAllCoroutines();
|
|
StartCoroutine(FadeOutHint());
|
|
}
|
|
loopRunning = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the hint with appropriate fade.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The main timer logic coroutine. Pass the min/max delay to use and the function to evaluate the desired state.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the duration of the animation name passed in, or 0 if the state name is not found.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return true if either of the user's hands are being tracked.
|
|
/// Return false if neither of the user's hands are being tracked.
|
|
/// </summary>
|
|
private bool IsHandTracked()
|
|
{
|
|
return HandJointUtils.FindHand(Handedness.Right) != null || HandJointUtils.FindHand(Handedness.Left) != null;
|
|
}
|
|
}
|
|
}
|