// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Utilities;
using System.Collections.Generic;
using Unity.Profiling;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Input
{
///
/// Class providing a base implementation of the interface.
///
public abstract class BaseInputDeviceManager : BaseDataProvider, IMixedRealityInputDeviceManager
{
private bool enablePointerCache = true;
///
/// Control mechanism to enable/disable use of Pointer Cache in request/recycling of pointers by Input System
///
public bool EnablePointerCache
{
get => enablePointerCache;
set
{
if (enablePointerCache != value)
{
enablePointerCache = value;
if (!enablePointerCache)
{
DestroyPointerCache();
}
}
}
}
///
/// The input system configuration profile in use in the application.
///
protected MixedRealityInputSystemProfile InputSystemProfile => Service?.InputSystemProfile;
///
public virtual IMixedRealityController[] GetActiveControllers() => System.Array.Empty();
///
/// 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.
[System.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.")]
protected BaseInputDeviceManager(
IMixedRealityServiceRegistrar registrar,
IMixedRealityInputSystem inputSystem,
string name,
uint priority,
BaseMixedRealityProfile profile) : 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.
protected BaseInputDeviceManager(
IMixedRealityInputSystem inputSystem,
string name,
uint priority,
BaseMixedRealityProfile profile) : base(inputSystem, name, priority, profile) { }
#region Private members
private struct PointerConfig
{
public PointerOption profile;
public Stack cache;
}
private PointerConfig[] pointerConfigurations = System.Array.Empty();
private class PointerEqualityComparer : IEqualityComparer
{
private static PointerEqualityComparer defaultComparer;
internal static PointerEqualityComparer Default => defaultComparer ?? (defaultComparer = new PointerEqualityComparer());
///
/// Check that references equals for two pointers
///
public bool Equals(IMixedRealityPointer p1, IMixedRealityPointer p2)
{
return ReferenceEquals(p1, p2);
}
///
/// Unity objects have unique equals comparison and to check keys in a dictionary,
/// we want the hash code match to be Unity's unique InstanceID to compare objects.
///
public int GetHashCode(IMixedRealityPointer pointer)
{
if (pointer is MonoBehaviour pointerObj)
{
return pointerObj.GetInstanceID();
}
else
{
return pointer.GetHashCode();
}
}
}
private static readonly ProfilerMarker RequestPointersPerfMarker = new ProfilerMarker("[MRTK] BaseInputDeviceManager.RequestPointers");
// Active pointers associated with the config index they were spawned from
private readonly Dictionary activePointersToConfig
= new Dictionary(PointerEqualityComparer.Default);
#endregion
#region IMixedRealityService implementation
///
public override void Initialize()
{
base.Initialize();
if (InputSystemProfile != null && InputSystemProfile.PointerProfile != null)
{
var initPointerOptions = InputSystemProfile.PointerProfile.PointerOptions;
// If we were previously initialized, then clear our old pointer cache
if (pointerConfigurations != null && pointerConfigurations.Length > 0)
{
DestroyPointerCache();
}
pointerConfigurations = new PointerConfig[initPointerOptions.Length];
activePointersToConfig.Clear();
for (int i = 0; i < initPointerOptions.Length; i++)
{
pointerConfigurations[i].profile = initPointerOptions[i];
pointerConfigurations[i].cache = new Stack();
}
}
}
///
public override void Destroy()
{
DestroyPointerCache();
// Loop through active pointers in scene, destroy all gameobjects and clear our tracking dictionary
foreach (var pointer in activePointersToConfig.Keys)
{
if (pointer.TryGetMonoBehaviour(out MonoBehaviour pointerComponent))
{
GameObjectExtensions.DestroyGameObject(pointerComponent.gameObject);
}
}
pointerConfigurations = System.Array.Empty();
activePointersToConfig.Clear();
base.Destroy();
}
#endregion
#region Pointer utilization and caching
///
/// Request an array of pointers for the controller type.
///
/// The controller type making the request for pointers.
/// The handedness of the controller making the request.
/// Only register pointers with a specific type.
protected virtual IMixedRealityPointer[] RequestPointers(SupportedControllerType controllerType, Handedness controllingHand)
{
using (RequestPointersPerfMarker.Auto())
{
var returnPointers = new List();
CleanActivePointers();
for (int i = 0; i < pointerConfigurations.Length; i++)
{
var option = pointerConfigurations[i].profile;
if (option.ControllerType.IsMaskSet(controllerType) && option.Handedness.IsMaskSet(controllingHand))
{
IMixedRealityPointer requestedPointer = null;
if (EnablePointerCache)
{
var pointerCache = pointerConfigurations[i].cache;
while (pointerCache.Count > 0)
{
var p = pointerCache.Pop();
if (p.TryGetMonoBehaviour(out MonoBehaviour pointerComponent))
{
pointerComponent.gameObject.SetActive(true);
// We got pointer from cache, continue to next pointer option to review
requestedPointer = p;
DebugUtilities.LogVerboseFormat("RequestPointers: Reusing a cached pointer {0} for controller type {1} and handedness {2}",
requestedPointer,
controllerType,
controllingHand);
break;
}
}
}
if (requestedPointer == null)
{
// We couldn't obtain a pointer from our cache, resort to creating a new one
requestedPointer = CreatePointer(ref option);
}
if (requestedPointer != null)
{
// Track pointer for recycling
activePointersToConfig.Add(requestedPointer, (uint)i);
returnPointers.Add(requestedPointer);
}
}
}
return returnPointers.Count == 0 ? null : returnPointers.ToArray();
}
}
private static readonly ProfilerMarker RecyclePointersPerfMarker = new ProfilerMarker("[MRTK] BaseInputDeviceManager.RecyclePointers");
///
/// Recycle all pointers associated with the provided .
/// This involves reseting the pointer, disabling the pointer GameObject, and possibly caching it for re-use.
///
protected virtual void RecyclePointers(IMixedRealityInputSource inputSource)
{
using (RecyclePointersPerfMarker.Auto())
{
if (inputSource != null)
{
CleanActivePointers();
var pointers = inputSource.Pointers;
for (int i = 0; i < pointers.Length; i++)
{
var pointer = pointers[i];
if (pointers[i].TryGetMonoBehaviour(out MonoBehaviour pointerComponent))
{
// Unfortunately, it's possible the gameobject source is *being* destroyed so we are not null now but will be soon.
// At least if this is a controller we know about and we expect it to be destroyed, skip
if (pointer is IMixedRealityControllerPoseSynchronizer controller && controller.DestroyOnSourceLost)
{
continue;
}
if (EnablePointerCache)
{
pointer.Reset();
pointerComponent.gameObject.SetActive(false);
if (EnablePointerCache && activePointersToConfig.ContainsKey(pointer))
{
uint pointerOptionIndex = activePointersToConfig[pointer];
activePointersToConfig.Remove(pointer);
// Add our pointer back to our cache
pointerConfigurations[(int)pointerOptionIndex].cache.Push(pointer);
}
}
else
{
GameObjectExtensions.DestroyGameObject(pointerComponent.gameObject);
}
}
}
}
}
}
private static readonly ProfilerMarker CreatePointerPerfMarker = new ProfilerMarker("[MRTK] BaseInputDeviceManager.CreatePointer");
///
/// Instantiate the Pointer prefab with supplied PointerOption details. If there is no IMixedRealityPointer on the prefab, then destroy and log error
///
///
/// PointerOption is passed by ref to reduce copy overhead of struct
///
private IMixedRealityPointer CreatePointer(ref PointerOption option)
{
using (CreatePointerPerfMarker.Auto())
{
GameObject pointerObject = Object.Instantiate(option.PointerPrefab, MixedRealityPlayspace.Transform);
IMixedRealityPointer pointer = pointerObject.GetComponent();
if (pointer == null)
{
Debug.LogError($"Ensure that the prefab '{option.PointerPrefab.name}' listed under Input -> Pointers -> Pointer Options has an {typeof(IMixedRealityPointer).Name} component.\nThis prefab can't be used as a pointer as configured and won't be instantiated.");
GameObjectExtensions.DestroyGameObject(pointerObject);
}
// Make sure we init the pointer with the correct raycast LayerMasks, if needed
if (pointer.PrioritizedLayerMasksOverride == null || pointer.PrioritizedLayerMasksOverride.Length == 0)
{
pointer.PrioritizedLayerMasksOverride = option.PrioritizedLayerMasks;
}
return pointer;
}
}
private static readonly ProfilerMarker CleanActivePointersPerfMarker = new ProfilerMarker("[MRTK] BaseInputDeviceManager.CleanActivePointers");
///
/// This class tracks pointers that have been requested and thus are considered "active" GameObjects in the scene.
/// As GameObjects, these pointers may be destroyed and thus their entry becomes "null" although the managed object is not destroyed
/// This helper loops through all dictionary entries and checks if it is null, if so it is removed
///
private void CleanActivePointers()
{
using (CleanActivePointersPerfMarker.Auto())
{
var removal = new List();
var enumerator = activePointersToConfig.GetEnumerator();
while (enumerator.MoveNext())
{
var pointer = enumerator.Current.Key;
if (pointer.IsNull())
{
removal.Add(pointer);
}
}
for (int i = 0; i < removal.Count; i++)
{
activePointersToConfig.Remove(removal[i]);
}
}
}
///
/// Wipes references to cached pointers for every pointer configuration option. All GameObject references are likewise destroyed
///
private void DestroyPointerCache()
{
for (int i = 0; i < pointerConfigurations.Length; i++)
{
while (pointerConfigurations[i].cache.Count > 0)
{
if (pointerConfigurations[i].cache.Pop().TryGetMonoBehaviour(out MonoBehaviour pointerComponent))
{
GameObjectExtensions.DestroyGameObject(pointerComponent.gameObject);
}
}
}
}
#endregion
}
}