2026-05-30 17:41:31 +07:00
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using UnityEngine;
|
2026-06-04 15:41:01 +07:00
|
|
|
using UnityEngine.AI; // Cần thiết để dùng NavMesh
|
2026-05-30 17:41:31 +07:00
|
|
|
|
2026-06-04 15:41:01 +07:00
|
|
|
[RequireComponent(typeof(NavMeshAgent))] // Tự động thêm component này nếu chưa có
|
2026-05-30 17:41:31 +07:00
|
|
|
public class EnemyAI : MonoBehaviour
|
|
|
|
|
{
|
2026-06-03 13:42:09 +07:00
|
|
|
[Header("References")]
|
2026-05-30 17:41:31 +07:00
|
|
|
public Transform player;
|
2026-06-03 13:42:09 +07:00
|
|
|
|
|
|
|
|
[Header("Detection")]
|
2026-05-30 17:41:31 +07:00
|
|
|
public float detectRange = 10f;
|
|
|
|
|
public float moveSpeed = 3f;
|
|
|
|
|
public float rotateSpeed = 50f;
|
2026-06-03 13:42:09 +07:00
|
|
|
|
2026-06-04 15:41:01 +07:00
|
|
|
[Header("Patrol Area")]
|
|
|
|
|
public float patrolRadius = 15f; // Bán kính khu vực tuần tra
|
|
|
|
|
public float patrolWaitTime = 2f; // Thời gian đứng chờ trước khi đi điểm khác
|
|
|
|
|
private Vector3 startPosition;
|
|
|
|
|
private float currentWaitTime;
|
|
|
|
|
|
2026-06-03 13:42:09 +07:00
|
|
|
[Header("Artifact")]
|
|
|
|
|
public bool playerHasArtifact;
|
|
|
|
|
|
|
|
|
|
[Header("Laser")]
|
|
|
|
|
public GameObject laserPrefab;
|
|
|
|
|
public Transform firePoint;
|
|
|
|
|
public float minShootDelay = 1f;
|
|
|
|
|
public float maxShootDelay = 3f;
|
|
|
|
|
|
|
|
|
|
private float nextShootTime;
|
2026-06-04 15:41:01 +07:00
|
|
|
private NavMeshAgent agent;
|
2026-05-30 17:41:31 +07:00
|
|
|
|
|
|
|
|
public Node behaviorTreeRoot;
|
|
|
|
|
|
2026-06-03 13:42:09 +07:00
|
|
|
private void Start()
|
2026-05-30 17:41:31 +07:00
|
|
|
{
|
2026-06-04 15:41:01 +07:00
|
|
|
agent = GetComponent<NavMeshAgent>();
|
|
|
|
|
agent.speed = moveSpeed;
|
|
|
|
|
|
|
|
|
|
// Lưu lại vị trí ban đầu để làm tâm của khu vực tuần tra
|
|
|
|
|
startPosition = transform.position;
|
|
|
|
|
|
2026-06-03 13:42:09 +07:00
|
|
|
nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay);
|
2026-05-30 17:41:31 +07:00
|
|
|
InitBehaviorTree();
|
2026-06-04 15:41:01 +07:00
|
|
|
FindPlayer();
|
2026-05-30 17:41:31 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 13:42:09 +07:00
|
|
|
private void Update()
|
2026-05-30 17:41:31 +07:00
|
|
|
{
|
2026-06-04 15:41:01 +07:00
|
|
|
// Nếu mất reference (Player chết hoặc chưa spawn), liên tục tìm lại
|
|
|
|
|
if (player == null)
|
|
|
|
|
{
|
|
|
|
|
FindPlayer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Chỉ chạy AI nếu đã tìm thấy player (hoặc bạn có thể cho tuần tra ngay cả khi chưa có player tùy logic game)
|
2026-06-03 13:42:09 +07:00
|
|
|
behaviorTreeRoot?.Evaluate();
|
2026-05-30 17:41:31 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-04 15:41:01 +07:00
|
|
|
private void FindPlayer()
|
|
|
|
|
{
|
|
|
|
|
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
|
|
|
|
|
if (playerObj != null)
|
|
|
|
|
{
|
|
|
|
|
player = playerObj.transform;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 13:42:09 +07:00
|
|
|
private void InitBehaviorTree()
|
2026-05-30 17:41:31 +07:00
|
|
|
{
|
2026-06-03 13:42:09 +07:00
|
|
|
// Player có artifact -> focus + shoot
|
|
|
|
|
var laserSequence = new Sequence(new List<Node>
|
|
|
|
|
{
|
|
|
|
|
new TaskNode(CheckHasArtifact),
|
|
|
|
|
new TaskNode(ActionFocusAndShoot)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Thấy player -> chạy tới
|
2026-05-30 17:41:31 +07:00
|
|
|
var chaseSequence = new Sequence(new List<Node>
|
|
|
|
|
{
|
|
|
|
|
new TaskNode(CheckCanSeePlayer),
|
|
|
|
|
new TaskNode(ActionMoveToPlayer)
|
|
|
|
|
});
|
2026-06-03 13:42:09 +07:00
|
|
|
|
2026-06-04 15:41:01 +07:00
|
|
|
// Không thấy ai -> Tuần tra bằng NavMesh
|
|
|
|
|
var patrolNode = new TaskNode(ActionPatrol);
|
2026-06-03 13:42:09 +07:00
|
|
|
|
2026-05-30 17:41:31 +07:00
|
|
|
behaviorTreeRoot = new Selector(new List<Node>
|
|
|
|
|
{
|
2026-06-03 13:42:09 +07:00
|
|
|
laserSequence,
|
2026-05-30 17:41:31 +07:00
|
|
|
chaseSequence,
|
2026-06-04 15:41:01 +07:00
|
|
|
patrolNode
|
2026-05-30 17:41:31 +07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 13:42:09 +07:00
|
|
|
#region CONDITIONS
|
|
|
|
|
|
|
|
|
|
private NodeState CheckHasArtifact()
|
|
|
|
|
{
|
2026-06-04 15:41:01 +07:00
|
|
|
return playerHasArtifact ? NodeState.Success : NodeState.Failure;
|
2026-06-03 13:42:09 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private NodeState CheckCanSeePlayer()
|
|
|
|
|
{
|
2026-06-04 15:41:01 +07:00
|
|
|
if (player == null) return NodeState.Failure;
|
2026-06-03 13:42:09 +07:00
|
|
|
|
2026-06-04 15:41:01 +07:00
|
|
|
float distance = Vector3.Distance(transform.position, player.position);
|
2026-06-03 13:42:09 +07:00
|
|
|
|
|
|
|
|
if (distance <= detectRange)
|
|
|
|
|
{
|
|
|
|
|
return NodeState.Success;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NodeState.Failure;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region ACTIONS
|
|
|
|
|
|
2026-06-04 15:41:01 +07:00
|
|
|
private NodeState ActionPatrol()
|
2026-05-30 17:41:31 +07:00
|
|
|
{
|
2026-06-04 17:49:04 +07:00
|
|
|
// Debug.Log("Patrolling...");
|
2026-06-04 15:41:01 +07:00
|
|
|
agent.isStopped = false; // Đảm bảo NPC được phép di chuyển
|
|
|
|
|
agent.speed = moveSpeed * 0.5f; // Đi dạo nên đi chậm lại một chút
|
2026-06-03 13:42:09 +07:00
|
|
|
|
2026-06-04 15:41:01 +07:00
|
|
|
// Kiểm tra xem NPC đã đến điểm đích chưa
|
|
|
|
|
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
|
|
|
|
|
{
|
|
|
|
|
currentWaitTime += Time.deltaTime;
|
|
|
|
|
|
|
|
|
|
// Chờ một lúc rồi mới chọn điểm mới
|
|
|
|
|
if (currentWaitTime >= patrolWaitTime)
|
|
|
|
|
{
|
|
|
|
|
// Tìm một điểm ngẫu nhiên trong bán kính cho trước
|
|
|
|
|
Vector3 randomDirection = Random.insideUnitSphere * patrolRadius;
|
|
|
|
|
randomDirection += startPosition;
|
|
|
|
|
NavMeshHit hit;
|
|
|
|
|
|
|
|
|
|
// Đảm bảo điểm ngẫu nhiên nằm trên bề mặt NavMesh hợp lệ
|
|
|
|
|
if (NavMesh.SamplePosition(randomDirection, out hit, patrolRadius, 1))
|
|
|
|
|
{
|
|
|
|
|
agent.SetDestination(hit.position);
|
|
|
|
|
}
|
|
|
|
|
currentWaitTime = 0f;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-03 13:42:09 +07:00
|
|
|
|
2026-05-30 17:41:31 +07:00
|
|
|
return NodeState.Running;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private NodeState ActionMoveToPlayer()
|
|
|
|
|
{
|
2026-06-04 15:41:01 +07:00
|
|
|
if (player == null) return NodeState.Failure;
|
2026-06-03 13:42:09 +07:00
|
|
|
|
2026-06-04 17:49:04 +07:00
|
|
|
// Debug.Log("Chasing Player");
|
2026-06-04 15:41:01 +07:00
|
|
|
|
|
|
|
|
agent.isStopped = false;
|
|
|
|
|
agent.speed = moveSpeed; // Phục hồi tốc độ rượt đuổi
|
|
|
|
|
agent.SetDestination(player.position);
|
2026-06-03 13:42:09 +07:00
|
|
|
|
2026-05-30 17:41:31 +07:00
|
|
|
return NodeState.Running;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 13:42:09 +07:00
|
|
|
private NodeState ActionFocusAndShoot()
|
2026-05-30 17:41:31 +07:00
|
|
|
{
|
2026-06-04 15:41:01 +07:00
|
|
|
if (player == null) return NodeState.Failure;
|
2026-06-03 13:42:09 +07:00
|
|
|
|
2026-06-04 17:49:04 +07:00
|
|
|
// Debug.Log("Focus and Shoot!");
|
2026-06-04 15:41:01 +07:00
|
|
|
|
|
|
|
|
// Dừng NavMeshAgent lại để đứng bắn, tránh bị trượt
|
|
|
|
|
agent.isStopped = true;
|
2026-06-03 13:42:09 +07:00
|
|
|
|
2026-06-04 15:41:01 +07:00
|
|
|
// Focus player
|
|
|
|
|
Vector3 dir = player.position - transform.position;
|
2026-06-03 13:42:09 +07:00
|
|
|
dir.y = 0f;
|
|
|
|
|
|
|
|
|
|
if (dir != Vector3.zero)
|
|
|
|
|
{
|
2026-06-04 15:41:01 +07:00
|
|
|
Quaternion targetRotation = Quaternion.LookRotation(dir);
|
|
|
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime);
|
2026-05-30 17:41:31 +07:00
|
|
|
}
|
2026-06-03 13:42:09 +07:00
|
|
|
|
|
|
|
|
// Shoot with random delay
|
|
|
|
|
if (Time.time >= nextShootTime)
|
2026-05-30 17:41:31 +07:00
|
|
|
{
|
2026-06-03 13:42:09 +07:00
|
|
|
ShootLaser();
|
2026-06-04 15:41:01 +07:00
|
|
|
nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay);
|
2026-05-30 17:41:31 +07:00
|
|
|
}
|
2026-06-03 13:42:09 +07:00
|
|
|
|
|
|
|
|
return NodeState.Running;
|
2026-05-30 17:41:31 +07:00
|
|
|
}
|
2026-06-03 13:42:09 +07:00
|
|
|
|
|
|
|
|
private void ShootLaser()
|
|
|
|
|
{
|
2026-06-04 15:41:01 +07:00
|
|
|
if (laserPrefab == null || firePoint == null) return;
|
|
|
|
|
Instantiate(laserPrefab, firePoint.position, firePoint.rotation);
|
2026-06-04 17:49:04 +07:00
|
|
|
// Debug.Log("Laser Shot!");
|
2026-06-03 13:42:09 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
2026-05-30 17:41:31 +07:00
|
|
|
}
|