using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using RR.Common.Controls; using RR.Common.Settings; using RR.Game2D.Core.Managers; namespace RR.Common.Managers { /// /// Менеджер отслеживания бездействия игрока. /// При отсутствии ввода в течение заданного времени выкидывает игрока в главное меню. /// public class InactivityManager : MonoBehaviour { public static InactivityManager Instance { get; private set; } [Header("Настройки бездействия")] [SerializeField] private float inactivityTimeout = 300f; // 5 минут в секундах [SerializeField] private bool enableInactivityCheck = true; [Header("Отладка")] [SerializeField] private bool showDebugInfo; private float lastInputTime; private bool isInMainMenu = true; private bool isGameActive; private InputSnapshot lastInputSnapshot; private bool hasInputSnapshot; private string lastComInputState = "11111111111111111111111111"; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); Initialize(); } private void Initialize() { lastInputTime = Time.unscaledTime; // Подписываемся на все возможные источники ввода SubscribeToInputEvents(); // Подписываемся на события смены сцены UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; // Подписываемся на изменения настроек var settingsManager = SettingsManager.Instance; if (settingsManager != null) { settingsManager.OnSettingsLoaded += LoadSettingsValues; settingsManager.OnSettingsSaved += LoadSettingsValues; LoadSettingsValues(); } Debug.Log("[InactivityManager] Инициализирован"); } private void OnDestroy() { UnsubscribeFromInputEvents(); UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded; var settingsManager = SettingsManager.Instance; if (settingsManager != null) { settingsManager.OnSettingsLoaded -= LoadSettingsValues; settingsManager.OnSettingsSaved -= LoadSettingsValues; } } /// /// Загружает значения из настроек /// private void LoadSettingsValues() { var settings = SettingsManager.Instance; if (settings == null) { Debug.LogWarning("[InactivityManager] SettingsManager is null!"); return; } inactivityTimeout = settings.GetInt("gameplay.inactivityTimeout"); if (inactivityTimeout == 0) { Debug.Log("[InactivityManager] Отслеживание бездействия отключено (таймаут = 0)"); } else { Debug.Log($"[InactivityManager] Таймаут бездействия: {inactivityTimeout}s"); } } private void SubscribeToInputEvents() { // Подписываемся на события ComSystemManager (кнопки автомата) if (ComSystemManager.OnInputReceived != null) { ComSystemManager.OnInputReceived.AddListener(OnComInput); } // InputSystem.onAnyButtonPress - это событие, которое срабатывает каждый раз // когда нажимается любая кнопка. Мы просто слушаем его в Update через CheckForAnyInput } private void UnsubscribeFromInputEvents() { if (ComSystemManager.OnInputReceived != null) { ComSystemManager.OnInputReceived.RemoveListener(OnComInput); } } private void Update() { // Проверяем, изменилось ли состояние ввода в этом кадре CheckForInputStateChanges(); // Проверяем таймер бездействия if (enableInactivityCheck && inactivityTimeout > 0f && !isInMainMenu && isGameActive) { float timeSinceLastInput = Time.unscaledTime - lastInputTime; if (showDebugInfo && Time.frameCount % 60 == 0) { Debug.Log($"[InactivityManager] Время бездействия: {timeSinceLastInput:F1}s / {inactivityTimeout}s"); } if (timeSinceLastInput >= inactivityTimeout) { OnInactivityTimeout(); } } } /// /// Проверяет, изменилось ли состояние ввода с прошлого кадра. /// Зажатая кнопка без изменений считается бездействием. /// private void CheckForInputStateChanges() { if (isInMainMenu || !isGameActive) return; InputSnapshot currentSnapshot = BuildInputSnapshot(); if (!hasInputSnapshot) { lastInputSnapshot = currentSnapshot; hasInputSnapshot = true; return; } if (!currentSnapshot.Equals(lastInputSnapshot)) { lastInputSnapshot = currentSnapshot; ResetInactivityTimer(); } } private void OnComInput(string input) { lastComInputState = string.IsNullOrWhiteSpace(input) ? "11111111111111111111111111" : input; } /// /// Сбрасывает таймер бездействия /// public void ResetInactivityTimer() { lastInputTime = Time.unscaledTime; if (showDebugInfo) { Debug.Log("[InactivityManager] Таймер сброшен"); } } /// /// Вызывается при достижении таймаута бездействия /// private void OnInactivityTimeout() { Debug.Log("[InactivityManager] Таймаут бездействия! Возврат в главное меню..."); enableInactivityCheck = false; // Отключаем проверку до следующей игры // Останавливаем игру if (GameManager.Instance != null) { GameManager.Instance.SetPause(false); GameManager.Instance.LoadScene("MainMenu", false); } ResetInactivityTimer(); } /// /// Обработчик загрузки сцены /// private void OnSceneLoaded(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode) { isInMainMenu = scene.name == "MainMenu"; if (isInMainMenu) { enableInactivityCheck = false; isGameActive = false; hasInputSnapshot = false; Debug.Log("[InactivityManager] Главное меню загружено - проверка отключена"); } ResetInactivityTimer(); } /// /// Включает проверку бездействия (вызывается при старте игры) /// public void EnableInactivityCheck() { if (inactivityTimeout <= 0f) { enableInactivityCheck = false; isGameActive = false; ResetInactivityTimer(); Debug.Log("[InactivityManager] Проверка бездействия не включена: таймаут = 0"); return; } enableInactivityCheck = true; isGameActive = true; lastComInputState = GetCurrentComInputState(); lastInputSnapshot = BuildInputSnapshot(); hasInputSnapshot = true; ResetInactivityTimer(); Debug.Log("[InactivityManager] Проверка бездействия включена"); } /// /// Отключает проверку бездействия /// public void DisableInactivityCheck() { enableInactivityCheck = false; isGameActive = false; hasInputSnapshot = false; Debug.Log("[InactivityManager] Проверка бездействия отключена"); } /// /// Устанавливает время таймаута бездействия. /// Если установлено 0, отслеживание бездействия отключается. /// public void SetInactivityTimeout(float timeoutSeconds) { inactivityTimeout = Mathf.Max(0f, timeoutSeconds); // 0 = отключено, минимум 0 if (inactivityTimeout == 0f) { enableInactivityCheck = false; isGameActive = false; hasInputSnapshot = false; Debug.Log("[InactivityManager] Отслеживание бездействия отключено (таймаут = 0)"); } else { Debug.Log($"[InactivityManager] Таймаут установлен: {inactivityTimeout}s"); } } private InputSnapshot BuildInputSnapshot() { return new InputSnapshot( BuildKeyboardStateHash(), BuildMouseStateHash(), BuildGamepadStateHash(), BuildComStateHash()); } private static int BuildKeyboardStateHash() { Keyboard keyboard = Keyboard.current; if (keyboard == null) { return 0; } int hash = 17; foreach (KeyControl key in keyboard.allKeys) { if (key.isPressed) { hash = hash * 31 + (int)key.keyCode; } } return hash; } private static int BuildMouseStateHash() { Mouse mouse = Mouse.current; if (mouse == null) { return 0; } Vector2 position = mouse.position.ReadValue(); Vector2 scroll = mouse.scroll.ReadValue(); int hash = 17; hash = hash * 31 + (mouse.leftButton.isPressed ? 1 : 0); hash = hash * 31 + (mouse.rightButton.isPressed ? 1 : 0); hash = hash * 31 + (mouse.middleButton.isPressed ? 1 : 0); hash = hash * 31 + Mathf.RoundToInt(position.x); hash = hash * 31 + Mathf.RoundToInt(position.y); hash = hash * 31 + Mathf.RoundToInt(scroll.x); hash = hash * 31 + Mathf.RoundToInt(scroll.y); return hash; } private static int BuildGamepadStateHash() { Gamepad gamepad = Gamepad.current; if (gamepad == null) { return 0; } int hash = 17; foreach (InputControl control in gamepad.allControls) { if (control is ButtonControl button && button.isPressed) { hash = hash * 31 + control.name.GetHashCode(); } } Vector2 leftStick = QuantizeStick(gamepad.leftStick.ReadValue()); Vector2 rightStick = QuantizeStick(gamepad.rightStick.ReadValue()); float leftTrigger = QuantizeAxis(gamepad.leftTrigger.ReadValue()); float rightTrigger = QuantizeAxis(gamepad.rightTrigger.ReadValue()); hash = hash * 31 + Mathf.RoundToInt(leftStick.x * 10f); hash = hash * 31 + Mathf.RoundToInt(leftStick.y * 10f); hash = hash * 31 + Mathf.RoundToInt(rightStick.x * 10f); hash = hash * 31 + Mathf.RoundToInt(rightStick.y * 10f); hash = hash * 31 + Mathf.RoundToInt(leftTrigger * 10f); hash = hash * 31 + Mathf.RoundToInt(rightTrigger * 10f); return hash; } private int BuildComStateHash() { string currentState = GetCurrentComInputState(); if (string.IsNullOrWhiteSpace(currentState)) { return 0; } int hash = 17; for (int i = 0; i < currentState.Length; i++) { hash = hash * 31 + currentState[i]; } return hash; } private string GetCurrentComInputState() { string currentState = ComSystemManager.GetLastInputState(); if (!string.IsNullOrWhiteSpace(currentState)) { lastComInputState = currentState; } return string.IsNullOrWhiteSpace(lastComInputState) ? "11111111111111111111111111" : lastComInputState; } private static Vector2 QuantizeStick(Vector2 value) { return new Vector2(QuantizeAxis(value.x), QuantizeAxis(value.y)); } private static float QuantizeAxis(float value) { if (Mathf.Abs(value) < 0.15f) return 0f; return Mathf.Round(value * 10f) / 10f; } private readonly struct InputSnapshot : System.IEquatable { private readonly int _keyboardHash; private readonly int _mouseHash; private readonly int _gamepadHash; private readonly int _comHash; public InputSnapshot(int keyboardHash, int mouseHash, int gamepadHash, int comHash) { _keyboardHash = keyboardHash; _mouseHash = mouseHash; _gamepadHash = gamepadHash; _comHash = comHash; } public bool Equals(InputSnapshot other) { return _keyboardHash == other._keyboardHash && _mouseHash == other._mouseHash && _gamepadHash == other._gamepadHash && _comHash == other._comHash; } } /// /// Получает текущее время таймаута /// public float GetInactivityTimeout() => inactivityTimeout; /// /// Получает время с момента последнего ввода /// public float GetTimeSinceLastInput() => Time.unscaledTime - lastInputTime; } }