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; // ── Gameplay Events ─────────────────────────────────────────────────── public event Action 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 NavigateEvent; public event Action SubmitEvent; public event Action CancelEvent; public event Action PointEvent; /// 小地图视野档位切换(默认绑定:Tab 键 / 手柄右肩键)。 public event Action CycleMinimapZoomEvent; /// 全屏地图"居中到玩家"(默认绑定:F 键 / 手柄右摇杆按下)。 public event Action MapCenterEvent; // ── Polling ─────────────────────────────────────────────────────────── public Vector2 MoveInput { get; private set; } /// 跳跃键当前是否处于按下状态。供 JumpState 在进入时检测短按是否已松开。 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(); 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); if (_ui != null) { BindPerformed(_ui, "Navigate", ctx => NavigateEvent?.Invoke(ctx.ReadValue())); BindCanceled(_ui, "Navigate", _ => NavigateEvent?.Invoke(Vector2.zero)); BindStarted(_ui, "Submit", () => SubmitEvent?.Invoke()); BindStarted(_ui, "Cancel", () => CancelEvent?.Invoke()); BindStarted(_ui, "Pause", HandlePause); BindPerformed(_ui, "Point", ctx => PointEvent?.Invoke(ctx.ReadValue())); // R13-N3 小地图缩放档位切换;Action 名称需在 InputActionAsset UI Map 中添加(可选) BindStarted(_ui, "CycleMinimapZoom", () => CycleMinimapZoomEvent?.Invoke()); // R13-N4 全屏地图居中;Action 名称需在 InputActionAsset UI Map 中添加(可选) BindStarted(_ui, "MapCenter", () => MapCenterEvent?.Invoke()); } _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 callback) { var action = map.FindAction(name, throwIfNotFound: false); if (action != null) action.performed += callback; } private static void BindCanceled(InputActionMap map, string name, Action 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"; /// 查找单个 Action(供 RebindActionRow 显示当前绑定路径)。 public InputAction FindAction(string name) => _inputActions?.FindAction(name); /// 返回 Gameplay Map 所有 Action(供 ConflictDetector 扫描冲突)。 public IEnumerable GetAllActionMap() { var map = _inputActions?.FindActionMap("Gameplay"); return map != null ? (IEnumerable)map.actions : Array.Empty(); } /// /// 启动交互式重绑定(Unity InputSystem RebindingOperation)。 /// 调用方无需持有返回值;完成或取消后自动 Dispose。 /// 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(); } /// 将当前绑定覆盖序列化为 JSON,存入 PlayerPrefs(key = "InputBindings")。 public void SaveBindingOverrides() { if (_inputActions == null) return; PlayerPrefs.SetString(PrefKey, _inputActions.SaveBindingOverridesAsJson()); PlayerPrefs.Save(); } /// 从 PlayerPrefs 加载并应用绑定覆盖(首次启动时无操作)。 public void LoadBindingOverrides() { if (_inputActions != null && PlayerPrefs.HasKey(PrefKey)) _inputActions.LoadBindingOverridesFromJson(PlayerPrefs.GetString(PrefKey)); } /// 重置所有绑定为默认值并清除 PlayerPrefs 记录。 public void ResetBindings() { _inputActions?.RemoveAllBindingOverrides(); PlayerPrefs.DeleteKey(PrefKey); } } }