181 lines
7.7 KiB
C#
181 lines
7.7 KiB
C#
using UnityEngine;
|
||
using System;
|
||
using UnityEngine.InputSystem;
|
||
|
||
/// <summary>
|
||
/// Централизованный ввод на основе New Input System.
|
||
/// Singleton, DontDestroyOnLoad.
|
||
/// Биндинги сохраняются в PlayerPrefs автоматически при изменении.
|
||
/// </summary>
|
||
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; }
|
||
/// <summary>ЛКМ удержана (разрушение блока). False когда инвентарь открыт.</summary>
|
||
public bool BreakHeld { get; private set; }
|
||
/// <summary>ПКМ нажата в этом кадре (постановка блока). False когда инвентарь открыт.</summary>
|
||
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<int> 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<InputActionAsset>("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>() : Vector2.zero;
|
||
LookInput = gameplayActive ? _gameplayMap["Look"].ReadValue<Vector2>() : 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] Биндинги сброшены к умолчаниям");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Интерактивный ремаппинг. Передайте имя action, имя map и callback для UI.
|
||
/// Пример: StartRebind("Jump", "Gameplay", () => UpdateUI());
|
||
/// </summary>
|
||
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("<Mouse>/position")
|
||
.WithControlsExcluding("<Mouse>/delta")
|
||
.OnComplete(op =>
|
||
{
|
||
op.Dispose();
|
||
action.Enable();
|
||
SaveBindings();
|
||
onComplete?.Invoke();
|
||
})
|
||
.Start();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Доступ к InputActionAsset для UI-ремаппинга и отображения текущих биндингов.
|
||
/// </summary>
|
||
public InputActionAsset GetActions() => actions;
|
||
}
|