maze_switching (#82)
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #82
This commit is contained in:
@@ -9,9 +9,10 @@ namespace Input
|
||||
/// <summary>
|
||||
/// Base class for player movement controllers.
|
||||
/// Handles tap-to-move and hold-to-move input with pathfinding or direct movement.
|
||||
/// Implements IInteractingCharacter to enable interaction with items.
|
||||
/// Derived classes can override to add specialized behavior (e.g., shader updates).
|
||||
/// </summary>
|
||||
public abstract class BasePlayerMovementController : ManagedBehaviour, ITouchInputConsumer
|
||||
public abstract class BasePlayerMovementController : ManagedBehaviour, ITouchInputConsumer, IInteractingCharacter
|
||||
{
|
||||
[Header("Movement")]
|
||||
[SerializeField] protected float moveSpeed = 5f;
|
||||
@@ -42,6 +43,12 @@ namespace Input
|
||||
public event System.Action OnMovementStarted;
|
||||
public event System.Action OnMovementStopped;
|
||||
|
||||
// IInteractingCharacter implementation - scripted movement for interactions
|
||||
private Coroutine _moveToCoroutine;
|
||||
private bool _interruptMoveTo;
|
||||
public event System.Action OnArrivedAtTarget;
|
||||
public event System.Action OnMoveToCancelled;
|
||||
|
||||
// Components
|
||||
protected AIPath _aiPath;
|
||||
protected Animator _animator;
|
||||
@@ -67,13 +74,6 @@ namespace Input
|
||||
{
|
||||
base.OnManagedStart();
|
||||
|
||||
// Register with InputManager
|
||||
if (InputManager.Instance != null)
|
||||
{
|
||||
InputManager.Instance.SetDefaultConsumer(this);
|
||||
Logging.Debug($"[{GetType().Name}] Registered as default input consumer");
|
||||
}
|
||||
|
||||
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ namespace Input
|
||||
|
||||
public virtual void OnTap(Vector2 worldPosition)
|
||||
{
|
||||
InterruptMoveTo(); // Cancel any scripted movement
|
||||
Logging.Debug($"[{GetType().Name}] OnTap at {worldPosition}");
|
||||
if (_aiPath != null)
|
||||
{
|
||||
@@ -116,6 +117,7 @@ namespace Input
|
||||
|
||||
public virtual void OnHoldStart(Vector2 worldPosition)
|
||||
{
|
||||
InterruptMoveTo(); // Cancel any scripted movement
|
||||
Logging.Debug($"[{GetType().Name}] OnHoldStart at {worldPosition}");
|
||||
_lastHoldPosition = worldPosition;
|
||||
_isHolding = true;
|
||||
@@ -325,6 +327,103 @@ namespace Input
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IInteractingCharacter Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Controller-driven interaction movement. Base implementation moves this controller to the interactable.
|
||||
/// Override in derived classes for custom behavior (e.g., PlayerTouchController handles follower dispatch).
|
||||
/// </summary>
|
||||
public virtual async System.Threading.Tasks.Task<bool> MoveToInteractableAsync(Interactions.InteractableBase interactable)
|
||||
{
|
||||
// Default behavior: move self to interactable position
|
||||
Vector3 targetPosition = interactable.transform.position;
|
||||
|
||||
// Check for custom CharacterMoveToTarget
|
||||
var moveTargets = interactable.GetComponentsInChildren<Interactions.CharacterMoveToTarget>();
|
||||
foreach (var target in moveTargets)
|
||||
{
|
||||
if (target.characterType == Interactions.CharacterToInteract.Trafalgar ||
|
||||
target.characterType == Interactions.CharacterToInteract.Both)
|
||||
{
|
||||
targetPosition = target.GetTargetPosition();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Use MovementUtilities to handle movement
|
||||
return await Utils.MovementUtilities.MoveToPositionAsync(this, targetPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the character to a specific target position and notifies via events when arrived or cancelled.
|
||||
/// This is used by systems like interactions to orchestrate scripted movement.
|
||||
/// </summary>
|
||||
public virtual void MoveToAndNotify(Vector3 target)
|
||||
{
|
||||
// Cancel any previous move-to coroutine
|
||||
if (_moveToCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_moveToCoroutine);
|
||||
}
|
||||
|
||||
_interruptMoveTo = false;
|
||||
// Ensure pathfinding is enabled for MoveToAndNotify
|
||||
if (_aiPath != null)
|
||||
{
|
||||
_aiPath.enabled = true;
|
||||
_aiPath.canMove = true;
|
||||
_aiPath.isStopped = false;
|
||||
}
|
||||
_moveToCoroutine = StartCoroutine(MoveToTargetCoroutine(target));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels any in-progress MoveToAndNotify operation and fires the cancellation event.
|
||||
/// </summary>
|
||||
public virtual void InterruptMoveTo()
|
||||
{
|
||||
_interruptMoveTo = true;
|
||||
_isHolding = false;
|
||||
_directMoveVelocity = Vector3.zero;
|
||||
if (Settings != null && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct && _aiPath != null)
|
||||
_aiPath.enabled = false;
|
||||
OnMoveToCancelled?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine for moving the character to a target position and firing arrival/cancel events.
|
||||
/// </summary>
|
||||
protected virtual System.Collections.IEnumerator MoveToTargetCoroutine(Vector3 target)
|
||||
{
|
||||
if (_aiPath != null)
|
||||
{
|
||||
_aiPath.destination = target;
|
||||
_aiPath.maxSpeed = Settings.MoveSpeed;
|
||||
_aiPath.maxAcceleration = Settings.MaxAcceleration;
|
||||
}
|
||||
|
||||
while (!_interruptMoveTo)
|
||||
{
|
||||
Vector2 current2D = new Vector2(transform.position.x, transform.position.y);
|
||||
Vector2 target2D = new Vector2(target.x, target.y);
|
||||
float dist = Vector2.Distance(current2D, target2D);
|
||||
if (dist <= Settings.StopDistance + 0.2f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_moveToCoroutine = null;
|
||||
if (!_interruptMoveTo)
|
||||
{
|
||||
OnArrivedAtTarget?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
47
Assets/Scripts/Input/IInteractingCharacter.cs
Normal file
47
Assets/Scripts/Input/IInteractingCharacter.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for characters that can participate in scripted interactions.
|
||||
/// Provides movement-to-target with arrival/cancellation notifications.
|
||||
/// Implemented by BasePlayerMovementController to enable all controllers to interact with items.
|
||||
/// </summary>
|
||||
public interface IInteractingCharacter
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller-driven interaction movement. Each controller implements its own behavior
|
||||
/// based on the interactable's settings (characterToInteract, CharacterMoveToTarget, etc.)
|
||||
/// </summary>
|
||||
/// <param name="interactable">The interactable to move to</param>
|
||||
/// <returns>True if movement succeeded and character arrived, false if cancelled/failed</returns>
|
||||
Task<bool> MoveToInteractableAsync(Interactions.InteractableBase interactable);
|
||||
|
||||
/// <summary>
|
||||
/// Moves character to target position and notifies when arrived/cancelled
|
||||
/// </summary>
|
||||
void MoveToAndNotify(Vector3 target);
|
||||
|
||||
/// <summary>
|
||||
/// Interrupts any in-progress MoveToAndNotify operation
|
||||
/// </summary>
|
||||
void InterruptMoveTo();
|
||||
|
||||
/// <summary>
|
||||
/// Fired when character arrives at MoveToAndNotify target
|
||||
/// </summary>
|
||||
event System.Action OnArrivedAtTarget;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when MoveToAndNotify is cancelled/interrupted
|
||||
/// </summary>
|
||||
event System.Action OnMoveToCancelled;
|
||||
|
||||
/// <summary>
|
||||
/// Character's transform (for position queries)
|
||||
/// </summary>
|
||||
Transform transform { get; }
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Input/IInteractingCharacter.cs.meta
Normal file
3
Assets/Scripts/Input/IInteractingCharacter.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d1b1be281334b9390cc96d7a8ff3132
|
||||
timeCreated: 1765754798
|
||||
@@ -33,6 +33,9 @@ namespace Input
|
||||
|
||||
// Track which consumer is handling the current hold operation
|
||||
private ITouchInputConsumer _activeHoldConsumer;
|
||||
|
||||
// Controller registration system
|
||||
private readonly Dictionary<string, ITouchInputConsumer> _registeredControllers = new Dictionary<string, ITouchInputConsumer>();
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance of the InputManager. No longer creates an instance if one doesn't exist.
|
||||
@@ -408,5 +411,126 @@ namespace Input
|
||||
consumer.OnTap(worldPos);
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Controller Registration System
|
||||
|
||||
/// <summary>
|
||||
/// Registers a controller with a unique name for later switching.
|
||||
/// </summary>
|
||||
/// <param name="controllerName">Unique name for the controller</param>
|
||||
/// <param name="controller">The controller instance to register</param>
|
||||
/// <param name="setAsDefaultConsumer">If true, sets this controller as the default input consumer</param>
|
||||
public void RegisterController(string controllerName, ITouchInputConsumer controller, bool setAsDefaultConsumer = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(controllerName))
|
||||
{
|
||||
Debug.LogError("[InputManager] Cannot register controller with null or empty name.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (controller == null)
|
||||
{
|
||||
Debug.LogError($"[InputManager] Cannot register null controller for name: {controllerName}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_registeredControllers.ContainsKey(controllerName))
|
||||
{
|
||||
Debug.LogWarning($"[InputManager] Controller with name '{controllerName}' is already registered. Overwriting.");
|
||||
}
|
||||
|
||||
_registeredControllers[controllerName] = controller;
|
||||
Logging.Debug($"Controller registered: {controllerName}");
|
||||
|
||||
if (setAsDefaultConsumer)
|
||||
{
|
||||
SetDefaultConsumer(controller);
|
||||
Logging.Debug($"Controller '{controllerName}' set as default consumer.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a controller by name.
|
||||
/// </summary>
|
||||
/// <param name="controllerName">Name of the controller to unregister</param>
|
||||
public void UnregisterController(string controllerName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(controllerName))
|
||||
{
|
||||
Debug.LogError("[InputManager] Cannot unregister controller with null or empty name.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_registeredControllers.Remove(controllerName))
|
||||
{
|
||||
Logging.Debug($"Controller unregistered: {controllerName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[InputManager] Attempted to unregister non-existent controller: {controllerName}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a registered controller by name.
|
||||
/// </summary>
|
||||
/// <param name="controllerName">Name of the controller to retrieve</param>
|
||||
/// <returns>The controller if found, null otherwise</returns>
|
||||
public ITouchInputConsumer GetController(string controllerName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(controllerName))
|
||||
{
|
||||
Debug.LogError("[InputManager] Cannot get controller with null or empty name.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_registeredControllers.TryGetValue(controllerName, out ITouchInputConsumer controller))
|
||||
{
|
||||
return controller;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"[InputManager] Controller not found: {controllerName}");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches to a registered controller by name, setting it as the default consumer.
|
||||
/// </summary>
|
||||
/// <param name="controllerName">Name of the controller to switch to</param>
|
||||
/// <returns>True if the switch was successful, false otherwise</returns>
|
||||
public bool SwitchToController(string controllerName)
|
||||
{
|
||||
ITouchInputConsumer controller = GetController(controllerName);
|
||||
if (controller != null)
|
||||
{
|
||||
SetDefaultConsumer(controller);
|
||||
Logging.Debug($"Switched to controller: {controllerName}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a controller with the given name is registered.
|
||||
/// </summary>
|
||||
/// <param name="controllerName">Name to check</param>
|
||||
/// <returns>True if registered, false otherwise</returns>
|
||||
public bool IsControllerRegistered(string controllerName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(controllerName) && _registeredControllers.ContainsKey(controllerName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently active controller (the default consumer).
|
||||
/// This is the controller that currently has input control.
|
||||
/// </summary>
|
||||
/// <returns>The active controller, or null if no default consumer is set</returns>
|
||||
public ITouchInputConsumer GetActiveController()
|
||||
{
|
||||
return defaultConsumer;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,17 +16,12 @@ namespace Input
|
||||
|
||||
/// <summary>
|
||||
/// Handles player movement in response to tap and hold input events.
|
||||
/// Supports both direct and pathfinding movement modes, and provides event/callbacks for arrival/cancellation.
|
||||
/// Extends BasePlayerMovementController with save/load and MoveToAndNotify functionality.
|
||||
/// Supports both direct and pathfinding movement modes.
|
||||
/// Extends BasePlayerMovementController with save/load functionality.
|
||||
/// Interaction capability (MoveToAndNotify) is provided by base class.
|
||||
/// </summary>
|
||||
public class PlayerTouchController : BasePlayerMovementController
|
||||
{
|
||||
// --- PlayerTouchController-specific features (MoveToAndNotify) ---
|
||||
public delegate void ArrivedAtTargetHandler();
|
||||
private Coroutine _moveToCoroutine;
|
||||
public event ArrivedAtTargetHandler OnArrivedAtTarget;
|
||||
public event System.Action OnMoveToCancelled;
|
||||
private bool _interruptMoveTo;
|
||||
|
||||
// Save system configuration
|
||||
public override bool AutoRegisterForSave => true;
|
||||
@@ -39,89 +34,126 @@ namespace Input
|
||||
_movementSettings = configs.DefaultPlayerMovement;
|
||||
}
|
||||
|
||||
#region ITouchInputConsumer Overrides (Add InterruptMoveTo)
|
||||
|
||||
public override void OnTap(Vector2 worldPosition)
|
||||
internal override void OnManagedStart()
|
||||
{
|
||||
InterruptMoveTo();
|
||||
base.OnTap(worldPosition);
|
||||
base.OnManagedStart();
|
||||
|
||||
// Register with InputManager as default consumer
|
||||
if (InputManager.Instance != null)
|
||||
{
|
||||
InputManager.Instance.RegisterController("trafalgar", this, setAsDefaultConsumer: true);
|
||||
Logging.Debug($"[PlayerTouchController] Registered controller '{gameObject.name}' as default consumer");
|
||||
}
|
||||
}
|
||||
|
||||
#region IInteractingCharacter Override
|
||||
|
||||
public override void OnHoldStart(Vector2 worldPosition)
|
||||
/// <summary>
|
||||
/// PlayerTouchController-specific interaction movement.
|
||||
/// Handles main character movement + follower dispatch based on interactable.characterToInteract setting.
|
||||
/// </summary>
|
||||
public override async System.Threading.Tasks.Task<bool> MoveToInteractableAsync(Interactions.InteractableBase interactable)
|
||||
{
|
||||
InterruptMoveTo();
|
||||
base.OnHoldStart(worldPosition);
|
||||
var characterToInteract = interactable.characterToInteract;
|
||||
|
||||
// If None, skip movement
|
||||
if (characterToInteract == Interactions.CharacterToInteract.None)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Determine stop distance based on interaction type
|
||||
float stopDistance;
|
||||
if (characterToInteract == Interactions.CharacterToInteract.Trafalgar)
|
||||
{
|
||||
// Move ONLY main character directly to item (close distance)
|
||||
stopDistance = Core.GameManager.Instance.PlayerStopDistanceDirectInteraction;
|
||||
}
|
||||
else // Pulver or Both
|
||||
{
|
||||
// Move main character to radius (far distance)
|
||||
stopDistance = Core.GameManager.Instance.PlayerStopDistance;
|
||||
}
|
||||
|
||||
// Calculate stop position for main character
|
||||
Vector3 stopPoint = interactable.transform.position;
|
||||
bool customTargetFound = false;
|
||||
|
||||
// Check for custom CharacterMoveToTarget for main character
|
||||
var moveTargets = interactable.GetComponentsInChildren<Interactions.CharacterMoveToTarget>();
|
||||
foreach (var target in moveTargets)
|
||||
{
|
||||
if (target.characterType == Interactions.CharacterToInteract.Trafalgar ||
|
||||
target.characterType == Interactions.CharacterToInteract.Both)
|
||||
{
|
||||
stopPoint = target.GetTargetPosition();
|
||||
customTargetFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no custom target, calculate based on distance
|
||||
if (!customTargetFound)
|
||||
{
|
||||
stopPoint = Utils.MovementUtilities.CalculateStopPosition(
|
||||
interactable.transform.position,
|
||||
transform.position,
|
||||
stopDistance
|
||||
);
|
||||
}
|
||||
|
||||
// Move main character
|
||||
bool mainCharacterArrived = await Utils.MovementUtilities.MoveToPositionAsync(this, stopPoint);
|
||||
|
||||
if (!mainCharacterArrived)
|
||||
{
|
||||
return false; // Movement cancelled
|
||||
}
|
||||
|
||||
// Handle follower dispatch based on interaction type
|
||||
if (characterToInteract == Interactions.CharacterToInteract.Pulver ||
|
||||
characterToInteract == Interactions.CharacterToInteract.Both)
|
||||
{
|
||||
// Find follower and dispatch to interactable
|
||||
var followerController = FindFirstObjectByType<FollowerController>();
|
||||
if (followerController != null)
|
||||
{
|
||||
// Determine follower target position
|
||||
Vector3 followerTarget = interactable.transform.position;
|
||||
|
||||
// Check for custom target for Pulver
|
||||
foreach (var target in moveTargets)
|
||||
{
|
||||
if (target.characterType == Interactions.CharacterToInteract.Pulver ||
|
||||
target.characterType == Interactions.CharacterToInteract.Both)
|
||||
{
|
||||
followerTarget = target.GetTargetPosition();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for follower to arrive
|
||||
var tcs = new System.Threading.Tasks.TaskCompletionSource<bool>();
|
||||
|
||||
void OnFollowerArrived()
|
||||
{
|
||||
followerController.OnPickupArrived -= OnFollowerArrived;
|
||||
followerController.ReturnToPlayer(transform);
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
followerController.OnPickupArrived += OnFollowerArrived;
|
||||
followerController.GoToPoint(followerTarget);
|
||||
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
return true; // Success
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Moves the player to a specific target position and notifies via events when arrived or cancelled.
|
||||
/// This is used by systems like Pickup.cs to orchestrate movement.
|
||||
/// </summary>
|
||||
public void MoveToAndNotify(Vector3 target)
|
||||
{
|
||||
// Cancel any previous move-to coroutine
|
||||
if (_moveToCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_moveToCoroutine);
|
||||
}
|
||||
|
||||
_interruptMoveTo = false;
|
||||
// Ensure pathfinding is enabled for MoveToAndNotify
|
||||
if (_aiPath != null)
|
||||
{
|
||||
_aiPath.enabled = true;
|
||||
_aiPath.canMove = true;
|
||||
_aiPath.isStopped = false;
|
||||
}
|
||||
_moveToCoroutine = StartCoroutine(MoveToTargetCoroutine(target));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels any in-progress MoveToAndNotify operation and fires the cancellation event.
|
||||
/// </summary>
|
||||
public void InterruptMoveTo()
|
||||
{
|
||||
_interruptMoveTo = true;
|
||||
_isHolding = false;
|
||||
_directMoveVelocity = Vector3.zero;
|
||||
if (Settings.DefaultHoldMovementMode == HoldMovementMode.Direct && _aiPath != null)
|
||||
_aiPath.enabled = false;
|
||||
OnMoveToCancelled?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine for moving the player to a target position and firing arrival/cancel events.
|
||||
/// </summary>
|
||||
private System.Collections.IEnumerator MoveToTargetCoroutine(Vector3 target)
|
||||
{
|
||||
if (_aiPath != null)
|
||||
{
|
||||
_aiPath.destination = target;
|
||||
_aiPath.maxSpeed = Settings.MoveSpeed;
|
||||
_aiPath.maxAcceleration = Settings.MaxAcceleration;
|
||||
}
|
||||
|
||||
while (!_interruptMoveTo)
|
||||
{
|
||||
Vector2 current2D = new Vector2(transform.position.x, transform.position.y);
|
||||
Vector2 target2D = new Vector2(target.x, target.y);
|
||||
float dist = Vector2.Distance(current2D, target2D);
|
||||
if (dist <= Settings.StopDistance + 0.2f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_moveToCoroutine = null;
|
||||
if (!_interruptMoveTo)
|
||||
{
|
||||
OnArrivedAtTarget?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
#region Save/Load Lifecycle Hooks
|
||||
|
||||
|
||||
Reference in New Issue
Block a user