using Cysharp.Threading.Tasks; using System; using System.Threading; using UnityEngine; using UnityEngine.XR.Management; public static class XRController { // ========== Поля состояния ========== private static bool _isInitializing = false; private static bool _isXRRunning = false; private static bool _shouldXRBeRunning = false; // Флаг, что XR должен быть включен (пользователь хочет VR) private static bool _isFocusMonitoringEnabled = false; private static CancellationTokenSource _focusCts; private static CancellationTokenSource _operationCts; // Для отмены текущих операций // ========== Публичные свойства ========== /// /// Активен ли XR в данный момент (подсистемы запущены) /// public static bool IsXRRunning => _isXRRunning; /// /// Инициализирован ли XR Loader /// public static bool IsInitialized => XRGeneralSettings.Instance?.Manager?.isInitializationComplete ?? false; /// /// Идет ли процесс инициализации /// public static bool IsInitializing => _isInitializing; /// /// Должен ли XR быть включен (пользовательский запрос) /// public static bool ShouldXRBeRunning => _shouldXRBeRunning; /// /// Событие при включении XR /// public static event Action OnXREnabled; /// /// Событие при выключении XR /// public static event Action OnXRDisabled; /// /// Событие при потере фокуса XR (например, открыто системное меню) /// public static event Action OnXRFocusLost; /// /// Событие при возврате фокуса XR /// public static event Action OnXRFocusRegained; // ========== Публичные методы ========== /// /// Включает XR (инициализация + запуск подсистем) /// /// Токен для отмены операции /// Успешность включения XR public static async UniTask EnableXR(CancellationToken cancellationToken = default) { // Если XR уже запущен и работает if (_isXRRunning) { Debug.Log("[XRController] XR already running"); _shouldXRBeRunning = true; return true; } // Если уже идет инициализация, ждем её завершения if (_isInitializing) { Debug.Log("[XRController] XR initialization already in progress, waiting..."); var waitCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); while (_isInitializing && !waitCts.Token.IsCancellationRequested) { await UniTask.Yield(waitCts.Token); } if (waitCts.Token.IsCancellationRequested) { return false; } return _isXRRunning; } _isInitializing = true; _shouldXRBeRunning = true; // Отменяем предыдущие операции _operationCts?.Cancel(); _operationCts?.Dispose(); _operationCts = new CancellationTokenSource(); var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _operationCts.Token); try { var manager = XRGeneralSettings.Instance.Manager; if (manager == null) { Debug.LogError("[XRController] XRGeneralSettings.Instance.Manager is null! Check XR Plug-in Management settings."); return false; } // Инициализируем loader, если еще не инициализирован if (!manager.isInitializationComplete) { Debug.Log("[XRController] Initializing XR Loader..."); var initSuccess = await InitializeLoaderAsync(manager, linkedCts.Token); if (linkedCts.Token.IsCancellationRequested) { Debug.Log("[XRController] Initialization cancelled"); return false; } if (!initSuccess) { Debug.LogError("[XRController] Failed to initialize XR Loader"); return false; } if (manager.activeLoader == null) { Debug.LogError("[XRController] No active XR Loader after initialization"); return false; } Debug.Log("[XRController] XR Loader initialized successfully"); } // Запускаем подсистемы XR Debug.Log("[XRController] Starting XR subsystems..."); manager.StartSubsystems(); // Даем время на стабилизацию XR await UniTask.Delay(100, cancellationToken: linkedCts.Token); _isXRRunning = true; // Запускаем мониторинг фокуса StartFocusMonitoring(); Debug.Log("[XRController] XR successfully enabled"); // Вызываем событие на главном потоке await UniTask.SwitchToMainThread(linkedCts.Token); OnXREnabled?.Invoke(); return true; } catch (OperationCanceledException) { Debug.Log("[XRController] XR enabling was cancelled"); return false; } catch (Exception ex) { Debug.LogError($"[XRController] Failed to enable XR: {ex.Message}\n{ex.StackTrace}"); return false; } finally { _isInitializing = false; linkedCts.Dispose(); } } /// /// Выключает XR (останавливает подсистемы и деинициализирует loader) /// public static async UniTask DisableXR() { if (!_isXRRunning && !IsInitialized) { Debug.Log("[XRController] XR is not running"); _shouldXRBeRunning = false; return; } // Ждем, если инициализация еще идет while (_isInitializing) { await UniTask.Yield(); } // Отменяем операции _operationCts?.Cancel(); // Останавливаем мониторинг фокуса StopFocusMonitoring(); var manager = XRGeneralSettings.Instance.Manager; if (manager != null && manager.isInitializationComplete) { Debug.Log("[XRController] Stopping XR subsystems..."); manager.StopSubsystems(); _isXRRunning = false; // Даем время на остановку await UniTask.Delay(50); Debug.Log("[XRController] Deinitializing XR Loader..."); manager.DeinitializeLoader(); } _shouldXRBeRunning = false; Debug.Log("[XRController] XR disabled"); // Вызываем событие на главном потоке await UniTask.SwitchToMainThread(); OnXRDisabled?.Invoke(); } /// /// Перезапускает XR (полезно при потере фокуса или ошибках) /// public static async UniTask RestartXR() { Debug.Log("[XRController] Restarting XR..."); bool wasRunning = _isXRRunning; // Останавливаем XR await DisableXR(); // Небольшая пауза перед перезапуском await UniTask.Delay(200); // Запускаем снова, сохраняя флаг _shouldXRBeRunning _shouldXRBeRunning = wasRunning; if (wasRunning) { return await EnableXR(); } return false; } /// /// Проверяет, активен ли XR и есть ли фокус /// public static bool IsXRActiveAndFocused() { return _isXRRunning && Application.isFocused; } // ========== Приватные методы ========== /// /// Обертка для IEnumerator InitializeLoader в стиле UniTask /// private static async UniTask InitializeLoaderAsync(XRManagerSettings manager, CancellationToken cancellationToken) { var initOperation = manager.InitializeLoader(); // Выполняем корутину через UniTask while (initOperation.MoveNext()) { if (cancellationToken.IsCancellationRequested) { return false; } await UniTask.Yield(cancellationToken); } return manager.activeLoader != null; } /// /// Запускает мониторинг фокуса приложения /// private static void StartFocusMonitoring() { if (_isFocusMonitoringEnabled) return; StopFocusMonitoring(); // Останавливаем предыдущий, если был _focusCts = new CancellationTokenSource(); MonitorApplicationFocusAsync(_focusCts.Token).Forget(); _isFocusMonitoringEnabled = true; Debug.Log("[XRController] Focus monitoring started"); } /// /// Останавливает мониторинг фокуса /// private static void StopFocusMonitoring() { if (!_isFocusMonitoringEnabled) return; _focusCts?.Cancel(); _focusCts?.Dispose(); _focusCts = null; _isFocusMonitoringEnabled = false; Debug.Log("[XRController] Focus monitoring stopped"); } /// /// Фоновая задача для отслеживания фокуса приложения /// private static async UniTaskVoid MonitorApplicationFocusAsync(CancellationToken token) { bool lastHasFocus = Application.isFocused; while (!token.IsCancellationRequested) { try { // Переключаемся на главный поток для доступа к Application.isFocused await UniTask.SwitchToMainThread(token); bool hasFocus = Application.isFocused; // Обработка потери фокуса if (!hasFocus && lastHasFocus && _shouldXRBeRunning && _isXRRunning) { Debug.Log("[XRController] Focus lost! XR session paused."); await UniTask.SwitchToMainThread(token); OnXRFocusLost?.Invoke(); // Останавливаем XR подсистемы, но не деинициализируем полностью await PauseXRSessionAsync(token); } // Обработка возврата фокуса else if (hasFocus && !lastHasFocus && _shouldXRBeRunning && !_isXRRunning) { Debug.Log("[XRController] Focus regained! Resuming XR session..."); // Пытаемся восстановить XR сессию var resumeSuccess = await ResumeXRSessionAsync(token); if (resumeSuccess) { await UniTask.SwitchToMainThread(token); OnXRFocusRegained?.Invoke(); Debug.Log("[XRController] XR session resumed successfully"); } else { Debug.LogWarning("[XRController] Failed to resume XR session, attempting full restart..."); await RestartXR(); } } lastHasFocus = hasFocus; // Проверяем раз в 500 мс для более быстрой реакции await UniTask.Delay(500, cancellationToken: token); } catch (OperationCanceledException) { break; } catch (Exception ex) { Debug.LogError($"[XRController] Error in focus monitoring: {ex.Message}"); await UniTask.Delay(1000, cancellationToken: token); } } Debug.Log("[XRController] Focus monitoring stopped"); } /// /// Приостанавливает XR сессию (без полной деинициализации) /// private static async UniTask PauseXRSessionAsync(CancellationToken token) { if (!_isXRRunning) return; var manager = XRGeneralSettings.Instance.Manager; if (manager != null && manager.isInitializationComplete) { manager.StopSubsystems(); _isXRRunning = false; await UniTask.Delay(50, cancellationToken: token); Debug.Log("[XRController] XR session paused"); } } /// /// Возобновляет XR сессию после паузы /// private static async UniTask ResumeXRSessionAsync(CancellationToken token) { if (_isXRRunning) return true; if (_isInitializing) return false; var manager = XRGeneralSettings.Instance.Manager; if (manager == null || !manager.isInitializationComplete || manager.activeLoader == null) { Debug.Log("[XRController] Cannot resume - loader not initialized, need full restart"); return false; } try { Debug.Log("[XRController] Resuming XR subsystems..."); manager.StartSubsystems(); await UniTask.Delay(100, cancellationToken: token); _isXRRunning = true; return true; } catch (Exception ex) { Debug.LogError($"[XRController] Failed to resume XR: {ex.Message}"); return false; } } // ========== Методы для отладки ========== /// /// Возвращает строковое представление текущего состояния XRController /// public static string GetDebugInfo() { return $"[XRController Debug]\n" + $"- IsXRRunning: {_isXRRunning}\n" + $"- IsInitialized: {IsInitialized}\n" + $"- IsInitializing: {_isInitializing}\n" + $"- ShouldXRBeRunning: {_shouldXRBeRunning}\n" + $"- IsFocusMonitoringEnabled: {_isFocusMonitoringEnabled}\n" + $"- Application.isFocused: {Application.isFocused}\n" + $"- RunInBackground: {Application.runInBackground}"; } /// /// Принудительно сбрасывает состояние (только для отладки/аварийного восстановления) /// public static async UniTask ForceReset() { Debug.LogWarning("[XRController] Force resetting XR state..."); await DisableXR(); _isInitializing = false; _isXRRunning = false; _shouldXRBeRunning = false; _isFocusMonitoringEnabled = false; _focusCts?.Dispose(); _focusCts = null; _operationCts?.Dispose(); _operationCts = null; Debug.Log("[XRController] Force reset complete"); } }