Files
AppleHillsProduction/Assets/Scripts/Input/InputManager.cs
2025-12-07 20:34:43 +01:00

413 lines
16 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
using AppleHills.Core.Settings;
using Core;
using Core.Lifecycle;
namespace Input
{
public enum InputMode
{
Game,
UI,
GameAndUI,
InputDisabled
}
/// <summary>
/// Handles input events and dispatches them to the appropriate ITouchInputConsumer.
/// Supports tap and hold/drag logic, with interactable delegation and debug logging.
/// </summary>
public class InputManager : ManagedBehaviour
{
private const string UiActions = "UI";
private const string GameActions = "PlayerTouch";
private static InputManager _instance;
// Override consumer stack - using a list to support multiple overrides that can be removed in LIFO order
private readonly List<ITouchInputConsumer> _overrideConsumers = new List<ITouchInputConsumer>();
// Track which consumer is handling the current hold operation
private ITouchInputConsumer _activeHoldConsumer;
/// <summary>
/// Singleton instance of the InputManager. No longer creates an instance if one doesn't exist.
/// </summary>
public static InputManager Instance => _instance;
// Settings reference
private IInteractionSettings _interactionSettings;
private PlayerInput playerInput;
private InputAction tapMoveAction;
private InputAction holdMoveAction;
private InputAction positionAction;
private ITouchInputConsumer defaultConsumer;
private bool isHoldActive;
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
internal override void OnManagedAwake()
{
// Set instance immediately (early initialization)
_instance = this;
// Load verbosity settings early
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
// Initialize settings reference early (GameManager sets these up in its Awake)
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
// Set up PlayerInput component and actions - critical for input to work
playerInput = GetComponent<PlayerInput>();
if (playerInput == null)
{
Debug.LogError("[InputManager] InputManager requires a PlayerInput component attached to the same GameObject.");
return;
}
tapMoveAction = playerInput.actions.FindAction("TapMove", false);
holdMoveAction = playerInput.actions.FindAction("HoldMove", false);
positionAction = playerInput.actions.FindAction("TouchPosition", false);
if (tapMoveAction != null)
tapMoveAction.performed += OnTapMovePerformed;
if (holdMoveAction != null)
{
holdMoveAction.performed += OnHoldMoveStarted;
holdMoveAction.canceled += OnHoldMoveCanceled;
}
// Initialize input mode for current scene
SwitchInputOnSceneLoaded(SceneManager.GetActiveScene().name);
}
internal override void OnManagedStart()
{
// Subscribe to scene load events from SceneManagerService
// This must happen in ManagedStart because SceneManagerService instance needs to be set first
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
}
}
/// <summary>
/// Called when any scene finishes loading. Restores input to GameAndUI mode.
/// </summary>
private void OnSceneLoadCompleted(string sceneName)
{
Logging.Debug($"Scene loaded: {sceneName}, restoring input mode");
SwitchInputOnSceneLoaded(sceneName);
}
internal override void OnManagedDestroy()
{
// Unsubscribe from SceneManagerService events
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
}
// Input action cleanup happens automatically
}
private void SwitchInputOnSceneLoaded(string sceneName)
{
if (sceneName.ToLower().Contains("mainmenu"))
{
SetInputMode(InputMode.GameAndUI);
}
else
{
SetInputMode(InputMode.GameAndUI);
}
}
public void SetInputMode(InputMode inputMode)
{
switch (inputMode)
{
case InputMode.UI:
playerInput.actions.FindActionMap(UiActions).Enable();
playerInput.actions.FindActionMap(GameActions).Disable();
break;
case InputMode.Game:
playerInput.actions.FindActionMap(UiActions).Disable();
playerInput.actions.FindActionMap(GameActions).Enable();
break;
case InputMode.GameAndUI:
playerInput.actions.FindActionMap(UiActions).Enable();
playerInput.actions.FindActionMap(GameActions).Enable();
break;
case InputMode.InputDisabled:
playerInput.actions.FindActionMap(UiActions).Disable();
playerInput.actions.FindActionMap(GameActions).Disable();
break;
}
}
void OnDisable()
{
if (tapMoveAction != null)
tapMoveAction.performed -= OnTapMovePerformed;
if (holdMoveAction != null)
{
holdMoveAction.performed -= OnHoldMoveStarted;
holdMoveAction.canceled -= OnHoldMoveCanceled;
}
}
/// <summary>
/// Sets the default ITouchInputConsumer to receive input events.
/// </summary>
public void SetDefaultConsumer(ITouchInputConsumer consumer)
{
defaultConsumer = consumer;
}
/// <summary>
/// Handles tap input, delegates to interactable if present, otherwise to default consumer.
/// </summary>
private void OnTapMovePerformed(InputAction.CallbackContext ctx)
{
Vector2 screenPos = positionAction.ReadValue<Vector2>();
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
Vector2 worldPos2D = new Vector2(worldPos.x, worldPos.y);
Logging.Debug($"TapMove performed at {worldPos2D}");
// First try to delegate to an override consumer if available
if (TryDelegateToOverrideConsumer(screenPos, worldPos2D))
{
Logging.Debug("Tap delegated to override consumer");
return;
}
// Then try to delegate to any ITouchInputConsumer (UI or world interactable)
if (!TryDelegateToAnyInputConsumer(screenPos, worldPos2D))
{
Logging.Debug("No input consumer found, forwarding tap to default consumer");
defaultConsumer?.OnTap(worldPos2D);
}
else
{
Logging.Debug("Tap delegated to input consumer");
}
}
/// <summary>
/// Handles the start of a hold input.
/// </summary>
private void OnHoldMoveStarted(InputAction.CallbackContext ctx)
{
isHoldActive = true;
Vector2 screenPos = positionAction.ReadValue<Vector2>();
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
Vector2 worldPos2D = new Vector2(worldPos.x, worldPos.y);
Logging.Debug($"HoldMove started at {worldPos2D}");
// First check for override consumers
if (_overrideConsumers.Count > 0)
{
_activeHoldConsumer = _overrideConsumers[_overrideConsumers.Count - 1];
Logging.Debug($"Hold delegated to override consumer: {_activeHoldConsumer}");
_activeHoldConsumer.OnHoldStart(worldPos2D);
return;
}
// If no override consumers, use default consumer
_activeHoldConsumer = defaultConsumer;
defaultConsumer?.OnHoldStart(worldPos2D);
}
/// <summary>
/// Handles the end of a hold input.
/// </summary>
private void OnHoldMoveCanceled(InputAction.CallbackContext ctx)
{
if (!isHoldActive) return;
isHoldActive = false;
Vector2 screenPos = positionAction.ReadValue<Vector2>();
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
Vector2 worldPos2D = new Vector2(worldPos.x, worldPos.y);
Logging.Debug($"HoldMove canceled at {worldPos2D}");
// Notify the active hold consumer that the hold has ended
_activeHoldConsumer?.OnHoldEnd(worldPos2D);
_activeHoldConsumer = null;
}
/// <summary>
/// Continuously updates hold move input while active.
/// </summary>
void Update()
{
if (isHoldActive && holdMoveAction != null && holdMoveAction.phase == InputActionPhase.Performed)
{
Vector2 screenPos = positionAction.ReadValue<Vector2>();
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
Vector2 worldPos2D = new Vector2(worldPos.x, worldPos.y);
// LogDebugMessage($"HoldMove update at {worldPos2D}");
// Send hold move updates to the active hold consumer
_activeHoldConsumer?.OnHoldMove(worldPos2D);
}
}
/// <summary>
/// Attempts to delegate a tap to any ITouchInputConsumer at the given position.
/// Checks both UI elements and world interactables.
/// </summary>
private bool TryDelegateToAnyInputConsumer(Vector2 screenPos, Vector2 worldPos)
{
// First check if we hit a UI element implementing ITouchInputConsumer
if (TryDelegateToUIInputConsumer(screenPos))
{
return true;
}
// If no UI element with ITouchInputConsumer, try world interactables
return TryDelegateToInteractable(worldPos);
}
/// <summary>
/// Attempts to delegate a tap to a UI element implementing ITouchInputConsumer.
/// </summary>
private bool TryDelegateToUIInputConsumer(Vector2 screenPos)
{
// Check for UI elements under the pointer
var eventData = new PointerEventData(EventSystem.current)
{
position = screenPos
};
var results = new System.Collections.Generic.List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, results);
foreach (var result in results)
{
// Try to get ITouchInputConsumer component
var consumer = result.gameObject.GetComponent<ITouchInputConsumer>();
if (consumer == null)
{
consumer = result.gameObject.GetComponentInParent<ITouchInputConsumer>();
}
if (consumer != null)
{
Logging.Debug($"Delegating tap to UI consumer at {screenPos} (GameObject: {result.gameObject.name})");
consumer.OnTap(screenPos);
return true;
}
}
return false;
}
/// <summary>
/// Attempts to delegate a tap to an interactable at the given world position.
/// Traces on the "Interactable" channel and logs detailed info.
/// </summary>
private bool TryDelegateToInteractable(Vector2 worldPos)
{
// First try with the interaction layer mask if available
if (_interactionSettings != null)
{
LayerMask mask = _interactionSettings.InteractableLayerMask;
Collider2D hitWithMask = Physics2D.OverlapPoint(worldPos, mask);
if (hitWithMask != null)
{
var consumer = hitWithMask.GetComponent<ITouchInputConsumer>();
if (consumer == null)
{
consumer = hitWithMask.GetComponentInParent<ITouchInputConsumer>();
}
if (consumer != null)
{
Logging.Debug($"Delegating tap to consumer at {worldPos} (GameObject: {hitWithMask.gameObject.name})");
consumer.OnTap(worldPos);
return true;
}
}
}
// If no hit with the mask or no settings available, try with all colliders
Collider2D hit = Physics2D.OverlapPoint(worldPos);
if (hit != null)
{
var consumer = hit.GetComponent<ITouchInputConsumer>();
if (consumer != null)
{
Logging.Debug($"Delegating tap to consumer at {worldPos} (GameObject: {hit.gameObject.name})");
consumer.OnTap(worldPos);
return true;
}
Logging.Debug($"Collider2D hit at {worldPos} (GameObject: {hit.gameObject.name}), but no ITouchInputConsumer found.");
}
else
{
Logging.Debug($"No Collider2D found at {worldPos} for interactable delegation.");
}
return false;
}
/// <summary>
/// Registers an override consumer to receive input events.
/// The most recently registered consumer will receive input first.
/// </summary>
public void RegisterOverrideConsumer(ITouchInputConsumer consumer)
{
if (consumer == null || _overrideConsumers.Contains(consumer))
return;
_overrideConsumers.Add(consumer);
Logging.Debug($"Override consumer registered: {consumer}");
}
/// <summary>
/// Unregisters an override consumer, removing it from the input event delegation.
/// </summary>
public void UnregisterOverrideConsumer(ITouchInputConsumer consumer)
{
if (consumer == null || !_overrideConsumers.Contains(consumer))
return;
// If this is the active hold consumer, reset it
if (_activeHoldConsumer == consumer)
{
_activeHoldConsumer = null;
}
_overrideConsumers.Remove(consumer);
Logging.Debug($"Override consumer unregistered: {consumer}");
}
/// <summary>
/// Clears all registered override consumers.
/// </summary>
public void ClearOverrideConsumers()
{
_activeHoldConsumer = null;
_overrideConsumers.Clear();
Logging.Debug("All override consumers cleared.");
}
/// <summary>
/// Attempts to delegate a tap to the topmost registered override consumer, if any.
/// </summary>
private bool TryDelegateToOverrideConsumer(Vector2 screenPos, Vector2 worldPos)
{
if (_overrideConsumers.Count == 0)
return false;
// Get the topmost override consumer (last registered)
var consumer = _overrideConsumers[_overrideConsumers.Count - 1];
Logging.Debug($"Delegating tap to override consumer at {worldPos} (GameObject: {consumer})");
consumer.OnTap(worldPos);
return true;
}
}
}