Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions org.mixedrealitytoolkit.input/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## Unreleased

### Added

* Added `SpeechHoverMode` and `HoverMode` configuration to `SpeechInteractor` to specify which hover state satisfies the focus requirement for voice commands. [PR #1147](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1147)

### Changed

* Reserialized MRTK XR Rig prefab to remove stale serialized fields. [PR #1110](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1110)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@

namespace MixedReality.Toolkit.Input
{
/// <summary>
/// Specifies which hover state satisfies the focus requirement for voice commands on a <see cref="SpeechInteractor"/>.
/// </summary>
public enum SpeechHoverMode
{
/// <summary>
/// Any hover state satisfies the focus requirement.
/// </summary>
Any = 0,

/// <summary>
/// Only passive gaze hover (e.g. eye gaze, head gaze) satisfies the focus requirement.
/// </summary>
Gaze = 1,

/// <summary>
/// Only active hover (e.g. ray, poke, grab) satisfies the focus requirement.
/// </summary>
Active = 2,
}

/// <summary>
/// A <see cref="ISpeechInteractor"/> that is driven by a <see cref="Subsystems.IKeywordRecognitionSubsystem"/>.
/// </summary>
Expand All @@ -18,13 +39,26 @@ namespace MixedReality.Toolkit.Input
/// <br/>
/// At the time <see cref="SpeechInteractor"/> was created Unity's XRI did not support selecting
/// more than one interactable at a time. Because of this limitation, the
/// <see cref="SpeechInteractor"/> drops part of the selection lifecycle management provided by
/// <see cref="SpeechInteractor"/> drops part of the selection lifecycle management provided by
/// Unity's XRI and manually informs the interaction manager to enter and exit selection states.
/// </para>
/// </remarks>
[AddComponentMenu("MRTK/Input/Speech Interactor")]
public class SpeechInteractor : XRBaseInteractor, ISpeechInteractor
{
[SerializeField]
[Tooltip("Which hover state is required on an interactable (when VoiceRequiresFocus is true) for a voice command to select it?")]
private SpeechHoverMode hoverMode = SpeechHoverMode.Any;

/// <summary>
/// Which hover state is required on an interactable (when VoiceRequiresFocus is true) for a voice command to select it?
/// </summary>
public SpeechHoverMode HoverMode
{
get => hoverMode;
set => hoverMode = value;
}

[SerializeField]
[Tooltip("How long does the interactor remain selecting the interactable after recognizing a voice command?")]
private float voiceCommandTriggerTime = 0.3f;
Expand All @@ -34,7 +68,7 @@ public class SpeechInteractor : XRBaseInteractor, ISpeechInteractor
/// </summary>
public float VoiceCommandTriggerTime => voiceCommandTriggerTime;

private Dictionary<string, List<StatefulInteractable>> keywordDictionary = new Dictionary<string, List<StatefulInteractable>>();
private readonly Dictionary<string, HashSet<StatefulInteractable>> keywordDictionary = new Dictionary<string, HashSet<StatefulInteractable>>();
private List<(StatefulInteractable, float)> selectedInteractables = new List<(StatefulInteractable, float)>();

/// <summary>
Expand All @@ -47,7 +81,7 @@ public class SpeechInteractor : XRBaseInteractor, ISpeechInteractor
/// Register a new <see cref="StatefulInteractable"/> and an associated keyword with this <see cref="SpeechInteractor"/>.
/// </summary>
/// <remarks>
/// When this <see cref="SpeechInteractor"/> recognizes the provided <paramref name="keyword"/>,
/// When this <see cref="SpeechInteractor"/> recognizes the provided <paramref name="keyword"/>,
/// <see cref="XRInteractionManager.SelectEnter(IXRSelectInteractor, IXRSelectInteractable)"/> is called, passing along the provided
/// <paramref name="interactable"/>.
/// </remarks>
Expand All @@ -58,13 +92,13 @@ public void RegisterInteractable(StatefulInteractable interactable, string keywo
if (!string.IsNullOrWhiteSpace(keyword))
{
keyword = keyword.ToLower();
if (keywordDictionary.TryGetValue(keyword, out List<StatefulInteractable> interactableList))
if (keywordDictionary.TryGetValue(keyword, out HashSet<StatefulInteractable> interactables))
{
interactableList.Add(interactable);
interactables.Add(interactable);
}
else
{
keywordDictionary.Add(keyword, new List<StatefulInteractable> { interactable });
keywordDictionary.Add(keyword, new HashSet<StatefulInteractable> { interactable });
var subsystem = XRSubsystemHelpers.KeywordRecognitionSubsystem;
if (subsystem != null)
{
Expand All @@ -89,7 +123,7 @@ public void UnregisterInteractable(StatefulInteractable interactable, string key
if (!string.IsNullOrWhiteSpace(keyword))
{
keyword = keyword.ToLower();
if (keywordDictionary.TryGetValue(keyword, out List<StatefulInteractable> interactableList) && interactableList.Remove(interactable))
if (keywordDictionary.TryGetValue(keyword, out HashSet<StatefulInteractable> interactables) && interactables.Remove(interactable))
{
return;
}
Expand All @@ -103,21 +137,25 @@ public void UnregisterInteractable(StatefulInteractable interactable, string key
private static readonly ProfilerMarker OnKeywordRecognizedPerfMarker =
new ProfilerMarker("[MRTK] SpeechInteractor.OnKeywordRecognized");

private void OnKeywordRecognized(string keyword)
/// <summary>
/// Invoked when a keyword registered with this interactor is recognized.
/// </summary>
/// <param name="keyword">The recognized keyword.</param>
protected internal virtual void OnKeywordRecognized(string keyword)
{
using (OnKeywordRecognizedPerfMarker.Auto())
{
if (keywordDictionary.TryGetValue(keyword, out List<StatefulInteractable> interactableList))
if (keywordDictionary.TryGetValue(keyword, out HashSet<StatefulInteractable> interactables))
{
if (interactableList.Count > 0 && interactionManager == null)
if (interactables.Count > 0 && interactionManager == null)
{
Debug.LogError("The speech interactor does not have an Interaction Manager.");
return;
}

foreach (var interactable in interactableList)
foreach (var interactable in interactables)
{
if (!interactable.VoiceRequiresFocus || interactable.isHovered)
if (!interactable.VoiceRequiresFocus || IsHoverRequirementMet(interactable))
{
selectedInteractables.Insert(0, (interactable, VoiceCommandTriggerTime));
interactionManager.SelectEnter(this, interactable as IXRSelectInteractable);
Expand All @@ -127,6 +165,30 @@ private void OnKeywordRecognized(string keyword)
}
}

/// <summary>
/// Determines whether the hover condition is met for the given interactable based on <see cref="HoverMode"/>.
/// </summary>
/// <param name="interactable">The interactable to evaluate.</param>
/// <returns><see langword="true"/> if the hover requirement is met, otherwise <see langword="false"/>.</returns>
protected virtual bool IsHoverRequirementMet(StatefulInteractable interactable)
{
if (interactable == null)
{
return false;
}

switch (hoverMode)
{
case SpeechHoverMode.Gaze:
return interactable.IsGazeHovered?.Active == true;
case SpeechHoverMode.Active:
return interactable.IsActiveHovered?.Active == true;
case SpeechHoverMode.Any:
default:
return interactable.isHovered;
}
}

private static readonly ProfilerMarker ProcessInteractorPerfMarker =
new ProfilerMarker("[MRTK] SpeechInteractor.ProcessInteractor");

Expand Down
Loading
Loading