using UnityEngine; using UnityEngine.AI; /// /// Call of Duty Zombies-style enemy AI. /// States: Seeking (chase player) → BreakingBarrier (hammer a blocked door/window) → Attacking (melee the player) /// /// Setup requirements: /// - NavMeshAgent on this GameObject /// - Player GameObject tagged "Player" with a PlayerHealth component /// - Barrier objects on a layer assigned to barrierLayer, with a Barrier component + NavMeshObstacle (carving on) /// [RequireComponent(typeof(NavMeshAgent))] public class AIBehaviour : MonoBehaviour { public enum ZombieState { Seeking, BreakingBarrier, Attacking, Dead } // ------------------------------------------------------------------------- // Inspector // ------------------------------------------------------------------------- [Header("Movement")] public float moveSpeed = 3.5f; public float acceleration = 12f; [Header("Attack — Player")] public float attackRange = 1.8f; public float attackDamage = 15f; [Tooltip("Attacks per second")] public float attackRate = 1f; [Header("Attack — Barrier")] public float barrierDetectRange = 1.6f; public float barrierDamage = 25f; [Tooltip("Barrier hits per second")] public float barrierAttackRate = 1.5f; public LayerMask barrierLayer; [Header("Health")] public float maxHealth = 100f; [Header("Animation (optional)")] public Animator animator; // ------------------------------------------------------------------------- // Private state // ------------------------------------------------------------------------- private NavMeshAgent _agent; private Transform _player; private PlayerHealth _playerHealth; private ZombieState _state = ZombieState.Seeking; private float _health; private float _attackTimer; private float _barrierTimer; private Barrier _targetBarrier; // ------------------------------------------------------------------------- // Lifecycle // ------------------------------------------------------------------------- void Awake() { _agent = GetComponent(); _agent.speed = moveSpeed; _agent.acceleration = acceleration; _agent.stoppingDistance = attackRange * 0.85f; _agent.autoBraking = true; _health = maxHealth; } void Start() { GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); if (playerObj != null) { _player = playerObj.transform; _playerHealth = playerObj.GetComponent(); } else { Debug.LogWarning("[ZombieAI] No GameObject tagged 'Player' found."); } } void Update() { if (_state == ZombieState.Dead || _player == null) return; _attackTimer -= Time.deltaTime; _barrierTimer -= Time.deltaTime; switch (_state) { case ZombieState.Seeking: UpdateSeeking(); break; case ZombieState.BreakingBarrier: UpdateBreakingBarrier(); break; case ZombieState.Attacking: UpdateAttacking(); break; } UpdateAnimator(); } // ------------------------------------------------------------------------- // Seeking — navigate toward the player, detect barriers blocking the path // ------------------------------------------------------------------------- void UpdateSeeking() { float dist = Vector3.Distance(transform.position, _player.position); // Close enough to swing if (dist <= attackRange) { _agent.ResetPath(); TransitionTo(ZombieState.Attacking); return; } // Keep chasing _agent.SetDestination(_player.position); // If the NavMesh path can't reach the player, a barrier may be blocking if (_agent.pathStatus == NavMeshPathStatus.PathPartial || _agent.pathStatus == NavMeshPathStatus.PathInvalid) { Barrier b = ScanForBarrierAhead(); if (b != null) { _targetBarrier = b; _agent.ResetPath(); TransitionTo(ZombieState.BreakingBarrier); return; } } // Also do a short spherecast while moving in case the path is briefly valid // but a barrier is right in front of us (tight corridor) if (_agent.velocity.magnitude > 0.2f) { Barrier bAhead = ScanForBarrierAhead(); if (bAhead != null) { _targetBarrier = bAhead; _agent.ResetPath(); TransitionTo(ZombieState.BreakingBarrier); } } } // ------------------------------------------------------------------------- // Breaking barrier — face it and hammer until it falls // ------------------------------------------------------------------------- void UpdateBreakingBarrier() { // Barrier was destroyed (by us or another zombie) if (_targetBarrier == null || _targetBarrier.IsDestroyed) { _targetBarrier = null; TransitionTo(ZombieState.Seeking); return; } // Face the barrier FaceTarget(_targetBarrier.transform.position); // Hit on cooldown if (_barrierTimer <= 0f) { _targetBarrier.TakeDamage(barrierDamage); _barrierTimer = 1f / barrierAttackRate; TriggerAnimatorTrigger("BarrierHit"); } } // ------------------------------------------------------------------------- // Attacking — melee the player // ------------------------------------------------------------------------- void UpdateAttacking() { float dist = Vector3.Distance(transform.position, _player.position); // Player escaped melee range — resume chase if (dist > attackRange + 0.6f) { TransitionTo(ZombieState.Seeking); return; } FaceTarget(_player.position); if (_attackTimer <= 0f) { PlayerHealth ph = _player.GetComponent(); if (ph != null) ph.TakeDamage(attackDamage); _attackTimer = 1f / attackRate; TriggerAnimatorTrigger("Attack"); } } // ------------------------------------------------------------------------- // Public API — take damage / die // ------------------------------------------------------------------------- public void TakeDamage(float damage) { if (_state == ZombieState.Dead) return; _health -= damage; if (_health <= 0f) Die(); } void Die() { TransitionTo(ZombieState.Dead); _agent.ResetPath(); _agent.enabled = false; TriggerAnimatorTrigger("Die"); Destroy(gameObject, 3f); } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- /// Spherecast in the direction of movement to find a Barrier collider. Barrier ScanForBarrierAhead() { Vector3 origin = transform.position + Vector3.up * 0.8f; Vector3 dir = _agent.desiredVelocity.magnitude > 0.1f ? _agent.desiredVelocity.normalized : transform.forward; if (Physics.SphereCast(origin, 0.45f, dir, out RaycastHit hit, barrierDetectRange, barrierLayer, QueryTriggerInteraction.Ignore)) { Barrier b = hit.collider.GetComponentInParent(); if (b != null && !b.IsDestroyed) return b; } return null; } void FaceTarget(Vector3 targetPos) { Vector3 dir = (targetPos - transform.position); dir.y = 0f; if (dir.sqrMagnitude < 0.001f) return; Quaternion target = Quaternion.LookRotation(dir); transform.rotation = Quaternion.Slerp(transform.rotation, target, 10f * Time.deltaTime); } void TransitionTo(ZombieState next) { _state = next; } // ------------------------------------------------------------------------- // Animator bridge — all parameters are optional; missing params are ignored // ------------------------------------------------------------------------- void UpdateAnimator() { if (animator == null) return; SetAnimFloat("Speed", _agent.velocity.magnitude); SetAnimBool("IsAttacking", _state == ZombieState.Attacking); SetAnimBool("IsBreakingBarrier",_state == ZombieState.BreakingBarrier); SetAnimBool("IsDead", _state == ZombieState.Dead); } void SetAnimFloat(string name, float value) { if (animator.HasParameterByName(name)) animator.SetFloat(name, value); } void SetAnimBool(string name, bool value) { if (animator.HasParameterByName(name)) animator.SetBool(name, value); } void TriggerAnimatorTrigger(string name) { if (animator != null && animator.HasParameterByName(name)) animator.SetTrigger(name); } #if UNITY_EDITOR void OnDrawGizmosSelected() { Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, attackRange); Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, barrierDetectRange); } #endif } /// Extension to safely check Animator parameter existence public static class AnimatorExtensions { public static bool HasParameterByName(this Animator animator, string name) { foreach (AnimatorControllerParameter p in animator.parameters) if (p.name == name) return true; return false; } }