using UnityEngine; using UnityEngine.AI; /// /// AI controller for enemy dinosaurs. /// Handles patrol, chase, attack, and flee behaviors. /// public class AIController : MonoBehaviour { [Header("AI Settings")] [Tooltip("Detection range for spotting player")] public float detectionRange = 15f; [Tooltip("Aggro range - will chase if player enters")] public float aggroRange = 10f; [Tooltip("Distance to start attacking")] public float attackRange = 3.5f; [Tooltip("Distance to flee when low health")] public float fleeRange = 5f; [Tooltip("Health percentage to start fleeing")] [Range(0f, 1f)] public float fleeHealthPercent = 0.3f; [Tooltip("Time to lose interest in player (seconds)")] public float loseInterestTime = 5f; [Header("Movement")] [Tooltip("Walking speed")] public float walkSpeed = 3f; [Tooltip("Running speed when chasing")] public float chaseSpeed = 6f; [Tooltip("Turning speed")] public float rotationSpeed = 5f; [Header("Patrol")] [Tooltip("Patrol waypoints to follow")] public Transform[] patrolPoints; [Tooltip("Time to wait at each waypoint")] public float patrolWaitTime = 3f; [Tooltip("Random offset for patrol points")] public float patrolRandomOffset = 2f; [Header("State")] [Tooltip("Current AI state")] public AIState currentState = AIState.Idle; [Tooltip("Target to pursue (usually player)")] public Transform target; [Tooltip("Current patrol point index")] public int currentPatrolIndex = 0; [Tooltip("Timer for various states")] public float stateTimer = 0f; // Components private Animator animator; private CombatSystem combat; private UnityEngine.AI.NavMeshAgent navAgent; private float lastSeenPlayer; // Player reference private Transform player; void Start() { // Get components animator = GetComponent(); combat = GetComponent(); navAgent = GetComponent(); if (navAgent == null) { Debug.LogWarning("No NavMeshAgent on " + gameObject.name + ". Adding one."); navAgent = gameObject.AddComponent(); navAgent.speed = walkSpeed; navAgent.acceleration = 10f; navAgent.angularSpeed = 180f; navAgent.stoppingDistance = attackRange * 0.8f; } // Find player player = FindPlayer(); Debug.Log($"🦕 AI initialized on {gameObject.name} - State: {currentState}"); } void Update() { // Update state timer stateTimer += Time.deltaTime; // State machine switch (currentState) { case AIState.Idle: UpdateIdle(); break; case AIState.Patrol: UpdatePatrol(); break; case AIState.Chase: UpdateChase(); break; case AIState.Attack: UpdateAttack(); break; case AIState.Flee: UpdateFlee(); break; case AIState.Dead: break; } // Check for player detection CheckPlayerDetection(); // Check if should flee CheckFleeCondition(); // Update animations UpdateAnimations(); } void UpdateIdle() { // After idle, start patrolling if (stateTimer > 2f) { currentState = AIState.Patrol; stateTimer = 0f; navAgent.destination = GetNextPatrolPoint(); } // Play idle animation if (animator != null) { animator.SetFloat("Speed", 0f); } } void UpdatePatrol() { // Check if reached patrol point if (navAgent.remainingDistance <= navAgent.stoppingDistance) { stateTimer += Time.deltaTime; if (stateTimer >= patrolWaitTime) { currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length; navAgent.destination = GetNextPatrolPoint(); stateTimer = 0f; } } if (animator != null) { animator.SetFloat("Speed", 1f); } } void UpdateChase() { if (target == null) { currentState = AIState.Patrol; return; } // Set destination to target navAgent.destination = target.position; navAgent.speed = chaseSpeed; // Face target Quaternion lookRotation = Quaternion.LookRotation(target.position - transform.position); transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, rotationSpeed * Time.deltaTime); // Check if in attack range float distanceToTarget = Vector3.Distance(transform.position, target.position); if (distanceToTarget <= attackRange) { currentState = AIState.Attack; stateTimer = 0f; navAgent.speed = walkSpeed; } // Lose interest if player is out of range for too long if (distanceToTarget > aggroRange * 1.5f) { if (stateTimer >= loseInterestTime) { currentState = AIState.Patrol; navAgent.speed = walkSpeed; } } else { stateTimer = 0f; } if (animator != null) { animator.SetFloat("Speed", 2f); } } void UpdateAttack() { if (target == null) { currentState = AIState.Patrol; return; } // Face target Quaternion lookRotation = Quaternion.LookRotation(target.position - transform.position); transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, rotationSpeed * Time.deltaTime); // Attack using combat system if (combat != null && stateTimer >= combat.biteCooldown) { // Trigger bite attack manually for AI combat.PerformBite(); stateTimer = 0f; } // Check if should stop attacking float distanceToTarget = Vector3.Distance(transform.position, target.position); if (distanceToTarget > attackRange * 1.5f) { currentState = AIState.Chase; stateTimer = 0f; } } void UpdateFlee() { if (target == null) { currentState = AIState.Patrol; return; } // Run away from target Vector3 fleeDirection = (transform.position - target.position).normalized; Vector3 fleeDestination = transform.position + fleeDirection * fleeRange; navAgent.destination = fleeDestination; navAgent.speed = chaseSpeed; // Stop fleeing if health is recovered or player is far away float distanceToTarget = Vector3.Distance(transform.position, target.position); if (combat.HealthPercent > fleeHealthPercent + 0.2f || distanceToTarget > fleeRange * 2f) { currentState = AIState.Patrol; navAgent.speed = walkSpeed; } } void CheckPlayerDetection() { if (player == null) return; float distanceToPlayer = Vector3.Distance(transform.position, player.position); // Detect player in range if (distanceToPlayer <= detectionRange) { // Raycast to check line of sight Vector3 directionToPlayer = (player.position - transform.position).normalized; if (Physics.Raycast(transform.position + Vector3.up * 2f, directionToPlayer, out RaycastHit hit, detectionRange)) { if (hit.transform == player) { if (distanceToPlayer <= aggroRange) { target = player; currentState = AIState.Chase; stateTimer = 0f; lastSeenPlayer = Time.time; } } } } } void CheckFleeCondition() { if (combat != null && combat.HealthPercent <= fleeHealthPercent && currentState != AIState.Dead) { if (target != null) { currentState = AIState.Flee; stateTimer = 0f; } } } void UpdateAnimations() { if (animator == null) return; float speed = 0f; switch (currentState) { case AIState.Idle: speed = 0f; break; case AIState.Patrol: speed = 1f; break; case AIState.Chase: speed = 2f; break; case AIState.Attack: speed = 0.5f; break; case AIState.Flee: speed = 2.5f; break; } animator.SetFloat("Speed", speed); animator.SetFloat("Health", combat.HealthPercent); } Vector3 GetNextPatrolPoint() { if (patrolPoints.Length == 0) { // Random direction using Unity 6 compatible method return transform.position + Quaternion.Euler(0f, Random.Range(0f, 360f), 0f) * Vector3.forward * 10f; } Vector3 point = patrolPoints[currentPatrolIndex].position; // Add random offset point += new Vector3( Random.Range(-patrolRandomOffset, patrolRandomOffset), 0f, Random.Range(-patrolRandomOffset, patrolRandomOffset) ); return point; } Transform FindPlayer() { GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); return playerObj?.transform; } /// /// Called when AI dies. /// public void Die() { currentState = AIState.Dead; if (navAgent != null) { navAgent.enabled = false; } } void OnDrawGizmosSelected() { // Draw detection range Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, detectionRange); // Draw aggro range Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, aggroRange); // Draw patrol points if (patrolPoints != null) { Gizmos.color = Color.blue; foreach (Transform point in patrolPoints) { if (point != null) { Gizmos.DrawSphere(point.position, 0.5f); } } } } } public enum AIState { Idle, Patrol, Chase, Attack, Flee, Dead }