431 lines
16 KiB
C#
431 lines
16 KiB
C#
using UnityEngine;
|
|
using Pathfinding;
|
|
using AppleHills.Core.Settings;
|
|
using Core;
|
|
using Core.Lifecycle;
|
|
using Core.Settings;
|
|
|
|
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, IInteractingCharacter
|
|
{
|
|
[Header("Movement")]
|
|
[SerializeField] protected float moveSpeed = 5f;
|
|
|
|
[Header("Collision Simulation")]
|
|
[SerializeField] protected LayerMask obstacleMask;
|
|
[SerializeField] protected float colliderRadius = 0.5f;
|
|
|
|
// Movement state
|
|
protected Vector3 _targetPosition;
|
|
protected Vector3 _directMoveVelocity;
|
|
protected bool _isHolding;
|
|
protected Vector2 _lastHoldPosition;
|
|
protected Coroutine _pathfindingDragCoroutine;
|
|
protected float _pathfindingDragUpdateInterval = 0.1f;
|
|
|
|
// Settings reference (populated by derived classes in LoadSettings)
|
|
protected IPlayerMovementSettings _movementSettings;
|
|
protected IPlayerMovementSettings Settings => _movementSettings;
|
|
|
|
// Abstract method for derived classes to load their specific settings
|
|
protected abstract void LoadSettings();
|
|
|
|
// Movement tracking
|
|
protected bool _isMoving;
|
|
public bool IsMoving => _isMoving;
|
|
public bool IsHolding => _isHolding;
|
|
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;
|
|
protected Transform _artTransform;
|
|
protected SpriteRenderer _spriteRenderer;
|
|
|
|
// Animation tracking
|
|
protected Vector3 _lastDirectMoveDir = Vector3.right;
|
|
public Vector3 LastDirectMoveDir => _lastDirectMoveDir;
|
|
protected float _lastDirX;
|
|
protected float _lastDirY = -1f;
|
|
|
|
protected LogVerbosity _logVerbosity = LogVerbosity.Warning;
|
|
|
|
internal override void OnManagedAwake()
|
|
{
|
|
base.OnManagedAwake();
|
|
LoadSettings(); // Let derived class load appropriate settings
|
|
InitializeComponents();
|
|
}
|
|
|
|
internal override void OnManagedStart()
|
|
{
|
|
base.OnManagedStart();
|
|
|
|
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
|
|
}
|
|
|
|
protected virtual void InitializeComponents()
|
|
{
|
|
_aiPath = GetComponent<AIPath>();
|
|
_artTransform = transform.Find("CharacterArt");
|
|
if (_artTransform != null)
|
|
_animator = _artTransform.GetComponent<Animator>();
|
|
else
|
|
_animator = GetComponentInChildren<Animator>();
|
|
|
|
if (_artTransform != null)
|
|
_spriteRenderer = _artTransform.GetComponent<SpriteRenderer>();
|
|
if (_spriteRenderer == null)
|
|
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
|
}
|
|
|
|
protected virtual void Update()
|
|
{
|
|
UpdateMovementState();
|
|
UpdateAnimation();
|
|
}
|
|
|
|
#region ITouchInputConsumer Implementation
|
|
|
|
public virtual void OnTap(Vector2 worldPosition)
|
|
{
|
|
InterruptMoveTo(); // Cancel any scripted movement
|
|
Logging.Debug($"[{GetType().Name}] OnTap at {worldPosition}");
|
|
if (_aiPath != null)
|
|
{
|
|
_aiPath.enabled = true;
|
|
_aiPath.canMove = true;
|
|
_aiPath.isStopped = false;
|
|
SetTargetPosition(worldPosition);
|
|
_directMoveVelocity = Vector3.zero;
|
|
_isHolding = false;
|
|
}
|
|
}
|
|
|
|
public virtual void OnHoldStart(Vector2 worldPosition)
|
|
{
|
|
InterruptMoveTo(); // Cancel any scripted movement
|
|
Logging.Debug($"[{GetType().Name}] OnHoldStart at {worldPosition}");
|
|
_lastHoldPosition = worldPosition;
|
|
_isHolding = true;
|
|
|
|
if (Settings.DefaultHoldMovementMode == HoldMovementMode.Pathfinding && _aiPath != null)
|
|
{
|
|
_aiPath.enabled = true;
|
|
if (_pathfindingDragCoroutine != null) StopCoroutine(_pathfindingDragCoroutine);
|
|
_pathfindingDragCoroutine = StartCoroutine(PathfindingDragUpdateCoroutine());
|
|
}
|
|
else // Direct movement
|
|
{
|
|
if (_aiPath != null) _aiPath.enabled = false;
|
|
_directMoveVelocity = Vector3.zero;
|
|
}
|
|
}
|
|
|
|
public virtual void OnHoldUpdate(Vector2 worldPosition)
|
|
{
|
|
if (!_isHolding) return;
|
|
_lastHoldPosition = worldPosition;
|
|
|
|
if (Settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
|
|
{
|
|
if (_aiPath != null && _aiPath.enabled) _aiPath.enabled = false;
|
|
MoveDirectlyTo(worldPosition);
|
|
}
|
|
}
|
|
|
|
public virtual void OnHoldMove(Vector2 worldPosition)
|
|
{
|
|
// Alias for OnHoldUpdate for interface compatibility
|
|
OnHoldUpdate(worldPosition);
|
|
}
|
|
|
|
public virtual void OnHoldEnd(Vector2 worldPosition)
|
|
{
|
|
Logging.Debug($"[{GetType().Name}] OnHoldEnd at {worldPosition}");
|
|
_isHolding = false;
|
|
_directMoveVelocity = Vector3.zero;
|
|
|
|
if (_aiPath != null && Settings.DefaultHoldMovementMode == HoldMovementMode.Pathfinding)
|
|
{
|
|
if (_pathfindingDragCoroutine != null)
|
|
{
|
|
StopCoroutine(_pathfindingDragCoroutine);
|
|
_pathfindingDragCoroutine = null;
|
|
}
|
|
}
|
|
|
|
if (_aiPath != null && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
|
|
{
|
|
_aiPath.enabled = false;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Movement Methods
|
|
|
|
protected virtual void SetTargetPosition(Vector2 worldPosition)
|
|
{
|
|
if (_aiPath != null)
|
|
{
|
|
_aiPath.destination = worldPosition;
|
|
_aiPath.maxSpeed = Settings.MoveSpeed;
|
|
_aiPath.maxAcceleration = Settings.MaxAcceleration;
|
|
_aiPath.canMove = true;
|
|
_aiPath.isStopped = false;
|
|
}
|
|
}
|
|
|
|
protected virtual void MoveDirectlyTo(Vector2 worldPosition)
|
|
{
|
|
if (_aiPath == null) return;
|
|
|
|
Vector3 current = transform.position;
|
|
Vector3 target = new Vector3(worldPosition.x, worldPosition.y, current.z);
|
|
Vector3 toTarget = (target - current);
|
|
Vector3 direction = toTarget.normalized;
|
|
|
|
float maxSpeed = Settings.MoveSpeed;
|
|
float acceleration = Settings.MaxAcceleration;
|
|
|
|
_directMoveVelocity = Vector3.MoveTowards(_directMoveVelocity, direction * maxSpeed, acceleration * Time.deltaTime);
|
|
if (_directMoveVelocity.magnitude > maxSpeed)
|
|
{
|
|
_directMoveVelocity = _directMoveVelocity.normalized * maxSpeed;
|
|
}
|
|
|
|
Vector3 move = _directMoveVelocity * Time.deltaTime;
|
|
if (move.magnitude > toTarget.magnitude)
|
|
{
|
|
move = toTarget;
|
|
}
|
|
|
|
// Collision simulation
|
|
Vector3 adjustedVelocity = AdjustVelocityForObstacles(current, _directMoveVelocity);
|
|
Vector3 adjustedMove = adjustedVelocity * Time.deltaTime;
|
|
if (adjustedMove.magnitude > toTarget.magnitude)
|
|
{
|
|
adjustedMove = toTarget;
|
|
}
|
|
|
|
transform.position += adjustedMove;
|
|
_lastDirectMoveDir = _directMoveVelocity.normalized;
|
|
}
|
|
|
|
protected virtual Vector3 AdjustVelocityForObstacles(Vector3 position, Vector3 velocity)
|
|
{
|
|
if (velocity.sqrMagnitude < 0.0001f)
|
|
return velocity;
|
|
|
|
float moveDistance = velocity.magnitude * Time.deltaTime;
|
|
Vector2 origin = new Vector2(position.x, position.y);
|
|
Vector2 dir = velocity.normalized;
|
|
float rayLength = colliderRadius + moveDistance;
|
|
|
|
RaycastHit2D hit = Physics2D.Raycast(origin, dir, rayLength, obstacleMask);
|
|
|
|
if (hit.collider != null)
|
|
{
|
|
Vector2 tangent = new Vector2(-hit.normal.y, hit.normal.x);
|
|
float slideAmount = Vector2.Dot(velocity, tangent);
|
|
Vector3 slideVelocity = tangent * slideAmount;
|
|
return slideVelocity;
|
|
}
|
|
|
|
return velocity;
|
|
}
|
|
|
|
protected virtual System.Collections.IEnumerator PathfindingDragUpdateCoroutine()
|
|
{
|
|
while (_isHolding && _aiPath != null)
|
|
{
|
|
SetTargetPosition(_lastHoldPosition);
|
|
yield return new WaitForSeconds(_pathfindingDragUpdateInterval);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region State and Animation
|
|
|
|
protected virtual void UpdateMovementState()
|
|
{
|
|
bool isCurrentlyMoving = false;
|
|
|
|
if (_isHolding && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
|
|
{
|
|
isCurrentlyMoving = _directMoveVelocity.sqrMagnitude > 0.001f;
|
|
}
|
|
else if (_aiPath != null && _aiPath.enabled)
|
|
{
|
|
isCurrentlyMoving = _aiPath.velocity.sqrMagnitude > 0.001f;
|
|
}
|
|
|
|
if (isCurrentlyMoving && !_isMoving)
|
|
{
|
|
_isMoving = true;
|
|
OnMovementStarted?.Invoke();
|
|
Logging.Debug($"[{GetType().Name}] Movement started");
|
|
}
|
|
else if (!isCurrentlyMoving && _isMoving)
|
|
{
|
|
_isMoving = false;
|
|
OnMovementStopped?.Invoke();
|
|
Logging.Debug($"[{GetType().Name}] Movement stopped");
|
|
}
|
|
}
|
|
|
|
protected virtual void UpdateAnimation()
|
|
{
|
|
if (_animator == null || _aiPath == null) return;
|
|
|
|
float normalizedSpeed = 0f;
|
|
Vector3 velocity = Vector3.zero;
|
|
float maxSpeed = Settings.MoveSpeed;
|
|
|
|
if (_isHolding && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
|
|
{
|
|
normalizedSpeed = _directMoveVelocity.magnitude / maxSpeed;
|
|
velocity = _directMoveVelocity;
|
|
}
|
|
else if (_aiPath.enabled)
|
|
{
|
|
normalizedSpeed = _aiPath.velocity.magnitude / maxSpeed;
|
|
velocity = _aiPath.velocity;
|
|
}
|
|
|
|
_animator.SetFloat("Speed", Mathf.Clamp01(normalizedSpeed));
|
|
|
|
if (velocity.sqrMagnitude > 0.01f)
|
|
{
|
|
Vector3 normalizedVelocity = velocity.normalized;
|
|
_lastDirX = normalizedVelocity.x;
|
|
_lastDirY = normalizedVelocity.y;
|
|
|
|
_animator.SetFloat("DirX", _lastDirX);
|
|
_animator.SetFloat("DirY", _lastDirY);
|
|
}
|
|
else
|
|
{
|
|
_animator.SetFloat("DirX", _lastDirX);
|
|
_animator.SetFloat("DirY", _lastDirY);
|
|
}
|
|
}
|
|
|
|
#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
|
|
}
|
|
}
|
|
|