maze_switching (#82)

Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Reviewed-on: #82
This commit is contained in:
2025-12-15 09:20:15 +00:00
parent 34a6c367cc
commit c62f169d08
32 changed files with 2226 additions and 1155 deletions

View File

@@ -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
}
}

View 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; }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0d1b1be281334b9390cc96d7a8ff3132
timeCreated: 1765754798

View File

@@ -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
}
}

View File

@@ -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