using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; using Ashwild.Core; namespace Ashwild.Player { /// /// The single, persistent source of input. It reads the project InputActionAsset and republishes /// every action onto the static PlayerEvents bus, so gameplay and UI both consume input the same /// way without depending on a player GameObject. Persists across scenes via its PersistentRoot /// parent, so it works in every scene. Replaces the old PlayerInput component + PlayerInputRouter. /// /// The UI map (Cancel) is always active; the Player map is only active in-game (between /// LocalPlayerSpawned and SessionStopped) so menu typing never triggers gameplay actions. /// [DisallowMultipleComponent] public class InputManager : MonoBehaviour { #region Constants private const string PlayerMapName = "Player"; private const string UIMapName = "UI"; #endregion #region Serialized Fields [Header("Input")] [Tooltip("The InputSystem_Actions asset (Player + UI maps).")] [SerializeField] private InputActionAsset actions; #endregion #region State /// /// Global access point (rarely needed — consumers read PlayerEvents, not this). /// public static InputManager Instance { get; private set; } private InputActionMap playerMap; private InputActionMap uiMap; /// /// Undo callbacks recorded while wiring actions, invoked on teardown. /// private readonly List teardown = new List(); #endregion #region Unity Lifecycle /// /// Establishes the persistent singleton and wires every action to the bus. /// private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; // Detach to the scene root and persist — leaves the (possibly mixed) Managers parent // behind so non-persistent siblings die with the scene as usual. Persistence.Persist(gameObject); if (actions == null) { Debug.LogError("[InputManager] No InputActionAsset assigned.", this); return; } playerMap = actions.FindActionMap(PlayerMapName, false); uiMap = actions.FindActionMap(UIMapName, false); WirePlayerMap(); WireUIMap(); // Cancel (back / pause / close) must work everywhere, menu included. uiMap?.Enable(); } /// /// Subscribes to the session lifecycle that gates the gameplay map. /// private void OnEnable() { PlayerEvents.LocalPlayerSpawned += EnableGameplay; PlayerEvents.SessionStopped += DisableGameplay; } /// /// Unsubscribes — mirrors OnEnable. /// private void OnDisable() { PlayerEvents.LocalPlayerSpawned -= EnableGameplay; PlayerEvents.SessionStopped -= DisableGameplay; } /// /// Tears down all action subscriptions and disables the maps. /// private void OnDestroy() { if (Instance == this) Instance = null; foreach (Action undo in teardown) undo(); teardown.Clear(); playerMap?.Disable(); uiMap?.Disable(); } #endregion #region Gameplay Map Gating /// /// Enables gameplay input once the local player exists. /// private void EnableGameplay() => playerMap?.Enable(); /// /// Disables gameplay input when the session ends (back to the menu). /// private void DisableGameplay() => playerMap?.Disable(); #endregion #region Wiring /// /// Maps every gameplay action to its PlayerEvents raiser. /// private void WirePlayerMap() { if (playerMap == null) { Debug.LogError($"[InputManager] No '{PlayerMapName}' action map found.", this); return; } WireVector2(playerMap, "Move", PlayerEvents.RaiseMoveInput); WireVector2(playerMap, "Look", PlayerEvents.RaiseLookInput); WireButton(playerMap, "Jump", PlayerEvents.RaiseJumpPressed); WireHold(playerMap, "Sprint", PlayerEvents.RaiseSprintHeld); WireButton(playerMap, "Crouch", PlayerEvents.RaiseCrouchPressed); WireButton(playerMap, "Attack", PlayerEvents.RaiseAttackPressed); WireButton(playerMap, "Interact", PlayerEvents.RaiseInteractPressed); WireButton(playerMap, "Drop", PlayerEvents.RaiseDropPressed); WireButton(playerMap, "Inventory", PlayerEvents.RaiseInventoryTogglePressed); WireScrollY(playerMap, "HotbarScroll", PlayerEvents.RaiseHotbarScroll); for (int i = 0; i < 10; i++) { int slot = i; WireButton(playerMap, $"HotbarSlot{i + 1}", () => PlayerEvents.RaiseHotbarSlotPressed(slot)); } } /// /// Maps the UI Cancel action to the bus. /// private void WireUIMap() { if (uiMap == null) { Debug.LogError($"[InputManager] No '{UIMapName}' action map found.", this); return; } WireButton(uiMap, "Cancel", PlayerEvents.RaiseCancelPressed); } #endregion #region Wiring Helpers /// /// Button press → parameterless raise. /// private void WireButton(InputActionMap map, string name, Action raise) { InputAction action = Resolve(map, name); if (action == null) return; void Handler(InputAction.CallbackContext _) => raise(); action.performed += Handler; teardown.Add(() => action.performed -= Handler); } /// /// Hold action → true on press, false on release. /// private void WireHold(InputActionMap map, string name, Action raise) { InputAction action = Resolve(map, name); if (action == null) return; void Down(InputAction.CallbackContext _) => raise(true); void Up(InputAction.CallbackContext _) => raise(false); action.performed += Down; action.canceled += Up; teardown.Add(() => { action.performed -= Down; action.canceled -= Up; }); } /// /// Vector2 value → current value on change, zero on release. /// private void WireVector2(InputActionMap map, string name, Action raise) { InputAction action = Resolve(map, name); if (action == null) return; void Value(InputAction.CallbackContext c) => raise(c.ReadValue()); void Reset(InputAction.CallbackContext _) => raise(Vector2.zero); action.performed += Value; action.canceled += Reset; teardown.Add(() => { action.performed -= Value; action.canceled -= Reset; }); } /// /// Scroll value → forwards its Y delta. /// private void WireScrollY(InputActionMap map, string name, Action raise) { InputAction action = Resolve(map, name); if (action == null) return; void Value(InputAction.CallbackContext c) => raise(c.ReadValue().y); action.performed += Value; teardown.Add(() => action.performed -= Value); } /// /// Finds an action in a map, warning (not throwing) when it is missing. /// private InputAction Resolve(InputActionMap map, string name) { InputAction action = map.FindAction(name, false); if (action == null) Debug.LogWarning($"[InputManager] Action '{name}' not found in map '{map.name}'.", this); return action; } #endregion } }