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 } /// /// Handles input events and dispatches them to the appropriate ITouchInputConsumer. /// Supports tap and hold/drag logic, with interactable delegation and debug logging. /// 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 _overrideConsumers = new List(); // Track which consumer is handling the current hold operation private ITouchInputConsumer _activeHoldConsumer; /// /// Singleton instance of the InputManager. No longer creates an instance if one doesn't exist. /// 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().inputLogVerbosity; // Initialize settings reference early (GameManager sets these up in its Awake) _interactionSettings = GameManager.GetSettingsObject(); // Set up PlayerInput component and actions - critical for input to work playerInput = GetComponent(); 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; } } /// /// Called when any scene finishes loading. Restores input to GameAndUI mode. /// 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; } } /// /// Sets the default ITouchInputConsumer to receive input events. /// public void SetDefaultConsumer(ITouchInputConsumer consumer) { defaultConsumer = consumer; } /// /// Handles tap input, delegates to interactable if present, otherwise to default consumer. /// private void OnTapMovePerformed(InputAction.CallbackContext ctx) { Vector2 screenPos = positionAction.ReadValue(); 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"); } } /// /// Handles the start of a hold input. /// private void OnHoldMoveStarted(InputAction.CallbackContext ctx) { isHoldActive = true; Vector2 screenPos = positionAction.ReadValue(); 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); } /// /// Handles the end of a hold input. /// private void OnHoldMoveCanceled(InputAction.CallbackContext ctx) { if (!isHoldActive) return; isHoldActive = false; Vector2 screenPos = positionAction.ReadValue(); 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; } /// /// Continuously updates hold move input while active. /// void Update() { if (isHoldActive && holdMoveAction != null && holdMoveAction.phase == InputActionPhase.Performed) { Vector2 screenPos = positionAction.ReadValue(); 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); } } /// /// Attempts to delegate a tap to any ITouchInputConsumer at the given position. /// Checks both UI elements and world interactables. /// 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); } /// /// Attempts to delegate a tap to a UI element implementing ITouchInputConsumer. /// 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(); EventSystem.current.RaycastAll(eventData, results); foreach (var result in results) { // Try to get ITouchInputConsumer component var consumer = result.gameObject.GetComponent(); if (consumer == null) { consumer = result.gameObject.GetComponentInParent(); } if (consumer != null) { Logging.Debug($"Delegating tap to UI consumer at {screenPos} (GameObject: {result.gameObject.name})"); consumer.OnTap(screenPos); return true; } } return false; } /// /// Attempts to delegate a tap to an interactable at the given world position. /// Traces on the "Interactable" channel and logs detailed info. /// 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(); if (consumer == null) { consumer = hitWithMask.GetComponentInParent(); } 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(); 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; } /// /// Registers an override consumer to receive input events. /// The most recently registered consumer will receive input first. /// public void RegisterOverrideConsumer(ITouchInputConsumer consumer) { if (consumer == null || _overrideConsumers.Contains(consumer)) return; _overrideConsumers.Add(consumer); Logging.Debug($"Override consumer registered: {consumer}"); } /// /// Unregisters an override consumer, removing it from the input event delegation. /// 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}"); } /// /// Clears all registered override consumers. /// public void ClearOverrideConsumers() { _activeHoldConsumer = null; _overrideConsumers.Clear(); Logging.Debug("All override consumers cleared."); } /// /// Attempts to delegate a tap to the topmost registered override consumer, if any. /// 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; } } }