464 lines
16 KiB
C#
464 lines
16 KiB
C#
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; // Для отмены текущих операций
|
|
|
|
// ========== Публичные свойства ==========
|
|
/// <summary>
|
|
/// Активен ли XR в данный момент (подсистемы запущены)
|
|
/// </summary>
|
|
public static bool IsXRRunning => _isXRRunning;
|
|
|
|
/// <summary>
|
|
/// Инициализирован ли XR Loader
|
|
/// </summary>
|
|
public static bool IsInitialized => XRGeneralSettings.Instance?.Manager?.isInitializationComplete ?? false;
|
|
|
|
/// <summary>
|
|
/// Идет ли процесс инициализации
|
|
/// </summary>
|
|
public static bool IsInitializing => _isInitializing;
|
|
|
|
/// <summary>
|
|
/// Должен ли XR быть включен (пользовательский запрос)
|
|
/// </summary>
|
|
public static bool ShouldXRBeRunning => _shouldXRBeRunning;
|
|
|
|
/// <summary>
|
|
/// Событие при включении XR
|
|
/// </summary>
|
|
public static event Action OnXREnabled;
|
|
|
|
/// <summary>
|
|
/// Событие при выключении XR
|
|
/// </summary>
|
|
public static event Action OnXRDisabled;
|
|
|
|
/// <summary>
|
|
/// Событие при потере фокуса XR (например, открыто системное меню)
|
|
/// </summary>
|
|
public static event Action OnXRFocusLost;
|
|
|
|
/// <summary>
|
|
/// Событие при возврате фокуса XR
|
|
/// </summary>
|
|
public static event Action OnXRFocusRegained;
|
|
|
|
// ========== Публичные методы ==========
|
|
|
|
/// <summary>
|
|
/// Включает XR (инициализация + запуск подсистем)
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Токен для отмены операции</param>
|
|
/// <returns>Успешность включения XR</returns>
|
|
public static async UniTask<bool> 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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Выключает XR (останавливает подсистемы и деинициализирует loader)
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Перезапускает XR (полезно при потере фокуса или ошибках)
|
|
/// </summary>
|
|
public static async UniTask<bool> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Проверяет, активен ли XR и есть ли фокус
|
|
/// </summary>
|
|
public static bool IsXRActiveAndFocused()
|
|
{
|
|
return _isXRRunning && Application.isFocused;
|
|
}
|
|
|
|
// ========== Приватные методы ==========
|
|
|
|
/// <summary>
|
|
/// Обертка для IEnumerator InitializeLoader в стиле UniTask
|
|
/// </summary>
|
|
private static async UniTask<bool> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Запускает мониторинг фокуса приложения
|
|
/// </summary>
|
|
private static void StartFocusMonitoring()
|
|
{
|
|
if (_isFocusMonitoringEnabled) return;
|
|
|
|
StopFocusMonitoring(); // Останавливаем предыдущий, если был
|
|
_focusCts = new CancellationTokenSource();
|
|
MonitorApplicationFocusAsync(_focusCts.Token).Forget();
|
|
_isFocusMonitoringEnabled = true;
|
|
|
|
Debug.Log("[XRController] Focus monitoring started");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Останавливает мониторинг фокуса
|
|
/// </summary>
|
|
private static void StopFocusMonitoring()
|
|
{
|
|
if (!_isFocusMonitoringEnabled) return;
|
|
|
|
_focusCts?.Cancel();
|
|
_focusCts?.Dispose();
|
|
_focusCts = null;
|
|
_isFocusMonitoringEnabled = false;
|
|
|
|
Debug.Log("[XRController] Focus monitoring stopped");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Фоновая задача для отслеживания фокуса приложения
|
|
/// </summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Приостанавливает XR сессию (без полной деинициализации)
|
|
/// </summary>
|
|
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");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Возобновляет XR сессию после паузы
|
|
/// </summary>
|
|
private static async UniTask<bool> 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;
|
|
}
|
|
}
|
|
|
|
// ========== Методы для отладки ==========
|
|
|
|
/// <summary>
|
|
/// Возвращает строковое представление текущего состояния XRController
|
|
/// </summary>
|
|
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}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Принудительно сбрасывает состояние (только для отладки/аварийного восстановления)
|
|
/// </summary>
|
|
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");
|
|
}
|
|
} |