2025-09-25 10:06:51 +00:00
using System ;
2025-10-10 14:31:51 +02:00
using System.Collections.Generic ; // Added for List<ITouchInputConsumer>
2025-09-25 10:06:51 +00:00
using UnityEngine ;
using UnityEngine.EventSystems ;
2025-09-01 15:04:15 +02:00
using UnityEngine.InputSystem ;
2025-09-25 10:06:51 +00:00
using UnityEngine.SceneManagement ;
2025-10-07 10:44:26 +02:00
using AppleHills.Core.Settings ; // Added for IInteractionSettings
2025-09-01 15:04:15 +02:00
2025-09-25 10:06:51 +00:00
namespace Input
2025-09-01 15:04:15 +02:00
{
2025-09-25 10:06:51 +00:00
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 : MonoBehaviour
2025-09-03 16:55:21 +02:00
{
2025-09-25 10:06:51 +00:00
private const string UiActions = "UI" ;
private const string GameActions = "PlayerTouch" ;
private static InputManager _instance ;
private static bool _isQuitting = false ;
2025-10-10 14:31:51 +02:00
// 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 > ( ) ;
2025-10-13 09:22:18 +02:00
// Track which consumer is handling the current hold operation
private ITouchInputConsumer _activeHoldConsumer ;
2025-09-25 10:06:51 +00:00
public static InputManager Instance
2025-09-03 16:55:21 +02:00
{
2025-09-25 10:06:51 +00:00
get
2025-09-03 16:55:21 +02:00
{
2025-09-25 10:06:51 +00:00
if ( _instance = = null & & Application . isPlaying & & ! _isQuitting )
2025-09-03 16:55:21 +02:00
{
2025-09-25 10:06:51 +00:00
_instance = FindAnyObjectByType < InputManager > ( ) ;
if ( _instance = = null )
{
var go = new GameObject ( "InputManager" ) ;
_instance = go . AddComponent < InputManager > ( ) ;
}
2025-09-03 16:55:21 +02:00
}
2025-09-25 10:06:51 +00:00
return _instance ;
2025-09-03 16:55:21 +02:00
}
}
2025-10-07 10:44:26 +02:00
// Settings reference
private IInteractionSettings _interactionSettings ;
2025-09-25 10:06:51 +00:00
private PlayerInput playerInput ;
private InputAction tapMoveAction ;
private InputAction holdMoveAction ;
private InputAction positionAction ;
private ITouchInputConsumer defaultConsumer ;
private bool isHoldActive ;
2025-09-05 14:10:42 +02:00
2025-09-25 10:06:51 +00:00
void Awake ( )
2025-09-01 15:04:15 +02:00
{
2025-09-25 10:06:51 +00:00
_instance = this ;
2025-10-07 10:44:26 +02:00
// Initialize settings reference
_interactionSettings = GameManager . GetSettingsObject < IInteractionSettings > ( ) ;
2025-09-25 10:06:51 +00:00
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 ) ;
2025-09-01 15:04:15 +02:00
}
2025-09-25 10:06:51 +00:00
private void Start ( )
2025-09-02 15:12:36 +02:00
{
2025-10-07 08:32:32 +02:00
// SceneManagerService.Instance.SceneLoadCompleted += SwitchInputOnSceneLoaded;
2025-09-25 10:06:51 +00:00
SwitchInputOnSceneLoaded ( SceneManager . GetActiveScene ( ) . name ) ;
2025-09-02 15:12:36 +02:00
}
2025-09-01 15:04:15 +02:00
2025-09-25 10:06:51 +00:00
private void SwitchInputOnSceneLoaded ( string sceneName )
2025-09-02 15:12:36 +02:00
{
2025-09-25 10:06:51 +00:00
if ( sceneName . ToLower ( ) . Contains ( "mainmenu" ) )
{
Debug . Log ( "[InputManager] SwitchInputOnSceneLoaded - Setting InputMode to UI for MainMenu" ) ;
2025-10-13 14:25:11 +02:00
SetInputMode ( InputMode . GameAndUI ) ;
2025-09-25 10:06:51 +00:00
}
else
{
Debug . Log ( "[InputManager] SwitchInputOnSceneLoaded - Setting InputMode to PlayerTouch" ) ;
SetInputMode ( InputMode . GameAndUI ) ;
}
2025-09-02 15:12:36 +02:00
}
2025-09-01 15:04:15 +02:00
2025-09-25 10:06:51 +00:00
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 OnEnable ( )
{
if ( tapMoveAction ! = null )
tapMoveAction . performed + = OnTapMovePerformed ;
if ( holdMoveAction ! = null )
{
holdMoveAction . performed + = OnHoldMoveStarted ;
holdMoveAction . canceled + = OnHoldMoveCanceled ;
}
}
2025-09-08 08:45:13 +02:00
2025-09-25 10:06:51 +00:00
void OnDisable ( )
{
if ( tapMoveAction ! = null )
tapMoveAction . performed - = OnTapMovePerformed ;
if ( holdMoveAction ! = null )
{
holdMoveAction . performed - = OnHoldMoveStarted ;
holdMoveAction . canceled - = OnHoldMoveCanceled ;
}
}
2025-09-01 15:04:15 +02:00
2025-09-25 10:06:51 +00:00
void OnApplicationQuit ( )
2025-09-05 14:10:42 +02:00
{
2025-09-25 10:06:51 +00:00
_isQuitting = true ;
2025-09-05 15:03:52 +02:00
}
2025-09-25 10:06:51 +00:00
/// <summary>
/// Sets the default ITouchInputConsumer to receive input events.
/// </summary>
public void SetDefaultConsumer ( ITouchInputConsumer consumer )
2025-09-05 15:03:52 +02:00
{
2025-09-25 10:06:51 +00:00
defaultConsumer = consumer ;
2025-09-05 14:10:42 +02:00
}
2025-09-01 15:04:15 +02:00
2025-09-25 10:06:51 +00:00
/// <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 ) ;
Debug . Log ( $"[InputManager] TapMove performed at {worldPos2D}" ) ;
2025-10-10 14:31:51 +02:00
// First try to delegate to an override consumer if available
if ( TryDelegateToOverrideConsumer ( screenPos , worldPos2D ) )
{
Debug . Log ( "[InputManager] Tap delegated to override consumer" ) ;
return ;
}
// Then try to delegate to any ITouchInputConsumer (UI or world interactable)
if ( ! TryDelegateToAnyInputConsumer ( screenPos , worldPos2D ) )
2025-09-25 10:06:51 +00:00
{
2025-10-10 14:31:51 +02:00
Debug . Log ( "[InputManager] No input consumer found, forwarding tap to default consumer" ) ;
2025-09-25 10:06:51 +00:00
defaultConsumer ? . OnTap ( worldPos2D ) ;
}
else
{
2025-10-10 14:31:51 +02:00
Debug . Log ( "[InputManager] Tap delegated to input consumer" ) ;
2025-09-25 10:06:51 +00:00
}
}
2025-09-02 15:12:36 +02:00
2025-09-25 10:06:51 +00:00
/// <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 ) ;
Debug . Log ( $"[InputManager] HoldMove started at {worldPos2D}" ) ;
2025-10-13 09:22:18 +02:00
// First check for override consumers
if ( _overrideConsumers . Count > 0 )
{
_activeHoldConsumer = _overrideConsumers [ _overrideConsumers . Count - 1 ] ;
Debug . Log ( $"[InputManager] Hold delegated to override consumer: {_activeHoldConsumer}" ) ;
_activeHoldConsumer . OnHoldStart ( worldPos2D ) ;
return ;
}
// If no override consumers, use default consumer
_activeHoldConsumer = defaultConsumer ;
2025-09-25 10:06:51 +00:00
defaultConsumer ? . OnHoldStart ( worldPos2D ) ;
}
2025-09-02 15:12:36 +02:00
2025-09-25 10:06:51 +00:00
/// <summary>
/// Handles the end of a hold input.
/// </summary>
private void OnHoldMoveCanceled ( InputAction . CallbackContext ctx )
2025-09-02 15:12:36 +02:00
{
2025-09-25 10:06:51 +00:00
if ( ! isHoldActive ) return ;
isHoldActive = false ;
2025-09-05 15:03:52 +02:00
Vector2 screenPos = positionAction . ReadValue < Vector2 > ( ) ;
2025-09-05 14:10:42 +02:00
Vector3 worldPos = Camera . main . ScreenToWorldPoint ( screenPos ) ;
2025-09-02 15:12:36 +02:00
Vector2 worldPos2D = new Vector2 ( worldPos . x , worldPos . y ) ;
2025-09-25 10:06:51 +00:00
Debug . Log ( $"[InputManager] HoldMove canceled at {worldPos2D}" ) ;
2025-10-13 09:22:18 +02:00
// Notify the active hold consumer that the hold has ended
_activeHoldConsumer ? . OnHoldEnd ( worldPos2D ) ;
_activeHoldConsumer = null ;
2025-09-02 15:12:36 +02:00
}
2025-09-01 15:04:15 +02:00
2025-09-25 10:06:51 +00:00
/// <summary>
/// Continuously updates hold move input while active.
/// </summary>
void Update ( )
2025-09-01 16:14:21 +02:00
{
2025-09-25 10:06:51 +00:00
if ( isHoldActive & & holdMoveAction ! = null & & holdMoveAction . phase = = InputActionPhase . Performed )
2025-09-10 16:42:43 +02:00
{
2025-09-25 10:06:51 +00:00
Vector2 screenPos = positionAction . ReadValue < Vector2 > ( ) ;
Vector3 worldPos = Camera . main . ScreenToWorldPoint ( screenPos ) ;
Vector2 worldPos2D = new Vector2 ( worldPos . x , worldPos . y ) ;
// Debug.Log($"[InputManager] HoldMove update at {worldPos2D}");
2025-10-13 09:22:18 +02:00
// Send hold move updates to the active hold consumer
_activeHoldConsumer ? . OnHoldMove ( worldPos2D ) ;
2025-09-01 16:14:21 +02:00
}
2025-09-08 12:44:31 +02:00
}
2025-09-25 10:06:51 +00:00
2025-10-10 14:31:51 +02:00
/// <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 )
{
Debug . unityLogger . Log ( "Interactable" , $"[InputManager] Delegating tap to UI consumer at {screenPos} (GameObject: {result.gameObject.name})" ) ;
consumer . OnTap ( screenPos ) ;
return true ;
}
}
return false ;
}
2025-09-25 10:06:51 +00:00
/// <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 )
2025-09-08 12:44:31 +02:00
{
2025-10-10 14:31:51 +02:00
// 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 )
{
Debug . unityLogger . Log ( "Interactable" , $"[InputManager] 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 ) ;
2025-09-25 10:06:51 +00:00
if ( hit ! = null )
{
var consumer = hit . GetComponent < ITouchInputConsumer > ( ) ;
if ( consumer ! = null )
{
Debug . unityLogger . Log ( "Interactable" , $"[InputManager] Delegating tap to consumer at {worldPos} (GameObject: {hit.gameObject.name})" ) ;
consumer . OnTap ( worldPos ) ;
return true ;
}
Debug . unityLogger . Log ( "Interactable" , $"[InputManager] Collider2D hit at {worldPos} (GameObject: {hit.gameObject.name}), but no ITouchInputConsumer found." ) ;
}
else
{
Debug . unityLogger . Log ( "Interactable" , $"[InputManager] No Collider2D found at {worldPos} for interactable delegation." ) ;
}
return false ;
2025-09-01 16:14:21 +02:00
}
2025-10-10 14:31:51 +02:00
/// <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 ) ;
Debug . Log ( $"[InputManager] 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 ;
2025-10-13 09:22:18 +02:00
// If this is the active hold consumer, reset it
if ( _activeHoldConsumer = = consumer )
{
_activeHoldConsumer = null ;
}
2025-10-10 14:31:51 +02:00
_overrideConsumers . Remove ( consumer ) ;
Debug . Log ( $"[InputManager] Override consumer unregistered: {consumer}" ) ;
}
/// <summary>
/// Clears all registered override consumers.
/// </summary>
public void ClearOverrideConsumers ( )
{
2025-10-13 09:22:18 +02:00
_activeHoldConsumer = null ;
2025-10-10 14:31:51 +02:00
_overrideConsumers . Clear ( ) ;
Debug . Log ( "[InputManager] 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 ] ;
Debug . unityLogger . Log ( "Interactable" , $"[InputManager] Delegating tap to override consumer at {worldPos} (GameObject: {consumer})" ) ;
consumer . OnTap ( worldPos ) ;
return true ;
}
2025-09-01 15:04:15 +02:00
}
}