using UnityEngine; using System; using UnityEngine.InputSystem; /// /// Централизованный ввод на основе New Input System. /// Singleton, DontDestroyOnLoad. /// Биндинги сохраняются в PlayerPrefs автоматически при изменении. /// public class PlayerInput : MonoBehaviour { public static PlayerInput Instance { get; private set; } // ── Ссылка на InputActionAsset ──────────────────────────────────────────── [Tooltip("Назначьте Assets/Input/PlayerControls.inputactions")] public InputActionAsset actions; // ── Непрерывные значения ────────────────────────────────────────────────── public Vector2 MoveInput { get; private set; } public Vector2 LookInput { get; private set; } public bool Crouch { get; private set; } public bool Run { get; private set; } /// ЛКМ удержана (разрушение блока). False когда инвентарь открыт. public bool BreakHeld { get; private set; } /// ПКМ нажата в этом кадре (постановка блока). False когда инвентарь открыт. public bool PlacePressed { get; private set; } // ── Дискретные события ──────────────────────────────────────────────────── public event Action OnToggleInventory; public event Action OnDropPressed; public event Action OnPickupPressed; public event Action OnJumpPressed; public event Action OnHotbarSelect; // ── Включение/выключение gameplay-ввода (инвентарь открыт) ─────────────── public bool Enabled { get => _gameplayMap != null && _gameplayMap.enabled; set { if (_gameplayMap == null) return; if (value) _gameplayMap.Enable(); else _gameplayMap.Disable(); } } // ── Приватные поля ──────────────────────────────────────────────────────── private InputActionMap _gameplayMap; private InputActionMap _uiMap; private const string BINDINGS_PREF_KEY = "PlayerBindings_v1"; // ── Lifecycle ───────────────────────────────────────────────────────────── void Awake() { if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); } else { Destroy(gameObject); return; } if (actions == null) { actions = Resources.Load("PlayerControls"); if (actions == null) { Debug.LogError("[PlayerInput] PlayerControls.inputactions не назначен и не найден в Resources!"); return; } } // Создаём рабочую копию чтобы не менять оригинальный asset actions = Instantiate(actions); // Загрузить сохранённые биндинги var saved = PlayerPrefs.GetString(BINDINGS_PREF_KEY, ""); if (!string.IsNullOrEmpty(saved)) actions.LoadBindingOverridesFromJson(saved); _gameplayMap = actions.FindActionMap("Gameplay", throwIfNotFound: true); _uiMap = actions.FindActionMap("UI", throwIfNotFound: true); // Подписка: дискретные кнопки _gameplayMap["Jump"].performed += _ => OnJumpPressed?.Invoke(); _gameplayMap["Pickup"].performed += _ => OnPickupPressed?.Invoke(); _gameplayMap["Drop"].performed += _ => OnDropPressed?.Invoke(); // UI map всегда активна — нет double-fire бага _uiMap["ToggleInventory"].performed += _ => OnToggleInventory?.Invoke(); _uiMap["CloseInventory"].performed += _ => OnToggleInventory?.Invoke(); for (int i = 0; i < 9; i++) { int idx = i; _gameplayMap[$"Hotbar{idx}"].performed += _ => OnHotbarSelect?.Invoke(idx); } _gameplayMap.Enable(); _uiMap.Enable(); } void OnDestroy() { _gameplayMap?.Disable(); _uiMap?.Disable(); } void Update() { bool gameplayActive = _gameplayMap != null && _gameplayMap.enabled; MoveInput = gameplayActive ? _gameplayMap["Move"].ReadValue() : Vector2.zero; LookInput = gameplayActive ? _gameplayMap["Look"].ReadValue() : Vector2.zero; Crouch = gameplayActive && _gameplayMap["Crouch"].IsPressed(); Run = gameplayActive && _gameplayMap["Run"].IsPressed(); // Мышь через New Input System — отключаются вместе с gameplay var mouse = UnityEngine.InputSystem.Mouse.current; BreakHeld = gameplayActive && (mouse?.leftButton.isPressed ?? false); PlacePressed = gameplayActive && (mouse?.rightButton.wasPressedThisFrame ?? false); } // ── Сохранение/загрузка биндингов ──────────────────────────────────────── [ContextMenu("Save Bindings")] public void SaveBindings() { if (actions == null) return; PlayerPrefs.SetString(BINDINGS_PREF_KEY, actions.SaveBindingOverridesAsJson()); PlayerPrefs.Save(); Debug.Log("[PlayerInput] Биндинги сохранены"); } [ContextMenu("Reset Bindings to Defaults")] public void ResetBindings() { if (actions == null) return; foreach (var action in actions) action.RemoveAllBindingOverrides(); PlayerPrefs.DeleteKey(BINDINGS_PREF_KEY); Debug.Log("[PlayerInput] Биндинги сброшены к умолчаниям"); } /// /// Интерактивный ремаппинг. Передайте имя action, имя map и callback для UI. /// Пример: StartRebind("Jump", "Gameplay", () => UpdateUI()); /// public void StartRebind(string actionName, string mapName, Action onComplete) { var map = actions?.FindActionMap(mapName); var action = map?.FindAction(actionName); if (action == null) { Debug.LogWarning($"[PlayerInput] Действие '{actionName}' в map '{mapName}' не найдено"); return; } action.Disable(); action.PerformInteractiveRebinding() .WithControlsExcluding("/position") .WithControlsExcluding("/delta") .OnComplete(op => { op.Dispose(); action.Enable(); SaveBindings(); onComplete?.Invoke(); }) .Start(); } /// /// Доступ к InputActionAsset для UI-ремаппинга и отображения текущих биндингов. /// public InputActionAsset GetActions() => actions; }