// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Utilities; using Microsoft.MixedReality.Toolkit.Input; using Microsoft.MixedReality.Toolkit.Windows.Input; using Microsoft.MixedReality.Toolkit.Windows.Utilities; using System; using System.Collections.Generic; using UnityEngine; #if HP_CONTROLLER_ENABLED using Microsoft.MixedReality.Input; using MotionControllerHandedness = Microsoft.MixedReality.Input.Handedness; using Handedness = Microsoft.MixedReality.Toolkit.Utilities.Handedness; #endif #if UNITY_WSA using System.Linq; using Unity.Profiling; using UnityEngine.XR.WSA.Input; using WsaGestureSettings = UnityEngine.XR.WSA.Input.GestureSettings; #endif // UNITY_WSA #if WINDOWS_UWP using Windows.Perception; using Windows.Perception.People; using Windows.UI.Input.Spatial; #elif UNITY_WSA && DOTNETWINRT_PRESENT using Microsoft.Windows.Perception; using Microsoft.Windows.Perception.People; using Microsoft.Windows.UI.Input.Spatial; #endif namespace Microsoft.MixedReality.Toolkit.WindowsMixedReality.Input { [MixedRealityDataProvider( typeof(IMixedRealityInputSystem), SupportedPlatforms.WindowsUniversal, "Windows Mixed Reality Device Manager", supportedUnityXRPipelines: SupportedUnityXRPipelines.LegacyXR)] public class WindowsMixedRealityDeviceManager : BaseInputDeviceManager, IMixedRealityCapabilityCheck { /// /// Constructor. /// /// The instance that loaded the data provider. /// The instance that receives data from this provider. /// Friendly name of the service. /// Service priority. Used to determine order of instantiation. /// The service's configuration profile. [Obsolete("This constructor is obsolete (registrar parameter is no longer required) and will be removed in a future version of the Microsoft Mixed Reality Toolkit.")] public WindowsMixedRealityDeviceManager( IMixedRealityServiceRegistrar registrar, IMixedRealityInputSystem inputSystem, string name = null, uint priority = DefaultPriority, BaseMixedRealityProfile profile = null) : this(inputSystem, name, priority, profile) { Registrar = registrar; } /// /// Constructor. /// /// The instance that receives data from this provider. /// Friendly name of the service. /// Service priority. Used to determine order of instantiation. /// The service's configuration profile. public WindowsMixedRealityDeviceManager( IMixedRealityInputSystem inputSystem, string name = null, uint priority = DefaultPriority, BaseMixedRealityProfile profile = null) : base(inputSystem, name, priority, profile) { } #region IMixedRealityCapabilityCheck Implementation /// public bool CheckCapability(MixedRealityCapability capability) { if (WindowsApiChecker.IsMethodAvailable( "Windows.UI.Input.Spatial", "SpatialInteractionManager", "IsSourceKindSupported")) { #if WINDOWS_UWP switch (capability) { case MixedRealityCapability.ArticulatedHand: case MixedRealityCapability.GGVHand: return SpatialInteractionManager.IsSourceKindSupported(SpatialInteractionSourceKind.Hand); case MixedRealityCapability.MotionController: return SpatialInteractionManager.IsSourceKindSupported(SpatialInteractionSourceKind.Controller); } #endif // WINDOWS_UWP } else { if (!UnityEngine.XR.WSA.HolographicSettings.IsDisplayOpaque) { // HoloLens supports GGV hands return (capability == MixedRealityCapability.GGVHand); } else { // Windows Mixed Reality immersive devices support motion controllers return (capability == MixedRealityCapability.MotionController); } } return false; } #endregion IMixedRealityCapabilityCheck Implementation #if HP_CONTROLLER_ENABLED private MotionControllerWatcher motionControllerWatcher; /// /// Dictionary to capture all active HP controllers detected /// private readonly Dictionary trackedMotionControllerStates = new Dictionary(); #endif #if UNITY_WSA /// /// The initial size of interactionmanagerStates. /// /// /// This value is arbitrary but chosen to be a number larger than the typical expected number (to avoid /// having to do further allocations). /// public const int MaxInteractionSourceStates = 20; /// /// This number controls how much the interactionmanagerStates array should grow by each time it must /// be resized (larger) in order to accommodate more InteractionSourceState values. /// /// /// This must be a value greater than 1. /// private const int InteractionManagerStatesGrowthFactor = 2; /// /// Dictionary to capture all active controllers detected /// private readonly Dictionary activeControllers = new Dictionary(); /// /// Cache of the states captured from the Unity InteractionManager for UWP /// InteractionSourceState[] interactionManagerStates = new InteractionSourceState[MaxInteractionSourceStates]; /// /// The number of states captured most recently /// private int numInteractionManagerStates; /// /// The current source state reading for the Unity InteractionManager for UWP /// public InteractionSourceState[] LastInteractionManagerStateReading { get; protected set; } /// public override IMixedRealityController[] GetActiveControllers() { return activeControllers.Values.ToArray(); } #region Gesture Settings private static bool gestureRecognizerEnabled; /// /// Enables or disables the gesture recognizer. /// /// /// Automatically disabled navigation recognizer if enabled. /// public static bool GestureRecognizerEnabled { get { return gestureRecognizerEnabled; } set { if (gestureRecognizer == null) { gestureRecognizerEnabled = false; return; } gestureRecognizerEnabled = value; if (!Application.isPlaying) { return; } if (!gestureRecognizer.IsCapturingGestures() && gestureRecognizerEnabled) { NavigationRecognizerEnabled = false; gestureRecognizer.StartCapturingGestures(); } if (gestureRecognizer.IsCapturingGestures() && !gestureRecognizerEnabled) { gestureRecognizer.CancelGestures(); } } } private static bool navigationRecognizerEnabled; /// /// Enables or disables the navigation recognizer. /// /// /// Automatically disables the gesture recognizer if enabled. /// public static bool NavigationRecognizerEnabled { get { return navigationRecognizerEnabled; } set { if (navigationGestureRecognizer == null) { navigationRecognizerEnabled = false; return; } navigationRecognizerEnabled = value; if (!Application.isPlaying) { return; } if (!navigationGestureRecognizer.IsCapturingGestures() && navigationRecognizerEnabled) { GestureRecognizerEnabled = false; navigationGestureRecognizer.StartCapturingGestures(); } if (navigationGestureRecognizer.IsCapturingGestures() && !navigationRecognizerEnabled) { navigationGestureRecognizer.CancelGestures(); } } } private static WindowsGestureSettings gestureSettings = WindowsGestureSettings.Hold | WindowsGestureSettings.ManipulationTranslate; /// /// Current Gesture Settings for the GestureRecognizer /// public static WindowsGestureSettings GestureSettings { get { return gestureSettings; } set { gestureSettings = value; if (Application.isPlaying) { gestureRecognizer?.UpdateAndResetGestures(WSAGestureSettings); } } } private static WindowsGestureSettings navigationSettings = WindowsGestureSettings.NavigationX | WindowsGestureSettings.NavigationY | WindowsGestureSettings.NavigationZ; /// /// Current Navigation Gesture Recognizer Settings. /// public static WindowsGestureSettings NavigationSettings { get { return navigationSettings; } set { navigationSettings = value; if (Application.isPlaying && !useRailsNavigation) { navigationGestureRecognizer?.UpdateAndResetGestures(WSANavigationSettings); } } } private static WindowsGestureSettings railsNavigationSettings = WindowsGestureSettings.NavigationRailsX | WindowsGestureSettings.NavigationRailsY | WindowsGestureSettings.NavigationRailsZ; /// /// Current Navigation Gesture Recognizer Rails Settings. /// public static WindowsGestureSettings RailsNavigationSettings { get { return railsNavigationSettings; } set { railsNavigationSettings = value; if (Application.isPlaying && useRailsNavigation) { navigationGestureRecognizer?.UpdateAndResetGestures(WSARailsNavigationSettings); } } } private static bool useRailsNavigation = true; /// /// Should the Navigation Gesture Recognizer use Rails? /// public static bool UseRailsNavigation { get { return useRailsNavigation; } set { useRailsNavigation = value; if (Application.isPlaying) { navigationGestureRecognizer?.UpdateAndResetGestures(useRailsNavigation ? WSARailsNavigationSettings : WSANavigationSettings); } } } private MixedRealityInputAction holdAction = MixedRealityInputAction.None; private MixedRealityInputAction navigationAction = MixedRealityInputAction.None; private MixedRealityInputAction manipulationAction = MixedRealityInputAction.None; private MixedRealityInputAction selectAction = MixedRealityInputAction.None; private static GestureRecognizer gestureRecognizer; private static WsaGestureSettings WSAGestureSettings => (WsaGestureSettings)gestureSettings; private static GestureRecognizer navigationGestureRecognizer; private static WsaGestureSettings WSANavigationSettings => (WsaGestureSettings)navigationSettings; private static WsaGestureSettings WSARailsNavigationSettings => (WsaGestureSettings)railsNavigationSettings; #endregion Gesture Settings #region IMixedRealityDeviceManager Interface #if (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP private IMixedRealityGazeProviderHeadOverride mixedRealityGazeProviderHeadOverride = null; #endif // (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP /// public override void Enable() { if (!Application.isPlaying) { return; } if (InputSystemProfile == null) { return; } #if (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP if (WindowsMixedRealityUtilities.UtilitiesProvider == null) { WindowsMixedRealityUtilities.UtilitiesProvider = new WindowsMixedRealityUtilitiesProvider(); } mixedRealityGazeProviderHeadOverride = Service?.GazeProvider as IMixedRealityGazeProviderHeadOverride; #endif // (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP #if HP_CONTROLLER_ENABLED // Listens to events to track the HP Motion Controller motionControllerWatcher = new MotionControllerWatcher(); motionControllerWatcher.MotionControllerAdded += AddTrackedMotionController; motionControllerWatcher.MotionControllerRemoved += RemoveTrackedMotionController; var nowait = motionControllerWatcher.StartAsync(); #endif if (InputSystemProfile.GesturesProfile != null) { var gestureProfile = InputSystemProfile.GesturesProfile; GestureSettings = gestureProfile.ManipulationGestures; NavigationSettings = gestureProfile.NavigationGestures; RailsNavigationSettings = gestureProfile.RailsNavigationGestures; UseRailsNavigation = gestureProfile.UseRailsNavigation; for (int i = 0; i < gestureProfile.Gestures.Length; i++) { var gesture = gestureProfile.Gestures[i]; switch (gesture.GestureType) { case GestureInputType.Hold: holdAction = gesture.Action; break; case GestureInputType.Manipulation: manipulationAction = gesture.Action; break; case GestureInputType.Navigation: navigationAction = gesture.Action; break; case GestureInputType.Select: selectAction = gesture.Action; break; } } } RegisterGestureEvents(); RegisterNavigationEvents(); InteractionManager.InteractionSourceDetected += InteractionManager_InteractionSourceDetected; InteractionManager.InteractionSourceLost += InteractionManager_InteractionSourceLost; InteractionManager.InteractionSourcePressed += InteractionManager_InteractionSourcePressed; InteractionManager.InteractionSourceReleased += InteractionManager_InteractionSourceReleased; UpdateInteractionManagerReading(); // NOTE: We update the source state data, in case an app wants to query it on source detected. for (var i = 0; i < numInteractionManagerStates; i++) { GetOrAddController(interactionManagerStates[i]); } if (InputSystemProfile.GesturesProfile != null && InputSystemProfile.GesturesProfile.WindowsGestureAutoStart == AutoStartBehavior.AutoStart) { GestureRecognizerEnabled = true; } // Call the base here to ensure any early exits do not // artificially declare the service as enabled. base.Enable(); } private static readonly ProfilerMarker GetOrAddControllerPerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealityDeviceManager.GetOrAddController"); private void GetOrAddController(InteractionSourceState interactionSourceState) { using (GetOrAddControllerPerfMarker.Auto()) { // If this is a new detected controller, raise source detected event with input system // check needs to be here because GetOrAddController adds it to the activeControllers Dictionary // this could be cleaned up because that's not clear bool raiseSourceDetected = !activeControllers.ContainsKey(interactionSourceState.source.id); var controller = GetOrAddController(interactionSourceState.source); if (controller != null) { if (raiseSourceDetected) { Service?.RaiseSourceDetected(controller.InputSource, controller); } controller.UpdateController(interactionSourceState); } } } private static readonly ProfilerMarker UpdatePerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealityDeviceManager.Update"); /// public override void Update() { using (UpdatePerfMarker.Auto()) { base.Update(); #if (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP if (mixedRealityGazeProviderHeadOverride != null && mixedRealityGazeProviderHeadOverride.UseHeadGazeOverride && WindowsMixedRealityUtilities.SpatialCoordinateSystem != null) { SpatialPointerPose pointerPose = SpatialPointerPose.TryGetAtTimestamp(WindowsMixedRealityUtilities.SpatialCoordinateSystem, PerceptionTimestampHelper.FromHistoricalTargetTime(DateTimeOffset.Now)); if (pointerPose != null) { HeadPose head = pointerPose.Head; if (head != null) { mixedRealityGazeProviderHeadOverride.OverrideHeadGaze(head.Position.ToUnityVector3(), head.ForwardDirection.ToUnityVector3()); } } } #endif // (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP UpdateInteractionManagerReading(); for (var i = 0; i < numInteractionManagerStates; i++) { // SourceDetected gets raised when a new controller is detected and, if previously present, // when OnEnable is called. Do not create a new controller here. var controller = GetOrAddController(interactionManagerStates[i].source, false); if (controller != null) { controller.UpdateController(interactionManagerStates[i]); } } LastInteractionManagerStateReading = interactionManagerStates; } } private void RegisterGestureEvents() { if (holdAction != MixedRealityInputAction.None || manipulationAction != MixedRealityInputAction.None || selectAction != MixedRealityInputAction.None) { if (gestureRecognizer == null) { try { gestureRecognizer = new GestureRecognizer(); } catch (Exception ex) { Debug.LogWarning($"Failed to create gesture recognizer. OS version might not support it. Exception: {ex}"); gestureRecognizer = null; return; } gestureRecognizer.SetRecognizableGestures(WSAGestureSettings); } if (holdAction != MixedRealityInputAction.None) { gestureRecognizer.HoldStarted += GestureRecognizer_HoldStarted; gestureRecognizer.HoldCompleted += GestureRecognizer_HoldCompleted; gestureRecognizer.HoldCanceled += GestureRecognizer_HoldCanceled; } if (manipulationAction != MixedRealityInputAction.None) { gestureRecognizer.ManipulationStarted += GestureRecognizer_ManipulationStarted; gestureRecognizer.ManipulationUpdated += GestureRecognizer_ManipulationUpdated; gestureRecognizer.ManipulationCompleted += GestureRecognizer_ManipulationCompleted; gestureRecognizer.ManipulationCanceled += GestureRecognizer_ManipulationCanceled; } if (selectAction != MixedRealityInputAction.None) { gestureRecognizer.Tapped += GestureRecognizer_Tapped; } } } private void UnregisterGestureEvents() { if (gestureRecognizer == null) { return; } if (holdAction != MixedRealityInputAction.None) { gestureRecognizer.HoldStarted -= GestureRecognizer_HoldStarted; gestureRecognizer.HoldCompleted -= GestureRecognizer_HoldCompleted; gestureRecognizer.HoldCanceled -= GestureRecognizer_HoldCanceled; } if (manipulationAction != MixedRealityInputAction.None) { gestureRecognizer.ManipulationStarted -= GestureRecognizer_ManipulationStarted; gestureRecognizer.ManipulationUpdated -= GestureRecognizer_ManipulationUpdated; gestureRecognizer.ManipulationCompleted -= GestureRecognizer_ManipulationCompleted; gestureRecognizer.ManipulationCanceled -= GestureRecognizer_ManipulationCanceled; } if (selectAction != MixedRealityInputAction.None) { gestureRecognizer.Tapped -= GestureRecognizer_Tapped; } } private void RegisterNavigationEvents() { if (navigationAction != MixedRealityInputAction.None) { if (navigationGestureRecognizer == null) { try { navigationGestureRecognizer = new GestureRecognizer(); } catch (Exception ex) { Debug.LogWarning($"Failed to create gesture recognizer. OS version might not support it. Exception: {ex}"); navigationGestureRecognizer = null; return; } navigationGestureRecognizer.SetRecognizableGestures(useRailsNavigation ? WSARailsNavigationSettings : WSANavigationSettings); } navigationGestureRecognizer.NavigationStarted += NavigationGestureRecognizer_NavigationStarted; navigationGestureRecognizer.NavigationUpdated += NavigationGestureRecognizer_NavigationUpdated; navigationGestureRecognizer.NavigationCompleted += NavigationGestureRecognizer_NavigationCompleted; navigationGestureRecognizer.NavigationCanceled += NavigationGestureRecognizer_NavigationCanceled; } } private void UnregisterNavigationEvents() { if (navigationGestureRecognizer == null) { return; } navigationGestureRecognizer.NavigationStarted -= NavigationGestureRecognizer_NavigationStarted; navigationGestureRecognizer.NavigationUpdated -= NavigationGestureRecognizer_NavigationUpdated; navigationGestureRecognizer.NavigationCompleted -= NavigationGestureRecognizer_NavigationCompleted; navigationGestureRecognizer.NavigationCanceled -= NavigationGestureRecognizer_NavigationCanceled; } /// public override void Disable() { UnregisterGestureEvents(); gestureRecognizer?.Dispose(); gestureRecognizer = null; UnregisterNavigationEvents(); navigationGestureRecognizer?.Dispose(); navigationGestureRecognizer = null; InteractionManager.InteractionSourceDetected -= InteractionManager_InteractionSourceDetected; InteractionManager.InteractionSourceLost -= InteractionManager_InteractionSourceLost; InteractionManager.InteractionSourcePressed -= InteractionManager_InteractionSourcePressed; InteractionManager.InteractionSourceReleased -= InteractionManager_InteractionSourceReleased; InteractionSourceState[] states = InteractionManager.GetCurrentReading(); for (var i = 0; i < states.Length; i++) { RemoveController(states[i].source); } base.Disable(); } #endregion IMixedRealityDeviceManager Interface #region Controller Utilities /// /// Creates a unique key for the controller based on its vendor ID, product ID, version number, and handedness. /// private uint GetControllerId(uint vid, uint pid, uint version, uint handedness) { return (vid << 48) + (pid << 32) + (version << 16) + handedness; } #if HP_CONTROLLER_ENABLED private uint GetControllerId(MotionController mc) { var handedness = ((uint)(mc.Handedness == MotionControllerHandedness.Right ? 2 : (mc.Handedness == MotionControllerHandedness.Left ? 1 : 0))); return GetControllerId(mc.VendorId, mc.ProductId, mc.Version, handedness); } #endif private uint GetControllerId(InteractionSource interactionSource) { var handedness = ((uint)(interactionSource.handedness == InteractionSourceHandedness.Right ? 2 : (interactionSource.handedness == InteractionSourceHandedness.Left ? 1 : 0))); return GetControllerId(interactionSource.vendorId, interactionSource.productId, interactionSource.productVersion, handedness); } /// /// Retrieve the source controller from the Active Store, or create a new device and register it /// /// Source State provided by the SDK /// Should the Source be added as a controller if it isn't found? /// New or Existing Controller Input Source private BaseWindowsMixedRealitySource GetOrAddController(InteractionSource interactionSource, bool addController = true) { uint controllerId = GetControllerId(interactionSource); // If a device is already registered with the ID provided, just return it. if (activeControllers.ContainsKey(controllerId)) { var controller = activeControllers[controllerId] as BaseWindowsMixedRealitySource; Debug.Assert(controller != null); return controller; } if (!addController) { return null; } Handedness controllingHand; switch (interactionSource.handedness) { default: controllingHand = Handedness.None; break; case InteractionSourceHandedness.Left: controllingHand = Handedness.Left; break; case InteractionSourceHandedness.Right: controllingHand = Handedness.Right; break; } IMixedRealityPointer[] pointers = null; InputSourceType inputSourceType = InputSourceType.Other; switch (interactionSource.kind) { case InteractionSourceKind.Controller: if (interactionSource.supportsPointing) { pointers = RequestPointers(SupportedControllerType.WindowsMixedReality, controllingHand); } else { pointers = RequestPointers(SupportedControllerType.GGVHand, controllingHand); } inputSourceType = InputSourceType.Controller; break; case InteractionSourceKind.Hand: if (interactionSource.supportsPointing) { pointers = RequestPointers(SupportedControllerType.ArticulatedHand, controllingHand); } else { pointers = RequestPointers(SupportedControllerType.GGVHand, controllingHand); } inputSourceType = InputSourceType.Hand; break; case InteractionSourceKind.Voice: // set to null: when pointers are null we use head gaze, which is what we want for voice break; default: Debug.LogError($"Unknown new type in WindowsMixedRealityDeviceManager {interactionSource.kind}, make sure to add a SupportedControllerType"); break; } bool isHPController = !interactionSource.supportsTouchpad && interactionSource.kind == InteractionSourceKind.Controller; string kindModifier = interactionSource.kind.ToString(); string handednessModifier = controllingHand == Handedness.None ? string.Empty : controllingHand.ToString(); string inputSourceName = isHPController ? $"HP Motion {kindModifier} {handednessModifier}" : $"Mixed Reality {kindModifier} {handednessModifier}"; var inputSource = Service?.RequestNewGenericInputSource(inputSourceName, pointers, inputSourceType); BaseWindowsMixedRealitySource detectedController; if (interactionSource.supportsPointing) { if (interactionSource.kind == InteractionSourceKind.Hand) { detectedController = new WindowsMixedRealityArticulatedHand(TrackingState.NotTracked, controllingHand, inputSource); if (!detectedController.Enabled) { // Controller failed to be setup correctly. // Return null so we don't raise the source detected. return null; } } else if (interactionSource.kind == InteractionSourceKind.Controller) { if (isHPController) { // Add the controller as a HP Motion Controller HPMotionController hpController = new HPMotionController(TrackingState.NotTracked, controllingHand, inputSource); #if HP_CONTROLLER_ENABLED lock (trackedMotionControllerStates) { if (trackedMotionControllerStates.ContainsKey(controllerId)) { hpController.MotionControllerState = trackedMotionControllerStates[controllerId]; } } #endif detectedController = hpController; } else { detectedController = new WindowsMixedRealityController(TrackingState.NotTracked, controllingHand, inputSource); } } else { Debug.Log($"Unhandled source type {interactionSource.kind} detected."); return null; } } else { detectedController = new WindowsMixedRealityGGVHand(TrackingState.NotTracked, controllingHand, inputSource); } if (!detectedController.Enabled) { // Controller failed to be setup correctly. // Return null so we don't raise the source detected. return null; } for (int i = 0; i < detectedController.InputSource?.Pointers?.Length; i++) { detectedController.InputSource.Pointers[i].Controller = detectedController; } activeControllers.Add(controllerId, detectedController); return detectedController; } /// /// Remove the selected controller from the Active Store /// /// Source State provided by the SDK to remove private void RemoveController(InteractionSource interactionSource) { var controller = GetOrAddController(interactionSource, false); var controllerId = GetControllerId(interactionSource); if (controller != null) { RemoveControllerFromScene(controller); activeControllers.Remove(controllerId); } } /// /// Removes the controller from the scene and handles any additional cleanup /// private void RemoveControllerFromScene(BaseWindowsMixedRealitySource controller) { Service?.RaiseSourceLost(controller.InputSource, controller); RecyclePointers(controller.InputSource); var visualizer = controller.Visualizer; if (!visualizer.IsNull() && visualizer.GameObjectProxy != null) { visualizer.GameObjectProxy.SetActive(false); } } [Obsolete("This function exists to workaround a bug in Unity and will be removed in an upcoming release. For more details, see https://github.com/microsoft/MixedRealityToolkit-Unity/pull/8101")] public void RemoveControllerForSuspendWorkaround(InteractionSource interactionSource) { RemoveController(interactionSource); } #if HP_CONTROLLER_ENABLED private void AddTrackedMotionController(object sender, MotionController motionController) { lock (trackedMotionControllerStates) { uint controllerId = GetControllerId(motionController); trackedMotionControllerStates[controllerId] = new MotionControllerState(motionController); if (activeControllers.ContainsKey(controllerId) && activeControllers[controllerId] is HPMotionController hpController) { hpController.MotionControllerState = trackedMotionControllerStates[controllerId]; } } } private void RemoveTrackedMotionController(object sender, MotionController motionController) { lock (trackedMotionControllerStates) { uint controllerId = GetControllerId(motionController); trackedMotionControllerStates.Remove(controllerId); if (activeControllers.ContainsKey(controllerId) && activeControllers[controllerId] is HPMotionController hpController) { hpController.MotionControllerState = null; } } } #endif #endregion Controller Utilities #region Unity InteractionManager Events /// /// SDK Interaction Source Detected Event handler /// /// SDK source detected event arguments private void InteractionManager_InteractionSourceDetected(InteractionSourceDetectedEventArgs args) => GetOrAddController(args.state); /// /// SDK Interaction Source Pressed Event handler. Used only for voice. /// /// SDK source pressed event arguments private void InteractionManager_InteractionSourcePressed(InteractionSourcePressedEventArgs args) { if (args.state.source.kind == InteractionSourceKind.Voice) { var controller = GetOrAddController(args.state.source); if (controller != null) { controller.UpdateController(args.state); // On WMR, the voice recognizer does not actually register the phrase 'select' // when you add it to the speech commands profile. Therefore, simulate // the "select" voice command running to ensure that we get a select voice command // registered. This is used by FocusProvider to detect when the select pointer is active Service?.RaiseSpeechCommandRecognized(controller.InputSource, RecognitionConfidenceLevel.High, TimeSpan.MinValue, DateTime.Now, new SpeechCommands("select", KeyCode.Alpha1, MixedRealityInputAction.None)); } } } /// /// SDK Interaction Source Released Event handler. Used only for voice. /// /// SDK source released event arguments private void InteractionManager_InteractionSourceReleased(InteractionSourceReleasedEventArgs args) { if (args.state.source.kind == InteractionSourceKind.Voice) { GetOrAddController(args.state.source)?.UpdateController(args.state); } } /// /// SDK Interaction Source Lost Event handler /// /// SDK source updated event arguments private void InteractionManager_InteractionSourceLost(InteractionSourceLostEventArgs args) { RemoveController(args.state.source); } #endregion Unity InteractionManager Events #region Gesture Recognizer Events private void GestureRecognizer_HoldStarted(HoldStartedEventArgs args) { var controller = GetOrAddController(args.source, false); if (controller != null) { Service?.RaiseGestureStarted(controller, holdAction); } } private void GestureRecognizer_HoldCompleted(HoldCompletedEventArgs args) { var controller = GetOrAddController(args.source, false); if (controller != null) { Service?.RaiseGestureCompleted(controller, holdAction); } } private void GestureRecognizer_HoldCanceled(HoldCanceledEventArgs args) { var controller = GetOrAddController(args.source, false); if (controller != null) { Service?.RaiseGestureCanceled(controller, holdAction); } } private void GestureRecognizer_ManipulationStarted(ManipulationStartedEventArgs args) { var controller = GetOrAddController(args.source, false); if (controller != null) { Service?.RaiseGestureStarted(controller, manipulationAction); } } private void GestureRecognizer_ManipulationUpdated(ManipulationUpdatedEventArgs args) { var controller = GetOrAddController(args.source, false); if (controller != null) { Service?.RaiseGestureUpdated(controller, manipulationAction, args.cumulativeDelta); } } private void GestureRecognizer_ManipulationCompleted(ManipulationCompletedEventArgs args) { var controller = GetOrAddController(args.source, false); if (controller != null) { Service?.RaiseGestureCompleted(controller, manipulationAction, args.cumulativeDelta); } } private void GestureRecognizer_ManipulationCanceled(ManipulationCanceledEventArgs args) { var controller = GetOrAddController(args.source, false); if (controller != null) { Service?.RaiseGestureCanceled(controller, manipulationAction); } } private void GestureRecognizer_Tapped(TappedEventArgs args) { var controller = GetOrAddController(args.source, false); if (controller != null) { Service?.RaiseGestureCompleted(controller, selectAction); } } #endregion Gesture Recognizer Events #region Navigation Recognizer Events private void NavigationGestureRecognizer_NavigationStarted(NavigationStartedEventArgs args) { var controller = GetOrAddController(args.source, false); if (controller != null) { Service?.RaiseGestureStarted(controller, navigationAction); } } private void NavigationGestureRecognizer_NavigationUpdated(NavigationUpdatedEventArgs args) { var controller = GetOrAddController(args.source, false); if (controller != null) { Service?.RaiseGestureUpdated(controller, navigationAction, args.normalizedOffset); } } private void NavigationGestureRecognizer_NavigationCompleted(NavigationCompletedEventArgs args) { var controller = GetOrAddController(args.source, false); if (controller != null) { Service?.RaiseGestureCompleted(controller, navigationAction, args.normalizedOffset); } } private void NavigationGestureRecognizer_NavigationCanceled(NavigationCanceledEventArgs args) { var controller = GetOrAddController(args.source, false); if (controller != null) { Service?.RaiseGestureCanceled(controller, navigationAction); } } #endregion Navigation Recognizer Events #region Private Methods private static readonly ProfilerMarker UpdateInteractionManagerReadingPerfMarker = new ProfilerMarker("[MRTK] WindowsMixedRealityDeviceManager.UpdateInteractionManagerReading"); /// /// Gets the latest interaction manager states and counts from InteractionManager /// /// /// Abstracts away some of the array resize handling and another underlying Unity issue /// when InteractionManager.GetCurrentReading is called when there are no detected sources. /// private void UpdateInteractionManagerReading() { using (UpdateInteractionManagerReadingPerfMarker.Auto()) { int newSourceStateCount = InteractionManager.numSourceStates; // If there isn't enough space in the cache to hold the results, we should grow it so that it can, but also // grow it in a way that is unlikely to require re-allocations each time. if (newSourceStateCount > interactionManagerStates.Length) { interactionManagerStates = new InteractionSourceState[newSourceStateCount * InteractionManagerStatesGrowthFactor]; } // Note that InteractionManager.GetCurrentReading throws when invoked when the number of // source states is zero. In that case, we want to just update the number of read states to be zero. if (newSourceStateCount == 0) { // clean up existing controllers that didn't trigger the InteractionSourceLost event. // this can happen eg. when unity is registering cached controllers from a previous play session in the editor. // those actually don't exist in the current session and therefor won't receive the InteractionSourceLost once // Unity's InteractionManager catches up for (int i = 0; i < numInteractionManagerStates; ++i) { RemoveController(interactionManagerStates[i].source); } numInteractionManagerStates = newSourceStateCount; } else { numInteractionManagerStates = InteractionManager.GetCurrentReading(interactionManagerStates); } } } #endregion Private Methods #endif // UNITY_WSA } }