424 lines
16 KiB
C#
424 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;
|
|
|
|
public override int ManagedAwakePriority => 25; // Input infrastructure
|
|
|
|
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)
|
|
{
|
|
LogDebugMessage($"Scene loaded: {sceneName}, restoring input mode");
|
|
SwitchInputOnSceneLoaded(sceneName);
|
|
}
|
|
|
|
protected override void OnDestroy()
|
|
{
|
|
// Unsubscribe from SceneManagerService events
|
|
if (SceneManagerService.Instance != null)
|
|
{
|
|
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
|
|
}
|
|
|
|
base.OnDestroy();
|
|
// 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);
|
|
LogDebugMessage($"TapMove performed at {worldPos2D}");
|
|
|
|
// First try to delegate to an override consumer if available
|
|
if (TryDelegateToOverrideConsumer(screenPos, worldPos2D))
|
|
{
|
|
LogDebugMessage("Tap delegated to override consumer");
|
|
return;
|
|
}
|
|
|
|
// Then try to delegate to any ITouchInputConsumer (UI or world interactable)
|
|
if (!TryDelegateToAnyInputConsumer(screenPos, worldPos2D))
|
|
{
|
|
LogDebugMessage("No input consumer found, forwarding tap to default consumer");
|
|
defaultConsumer?.OnTap(worldPos2D);
|
|
}
|
|
else
|
|
{
|
|
LogDebugMessage("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);
|
|
LogDebugMessage($"HoldMove started at {worldPos2D}");
|
|
|
|
// First check for override consumers
|
|
if (_overrideConsumers.Count > 0)
|
|
{
|
|
_activeHoldConsumer = _overrideConsumers[_overrideConsumers.Count - 1];
|
|
LogDebugMessage($"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);
|
|
LogDebugMessage($"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)
|
|
{
|
|
LogDebugMessage($"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)
|
|
{
|
|
LogDebugMessage($"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)
|
|
{
|
|
LogDebugMessage($"Delegating tap to consumer at {worldPos} (GameObject: {hit.gameObject.name})");
|
|
consumer.OnTap(worldPos);
|
|
return true;
|
|
}
|
|
LogDebugMessage($"Collider2D hit at {worldPos} (GameObject: {hit.gameObject.name}), but no ITouchInputConsumer found.");
|
|
}
|
|
else
|
|
{
|
|
LogDebugMessage($"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);
|
|
LogDebugMessage($"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);
|
|
LogDebugMessage($"Override consumer unregistered: {consumer}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all registered override consumers.
|
|
/// </summary>
|
|
public void ClearOverrideConsumers()
|
|
{
|
|
_activeHoldConsumer = null;
|
|
_overrideConsumers.Clear();
|
|
LogDebugMessage("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];
|
|
LogDebugMessage($"Delegating tap to override consumer at {worldPos} (GameObject: {consumer})");
|
|
consumer.OnTap(worldPos);
|
|
return true;
|
|
}
|
|
|
|
private void LogDebugMessage(string message)
|
|
{
|
|
if (_logVerbosity <= LogVerbosity.Debug)
|
|
{
|
|
Logging.Debug($"[InputManager] {message}");
|
|
}
|
|
}
|
|
}
|
|
}
|