using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
namespace MedievalShopkeeper
{
///
/// Player Controller designed for a Medieval Shopkeeper game.
/// Supports PC (Left-Click to Move, Right-Click to Interact) and Mobile (Tap to Move / Tap Interactable to Approach & Interact).
/// Uses NavMeshAgent for shop navigation and Unity's New Input System.
///
[RequireComponent(typeof(NavMeshAgent))]
[SelectionBase]
public class PlayerController : MonoBehaviour
{
[Header("References")]
[Tooltip("The camera used for raycasting screen clicks. Defaults to Camera.main.")]
[SerializeField] private Camera playerCamera;
[Tooltip("NavMeshAgent component. If unassigned, will be retrieved on Awake.")]
[SerializeField] private NavMeshAgent agent;
[Tooltip("Optional Animator component for walking and carrying animations.")]
[SerializeField] private Animator animator;
[Header("Movement Settings")]
[Tooltip("Standard walking speed of the shopkeeper.")]
[SerializeField] private float walkSpeed = 4.0f;
[Tooltip("Turning speed in degrees per second.")]
[SerializeField] private float turnSpeed = 1080f;
[Tooltip("If true, turns on the spot when changing directions sharply instead of making wide circular arcs.")]
[SerializeField] private bool turnInPlace = true;
[Tooltip("Turning/angular speed in degrees per second.")]
[SerializeField] private float agentAngularSpeed = 720f;
[Tooltip("Acceleration of the agent for crisp starting and stopping.")]
[SerializeField] private float agentAcceleration = 24f;
[Tooltip("Speed multiplier applied when carrying stock boxes, crates, or heavy items.")]
[Range(0.1f, 1f)]
[SerializeField] private float carrySpeedMultiplier = 0.8f;
[Tooltip("Distance threshold to consider the destination reached.")]
[SerializeField] private float destinationThreshold = 0.35f;
[Tooltip("Shop floor plane Y-level for raycast fallback.")]
[SerializeField] private float floorYLevel = 0f;
[Tooltip("Layers representing walkable ground/floor in the shop.")]
[SerializeField] private LayerMask groundLayers = ~0;
[Header("Interaction Settings")]
[Tooltip("Layers representing interactable objects (shelves, stock crates, NPCs, counters).")]
[SerializeField] private LayerMask interactableLayers = ~0;
[Tooltip("Default interaction range if the target does not specify one.")]
[SerializeField] private float defaultInteractionDistance = 2.0f;
[Tooltip("On mobile / touch: tapping an interactable automatically moves to it and interacts.")]
[SerializeField] private bool smartTouchInteraction = true;
[Tooltip("On PC: allow left-click on an interactable to also trigger interaction.")]
[SerializeField] private bool leftClickAlsoInteracts = false;
[Header("Shopkeeper Carry System")]
[Tooltip("Transform socket (e.g. hands or chest) where carried items, boxes, or crates will be attached.")]
[SerializeField] private Transform carrySocket;
[Header("Optional Input Actions (New Input System)")]
[Tooltip("Optional: Action for movement click/tap. If unassigned, defaults to Mouse Left-Click / Touch.")]
[SerializeField] private InputActionReference moveAction;
[Tooltip("Optional: Action for interaction click. If unassigned, defaults to Mouse Right-Click.")]
[SerializeField] private InputActionReference interactAction;
[Tooltip("Optional: Action for pointer position. If unassigned, reads Mouse/Touch position.")]
[SerializeField] private InputActionReference pointerPositionAction;
[Header("Visual Feedback (Optional)")]
[Tooltip("Optional prefab instantiated or moved to show the click/destination point.")]
[SerializeField] private GameObject destinationMarkerPrefab;
[Header("Debug")]
[Tooltip("Output detailed logs comparing clicked destination vs NavMesh destination.")]
[SerializeField] private bool enableDebugLogs = true;
[Header("Animation Parameters")]
[SerializeField] private string speedAnimParam = "Speed";
[SerializeField] private string isMovingAnimParam = "IsMoving";
[SerializeField] private string isCarryingAnimParam = "IsCarrying";
// Runtime state
private IInteractable pendingTarget;
private IInteractable currentHovered;
private GameObject destinationMarkerInstance;
private GameObject currentCarriedItem;
private Vector3 lastClickedDestination;
private Vector3 lastNavMeshDestination;
private bool hasActiveMoveCommand;
private int speedHash;
private int isMovingHash;
private int isCarryingHash;
// Public properties
public NavMeshAgent Agent => agent;
public IInteractable PendingTarget => pendingTarget;
public IInteractable CurrentHovered => currentHovered;
public GameObject CurrentCarriedItem => currentCarriedItem;
public bool IsCarryingItem => currentCarriedItem != null;
public Vector3 LastClickedDestination => lastClickedDestination;
public Vector3 LastNavMeshDestination => lastNavMeshDestination;
// Events
public event Action OnDestinationSet;
public event Action OnInteractionTargetSet;
public event Action OnInteracted;
public event Action OnHoverChanged;
public event Action OnItemPickedUp;
public event Action OnItemDropped;
private void Awake()
{
if (agent == null)
{
agent = GetComponent();
}
FindCamera();
// Ensure the camera is statically positioned overhead with TopDownCamera
if (playerCamera != null)
{
TopDownCamera topDownCam = playerCamera.GetComponent();
if (topDownCam == null)
{
topDownCam = playerCamera.gameObject.AddComponent();
}
topDownCam.ApplyCameraTransform();
}
if (animator == null)
{
animator = GetComponentInChildren();
}
if (animator != null)
{
animator.applyRootMotion = false; // Prevent animations from fighting NavMesh movement
}
// Check for offset visual models which cause orbiting
foreach (var r in GetComponentsInChildren())
{
Vector3 localPos = r.transform.localPosition;
localPos.y = 0; // Ignore vertical offset
if (localPos.sqrMagnitude > 0.25f) // If offset by more than 0.5m on X/Z
{
Debug.LogWarning($"[PlayerController] WARNING: Visual model '{r.gameObject.name}' is offset from the Player center by {localPos}. This will cause the character to orbit around the pivot point instead of rotating in place! Please center the model in the prefab.");
}
}
// Configure agent for responsive movement without circular orbiting
agent.speed = walkSpeed;
agent.angularSpeed = agentAngularSpeed;
agent.acceleration = agentAcceleration;
agent.autoBraking = true;
agent.stoppingDistance = destinationThreshold;
agent.updateRotation = false; // We handle rotation to eliminate wide circular loops
// Cache animation hashes
speedHash = Animator.StringToHash(speedAnimParam);
isMovingHash = Animator.StringToHash(isMovingAnimParam);
isCarryingHash = Animator.StringToHash(isCarryingAnimParam);
// Instantiate destination marker or create default click disc
if (destinationMarkerPrefab != null)
{
destinationMarkerInstance = Instantiate(destinationMarkerPrefab);
foreach (var col in destinationMarkerInstance.GetComponentsInChildren())
{
col.enabled = false;
}
destinationMarkerInstance.SetActive(false);
}
else
{
destinationMarkerInstance = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
destinationMarkerInstance.name = "ClickDestinationMarker";
destinationMarkerInstance.transform.localScale = new Vector3(0.5f, 0.01f, 0.5f);
var col = destinationMarkerInstance.GetComponent();
if (col != null) Destroy(col);
var rend = destinationMarkerInstance.GetComponent();
if (rend != null)
{
rend.material.color = new Color(0f, 0.85f, 1f, 0.8f);
}
destinationMarkerInstance.SetActive(false);
}
}
private void Start()
{
// Ensure the agent is cleanly snapped onto the NavMesh if placed slightly above or below
if (!agent.isOnNavMesh)
{
if (NavMesh.SamplePosition(transform.position, out NavMeshHit navHit, 5.0f, NavMesh.AllAreas))
{
agent.Warp(navHit.position);
if (enableDebugLogs)
{
Debug.Log($"[PlayerController] Agent snapped to NavMesh at: {navHit.position}");
}
}
else
{
Debug.LogWarning("[PlayerController] Player is not placed on a valid NavMesh! Please ensure the floor has a baked NavMesh.", this);
}
}
}
private void OnEnable()
{
moveAction?.action?.Enable();
interactAction?.action?.Enable();
pointerPositionAction?.action?.Enable();
}
private void OnDisable()
{
moveAction?.action?.Disable();
interactAction?.action?.Disable();
pointerPositionAction?.action?.Disable();
if (currentHovered != null)
{
currentHovered.OnHoverExit(this);
currentHovered = null;
}
}
private void Update()
{
// Suspend normal player interaction and movement if building mode is active
if (BuildManager.Instance != null && BuildManager.Instance.IsBuilding)
{
ClearHover(); // Remove UI prompts
return; // Let BuildManager handle input
}
HandleHover();
HandleInput();
HandleDestinationArrival();
HandlePendingInteraction();
UpdateRotation();
UpdateAnimation();
}
private void FindCamera()
{
if (playerCamera == null)
{
playerCamera = Camera.main;
if (playerCamera == null)
{
playerCamera = FindAnyObjectByType();
}
}
}
#region Input Handling
private void HandleInput()
{
Vector2 screenPos = GetPointerPosition();
// Ignore input if pointer is over UI elements
if (IsPointerOverUI(screenPos))
{
if (WasMovePressed() || WasInteractPressed())
{
if (enableDebugLogs)
{
Debug.LogWarning($"[PlayerController] Click at screenPos {screenPos} was BLOCKED by UI.");
}
}
return;
}
bool isTouchDevice = IsTouchscreenActive();
// 1. Primary input (Left Click on PC / Screen Tap on Mobile)
if (WasMovePressed())
{
if (enableDebugLogs)
{
Debug.Log($"[PlayerController] CLICK DETECTED at screen: {screenPos}");
}
if (isTouchDevice && smartTouchInteraction)
{
if (TryRaycastInteractable(screenPos, out IInteractable touchedInteractable))
{
CommandInteract(touchedInteractable);
return;
}
}
else if (leftClickAlsoInteracts)
{
if (TryRaycastInteractable(screenPos, out IInteractable clickedInteractable))
{
CommandInteract(clickedInteractable);
return;
}
}
// Move to ground point
if (TryRaycastGround(screenPos, out Vector3 groundPoint))
{
CommandMove(groundPoint);
}
else
{
if (enableDebugLogs)
{
Debug.LogWarning($"[PlayerController] Left Click at screenPos {screenPos} did NOT hit any ground or NavMesh.");
}
}
}
// 2. Secondary input (Right Click on PC)
if (WasInteractPressed())
{
if (TryRaycastInteractable(screenPos, out IInteractable interactable))
{
CommandInteract(interactable);
}
else
{
if (enableDebugLogs)
{
Debug.Log($"[PlayerController] Right Click at screenPos {screenPos} did NOT hit any IInteractable.");
}
}
}
}
private Vector2 GetPointerPosition()
{
if (pointerPositionAction != null && pointerPositionAction.action != null && pointerPositionAction.action.enabled)
{
return pointerPositionAction.action.ReadValue();
}
if (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.isPressed)
{
return Touchscreen.current.primaryTouch.position.ReadValue();
}
if (Mouse.current != null)
{
return Mouse.current.position.ReadValue();
}
if (Pointer.current != null)
{
return Pointer.current.position.ReadValue();
}
return Vector2.zero;
}
private bool WasMovePressed()
{
if (moveAction != null && moveAction.action != null)
{
return moveAction.action.WasPressedThisFrame();
}
if (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.wasPressedThisFrame)
{
return true;
}
if (Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame)
{
return true;
}
return false;
}
private bool WasInteractPressed()
{
if (interactAction != null && interactAction.action != null)
{
return interactAction.action.WasPressedThisFrame();
}
if (Mouse.current != null && Mouse.current.rightButton.wasPressedThisFrame)
{
return true;
}
return false;
}
private bool IsTouchscreenActive()
{
return Touchscreen.current != null && Touchscreen.current.primaryTouch.press.isPressed;
}
private bool IsPointerOverUI(Vector2 screenPosition)
{
if (EventSystem.current == null) return false;
if (EventSystem.current.IsPointerOverGameObject()) return true;
if (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.isPressed)
{
int touchId = Touchscreen.current.primaryTouch.touchId.ReadValue();
if (EventSystem.current.IsPointerOverGameObject(touchId)) return true;
}
PointerEventData eventData = new PointerEventData(EventSystem.current)
{
position = screenPosition
};
var results = new List();
EventSystem.current.RaycastAll(eventData, results);
return results.Count > 0;
}
#endregion
#region Raycasting
private bool TryRaycastGround(Vector2 screenPosition, out Vector3 hitPoint)
{
hitPoint = Vector3.zero;
FindCamera();
if (playerCamera == null) return false;
Ray ray = playerCamera.ScreenPointToRay(screenPosition);
// 1. Raycast against physics colliders, excluding the player character
RaycastHit[] hits = Physics.RaycastAll(ray, 300f, groundLayers, QueryTriggerInteraction.Ignore);
if (hits.Length > 0)
{
Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
foreach (var hit in hits)
{
if (hit.collider.transform.root == transform.root) continue;
// Sample onto NavMesh
if (NavMesh.SamplePosition(hit.point, out NavMeshHit navHit, 3.0f, NavMesh.AllAreas))
{
hitPoint = navHit.position;
return true;
}
hitPoint = hit.point;
return true;
}
}
// 2. Mathematical Plane Fallback: If no collider hit, intersect with shop floor Y plane
Plane floorPlane = new Plane(Vector3.up, new Vector3(0f, floorYLevel, 0f));
if (floorPlane.Raycast(ray, out float enterDist))
{
Vector3 planePoint = ray.GetPoint(enterDist);
if (NavMesh.SamplePosition(planePoint, out NavMeshHit navHit, 5.0f, NavMesh.AllAreas))
{
hitPoint = navHit.position;
return true;
}
hitPoint = planePoint;
return true;
}
return false;
}
private bool TryRaycastInteractable(Vector2 screenPosition, out IInteractable interactable)
{
interactable = null;
FindCamera();
if (playerCamera == null) return false;
Ray ray = playerCamera.ScreenPointToRay(screenPosition);
RaycastHit[] hits = Physics.RaycastAll(ray, 300f, interactableLayers, QueryTriggerInteraction.Collide);
if (hits.Length > 0)
{
Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
foreach (var hit in hits)
{
if (hit.collider.transform.root == transform.root) continue;
var found = hit.collider.GetComponentInParent();
if (found != null && found.CanInteract(this))
{
interactable = found;
return true;
}
}
}
return false;
}
#endregion
#region Movement & Interaction Execution
///
/// Commands the shopkeeper to walk to a position on the floor, canceling any active interaction target.
///
public void CommandMove(Vector3 destination)
{
ClearPendingTarget();
if (!agent.isOnNavMesh)
{
if (NavMesh.SamplePosition(transform.position, out NavMeshHit navHit, 5.0f, NavMesh.AllAreas))
{
agent.Warp(navHit.position);
}
else
{
Debug.LogWarning("[PlayerController] Cannot move: Agent is not on NavMesh!", this);
return;
}
}
lastClickedDestination = destination;
agent.stoppingDistance = destinationThreshold;
agent.isStopped = false;
agent.velocity = Vector3.zero; // Kill residual forward momentum to prevent circular turn arcs
bool setSuccess = agent.SetDestination(destination);
lastNavMeshDestination = agent.destination;
hasActiveMoveCommand = true;
if (enableDebugLogs)
{
float dist = Vector3.Distance(transform.position, destination);
Debug.Log($"[PlayerController] CLICKED: {destination} --> NAVMESH: {agent.destination} (Player at {transform.position}, Dist: {dist:F2}m, Success: {setSuccess}, PathStatus: {agent.pathStatus})");
}
ShowDestinationMarker(destination);
OnDestinationSet?.Invoke(destination);
}
///
/// Commands the shopkeeper to approach an interactable (shelf, crate, NPC) and interact upon arrival.
/// If already within range, interacts immediately.
///
public void CommandInteract(IInteractable target)
{
if (target == null || !target.CanInteract(this)) return;
float distance = GetDistanceToInteractable(target);
float interactionDistance = target.InteractionDistance > 0 ? target.InteractionDistance : defaultInteractionDistance;
if (enableDebugLogs)
{
Debug.Log($"[PlayerController] INTERACT COMMAND: Target: '{target.InteractionPrompt}' at {target.InteractionTransform.position} | Current Distance: {distance:F2}m (Allowed: {interactionDistance:F2}m)");
}
if (distance <= interactionDistance)
{
ClearPendingTarget();
agent.ResetPath();
agent.velocity = Vector3.zero;
FaceTarget(target.InteractionTransform.position);
ExecuteInteraction(target);
}
else
{
pendingTarget = target;
hasActiveMoveCommand = false;
lastClickedDestination = target.InteractionTransform.position;
agent.stoppingDistance = Mathf.Max(0.5f, interactionDistance * 0.85f);
agent.isStopped = false;
agent.SetDestination(target.InteractionTransform.position);
lastNavMeshDestination = agent.destination;
ShowDestinationMarker(target.InteractionTransform.position);
OnInteractionTargetSet?.Invoke(target);
}
}
///
/// Handles stopping cleanly when reaching ground move destinations to prevent orbiting/circling.
///
private void HandleDestinationArrival()
{
if (pendingTarget != null) return;
if (!hasActiveMoveCommand) return;
if (!agent.pathPending && agent.hasPath)
{
Vector3 flatPos = new Vector3(transform.position.x, 0, transform.position.z);
Vector3 flatDest = new Vector3(agent.destination.x, 0, agent.destination.z);
float flatDist = Vector3.Distance(flatPos, flatDest);
// Normal arrival: within stopping distance or remaining distance
if (flatDist <= agent.stoppingDistance || agent.remainingDistance <= agent.stoppingDistance)
{
if (enableDebugLogs)
{
Debug.Log($"[PlayerController] ARRIVED AT DESTINATION: Position: {transform.position} (Dist: {flatDist:F2}m)");
}
hasActiveMoveCommand = false;
agent.ResetPath();
agent.velocity = Vector3.zero;
HideDestinationMarker();
}
// Anti-orbiting safeguard: if agent is close and moving away from destination
else if (flatDist < 0.8f && Vector3.Dot(agent.velocity.normalized, (flatDest - flatPos).normalized) < 0f)
{
if (enableDebugLogs)
{
Debug.Log($"[PlayerController] OVERSHOOT PREVENTED (STOPPED): Position: {transform.position} (Dist: {flatDist:F2}m)");
}
hasActiveMoveCommand = false;
agent.ResetPath();
agent.velocity = Vector3.zero;
HideDestinationMarker();
}
}
}
private void HandlePendingInteraction()
{
if (pendingTarget == null) return;
if (!pendingTarget.CanInteract(this))
{
ClearPendingTarget();
return;
}
float interactionDistance = pendingTarget.InteractionDistance > 0 ? pendingTarget.InteractionDistance : defaultInteractionDistance;
float distance = GetDistanceToInteractable(pendingTarget);
if (distance <= interactionDistance || (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance))
{
IInteractable targetToInteract = pendingTarget;
ClearPendingTarget();
agent.ResetPath();
agent.velocity = Vector3.zero;
FaceTarget(targetToInteract.InteractionTransform.position);
ExecuteInteraction(targetToInteract);
}
}
private void ExecuteInteraction(IInteractable target)
{
HideDestinationMarker();
target.Interact(this);
OnInteracted?.Invoke(target);
}
public void ClearPendingTarget()
{
pendingTarget = null;
}
private float GetDistanceToInteractable(IInteractable target)
{
Vector3 targetPos = target.InteractionTransform.position;
Vector3 playerPos = transform.position;
targetPos.y = playerPos.y;
return Vector3.Distance(playerPos, targetPos);
}
private void UpdateRotation()
{
if (agent == null) return;
Vector3 targetDirection = Vector3.zero;
if (pendingTarget != null)
{
targetDirection = pendingTarget.InteractionTransform.position - transform.position;
}
else if (hasActiveMoveCommand && agent.hasPath)
{
targetDirection = agent.steeringTarget - transform.position;
}
targetDirection.y = 0;
if (targetDirection.sqrMagnitude > 0.001f)
{
Quaternion targetRot = Quaternion.LookRotation(targetDirection.normalized);
float angleDiff = Quaternion.Angle(transform.rotation, targetRot);
// Strict Turn-in-place: if angle is large, stop all movement until facing the target
if (turnInPlace && angleDiff > 10f)
{
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRot, turnSpeed * Time.deltaTime);
agent.velocity = Vector3.zero; // Completely halt forward momentum while turning!
agent.speed = 0f;
}
else
{
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRot, turnSpeed * Time.deltaTime);
float baseSpeed = IsCarryingItem ? (walkSpeed * carrySpeedMultiplier) : walkSpeed;
agent.speed = baseSpeed;
}
}
}
private void FaceTarget(Vector3 targetPosition)
{
Vector3 direction = (targetPosition - transform.position);
direction.y = 0;
if (direction.sqrMagnitude > 0.001f)
{
transform.rotation = Quaternion.LookRotation(direction);
}
}
#endregion
#region Hover Handling
private void HandleHover()
{
if (Mouse.current == null && Pointer.current == null) return;
Vector2 screenPos = GetPointerPosition();
if (IsPointerOverUI(screenPos))
{
ClearHover();
return;
}
if (TryRaycastInteractable(screenPos, out IInteractable hitInteractable) && hitInteractable.CanInteract(this))
{
if (currentHovered != hitInteractable)
{
ClearHover();
currentHovered = hitInteractable;
currentHovered.OnHoverEnter(this);
OnHoverChanged?.Invoke(currentHovered);
}
}
else
{
ClearHover();
}
}
private void ClearHover()
{
if (currentHovered != null)
{
currentHovered.OnHoverExit(this);
currentHovered = null;
OnHoverChanged?.Invoke(null);
}
}
#endregion
#region Shopkeeper Carry System
///
/// Attaches an item, delivery crate, or stock box into the shopkeeper's carry socket.
///
public bool CarryItem(GameObject item)
{
if (item == null || IsCarryingItem) return false;
currentCarriedItem = item;
Transform targetParent = carrySocket != null ? carrySocket : transform;
item.transform.SetParent(targetParent);
item.transform.localPosition = Vector3.zero;
item.transform.localRotation = Quaternion.identity;
if (item.TryGetComponent(out Rigidbody rb))
{
rb.isKinematic = true;
}
if (item.TryGetComponent(out Collider col))
{
col.enabled = false;
}
UpdateMovementSpeed();
OnItemPickedUp?.Invoke(item);
return true;
}
///
/// Drops the currently carried item onto the ground in front of the player.
///
public GameObject DropCarriedItem(Vector3? dropPosition = null)
{
if (!IsCarryingItem) return null;
GameObject dropped = currentCarriedItem;
currentCarriedItem = null;
dropped.transform.SetParent(null);
Vector3 targetDrop = dropPosition ?? (transform.position + transform.forward * 0.8f + Vector3.up * 0.2f);
dropped.transform.position = targetDrop;
if (dropped.TryGetComponent(out Collider col))
{
col.enabled = true;
}
if (dropped.TryGetComponent(out Rigidbody rb))
{
rb.isKinematic = false;
}
UpdateMovementSpeed();
OnItemDropped?.Invoke(dropped);
return dropped;
}
///
/// Removes the carried item reference without placing it in the world (e.g. placed onto a shelf).
///
public void ClearCarriedItem()
{
currentCarriedItem = null;
UpdateMovementSpeed();
}
private void UpdateMovementSpeed()
{
float speed = walkSpeed;
if (IsCarryingItem)
{
speed *= carrySpeedMultiplier;
}
agent.speed = speed;
}
#endregion
#region Visuals & Animation
private void ShowDestinationMarker(Vector3 position)
{
if (destinationMarkerInstance != null)
{
destinationMarkerInstance.transform.position = position + Vector3.up * 0.05f;
destinationMarkerInstance.SetActive(true);
}
}
private void HideDestinationMarker()
{
if (destinationMarkerInstance != null)
{
destinationMarkerInstance.SetActive(false);
}
}
private void UpdateAnimation()
{
if (animator == null) return;
float currentSpeed = agent.velocity.magnitude;
bool isMoving = agent.velocity.sqrMagnitude > 0.05f && !agent.isStopped && agent.hasPath;
animator.SetFloat(speedHash, currentSpeed);
animator.SetBool(isMovingHash, isMoving);
animator.SetBool(isCarryingHash, IsCarryingItem);
}
#endregion
#region Editor Gizmos
private void OnDrawGizmos()
{
// Draw Clicked Point in bright RED
if (lastClickedDestination != Vector3.zero)
{
Gizmos.color = Color.red;
Gizmos.DrawSphere(lastClickedDestination, 0.25f);
Gizmos.DrawLine(transform.position, lastClickedDestination);
}
// Draw NavMesh Agent Destination in bright GREEN
if (agent != null && (hasActiveMoveCommand || agent.hasPath))
{
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(agent.destination, agent.stoppingDistance);
Gizmos.DrawLine(transform.position, agent.destination);
}
}
private void OnDrawGizmosSelected()
{
if (agent != null && agent.hasPath)
{
Gizmos.color = Color.green;
Vector3 prev = transform.position;
foreach (Vector3 corner in agent.path.corners)
{
Gizmos.DrawLine(prev, corner);
Gizmos.DrawSphere(corner, 0.15f);
prev = corner;
}
}
if (pendingTarget != null && pendingTarget.InteractionTransform != null)
{
Gizmos.color = Color.cyan;
Gizmos.DrawLine(transform.position, pendingTarget.InteractionTransform.position);
Gizmos.DrawWireSphere(pendingTarget.InteractionTransform.position, pendingTarget.InteractionDistance);
}
if (carrySocket != null)
{
Gizmos.color = Color.magenta;
Gizmos.DrawWireCube(carrySocket.position, Vector3.one * 0.2f);
}
}
#endregion
}
}