223 lines
6.2 KiB
C#
223 lines
6.2 KiB
C#
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.AI;
|
||
|
|
|
||
|
|
[RequireComponent(typeof(NavMeshAgent))]
|
||
|
|
public class EnemyController : MonoBehaviour
|
||
|
|
{
|
||
|
|
public enum State { Idle, Chase, Attack, Dead }
|
||
|
|
private State currentState = State.Idle;
|
||
|
|
|
||
|
|
[Header("Stats")]
|
||
|
|
[SerializeField] private int maxHealth = 60;
|
||
|
|
[SerializeField] private int attackDamage = 15;
|
||
|
|
[SerializeField] private float attackRange = 1.8f;
|
||
|
|
[SerializeField] private float attackCooldown = 1.2f;
|
||
|
|
[SerializeField] private float detectionRadius = 18f;
|
||
|
|
|
||
|
|
[Header("Ranged Attack Settings (Optional)")]
|
||
|
|
[SerializeField] private bool isRangedEnemy = false;
|
||
|
|
[SerializeField] private GameObject rangedProjectilePrefab;
|
||
|
|
[SerializeField] private Transform projectileSpawnPoint;
|
||
|
|
[SerializeField] private float projectileSpeed = 25f;
|
||
|
|
|
||
|
|
[Header("References")]
|
||
|
|
[SerializeField] private Animator animator;
|
||
|
|
[SerializeField] private AudioSource audioSource;
|
||
|
|
[SerializeField] private AudioClip attackSound;
|
||
|
|
[SerializeField] private AudioClip hitSound;
|
||
|
|
[SerializeField] private AudioClip deathSound;
|
||
|
|
|
||
|
|
private NavMeshAgent agent;
|
||
|
|
private Transform playerTransform;
|
||
|
|
private int currentHealth;
|
||
|
|
private float nextAttackTime;
|
||
|
|
|
||
|
|
public State CurrentState => currentState;
|
||
|
|
public int CurrentHealth => currentHealth;
|
||
|
|
|
||
|
|
private void Awake()
|
||
|
|
{
|
||
|
|
agent = GetComponent<NavMeshAgent>();
|
||
|
|
if (animator == null)
|
||
|
|
{
|
||
|
|
animator = GetComponentInChildren<Animator>();
|
||
|
|
}
|
||
|
|
if (audioSource == null)
|
||
|
|
{
|
||
|
|
audioSource = GetComponent<AudioSource>();
|
||
|
|
}
|
||
|
|
currentHealth = maxHealth;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void Start()
|
||
|
|
{
|
||
|
|
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
|
||
|
|
if (playerObj != null)
|
||
|
|
{
|
||
|
|
playerTransform = playerObj.transform;
|
||
|
|
}
|
||
|
|
|
||
|
|
AreaClearanceManager.Instance?.RegisterEnemy(this);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void Update()
|
||
|
|
{
|
||
|
|
if (currentState == State.Dead) return;
|
||
|
|
|
||
|
|
// Re-acquire player reference if it was spawned dynamically
|
||
|
|
if (playerTransform == null)
|
||
|
|
{
|
||
|
|
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
|
||
|
|
if (playerObj != null)
|
||
|
|
{
|
||
|
|
playerTransform = playerObj.transform;
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
|
||
|
|
|
||
|
|
switch (currentState)
|
||
|
|
{
|
||
|
|
case State.Idle:
|
||
|
|
if (distanceToPlayer <= detectionRadius)
|
||
|
|
{
|
||
|
|
currentState = State.Chase;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case State.Chase:
|
||
|
|
if (agent.isOnNavMesh)
|
||
|
|
{
|
||
|
|
agent.SetDestination(playerTransform.position);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (animator != null)
|
||
|
|
{
|
||
|
|
animator.SetFloat("Speed", agent.velocity.magnitude);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (distanceToPlayer <= attackRange)
|
||
|
|
{
|
||
|
|
currentState = State.Attack;
|
||
|
|
if (agent.isOnNavMesh)
|
||
|
|
{
|
||
|
|
agent.ResetPath();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case State.Attack:
|
||
|
|
Vector3 lookTarget = new Vector3(playerTransform.position.x, transform.position.y, playerTransform.position.z);
|
||
|
|
transform.LookAt(lookTarget);
|
||
|
|
|
||
|
|
if (Time.time >= nextAttackTime)
|
||
|
|
{
|
||
|
|
PerformAttack();
|
||
|
|
nextAttackTime = Time.time + attackCooldown;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (distanceToPlayer > attackRange)
|
||
|
|
{
|
||
|
|
currentState = State.Chase;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void PerformAttack()
|
||
|
|
{
|
||
|
|
if (animator != null)
|
||
|
|
{
|
||
|
|
animator.SetTrigger("Attack");
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attackSound != null && audioSource != null)
|
||
|
|
{
|
||
|
|
audioSource.PlayOneShot(attackSound);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isRangedEnemy && rangedProjectilePrefab != null)
|
||
|
|
{
|
||
|
|
// Fire projectile towards player
|
||
|
|
Vector3 spawnPos = projectileSpawnPoint != null ? projectileSpawnPoint.position : transform.position + Vector3.up * 1.2f + transform.forward * 0.5f;
|
||
|
|
Vector3 aimDir = (playerTransform.position + Vector3.up * 1.0f - spawnPos).normalized;
|
||
|
|
GameObject projObj = Instantiate(rangedProjectilePrefab, spawnPos, Quaternion.LookRotation(aimDir));
|
||
|
|
if (projObj.TryGetComponent<Projectile>(out var proj))
|
||
|
|
{
|
||
|
|
proj.Initialize(attackDamage, projectileSpeed, "Enemy");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// Direct melee strike
|
||
|
|
if (PlayerHealthController.Instance != null)
|
||
|
|
{
|
||
|
|
PlayerHealthController.Instance.TakeDamage(attackDamage);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public void TakeDamage(int damage)
|
||
|
|
{
|
||
|
|
if (currentState == State.Dead) return;
|
||
|
|
|
||
|
|
currentHealth -= damage;
|
||
|
|
|
||
|
|
if (hitSound != null && audioSource != null)
|
||
|
|
{
|
||
|
|
audioSource.PlayOneShot(hitSound);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (animator != null)
|
||
|
|
{
|
||
|
|
animator.SetTrigger("Hit");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auto-aggro when damaged if idle
|
||
|
|
if (currentState == State.Idle)
|
||
|
|
{
|
||
|
|
currentState = State.Chase;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (currentHealth <= 0)
|
||
|
|
{
|
||
|
|
Die();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void Die()
|
||
|
|
{
|
||
|
|
currentState = State.Dead;
|
||
|
|
|
||
|
|
if (agent != null && agent.isOnNavMesh)
|
||
|
|
{
|
||
|
|
agent.isStopped = true;
|
||
|
|
}
|
||
|
|
if (agent != null)
|
||
|
|
{
|
||
|
|
agent.enabled = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (animator != null)
|
||
|
|
{
|
||
|
|
animator.SetBool("IsDead", true);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (deathSound != null && audioSource != null)
|
||
|
|
{
|
||
|
|
audioSource.PlayOneShot(deathSound);
|
||
|
|
}
|
||
|
|
|
||
|
|
Collider col = GetComponent<Collider>();
|
||
|
|
if (col != null)
|
||
|
|
{
|
||
|
|
col.enabled = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
AreaClearanceManager.Instance?.UnregisterEnemy(this);
|
||
|
|
Destroy(gameObject, 5f);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|