327 lines
15 KiB
C#
327 lines
15 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.InputSystem;
|
||
using BaseGames.Core.Events;
|
||
|
||
namespace BaseGames.Input
|
||
{
|
||
[CreateAssetMenu(menuName = "BaseGames/Input/InputReader")]
|
||
public class InputReaderSO : ScriptableObject
|
||
{
|
||
[SerializeField] private InputActionAsset _inputActions;
|
||
[SerializeField] private VoidEventChannelSO _onPauseRequested;
|
||
|
||
[Header("背包菜单(InventoryHub)")]
|
||
[Tooltip("打开统一背包菜单。对应 EVT_InventoryOpen。绑定到 UI/Gameplay Map 的 \"Inventory\" Action(可选)。")]
|
||
[SerializeField] private VoidEventChannelSO _onInventoryOpen;
|
||
[Tooltip("背包 Tab:下一页(L/R 肩键)。对应 EVT_InventoryTabNext。绑定 \"InventoryTabNext\" Action(可选)。")]
|
||
[SerializeField] private VoidEventChannelSO _onInventoryTabNext;
|
||
[Tooltip("背包 Tab:上一页(L/R 肩键)。对应 EVT_InventoryTabPrev。绑定 \"InventoryTabPrev\" Action(可选)。")]
|
||
[SerializeField] private VoidEventChannelSO _onInventoryTabPrev;
|
||
|
||
[Header("UI 操作")]
|
||
[Tooltip("UI 取消操作(ESC / 手柄 B·Circle)。由 UIManager 全局订阅,关闭当前栈顶面板。对应 EVT_UICancelPressed。")]
|
||
[SerializeField] private VoidEventChannelSO _onUICancelPressed;
|
||
|
||
// ── Gameplay Events ───────────────────────────────────────────────────
|
||
public event Action<Vector2> MoveEvent;
|
||
public event Action JumpStartedEvent;
|
||
public event Action JumpCancelledEvent;
|
||
public event Action AttackEvent;
|
||
public event Action ParryEvent;
|
||
public event Action DashEvent;
|
||
public event Action UseSpringEvent;
|
||
public event Action SwitchSkyFormEvent;
|
||
public event Action SwitchEarthFormEvent;
|
||
public event Action SwitchDeathFormEvent;
|
||
public event Action SoulSkillEvent;
|
||
public event Action SpiritSkill1StartedEvent;
|
||
public event Action SpiritSkill1CancelledEvent;
|
||
public event Action SpiritSkill2StartedEvent;
|
||
public event Action SpiritSkill2CancelledEvent;
|
||
public event Action SpellCastEvent;
|
||
public event Action InteractEvent;
|
||
public event Action InteractCancelledEvent;
|
||
|
||
// ── UI Events ─────────────────────────────────────────────────────────
|
||
public event Action PauseEvent;
|
||
public event Action<Vector2> NavigateEvent;
|
||
public event Action SubmitEvent;
|
||
public event Action CancelEvent;
|
||
public event Action<Vector2> PointEvent;
|
||
|
||
/// <summary>小地图视野档位切换(默认绑定:Tab 键 / 手柄右肩键)。</summary>
|
||
public event Action CycleMinimapZoomEvent;
|
||
|
||
/// <summary>全屏地图"居中到玩家"(默认绑定:F 键 / 手柄右摇杆按下)。</summary>
|
||
public event Action MapCenterEvent;
|
||
|
||
// ── Polling ───────────────────────────────────────────────────────────
|
||
public Vector2 MoveInput { get; private set; }
|
||
/// <summary>跳跃键当前是否处于按下状态。供 JumpState 在进入时检测短按是否已松开。</summary>
|
||
public bool IsJumpHeld => _jumpAction != null && _jumpAction.IsPressed();
|
||
|
||
// ── Runtime state ─────────────────────────────────────────────────────
|
||
private InputActionMap _gameplay;
|
||
private InputActionMap _ui;
|
||
private bool _isBound;
|
||
private InputAction _jumpAction;
|
||
|
||
private void EnsureInitialized()
|
||
{
|
||
if (_inputActions == null)
|
||
{
|
||
Debug.LogError("[InputReaderSO] _inputActions is NULL! Asset not assigned in inspector?");
|
||
return;
|
||
}
|
||
|
||
// Always disable first so the Input System tears down stale state,
|
||
// then re-enable to create a fresh state for this Play session.
|
||
_inputActions.Disable();
|
||
_inputActions.Enable();
|
||
|
||
_gameplay = _inputActions.FindActionMap("Gameplay", throwIfNotFound: false);
|
||
if (_gameplay == null)
|
||
Debug.LogError("[InputReaderSO] Could not find 'Gameplay' action map in asset!");
|
||
|
||
_ui = _inputActions.FindActionMap("UI", throwIfNotFound: false);
|
||
if (_ui == null)
|
||
Debug.LogWarning("[InputReaderSO] Could not find 'UI' action map in asset!");
|
||
|
||
if (_gameplay != null && !_isBound)
|
||
{
|
||
|
||
BindActions();
|
||
|
||
}
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
Debug.Assert(_onPauseRequested != null,
|
||
"[InputReaderSO] _onPauseRequested 未赋值,请在 Inspector 中指定 EVT_PauseRequested。", this);
|
||
// Reset private state on every OnEnable so stale ScriptableObject
|
||
// references from a previous Play session don't cause
|
||
// 'Map must be contained in state' errors.
|
||
_gameplay = null;
|
||
_ui = null;
|
||
_isBound = false;
|
||
_jumpAction = null;
|
||
EnsureInitialized();
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
_gameplay?.Disable();
|
||
_ui?.Disable();
|
||
_isBound = false;
|
||
}
|
||
|
||
// ── Action Map Switching ──────────────────────────────────────────────
|
||
public void EnableGameplayInput()
|
||
{
|
||
// Disable UI map if it's active
|
||
if (_ui != null && _ui.enabled)
|
||
{
|
||
|
||
_ui.Disable();
|
||
}
|
||
|
||
// Ensure gameplay map is enabled
|
||
if (_gameplay != null && !_gameplay.enabled)
|
||
{
|
||
|
||
_gameplay.Enable();
|
||
}
|
||
else if (_gameplay == null)
|
||
{
|
||
Debug.LogError("[InputReaderSO.EnableGameplayInput] _gameplay is NULL!");
|
||
}
|
||
}
|
||
|
||
public void EnableUIInput()
|
||
{
|
||
// Disable gameplay map if it's active
|
||
if (_gameplay != null && _gameplay.enabled)
|
||
{
|
||
|
||
_gameplay.Disable();
|
||
}
|
||
|
||
// Ensure UI map is enabled
|
||
if (_ui != null && !_ui.enabled)
|
||
{
|
||
|
||
_ui.Enable();
|
||
}
|
||
else if (_ui == null)
|
||
{
|
||
Debug.LogWarning("[InputReaderSO.EnableUIInput] _ui is NULL!");
|
||
}
|
||
}
|
||
|
||
public void DisableAllInput() { _gameplay?.Disable(); _ui?.Disable(); }
|
||
|
||
// ── Binding ───────────────────────────────────────────────────────────
|
||
private void BindActions()
|
||
{
|
||
if (_gameplay == null)
|
||
{
|
||
Debug.LogWarning("[InputReaderSO.BindActions] Skipped: _gameplay is NULL");
|
||
return;
|
||
}
|
||
|
||
|
||
|
||
BindPerformed(_gameplay, "Move", ctx =>
|
||
{
|
||
MoveInput = ctx.ReadValue<Vector2>();
|
||
MoveEvent?.Invoke(MoveInput);
|
||
});
|
||
BindCanceled(_gameplay, "Move", _ =>
|
||
{
|
||
MoveInput = Vector2.zero;
|
||
MoveEvent?.Invoke(Vector2.zero);
|
||
});
|
||
|
||
BindStarted(_gameplay, "Jump", () => JumpStartedEvent?.Invoke());
|
||
BindCanceled(_gameplay, "Jump", () => JumpCancelledEvent?.Invoke());
|
||
_jumpAction = _gameplay.FindAction("Jump", throwIfNotFound: false);
|
||
BindStarted(_gameplay, "Attack", () => AttackEvent?.Invoke());
|
||
BindStarted(_gameplay, "Parry", () => ParryEvent?.Invoke());
|
||
BindStarted(_gameplay, "Dash", () => DashEvent?.Invoke());
|
||
BindStarted(_gameplay, "UseSpring", () => UseSpringEvent?.Invoke());
|
||
BindStarted(_gameplay, "SwitchSkyForm", () => SwitchSkyFormEvent?.Invoke());
|
||
BindStarted(_gameplay, "SwitchEarthForm", () => SwitchEarthFormEvent?.Invoke());
|
||
BindStarted(_gameplay, "SwitchDeathForm", () => SwitchDeathFormEvent?.Invoke());
|
||
BindStarted(_gameplay, "SoulSkill", () => SoulSkillEvent?.Invoke());
|
||
BindStarted(_gameplay, "SpiritSkill1", () => SpiritSkill1StartedEvent?.Invoke());
|
||
BindCanceled(_gameplay, "SpiritSkill1", () => SpiritSkill1CancelledEvent?.Invoke());
|
||
BindStarted(_gameplay, "SpiritSkill2", () => SpiritSkill2StartedEvent?.Invoke());
|
||
BindCanceled(_gameplay, "SpiritSkill2", () => SpiritSkill2CancelledEvent?.Invoke());
|
||
BindStarted(_gameplay, "Spell", () => SpellCastEvent?.Invoke());
|
||
BindStarted(_gameplay, "Interact", () => InteractEvent?.Invoke());
|
||
BindCanceled(_gameplay, "Interact", () => InteractCancelledEvent?.Invoke());
|
||
|
||
|
||
BindStarted(_gameplay, "Pause", HandlePause);
|
||
// 背包菜单开关(可选 Action:默认绑定如 I 键 / 手柄 Select)。未在 Map 中定义则跳过。
|
||
BindStarted(_gameplay, "Inventory", () => _onInventoryOpen?.Raise());
|
||
|
||
if (_ui != null)
|
||
{
|
||
BindPerformed(_ui, "Navigate", ctx => NavigateEvent?.Invoke(ctx.ReadValue<Vector2>()));
|
||
BindCanceled(_ui, "Navigate", _ => NavigateEvent?.Invoke(Vector2.zero));
|
||
BindStarted(_ui, "Submit", () => SubmitEvent?.Invoke());
|
||
// UI 模式下 ESC 与手柄 Start 均归为 Cancel(返回/关闭栈顶);UI map 不再有 Pause action。
|
||
BindStarted(_ui, "Cancel", () => { CancelEvent?.Invoke(); _onUICancelPressed?.Raise(); });
|
||
BindPerformed(_ui, "Point", ctx => PointEvent?.Invoke(ctx.ReadValue<Vector2>()));
|
||
// R13-N3 小地图缩放档位切换;Action 名称需在 InputActionAsset UI Map 中添加(可选)
|
||
BindStarted(_ui, "CycleMinimapZoom", () => CycleMinimapZoomEvent?.Invoke());
|
||
// R13-N4 全屏地图居中;Action 名称需在 InputActionAsset UI Map 中添加(可选)
|
||
BindStarted(_ui, "MapCenter", () => MapCenterEvent?.Invoke());
|
||
// 背包 Tab 循环(L/R 肩键);Action 名称需在 InputActionAsset UI Map 中添加(可选)
|
||
BindStarted(_ui, "InventoryTabNext", () => _onInventoryTabNext?.Raise());
|
||
BindStarted(_ui, "InventoryTabPrev", () => _onInventoryTabPrev?.Raise());
|
||
}
|
||
|
||
_isBound = true;
|
||
}
|
||
|
||
private void HandlePause()
|
||
{
|
||
PauseEvent?.Invoke();
|
||
_onPauseRequested?.Raise();
|
||
}
|
||
|
||
private static void BindStarted(InputActionMap map, string name, Action callback)
|
||
{
|
||
var action = map.FindAction(name, throwIfNotFound: false);
|
||
if (action != null)
|
||
{
|
||
action.started += _ => callback();
|
||
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[BindStarted] Action '{name}' not found in map");
|
||
}
|
||
}
|
||
|
||
private static void BindPerformed(InputActionMap map, string name,
|
||
Action<InputAction.CallbackContext> callback)
|
||
{
|
||
var action = map.FindAction(name, throwIfNotFound: false);
|
||
if (action != null) action.performed += callback;
|
||
}
|
||
|
||
private static void BindCanceled(InputActionMap map, string name,
|
||
Action<InputAction.CallbackContext> callback)
|
||
{
|
||
var action = map.FindAction(name, throwIfNotFound: false);
|
||
if (action != null) action.canceled += callback;
|
||
}
|
||
|
||
private static void BindCanceled(InputActionMap map, string name, Action callback)
|
||
{
|
||
var action = map.FindAction(name, throwIfNotFound: false);
|
||
if (action != null) action.canceled += _ => callback();
|
||
}
|
||
|
||
// ── Rebinding API (P4-3) ──────────────────────────────────────────────
|
||
|
||
private const string PrefKey = "InputBindings";
|
||
|
||
/// <summary>查找单个 Action(供 RebindActionRow 显示当前绑定路径)。</summary>
|
||
public InputAction FindAction(string name)
|
||
=> _inputActions?.FindAction(name);
|
||
|
||
/// <summary>返回 Gameplay Map 所有 Action(供 ConflictDetector 扫描冲突)。</summary>
|
||
public IEnumerable<InputAction> GetAllActionMap()
|
||
{
|
||
var map = _inputActions?.FindActionMap("Gameplay");
|
||
return map != null ? (IEnumerable<InputAction>)map.actions : Array.Empty<InputAction>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动交互式重绑定(Unity InputSystem RebindingOperation)。
|
||
/// 调用方无需持有返回值;完成或取消后自动 Dispose。
|
||
/// </summary>
|
||
public void StartRebinding(
|
||
string actionName, int bindingIndex, Action onComplete, Action onCancel)
|
||
{
|
||
var action = _inputActions?.FindAction(actionName);
|
||
if (action == null) { onCancel?.Invoke(); return; }
|
||
|
||
action.Disable();
|
||
action.PerformInteractiveRebinding(bindingIndex)
|
||
.OnComplete(op => { op.Dispose(); action.Enable(); onComplete?.Invoke(); })
|
||
.OnCancel(op => { op.Dispose(); action.Enable(); onCancel?.Invoke(); })
|
||
.Start();
|
||
}
|
||
|
||
/// <summary>将当前绑定覆盖序列化为 JSON,存入 PlayerPrefs(key = "InputBindings")。</summary>
|
||
public void SaveBindingOverrides()
|
||
{
|
||
if (_inputActions == null) return;
|
||
PlayerPrefs.SetString(PrefKey, _inputActions.SaveBindingOverridesAsJson());
|
||
PlayerPrefs.Save();
|
||
}
|
||
|
||
/// <summary>从 PlayerPrefs 加载并应用绑定覆盖(首次启动时无操作)。</summary>
|
||
public void LoadBindingOverrides()
|
||
{
|
||
if (_inputActions != null && PlayerPrefs.HasKey(PrefKey))
|
||
_inputActions.LoadBindingOverridesFromJson(PlayerPrefs.GetString(PrefKey));
|
||
}
|
||
|
||
/// <summary>重置所有绑定为默认值并清除 PlayerPrefs 记录。</summary>
|
||
public void ResetBindings()
|
||
{
|
||
_inputActions?.RemoveAllBindingOverrides();
|
||
PlayerPrefs.DeleteKey(PrefKey);
|
||
}
|
||
}
|
||
}
|