sea-battle-vr/Assets/Scripts/XRController.cs
2026-06-02 17:04:29 +03:00

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");
}
}