
Mixed Reality
- 16 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
mixed-reality is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- mixed-reality
- AI & Agent Building
- AI-coding skill
Mixed Reality by the numbers
- 16 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #11,068 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill mixed-realityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 16 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Mixed Reality with .NET
Trigger On
- building or integrating mixed-reality solutions with C#
- working on HoloLens, MRTK, Azure mixed-reality services, or OpenXR-related code
- reviewing how .NET services support a mixed-reality client
Workflow
1. Acknowledge that much of Microsoft mixed-reality guidance is Unity-centered even when the implementation language is C#; do not pretend it is a standard .NET desktop stack. 2. Separate engine-side concerns, device capability concerns, and backend service integration so the system boundary stays understandable. 3. Use MRTK and OpenXR guidance intentionally, and verify current toolkit status before choosing a package or template path. See references/patterns.md for established architecture patterns. 4. Treat performance, input, and spatial UX as core constraints, not polish items. 5. When .NET mostly lives on the backend for a mixed-reality product, route that backend work through the relevant ASP.NET Core, SignalR, or Azure skill instead of overloading this one. 6. Validate with the actual device or emulator path whenever possible because editor-only success is not enough.
References
- references/patterns.md - MRTK service architecture, OpenXR feature plugins, input action patterns, spatial awareness observers, and cross-cutting patterns for dependency injection, object pooling, and graceful degradation.
- references/examples.md - Common HoloLens scenarios including spatial anchors, hand menus, spatial mapping physics, eye tracking, remote rendering, voice commands, QR code tracking, and shared multi-user experiences.
Deliver
- clear boundaries between device code and backend services
- mixed-reality guidance grounded in current Microsoft tooling
- realistic validation expectations for MR scenarios
Validate
- the chosen toolkit path is current enough for the project
- device-specific constraints are explicit
- backend and client responsibilities are not blurred
{
"version": "1.0.0",
"category": "Cross-Platform UI",
"package_prefix": "Microsoft.MixedReality"
}
Common HoloLens Scenarios
Scenario: Spatial Anchors for Persistent Holograms
Place holograms that persist across sessions using Azure Spatial Anchors:
public class PersistentHologramManager : MonoBehaviour
{
private CloudSpatialAnchorSession _anchorSession;
private readonly Dictionary<string, CloudSpatialAnchor> _anchors = new();
public async Task InitializeAsync(string accountId, string accountKey, string accountDomain)
{
_anchorSession = new CloudSpatialAnchorSession();
_anchorSession.Configuration.AccountId = accountId;
_anchorSession.Configuration.AccountKey = accountKey;
_anchorSession.Configuration.AccountDomain = accountDomain;
_anchorSession.AnchorLocated += OnAnchorLocated;
_anchorSession.LocateAnchorsCompleted += OnLocateAnchorsCompleted;
_anchorSession.Start();
}
public async Task<string> PlaceHologramAsync(GameObject hologram, Pose worldPose)
{
// Create native anchor
var nativeAnchor = hologram.AddComponent<WorldAnchor>();
// Create cloud anchor
var cloudAnchor = new CloudSpatialAnchor
{
LocalAnchor = nativeAnchor.GetNativeSpatialAnchorPtr()
};
// Wait for enough spatial data
while (!_anchorSession.IsReadyForCreate)
{
await Task.Delay(100);
float progress = _anchorSession.SessionStatus.RecommendedForCreateProgress;
Debug.Log($"Spatial data collection: {progress:P0}");
}
await _anchorSession.CreateAnchorAsync(cloudAnchor);
_anchors[cloudAnchor.Identifier] = cloudAnchor;
return cloudAnchor.Identifier;
}
public void LocateSavedAnchors(IEnumerable<string> anchorIds)
{
var criteria = new AnchorLocateCriteria
{
Identifiers = anchorIds.ToArray(),
Strategy = LocateStrategy.AnyStrategy
};
_anchorSession.CreateWatcher(criteria);
}
private void OnAnchorLocated(object sender, AnchorLocatedEventArgs args)
{
if (args.Status == LocateAnchorStatus.Located)
{
var anchor = args.Anchor;
// Restore hologram at anchor position
UnityDispatcher.InvokeOnMainThread(() =>
{
RestoreHologramAtAnchor(anchor);
});
}
}
}Scenario: Hand Menu with MRTK
Create a hand-attached menu that follows the user's palm:
public class HandMenuController : MonoBehaviour
{
[SerializeField] private GameObject menuRoot;
[SerializeField] private float activationAngle = 30f;
[SerializeField] private Handedness targetHand = Handedness.Left;
private IMixedRealityHandJointService _jointService;
private bool _isMenuActive;
private void Start()
{
_jointService = CoreServices.GetInputSystemDataProvider<IMixedRealityHandJointService>();
}
private void Update()
{
if (_jointService == null) return;
if (_jointService.IsHandTracked(targetHand))
{
var palmPose = _jointService.RequestJointTransform(TrackedHandJoint.Palm, targetHand);
UpdateMenuVisibility(palmPose);
if (_isMenuActive)
{
PositionMenu(palmPose);
}
}
else
{
HideMenu();
}
}
private void UpdateMenuVisibility(Transform palmTransform)
{
// Check if palm is facing user (gazing at palm)
var headPosition = CameraCache.Main.transform.position;
var palmToHead = (headPosition - palmTransform.position).normalized;
var palmNormal = palmTransform.up;
float angle = Vector3.Angle(palmNormal, palmToHead);
if (angle < activationAngle && !_isMenuActive)
{
ShowMenu();
}
else if (angle > activationAngle + 10f && _isMenuActive)
{
HideMenu();
}
}
private void PositionMenu(Transform palmTransform)
{
menuRoot.transform.position = palmTransform.position + palmTransform.up * 0.1f;
menuRoot.transform.rotation = Quaternion.LookRotation(
palmTransform.forward,
CameraCache.Main.transform.position - menuRoot.transform.position
);
}
private void ShowMenu()
{
_isMenuActive = true;
menuRoot.SetActive(true);
}
private void HideMenu()
{
_isMenuActive = false;
menuRoot.SetActive(false);
}
}Scenario: Spatial Mapping for Physics
Use spatial mapping meshes for hologram physics interactions:
public class SpatialPhysicsManager : MonoBehaviour, IMixedRealitySpatialAwarenessObservationHandler<SpatialAwarenessMeshObject>
{
[SerializeField] private PhysicMaterial surfaceMaterial;
[SerializeField] private LayerMask spatialMeshLayer;
private readonly Dictionary<int, MeshCollider> _meshColliders = new();
private void OnEnable()
{
CoreServices.SpatialAwarenessSystem?.RegisterHandler<IMixedRealitySpatialAwarenessObservationHandler<SpatialAwarenessMeshObject>>(this);
}
private void OnDisable()
{
CoreServices.SpatialAwarenessSystem?.UnregisterHandler<IMixedRealitySpatialAwarenessObservationHandler<SpatialAwarenessMeshObject>>(this);
}
public void OnObservationAdded(MixedRealitySpatialAwarenessEventData<SpatialAwarenessMeshObject> eventData)
{
var meshObject = eventData.SpatialObject;
AddPhysicsToMesh(meshObject);
}
public void OnObservationUpdated(MixedRealitySpatialAwarenessEventData<SpatialAwarenessMeshObject> eventData)
{
var meshObject = eventData.SpatialObject;
UpdateMeshPhysics(meshObject);
}
public void OnObservationRemoved(MixedRealitySpatialAwarenessEventData<SpatialAwarenessMeshObject> eventData)
{
if (_meshColliders.TryGetValue(eventData.Id, out var collider))
{
Destroy(collider);
_meshColliders.Remove(eventData.Id);
}
}
private void AddPhysicsToMesh(SpatialAwarenessMeshObject meshObject)
{
var meshFilter = meshObject.Filter;
if (meshFilter == null || meshFilter.sharedMesh == null) return;
var collider = meshObject.GameObject.AddComponent<MeshCollider>();
collider.sharedMesh = meshFilter.sharedMesh;
collider.material = surfaceMaterial;
meshObject.GameObject.layer = (int)Mathf.Log(spatialMeshLayer.value, 2);
_meshColliders[meshObject.Id] = collider;
}
private void UpdateMeshPhysics(SpatialAwarenessMeshObject meshObject)
{
if (_meshColliders.TryGetValue(meshObject.Id, out var collider))
{
collider.sharedMesh = meshObject.Filter.sharedMesh;
}
}
}Scenario: Eye Tracking for Gaze Selection
Implement eye-gaze targeting for accessible interaction:
public class EyeGazeSelector : MonoBehaviour
{
[SerializeField] private float dwellTimeThreshold = 0.8f;
[SerializeField] private float gazeStabilityRadius = 0.02f;
private IMixedRealityEyeGazeProvider _eyeGazeProvider;
private GameObject _currentTarget;
private float _dwellTimer;
private Vector3 _lastGazePoint;
private void Start()
{
_eyeGazeProvider = CoreServices.InputSystem?.EyeGazeProvider;
if (_eyeGazeProvider == null)
{
Debug.LogWarning("Eye tracking not available. Falling back to head gaze.");
}
}
private void Update()
{
if (!IsEyeTrackingAvailable())
{
UseHeadGazeFallback();
return;
}
ProcessEyeGaze();
}
private bool IsEyeTrackingAvailable()
{
return _eyeGazeProvider != null && _eyeGazeProvider.IsEyeTrackingEnabled;
}
private void ProcessEyeGaze()
{
var gazeOrigin = _eyeGazeProvider.GazeOrigin;
var gazeDirection = _eyeGazeProvider.GazeDirection;
if (Physics.Raycast(gazeOrigin, gazeDirection, out var hit, 10f))
{
var hitObject = hit.collider.gameObject;
if (hitObject != _currentTarget)
{
OnGazeTargetChanged(_currentTarget, hitObject);
_currentTarget = hitObject;
_dwellTimer = 0f;
}
else if (IsGazeStable(hit.point))
{
_dwellTimer += Time.deltaTime;
if (_dwellTimer >= dwellTimeThreshold)
{
OnDwellComplete(_currentTarget);
_dwellTimer = 0f;
}
}
_lastGazePoint = hit.point;
}
else
{
if (_currentTarget != null)
{
OnGazeTargetChanged(_currentTarget, null);
_currentTarget = null;
}
}
}
private bool IsGazeStable(Vector3 currentPoint)
{
return Vector3.Distance(currentPoint, _lastGazePoint) < gazeStabilityRadius;
}
private void OnGazeTargetChanged(GameObject previous, GameObject current)
{
previous?.GetComponent<IGazeTarget>()?.OnGazeExit();
current?.GetComponent<IGazeTarget>()?.OnGazeEnter();
}
private void OnDwellComplete(GameObject target)
{
target?.GetComponent<IGazeTarget>()?.OnDwellActivate();
}
private void UseHeadGazeFallback()
{
var headRay = new Ray(
CameraCache.Main.transform.position,
CameraCache.Main.transform.forward
);
// Similar raycast logic with head gaze
}
}
public interface IGazeTarget
{
void OnGazeEnter();
void OnGazeExit();
void OnDwellActivate();
}Scenario: Remote Rendering Integration
Stream high-fidelity 3D content from Azure Remote Rendering:
public class RemoteRenderingManager : MonoBehaviour
{
private RemoteRenderingClient _client;
private RenderingSession _session;
private RemoteManager _remoteManager;
public async Task ConnectAsync(RemoteRenderingConfig config)
{
var sessionConfig = new RenderingSessionCreationOptions
{
MaxLeaseInMinutes = 30,
Size = RenderingSessionVmSize.Standard
};
_client = ApiClientFactory.CreateRemoteRenderingClient(
config.AccountId,
config.AccountKey,
config.AccountDomain
);
_session = await _client.CreateNewRenderingSessionAsync(sessionConfig);
await _session.ConnectAsync(new RendererInitOptions
{
SessionId = _session.SessionId
});
_remoteManager = _session.Connection.RemoteManager;
}
public async Task<Entity> LoadModelAsync(string sasUrl)
{
var loadParams = new LoadModelFromSasOptions
{
ModelUri = sasUrl
};
var result = await _remoteManager.LoadModelFromSasAsync(loadParams);
return result.Root;
}
public void PositionModel(Entity modelRoot, Pose worldPose)
{
modelRoot.Position = worldPose.position.ToRemote();
modelRoot.Rotation = worldPose.rotation.ToRemote();
}
public async Task DisconnectAsync()
{
if (_session != null)
{
await _session.StopAsync();
_session = null;
}
}
}Scenario: Voice Commands with MRTK
Implement voice-activated commands:
public class VoiceCommandHandler : MonoBehaviour, IMixedRealitySpeechHandler
{
private readonly Dictionary<string, Action> _commands = new();
private void Start()
{
RegisterCommands();
// Ensure speech system is enabled
if (CoreServices.InputSystem?.IsRegistered(gameObject) == false)
{
CoreServices.InputSystem?.RegisterHandler<IMixedRealitySpeechHandler>(this);
}
}
private void RegisterCommands()
{
_commands["select"] = () => SelectCurrentTarget();
_commands["menu"] = () => ToggleMainMenu();
_commands["reset"] = () => ResetScene();
_commands["help"] = () => ShowHelp();
_commands["place here"] = () => PlaceObjectAtGaze();
}
public void OnSpeechKeywordRecognized(SpeechEventData eventData)
{
var keyword = eventData.Command.Keyword.ToLowerInvariant();
if (_commands.TryGetValue(keyword, out var action))
{
eventData.Use();
action.Invoke();
// Provide audio feedback
AudioFeedback.PlayConfirmation();
}
}
private void SelectCurrentTarget()
{
var focusProvider = CoreServices.InputSystem?.FocusProvider;
var focusedObject = focusProvider?.GetFocusedObject(
focusProvider.PrimaryPointer
);
focusedObject?.GetComponent<ISelectable>()?.Select();
}
private void ToggleMainMenu() { /* Implementation */ }
private void ResetScene() { /* Implementation */ }
private void ShowHelp() { /* Implementation */ }
private void PlaceObjectAtGaze() { /* Implementation */ }
}Scenario: QR Code Tracking
Detect and track QR codes for content anchoring:
public class QRCodeTracker : MonoBehaviour
{
private QRCodeWatcher _watcher;
private readonly Dictionary<Guid, QRCodeAnchor> _trackedCodes = new();
public event Action<QRCodeAnchor> OnQRCodeDetected;
public event Action<Guid> OnQRCodeLost;
private async void Start()
{
var accessStatus = await QRCodeWatcher.RequestAccessAsync();
if (accessStatus == QRCodeWatcherAccessStatus.Allowed)
{
_watcher = new QRCodeWatcher();
_watcher.Added += OnQRCodeAdded;
_watcher.Updated += OnQRCodeUpdated;
_watcher.Removed += OnQRCodeRemoved;
_watcher.Start();
}
else
{
Debug.LogError("QR code tracking access denied");
}
}
private void OnQRCodeAdded(object sender, QRCodeAddedEventArgs args)
{
UnityDispatcher.InvokeOnMainThread(() =>
{
var code = args.Code;
var anchor = CreateAnchorForCode(code);
_trackedCodes[code.Id] = anchor;
OnQRCodeDetected?.Invoke(anchor);
});
}
private void OnQRCodeUpdated(object sender, QRCodeUpdatedEventArgs args)
{
UnityDispatcher.InvokeOnMainThread(() =>
{
if (_trackedCodes.TryGetValue(args.Code.Id, out var anchor))
{
UpdateAnchorPose(anchor, args.Code);
}
});
}
private void OnQRCodeRemoved(object sender, QRCodeRemovedEventArgs args)
{
UnityDispatcher.InvokeOnMainThread(() =>
{
if (_trackedCodes.Remove(args.Code.Id))
{
OnQRCodeLost?.Invoke(args.Code.Id);
}
});
}
private QRCodeAnchor CreateAnchorForCode(QRCode code)
{
var spatialGraphNode = SpatialGraphInteropPreview.CreateLocatorForNode(
code.SpatialGraphNodeId
);
return new QRCodeAnchor
{
Id = code.Id,
Data = code.Data,
PhysicalSize = code.PhysicalSideLength,
SpatialNode = spatialGraphNode
};
}
}
public class QRCodeAnchor
{
public Guid Id { get; set; }
public string Data { get; set; }
public float PhysicalSize { get; set; }
public object SpatialNode { get; set; }
}Scenario: Shared Experience with Photon
Enable multi-user collaboration in mixed reality:
public class SharedExperienceManager : MonoBehaviourPunCallbacks
{
[SerializeField] private GameObject localAvatarPrefab;
[SerializeField] private GameObject remoteAvatarPrefab;
private Dictionary<int, GameObject> _remoteAvatars = new();
public async Task JoinSharedSessionAsync(string roomName)
{
PhotonNetwork.ConnectUsingSettings();
while (!PhotonNetwork.IsConnectedAndReady)
{
await Task.Yield();
}
var roomOptions = new RoomOptions
{
MaxPlayers = 8,
IsVisible = true
};
PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, TypedLobby.Default);
}
public override void OnJoinedRoom()
{
SpawnLocalAvatar();
SynchronizeSpatialAnchor();
}
public override void OnPlayerEnteredRoom(Player newPlayer)
{
Debug.Log($"Player {newPlayer.NickName} joined");
}
private void SpawnLocalAvatar()
{
var headPose = CameraCache.Main.transform;
var avatar = PhotonNetwork.Instantiate(
localAvatarPrefab.name,
headPose.position,
headPose.rotation
);
}
private void SynchronizeSpatialAnchor()
{
// Share Azure Spatial Anchor ID with other users
// so all users align to the same world coordinate system
if (PhotonNetwork.IsMasterClient)
{
// Master creates and shares anchor
CreateAndShareWorldAnchor();
}
else
{
// Others locate the shared anchor
LocateSharedWorldAnchor();
}
}
}MRTK and OpenXR Patterns
MRTK Architecture Patterns
Service-Based Architecture
MRTK uses a service locator pattern for core subsystems. Access services through the central service registry:
// Accessing MRTK services
var inputSystem = CoreServices.InputSystem;
var spatialAwarenessSystem = CoreServices.SpatialAwarenessSystem;
var diagnosticsSystem = CoreServices.DiagnosticsSystem;Input Action Mapping
Define input actions in configuration rather than hardcoding gesture detection:
// Define input action in MRTK configuration profile
// Then handle in code via IMixedRealityInputHandler
public class GrabHandler : MonoBehaviour, IMixedRealityInputHandler
{
public void OnInputDown(InputEventData eventData)
{
if (eventData.MixedRealityInputAction == grabAction)
{
// Handle grab start
}
}
public void OnInputUp(InputEventData eventData)
{
if (eventData.MixedRealityInputAction == grabAction)
{
// Handle grab release
}
}
}Spatial Awareness Observer Pattern
Subscribe to spatial mesh updates rather than polling:
public class SpatialMeshHandler : MonoBehaviour, IMixedRealitySpatialAwarenessObservationHandler<SpatialAwarenessMeshObject>
{
public void OnObservationAdded(MixedRealitySpatialAwarenessEventData<SpatialAwarenessMeshObject> eventData)
{
// New mesh detected
var meshObject = eventData.SpatialObject;
ProcessNewMesh(meshObject);
}
public void OnObservationUpdated(MixedRealitySpatialAwarenessEventData<SpatialAwarenessMeshObject> eventData)
{
// Existing mesh updated
}
public void OnObservationRemoved(MixedRealitySpatialAwarenessEventData<SpatialAwarenessMeshObject> eventData)
{
// Mesh no longer tracked
}
}Profile Configuration Pattern
Use scriptable object profiles for runtime configuration:
// Create custom profile inheriting from base
[CreateAssetMenu(fileName = "CustomHandTrackingProfile", menuName = "MRTK/Custom Hand Tracking Profile")]
public class CustomHandTrackingProfile : BaseMixedRealityProfile
{
[SerializeField]
private float gestureThreshold = 0.8f;
[SerializeField]
private HandTrackingMode trackingMode = HandTrackingMode.Full;
public float GestureThreshold => gestureThreshold;
public HandTrackingMode TrackingMode => trackingMode;
}OpenXR Patterns
Feature Plugin Pattern
Implement OpenXR features as discrete plugins:
#if UNITY_OPENXR
[OpenXRFeature(
UiName = "Custom Hand Tracking",
BuildTargetGroups = new[] { BuildTargetGroup.Standalone, BuildTargetGroup.WSA },
Company = "YourCompany",
Version = "1.0.0",
FeatureId = "com.yourcompany.openxr.handtracking"
)]
public class CustomHandTrackingFeature : OpenXRFeature
{
protected override bool OnInstanceCreate(ulong instance)
{
// Initialize OpenXR extension
return base.OnInstanceCreate(instance);
}
protected override void OnSubsystemStart()
{
// Subsystem is starting
}
protected override void OnSubsystemStop()
{
// Subsystem is stopping
}
}
#endifAction-Based Input Pattern
Use OpenXR action bindings for cross-platform input:
// Define action map in code or via InputActionAsset
var selectAction = new InputAction("Select", InputActionType.Button);
selectAction.AddBinding("<XRController>{RightHand}/trigger");
selectAction.AddBinding("<HandInteraction>{RightHand}/select");
selectAction.performed += ctx => OnSelectPerformed();
selectAction.canceled += ctx => OnSelectCanceled();
selectAction.Enable();Session State Management
Handle OpenXR session lifecycle properly:
public class XRSessionManager : MonoBehaviour
{
private void OnEnable()
{
XRInputSubsystem.boundaryChanged += OnBoundaryChanged;
}
private void OnDisable()
{
XRInputSubsystem.boundaryChanged -= OnBoundaryChanged;
}
public void HandleSessionStateChange(XRSessionState state)
{
switch (state)
{
case XRSessionState.Ready:
InitializeXRContent();
break;
case XRSessionState.Focused:
ResumeExperience();
break;
case XRSessionState.Visible:
PauseInteractiveContent();
break;
case XRSessionState.Stopping:
SaveSessionState();
break;
}
}
}Cross-Cutting Patterns
Dependency Injection for XR Services
Abstract XR services behind interfaces for testability:
public interface ISpatialAnchorService
{
Task<SpatialAnchor> CreateAnchorAsync(Pose pose, string identifier);
Task<SpatialAnchor> LoadAnchorAsync(string identifier);
Task DeleteAnchorAsync(string identifier);
}
public class AzureSpatialAnchorsService : ISpatialAnchorService
{
private readonly CloudSpatialAnchorSession _session;
public async Task<SpatialAnchor> CreateAnchorAsync(Pose pose, string identifier)
{
var cloudAnchor = new CloudSpatialAnchor();
cloudAnchor.LocalAnchor = CreateNativeAnchor(pose);
cloudAnchor.AppProperties["id"] = identifier;
await _session.CreateAnchorAsync(cloudAnchor);
return new SpatialAnchor(cloudAnchor);
}
}Object Pooling for XR Performance
Pool frequently instantiated objects to avoid GC pressure:
public class XRObjectPool<T> where T : Component
{
private readonly Queue<T> _available = new();
private readonly T _prefab;
private readonly Transform _poolParent;
public T Get()
{
if (_available.Count > 0)
{
var obj = _available.Dequeue();
obj.gameObject.SetActive(true);
return obj;
}
return Object.Instantiate(_prefab, _poolParent);
}
public void Return(T obj)
{
obj.gameObject.SetActive(false);
_available.Enqueue(obj);
}
}Async Loading Pattern for MR Content
Load heavy assets asynchronously to maintain frame rate:
public class MRContentLoader
{
public async Task<GameObject> LoadHologramAsync(string assetPath, CancellationToken ct)
{
var handle = Addressables.LoadAssetAsync<GameObject>(assetPath);
while (!handle.IsDone)
{
ct.ThrowIfCancellationRequested();
await Task.Yield();
}
if (handle.Status == AsyncOperationStatus.Succeeded)
{
return Object.Instantiate(handle.Result);
}
throw new InvalidOperationException($"Failed to load asset: {assetPath}");
}
}Graceful Degradation Pattern
Handle missing device capabilities gracefully:
public class CapabilityManager
{
public bool HandTrackingAvailable { get; private set; }
public bool EyeTrackingAvailable { get; private set; }
public bool SpatialMappingAvailable { get; private set; }
public void DetectCapabilities()
{
HandTrackingAvailable = IsSubsystemAvailable<XRHandSubsystem>();
EyeTrackingAvailable = IsSubsystemAvailable<XREyeTrackingSubsystem>();
SpatialMappingAvailable = IsSubsystemAvailable<XRMeshSubsystem>();
}
private bool IsSubsystemAvailable<T>() where T : class, ISubsystem
{
var descriptors = new List<SubsystemDescriptorBase>();
SubsystemManager.GetSubsystemDescriptors(descriptors);
return descriptors.Any(d => d is T);
}
public void ConfigureInputForCapabilities(InputController controller)
{
if (HandTrackingAvailable)
{
controller.EnableHandTracking();
}
else
{
controller.EnableControllerFallback();
}
}
}