using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.AI; [RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(Rigidbody))] public class EnemyAI : MonoBehaviour { [Header("References")] public Transform player; [Header("Field of View")] [Range(0, 360)] public float viewAngle = 90f; public float viewRadius = 20f; public LayerMask targetLayerMask; // Gán layer của Player public LayerMask obstacleLayerMask; // Gán layer của Tường, chướng ngại vật private bool canSeePlayer = false; private Vector3 lastKnownPlayerPosition; private bool isInvestigating = false; [Header("Patrol Area")] public Transform[] patrolPoints; private int currentPatrolIndex = 0; public float moveSpeed = 3f; public float chaseSpeed = 5f; [Header("Artifact")] public bool playerHasArtifact; [Header("Laser")] public GameObject laserPrefab; public Transform firePoint; public float minShootDelay = 1f; public float maxShootDelay = 3f; public float rotateSpeed = 50f; [Header("Dodge Mechanics")] public float dodgeForce = 8f; // Lực đẩy văng đi public float dodgeDuration = 0.5f; // Thời gian nhào lộn/né public float dodgeCooldown = 3f; // Thời gian chờ giữa 2 lần né private float nextDodgeTime; private bool isDodging = false; private Rigidbody rb; private float nextShootTime; private NavMeshAgent agent; public Node behaviorTreeRoot; private void Start() { agent = GetComponent(); rb = GetComponent(); // Tự động tìm các điểm tuần tra nếu chưa gán if (patrolPoints == null || patrolPoints.Length == 0) { patrolPoints = GameObject.FindGameObjectsWithTag("PatrolPoint") .Select(go => go.transform).ToArray(); } nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay); InitBehaviorTree(); StartCoroutine(FindTargetWithDelay(0.1f)); // Chạy FOV quét mục tiêu } private void Update() { if (player == null) FindPlayer(); if (Input.GetMouseButtonDown(0) && canSeePlayer && !isDodging && Time.time >= nextDodgeTime) { StartCoroutine(DodgeRoutine()); } if (isDodging) return; behaviorTreeRoot?.Evaluate(); } private void FindPlayer() { GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); if (playerObj != null) player = playerObj.transform; } // Coroutine tối ưu việc quét mục tiêu private IEnumerator FindTargetWithDelay(float delay) { while (true) { yield return new WaitForSeconds(delay); FindVisibleTargets(); } } private void FindVisibleTargets() { canSeePlayer = false; Collider[] colliders = Physics.OverlapSphere(transform.position, viewRadius, targetLayerMask); foreach (var col in colliders) { Transform target = col.transform; Vector3 direction = (target.position - transform.position).normalized; float angle = Vector3.Angle(transform.forward, direction); // Nếu nằm trong góc nhìn if (angle < viewAngle / 2) { float distanceToTarget = Vector3.Distance(transform.position, target.position); // Nếu không có vật cản che khuất if (!Physics.Raycast(transform.position, direction, distanceToTarget, obstacleLayerMask)) { canSeePlayer = true; isInvestigating = true; lastKnownPlayerPosition = target.position; Debug.DrawLine(transform.position, target.position, Color.blue, 0.1f); break; // Thấy player rồi thì dừng vòng lặp } } } } private void InitBehaviorTree() { // 1. Cầm Artifact -> Đứng bắn var laserSequence = new Sequence(new List { new TaskNode(CheckHasArtifact), new TaskNode(ActionFocusAndShoot) }); // 2. Thấy Player -> Đuổi theo var chaseSequence = new Sequence(new List { new TaskNode(CheckCanSeePlayer), new TaskNode(ActionMoveToPlayer) }); // 3. Mất dấu Player -> Đi tới vị trí cuối cùng để điều tra var investigateSequence = new Sequence(new List { new TaskNode(CheckShouldInvestigate), new TaskNode(ActionInvestigate) }); // 4. Không có gì -> Tuần tra theo điểm var patrolNode = new TaskNode(ActionPatrol); behaviorTreeRoot = new Selector(new List { laserSequence, chaseSequence, investigateSequence, patrolNode }); } #region CONDITIONS private NodeState CheckHasArtifact() { return playerHasArtifact ? NodeState.Success : NodeState.Failure; } private NodeState CheckCanSeePlayer() { return canSeePlayer ? NodeState.Success : NodeState.Failure; } private NodeState CheckShouldInvestigate() { return isInvestigating ? NodeState.Success : NodeState.Failure; } #endregion #region ACTIONS private NodeState ActionPatrol() { if (patrolPoints.Length == 0) return NodeState.Failure; Debug.Log("Patrolling..."); agent.isStopped = false; agent.speed = moveSpeed; // Đi tới điểm tuần tra hiện tại agent.SetDestination(patrolPoints[currentPatrolIndex].position); // Nếu đã tới nơi, chuyển sang điểm tiếp theo if (agent.remainingDistance <= agent.stoppingDistance && !agent.pathPending) { currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length; } return NodeState.Running; } private NodeState ActionMoveToPlayer() { if (player == null) return NodeState.Failure; Debug.Log("Chasing Player..."); agent.isStopped = false; agent.speed = chaseSpeed; agent.SetDestination(player.position); return NodeState.Running; } private NodeState ActionInvestigate() { Debug.Log("Investigating last known position..."); agent.isStopped = false; agent.speed = moveSpeed; agent.SetDestination(lastKnownPlayerPosition); // Nếu đi tới nơi mà vẫn không thấy player -> Hủy điều tra, quay về tuần tra if (agent.remainingDistance <= agent.stoppingDistance && !agent.pathPending) { isInvestigating = false; return NodeState.Success; } return NodeState.Running; } private NodeState ActionFocusAndShoot() { if (player == null) return NodeState.Failure; agent.isStopped = true; // Đứng lại để bắn // Xoay người về phía player Vector3 dir = player.position - transform.position; dir.y = 0f; if (dir != Vector3.zero) { Quaternion targetRotation = Quaternion.LookRotation(dir); transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime); } // Bắn if (Time.time >= nextShootTime) { ShootLaser(); nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay); } return NodeState.Running; } private void ShootLaser() { if (laserPrefab == null || firePoint == null) return; Instantiate(laserPrefab, firePoint.position, firePoint.rotation); Debug.Log("Laser Shot!"); } #endregion #region DODGE MECHANIC private IEnumerator DodgeRoutine() { Debug.Log("Dodging!"); isDodging = true; nextDodgeTime = Time.time + dodgeCooldown; // 1. Tắt AI tìm đường để Vật lý tiếp quản agent.enabled = false; rb.isKinematic = false; // Đảm bảo Rigidbody có thể nhận lực // 2. Tính toán hướng né: Random nhảy sang Trái hoặc Phải int randomDirection = Random.Range(0, 2) == 0 ? -1 : 1; // Lấy vector hướng ngang của NPC nhân với trái (-1) hoặc phải (1) Vector3 dodgeDir = transform.right * randomDirection; // Có thể cộng thêm một chút lực nhảy lên (trục Y) nếu muốn NPC hơi nảy lên // dodgeDir.y = 0.5f; // 3. Tác dụng lực đẩy tức thời (Impulse) rb.AddForce(dodgeDir * dodgeForce, ForceMode.Impulse); // 4. Chờ NPC văng đi trong thời gian chỉ định yield return new WaitForSeconds(dodgeDuration); // 5. Thắng gấp (Dừng toàn bộ gia tốc vật lý lại) // Lưu ý: Unity 6 dùng linearVelocity thay vì velocity như các bản cũ rb.linearVelocity = Vector3.zero; rb.angularVelocity = Vector3.zero; // 6. Bật lại AI tìm đường rb.isKinematic = true; // Trả lại Rigidbody về trạng thái không ảnh hưởng vật lý agent.enabled = true; isDodging = false; } #endregion // Vẽ FOV trên Scene để dễ debug private void OnDrawGizmosSelected() { Gizmos.color = Color.white; Gizmos.DrawWireSphere(transform.position, viewRadius); Vector3 viewAngleA = DirFromAngle(-viewAngle / 2); Vector3 viewAngleB = DirFromAngle(viewAngle / 2); Gizmos.color = Color.yellow; Gizmos.DrawLine(transform.position, transform.position + viewAngleA * viewRadius); Gizmos.DrawLine(transform.position, transform.position + viewAngleB * viewRadius); } private Vector3 DirFromAngle(float angleInDegrees) { angleInDegrees += transform.eulerAngles.y; return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad)); } }