using UnityEngine; using UnityEngine.AI; /// /// Le PNJ choisit un point au hasard sur le NavMesh, s'y rend, attend un peu, puis recommence. /// Il se tourne d'abord vers sa direction, puis avance : pas de « glissade » en virage. /// Animation : le script informe l'Animator (Bool) ; l'Animator Controller décide quoi jouer. /// [RequireComponent(typeof(NavMeshAgent))] public class NPCWander : MonoBehaviour { [Header("Wander")] [Tooltip("Distance maximale : rayon de la zone d'errance, autour du point de départ du PNJ")] [SerializeField] private float wanderRadius = 10f; [Tooltip("Distance minimale entre le PNJ et son prochain point : évite les tout petits déplacements")] [SerializeField] private float minDistance = 1f; [SerializeField] private Vector2 pauseMinMax = new Vector2(1f, 3f); [Header("Movement")] [Tooltip("Vitesse du clip de marche (m/s). À ajuster jusqu'à ce que les pieds ne glissent plus.")] [SerializeField] private float walkSpeed = 1.3f; [Tooltip("Temps (en secondes) pour faire un demi-tour complet")] [SerializeField] private float halfTurnDuration = 0.75f; [Tooltip("Au-delà de cet angle, le PNJ pivote sur place avant d'avancer")] [SerializeField] private float maxAngleToMove = 45f; [Header("Animation (optional)")] [SerializeField] private Animator animator; [Tooltip("Nom exact du paramètre Bool dans l'Animator Controller")] [SerializeField] private string walkParameter = "walking"; // Vitesse (m/s) sous laquelle le PNJ est considéré immobile private const float WalkThreshold = 0.1f; // Nombre d'essais pour trouver un point assez loin avant de se contenter du plus éloigné private const int MaxAttempts = 10; private NavMeshAgent agent; private Vector3 wanderCenter; // point fixe autour duquel le PNJ erre private float departureTime = -1f; private int walkId; private void Awake() { agent = GetComponent(); agent.updateRotation = false; // la rotation est gérée par ce script agent.speed = walkSpeed; walkId = Animator.StringToHash(walkParameter); if (animator != null) animator.applyRootMotion = false; } private void Start() { wanderCenter = transform.position; if (!agent.isOnNavMesh) { Debug.LogWarning($"{name} n'est pas sur le NavMesh : vérifier le Bake et la position du personnage.", this); enabled = false; return; } PickDestination(); } private void Update() { Orient(); // L'Animator reflète toujours l'état réel du déplacement if (animator != null) animator.SetBool(walkId, agent.velocity.magnitude > WalkThreshold); bool arrived = !agent.pathPending && agent.remainingDistance <= agent.stoppingDistance; if (!arrived) return; if (departureTime < 0f) departureTime = Time.time + Random.Range(pauseMinMax.x, pauseMinMax.y); else if (Time.time >= departureTime) { PickDestination(); departureTime = -1f; } } /// /// Tourne le PNJ vers le prochain coin du chemin. /// Tant que l'écart d'angle est grand, l'agent est freiné : il pivote presque sur place. /// private void Orient() { if (!agent.hasPath) { agent.speed = walkSpeed; return; } Vector3 direction = agent.steeringTarget - transform.position; direction.y = 0f; if (direction.sqrMagnitude < 0.0001f) return; Quaternion target = Quaternion.LookRotation(direction); transform.rotation = Quaternion.RotateTowards(transform.rotation, target, (180f / halfTurnDuration) * Time.deltaTime); // 1 quand le PNJ fait face à sa direction, 0 quand l'écart dépasse maxAngleToMove float angle = Quaternion.Angle(transform.rotation, target); float factor = Mathf.Clamp01(1f - angle / maxAngleToMove); agent.speed = walkSpeed * factor; } /// /// Tire un point au hasard dans la zone d'errance, à au moins minDistance du PNJ. /// Si aucun essai ne convient (zone trop petite, obstacles), le point le plus éloigné est retenu. /// private void PickDestination() { bool found = false; Vector3 best = Vector3.zero; float bestDistance = -1f; for (int i = 0; i < MaxAttempts; i++) { // Point au hasard dans un disque horizontal autour du centre fixe Vector2 random = Random.insideUnitCircle * wanderRadius; Vector3 point = wanderCenter + new Vector3(random.x, 0f, random.y); if (!NavMesh.SamplePosition(point, out NavMeshHit hit, wanderRadius, NavMesh.AllAreas)) continue; // Distance mesurée à plat (sans tenir compte de la hauteur) Vector3 offset = hit.position - transform.position; offset.y = 0f; float distance = offset.magnitude; if (distance >= minDistance) { agent.SetDestination(hit.position); return; } if (distance > bestDistance) { best = hit.position; bestDistance = distance; found = true; } } if (found) agent.SetDestination(best); } // Affiche la zone d'errance et la destination dans la vue Scene (sélectionner le PNJ) private void OnDrawGizmosSelected() { // Zone d'errance : visible même hors Play. // En Play, elle reste au point de départ du PNJ. Vector3 center = Application.isPlaying ? wanderCenter : transform.position; Gizmos.color = Color.cyan; Gizmos.DrawWireSphere(center, wanderRadius); // Distance minimale : suit le PNJ Gizmos.color = new Color(1f, 0.5f, 0f); Gizmos.DrawWireSphere(transform.position, minDistance); // Destination courante (en Play seulement) if (agent == null || !agent.hasPath) return; Gizmos.color = Color.yellow; Gizmos.DrawLine(transform.position, agent.destination); Gizmos.DrawSphere(agent.destination, 0.2f); } }