Files
2026-06-22 16:18:34 +02:00

253 lines
8.5 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using Ashwild.Core;
namespace Ashwild.Player
{
/// <summary>
/// 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.
/// </summary>
[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
/// <summary>
/// Global access point (rarely needed — consumers read PlayerEvents, not this).
/// </summary>
public static InputManager Instance { get; private set; }
private InputActionMap playerMap;
private InputActionMap uiMap;
/// <summary>
/// Undo callbacks recorded while wiring actions, invoked on teardown.
/// </summary>
private readonly List<Action> teardown = new List<Action>();
#endregion
#region Unity Lifecycle
/// <summary>
/// Establishes the persistent singleton and wires every action to the bus.
/// </summary>
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();
}
/// <summary>
/// Subscribes to the session lifecycle that gates the gameplay map.
/// </summary>
private void OnEnable()
{
PlayerEvents.LocalPlayerSpawned += EnableGameplay;
PlayerEvents.SessionStopped += DisableGameplay;
}
/// <summary>
/// Unsubscribes — mirrors OnEnable.
/// </summary>
private void OnDisable()
{
PlayerEvents.LocalPlayerSpawned -= EnableGameplay;
PlayerEvents.SessionStopped -= DisableGameplay;
}
/// <summary>
/// Tears down all action subscriptions and disables the maps.
/// </summary>
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
/// <summary>
/// Enables gameplay input once the local player exists.
/// </summary>
private void EnableGameplay() => playerMap?.Enable();
/// <summary>
/// Disables gameplay input when the session ends (back to the menu).
/// </summary>
private void DisableGameplay() => playerMap?.Disable();
#endregion
#region Wiring
/// <summary>
/// Maps every gameplay action to its PlayerEvents raiser.
/// </summary>
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));
}
}
/// <summary>
/// Maps the UI Cancel action to the bus.
/// </summary>
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
/// <summary>
/// Button press → parameterless raise.
/// </summary>
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);
}
/// <summary>
/// Hold action → true on press, false on release.
/// </summary>
private void WireHold(InputActionMap map, string name, Action<bool> 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; });
}
/// <summary>
/// Vector2 value → current value on change, zero on release.
/// </summary>
private void WireVector2(InputActionMap map, string name, Action<Vector2> raise)
{
InputAction action = Resolve(map, name);
if (action == null) return;
void Value(InputAction.CallbackContext c) => raise(c.ReadValue<Vector2>());
void Reset(InputAction.CallbackContext _) => raise(Vector2.zero);
action.performed += Value;
action.canceled += Reset;
teardown.Add(() => { action.performed -= Value; action.canceled -= Reset; });
}
/// <summary>
/// Scroll value → forwards its Y delta.
/// </summary>
private void WireScrollY(InputActionMap map, string name, Action<float> raise)
{
InputAction action = Resolve(map, name);
if (action == null) return;
void Value(InputAction.CallbackContext c) => raise(c.ReadValue<Vector2>().y);
action.performed += Value;
teardown.Add(() => action.performed -= Value);
}
/// <summary>
/// Finds an action in a map, warning (not throwing) when it is missing.
/// </summary>
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
}
}