Files
WaveDefender/Assets/Scripts/AIBehaviour.cs

299 lines
9.8 KiB
C#
Raw Normal View History

2026-04-17 10:38:43 +01:00
using UnityEngine;
using UnityEngine.AI;
/// <summary>
/// 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)
/// </summary>
[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<NavMeshAgent>();
_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<PlayerHealth>();
}
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<PlayerHealth>();
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<Barrier>();
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;
}
}