diff --git a/Assets/Scripts/AI NPC/AnimatorAI.cs b/Assets/Scripts/AI NPC/AnimatorAI.cs new file mode 100644 index 00000000..7d96529a --- /dev/null +++ b/Assets/Scripts/AI NPC/AnimatorAI.cs @@ -0,0 +1,16 @@ +using UnityEngine; + +public class AnimatorAI : MonoBehaviour +{ + // Start is called once before the first execution of Update after the MonoBehaviour is created + void Start() + { + + } + + // Update is called once per frame + void Update() + { + + } +} diff --git a/Assets/Scripts/AI NPC/AnimatorAI.cs.meta b/Assets/Scripts/AI NPC/AnimatorAI.cs.meta new file mode 100644 index 00000000..f799db3c --- /dev/null +++ b/Assets/Scripts/AI NPC/AnimatorAI.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 35bba55c2a743d042ab1fff35e29db50 \ No newline at end of file diff --git a/Assets/Scripts/AI NPC/EnemyAI.cs b/Assets/Scripts/AI NPC/EnemyAI.cs index fd4949d6..2fbff0a8 100644 --- a/Assets/Scripts/AI NPC/EnemyAI.cs +++ b/Assets/Scripts/AI NPC/EnemyAI.cs @@ -1,41 +1,39 @@ using System.Collections; using System.Collections.Generic; -using System.Linq; using UnityEngine; -using UnityEngine.AI; +using UnityEngine.AI; +using System.Linq; [RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(Rigidbody))] +[RequireComponent(typeof(FieldOfView))] public class EnemyAI : MonoBehaviour { [Header("References")] public Transform player; + private NavMeshAgent agent; + private Rigidbody rb; + private FieldOfView fov; - [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; + [Header("Movement & Rotation")] public float moveSpeed = 3f; - public float chaseSpeed = 5f; + public float rotateSpeed = 50f; - [Header("Artifact")] + [Header("Patrol Waypoints")] + public Transform[] patrolPoints; + public float patrolWaitTime = 2f; + private int currentPatrolIndex = 0; + private float currentWaitTime; + + [Header("Artifact State")] public bool playerHasArtifact; - [Header("Laser")] + [Header("Laser Weapon")] public GameObject laserPrefab; public Transform firePoint; public float minShootDelay = 1f; public float maxShootDelay = 3f; - public float rotateSpeed = 50f; + private float nextShootTime; [Header("Conversation")] public string npcName = "Guard"; @@ -47,45 +45,42 @@ public class EnemyAI : MonoBehaviour private EnemyAI talkingPartner; private Hallucinate.UI.ChatBubble chatBubble; - [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; + [Header("Dodge Settings (Rigidbody)")] + public float dodgeForce = 8f; + public float dodgeDuration = 0.25f; + public float dodgeCooldown = 1.5f; private bool isDodging = false; - private Rigidbody rb; - - private float nextShootTime; - private NavMeshAgent agent; + private float nextDodgeTime; + + // Gốc của Cây hành vi public Node behaviorTreeRoot; private void Start() { agent = GetComponent(); rb = GetComponent(); + fov = GetComponent(); chatBubble = GetComponentInChildren(true); - // 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); + agent.speed = moveSpeed; + // Tự động tìm tất cả điểm PatrolPoint trong Map + patrolPoints = GameObject.FindGameObjectsWithTag("PatrolPoint") + .Select(go => go.transform).ToArray(); + + // Cấu hình Rigidbody để không bị đổ ngã khi va chạm vật lý thông thường + rb.isKinematic = true; + rb.freezeRotation = true; + + FindPlayer(); 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; + + // Thực thi cây hành vi liên tục mỗi khung hình behaviorTreeRoot?.Evaluate(); } @@ -95,95 +90,68 @@ public class EnemyAI : MonoBehaviour 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 + // Ưu tiên số 1: Kiểm tra và thực hiện né đòn + var dodgeNode = new TaskNode(CheckAndActionDodge); + + // Ưu tiên số 2: Có cổ vật -> Đứng lại tập trung bắn hạ 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 + // Ưu tiên số 3: Tương tác tầm nhìn (Đuổi theo hoặc Đi kiểm tra vết tích) + var trackingSelector = new Selector(new List { - new TaskNode(CheckCanSeePlayer), - new TaskNode(ActionMoveToPlayer) + // Nhìn thấy trực tiếp -> dí theo + new Sequence(new List { new TaskNode(CheckCanSeePlayer), new TaskNode(ActionChasePlayer) }), + // Mất dấu -> đi đến vị trí cuối cùng để điều tra + new Sequence(new List { new TaskNode(CheckHasInvestigateTarget), new TaskNode(ActionInvestigate) }) }); - // 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. Gần NPC khác -> nói chuyện (Mới) + // Ưu tiên số 4: Gần NPC khác -> nói chuyện (Mới) var talkSequence = new Sequence(new List { new TaskNode(CheckCanTalkToNPC), new TaskNode(ActionTalk) }); - // 5. Không có gì -> Tuần tra theo điểm + // Ưu tiên số 5: Mặc định đi tuần tra vòng quanh Map var patrolNode = new TaskNode(ActionPatrol); + // Tạo cây tổng hợp theo thứ tự ưu tiên từ trên xuống dưới behaviorTreeRoot = new Selector(new List { + dodgeNode, laserSequence, - chaseSequence, - investigateSequence, + trackingSelector, talkSequence, patrolNode }); } - #region CONDITIONS + #region CONDITIONS & COMPOSITE NODES + + private NodeState CheckAndActionDodge() + { + if (isDodging) return NodeState.Running; + + // ĐIỀU KIỆN NÉ: Phải nhìn thấy Player VÀ Player nhấn chuột trái VÀ hết cooldown né + if (fov.canSeePlayer && Input.GetMouseButtonDown(0) && Time.time >= nextDodgeTime) + { + StartCoroutine(DodgeRollRoutine()); + nextDodgeTime = Time.time + dodgeCooldown; + return NodeState.Running; + } + + return NodeState.Failure; + } private NodeState CheckCanTalkToNPC() { - if (playerHasArtifact || canSeePlayer) return NodeState.Failure; + if (playerHasArtifact || fov.canSeePlayer) return NodeState.Failure; if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure; if (isTalking) return NodeState.Success; @@ -213,46 +181,72 @@ public class EnemyAI : MonoBehaviour private NodeState CheckCanSeePlayer() { - if (canSeePlayer) StopConversation(); - return canSeePlayer ? NodeState.Success : NodeState.Failure; + if (fov.canSeePlayer) StopConversation(); + return fov.canSeePlayer ? NodeState.Success : NodeState.Failure; } - private NodeState CheckShouldInvestigate() + private NodeState CheckHasInvestigateTarget() { - return isInvestigating ? NodeState.Success : NodeState.Failure; + return fov.lastKnownPlayerPosition != Vector3.zero ? NodeState.Success : NodeState.Failure; } #endregion #region ACTIONS + // Coroutine xử lý né bằng lực đẩy Rigidbody một cách thực tế + private IEnumerator DodgeRollRoutine() + { + isDodging = true; + agent.enabled = false; // Tắt định vị NavMesh để nhường quyền cho Vật lý + rb.isKinematic = false; // Bật chế độ vật lý động để nhận lực lực đẩy + + // Tính toán hướng né: Vuông góc với hướng nhìn của Player (Tránh sang trái hoặc phải) + Vector3 directionToPlayer = (player.position - transform.position).normalized; + Vector3 perpendicularDir = new Vector3(-directionToPlayer.z, 0, directionToPlayer.x); + + // Chọn ngẫu nhiên trái hoặc phải + Vector3 dodgeDirection = (Random.Range(0, 2) == 0 ? perpendicularDir : -perpendicularDir).normalized; + + // Tác dụng lực đẩy Impulse tức thì + rb.AddForce(dodgeDirection * dodgeForce, ForceMode.Impulse); + + yield return new WaitForSeconds(dodgeDuration); + + // Kết thúc né: Trả lại quyền điều khiển cho NavMeshAgent + rb.linearVelocity = Vector3.zero; // Cú pháp chuẩn của Unity 6 (thay cho rb.velocity) + rb.isKinematic = true; + agent.enabled = true; + isDodging = false; + } + private NodeState ActionPatrol() { if (patrolPoints.Length == 0) return NodeState.Failure; Debug.Log("Patrolling..."); agent.isStopped = false; - agent.speed = moveSpeed; + agent.speed = moveSpeed * 0.6f; // Đi tuần tra chậm rãi quay theo hướng đi tự động của NavMesh - // Đ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) + if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance) { - currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length; + currentWaitTime += Time.deltaTime; + if (currentWaitTime >= patrolWaitTime) + { + currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length; + currentWaitTime = 0f; + } } - return NodeState.Running; } - private NodeState ActionMoveToPlayer() + private NodeState ActionChasePlayer() { - if (player == null) return NodeState.Failure; - - Debug.Log("Chasing Player..."); + Debug.Log("Chasing Player!"); agent.isStopped = false; - agent.speed = chaseSpeed; + agent.speed = moveSpeed; // Chạy nhanh hết tốc lực agent.SetDestination(player.position); return NodeState.Running; @@ -260,29 +254,26 @@ public class EnemyAI : MonoBehaviour private NodeState ActionInvestigate() { - Debug.Log("Investigating last known position..."); + Debug.Log("Investigating Last Position..."); agent.isStopped = false; - agent.speed = moveSpeed; - - agent.SetDestination(lastKnownPlayerPosition); + agent.speed = moveSpeed * 0.8f; + agent.SetDestination(fov.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) + if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance) { - isInvestigating = false; - return NodeState.Success; + // Đến nơi rồi mà không thấy ai, xóa vị trí cuối cùng để quay lại tuần tra + fov.lastKnownPlayerPosition = Vector3.zero; + return NodeState.Success; } - return NodeState.Running; } private NodeState ActionFocusAndShoot() { - if (player == null) return NodeState.Failure; + Debug.Log("Focus and Shoot!"); + agent.isStopped = true; // Dừng di chuyển để đứng ngắm bắn cố định - agent.isStopped = true; // Đứng lại để bắn - - // Xoay người về phía player + // Tự xoay người hướng thẳng về phía Player Vector3 dir = player.position - transform.position; dir.y = 0f; if (dir != Vector3.zero) @@ -291,7 +282,7 @@ public class EnemyAI : MonoBehaviour transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime); } - // Bắn + // Đếm ngược thời gian bắn ngẫu nhiên if (Time.time >= nextShootTime) { ShootLaser(); @@ -305,7 +296,6 @@ public class EnemyAI : MonoBehaviour { if (laserPrefab == null || firePoint == null) return; Instantiate(laserPrefab, firePoint.position, firePoint.rotation); - Debug.Log("Laser Shot!"); } private NodeState ActionTalk() @@ -381,63 +371,4 @@ public class EnemyAI : MonoBehaviour } #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)); - } -} \ No newline at end of file +} diff --git a/Assets/Scripts/AI NPC/FieldOfView.cs b/Assets/Scripts/AI NPC/FieldOfView.cs new file mode 100644 index 00000000..301d7b9a --- /dev/null +++ b/Assets/Scripts/AI NPC/FieldOfView.cs @@ -0,0 +1,52 @@ +using System.Collections; +using UnityEngine; + +public class FieldOfView : MonoBehaviour +{ + [Range(0, 360)] + public float viewAngle = 90f; + public float viewRadius = 20f; + public LayerMask obstacleLayerMask; + public LayerMask targetLayerMask; + + [HideInInspector] public bool canSeePlayer = false; + [HideInInspector] public Vector3 lastKnownPlayerPosition; + + void Start() + { + StartCoroutine(FindTargetWithDelay(0.1f)); + } + + IEnumerator FindTargetWithDelay(float delay) + { + while (true) + { + yield return new WaitForSeconds(delay); + FindVisibleTargets(); + } + } + + private void FindVisibleTargets() + { + canSeePlayer = false; + var colliders = Physics.OverlapSphere(transform.position, viewRadius, targetLayerMask); + + for (int i = 0; i < colliders.Length; i++) + { + var target = colliders[i].transform; + var direction = (target.position - transform.position).normalized; + var angle = Vector3.Angle(transform.forward, direction); + + if (angle < viewAngle / 2) + { + float distanceToTarget = Vector3.Distance(transform.position, target.position); + if (!Physics.Raycast(transform.position, direction, distanceToTarget, obstacleLayerMask)) + { + canSeePlayer = true; + lastKnownPlayerPosition = target.position; + Debug.DrawLine(transform.position, target.position, Color.blue, 1f); + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/AI NPC/FieldOfView.cs.meta b/Assets/Scripts/AI NPC/FieldOfView.cs.meta new file mode 100644 index 00000000..28f29a1a --- /dev/null +++ b/Assets/Scripts/AI NPC/FieldOfView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 210b37cfe4a84a34a91d0a9e58856a60 \ No newline at end of file