using UnityEngine; using UnityEngine.Events; /// /// Tourelle : quand la cible (le joueur) entre dans la zone de détection, la tourelle pivote vers elle /// et lui tire dessus à intervalle régulier. Elle cesse de tirer dès que la cible sort de la zone. /// Le projectile est un prefab avec un Rigidbody (le même que celui du script Tir). /// public class Turret : MonoBehaviour { [Header("Target")] [Tooltip("Vide = l'objet qui a le tag Player")] [SerializeField] private Transform target; [Tooltip("Hauteur visée au-dessus des pieds de la cible (m). Environ 1 = le centre du corps")] [SerializeField] private float aimHeight = 1f; [Header("Zone (m)")] [Tooltip("Cercle rouge. Cible à l'intérieur = la tourelle tire. Cible à l'extérieur = elle arrête")] [SerializeField] private float detectionRadius = 10f; [Tooltip("Coché = la tourelle ne tire que si elle voit la cible (aucun obstacle entre elle et la cible)")] [SerializeField] private bool requireLineOfSight = true; [Header("Rotation")] [Tooltip("Partie qui pivote vers la cible (ex. : la tête de la tourelle). Vide = rien ne pivote")] [SerializeField] private Transform pivot; [Tooltip("Coché = le pivot tourne seulement à l'horizontale. Décoché = il s'incline aussi vers la cible")] [SerializeField] private bool horizontalOnly = true; [Tooltip("Vitesse de rotation du pivot (degrés par seconde)")] [SerializeField] private float turnSpeed = 180f; [Tooltip("La tourelle tire seulement quand le pivot fait face à la cible, à cet angle près (degrés)")] [SerializeField] private float maxAngleToShoot = 10f; [Header("Shooting")] [Tooltip("Prefab du projectile, avec un Rigidbody (sur lui ou un enfant)")] [SerializeField] private GameObject bulletPrefab; [Tooltip("Point de départ du projectile : un Empty placé au bout du canon")] [SerializeField] private Transform fireStartingPoint; [Tooltip("Délai entre deux tirs (en secondes). 0.5 = deux tirs par seconde")] [SerializeField] private float fireInterval = 1f; [Tooltip("Délai avant le premier tir, quand la cible entre dans la zone (en secondes). Laisse au joueur le temps de réagir")] [SerializeField] private float firstShotDelay = 0.5f; [Tooltip("Vitesse du projectile (m/s)")] [SerializeField] private float bulletSpeed = 20f; [Tooltip("Durée de vie du projectile (en secondes), puis il est détruit")] [SerializeField] private float bulletLifetime = 3f; [Tooltip("Imprécision du tir (degrés). 0 = tir parfait")] [SerializeField] private float spread = 2f; [Space(6)] [Tooltip("Actions déclenchées à chaque tir (son, éclair du canon, particules…)")] [SerializeField] private UnityEvent onShoot; // Marge pour éviter les allers-retours quand la cible est pile sur le cercle (m) private const float LoseMargin = 0.5f; private Collider[] ownColliders; private bool targetInZone; private float nextShotTime; private void Awake() { // Les projectiles ne doivent pas toucher la tourelle elle-même ownColliders = GetComponentsInChildren(); } private void Start() { if (target == null) { GameObject player = GameObject.FindGameObjectWithTag("Player"); if (player != null) target = player.transform; } if (target == null) { Debug.LogWarning($"{name} n'a pas de cible : glisser le joueur dans le champ Target, ou lui donner le tag Player.", this); enabled = false; return; } if (bulletPrefab == null || fireStartingPoint == null) { Debug.LogWarning($"{name} : glisser le prefab du projectile et le point de départ (Fire Starting Point) dans l'Inspector.", this); enabled = false; } } private void Update() { float distance = Vector3.Distance(transform.position, target.position); // Zone de détection : la cible entre = la tourelle s'active, la cible sort = elle arrête if (!targetInZone && distance <= detectionRadius) { targetInZone = true; nextShotTime = Time.time + firstShotDelay; } else if (targetInZone && distance > detectionRadius + LoseMargin) { targetInZone = false; } if (!targetInZone) return; bool facing = TurnTowardsTarget(); if (Time.time >= nextShotTime && facing && (!requireLineOfSight || HasLineOfSight())) { Shoot(); nextShotTime = Time.time + fireInterval; } } private Vector3 AimPoint => target.position + Vector3.up * aimHeight; /// /// Tourne le pivot vers la cible. Retourne vrai s'il lui fait face (à Max Angle To Shoot près). /// Sans pivot, la tourelle est considérée toujours face à la cible. /// private bool TurnTowardsTarget() { if (pivot == null) return true; Vector3 direction = AimPoint - pivot.position; if (horizontalOnly) direction.y = 0f; if (direction.sqrMagnitude < 0.0001f) return true; Quaternion desiredRotation = Quaternion.LookRotation(direction); pivot.rotation = Quaternion.RotateTowards(pivot.rotation, desiredRotation, turnSpeed * Time.deltaTime); return Quaternion.Angle(pivot.rotation, desiredRotation) <= maxAngleToShoot; } /// /// Crée un projectile au bout du canon et le lance vers la cible. /// private void Shoot() { // Direction : du canon vers la cible, avec un peu d'imprécision Vector3 toTarget = AimPoint - fireStartingPoint.position; Quaternion direction = Quaternion.LookRotation(toTarget) * Quaternion.Euler(Random.Range(-spread, spread), Random.Range(-spread, spread), 0f); GameObject bullet = Instantiate(bulletPrefab, fireStartingPoint.position, direction * bulletPrefab.transform.rotation); // Le projectile ignore les colliders de la tourelle (sinon il peut exploser dans le canon) foreach (Collider bulletCollider in bullet.GetComponentsInChildren()) foreach (Collider own in ownColliders) Physics.IgnoreCollision(bulletCollider, own); Rigidbody body = bullet.GetComponentInChildren(); if (body != null) body.AddForce(direction * Vector3.forward * bulletSpeed, ForceMode.VelocityChange); else Debug.LogWarning($"{name} : le prefab {bulletPrefab.name} n'a pas de Rigidbody, il ne bougera pas.", this); Destroy(bullet, bulletLifetime); onShoot.Invoke(); } /// /// Vrai si rien ne bloque la vue entre le canon et la cible. /// Toucher un collider de la cible elle-même compte comme « vue dégagée ». /// private bool HasLineOfSight() { Vector3 origin = fireStartingPoint.position; RaycastHit[] hits = Physics.RaycastAll(origin, AimPoint - origin, Vector3.Distance(origin, AimPoint), Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore); float closest = float.MaxValue; Transform blocker = null; foreach (RaycastHit hit in hits) { if (hit.transform.IsChildOf(transform)) continue; // la tourelle ne se bloque pas elle-même if (hit.distance < closest) { closest = hit.distance; blocker = hit.transform; } } return blocker == null || blocker.IsChildOf(target); } // Affiche la zone au sol dans la vue Scene (sélectionner la tourelle) private void OnDrawGizmosSelected() { #if UNITY_EDITOR if (detectionRadius > 0f) { Vector3 center = transform.position + Vector3.up * 0.05f; UnityEditor.Handles.color = Color.red; UnityEditor.Handles.DrawWireDisc(center, Vector3.up, detectionRadius, 2f); GUIStyle style = new GUIStyle(UnityEditor.EditorStyles.boldLabel); style.normal.textColor = Color.red; UnityEditor.Handles.Label(center + Vector3.right * detectionRadius, $" Détection {detectionRadius:0.#} m", style); } #endif // Ligne de vue vers la cible (en Play) : verte = dégagée, rouge = bloquée if (requireLineOfSight && Application.isPlaying && target != null && fireStartingPoint != null) { Gizmos.color = HasLineOfSight() ? Color.green : Color.red; Gizmos.DrawLine(fireStartingPoint.position, AimPoint); } } }