253 lines
8.5 KiB
C#
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
|
|
}
|
|
}
|