From 4b45938ef7e3c6b39f15b57e4ba0b236b34507ea Mon Sep 17 00:00:00 2001 From: ngtuanz1 Date: Fri, 5 Jun 2026 15:59:33 +0700 Subject: [PATCH] update small --- Assets/Prefabs/NPC/xNPC.prefab | 37 ++- Assets/Scripts/AI NPC/AnimatorAI.cs | 16 ++ Assets/Scripts/AI NPC/AnimatorAI.cs.meta | 2 + Assets/Scripts/AI NPC/EnemyAI.cs | 310 +++++++++------------- Assets/Scripts/AI NPC/FieldOfView.cs | 52 ++++ Assets/Scripts/AI NPC/FieldOfView.cs.meta | 2 + 6 files changed, 218 insertions(+), 201 deletions(-) create mode 100644 Assets/Scripts/AI NPC/AnimatorAI.cs create mode 100644 Assets/Scripts/AI NPC/AnimatorAI.cs.meta create mode 100644 Assets/Scripts/AI NPC/FieldOfView.cs create mode 100644 Assets/Scripts/AI NPC/FieldOfView.cs.meta diff --git a/Assets/Prefabs/NPC/xNPC.prefab b/Assets/Prefabs/NPC/xNPC.prefab index 5764aaaa..17b2e98b 100644 --- a/Assets/Prefabs/NPC/xNPC.prefab +++ b/Assets/Prefabs/NPC/xNPC.prefab @@ -124,6 +124,7 @@ GameObject: - component: {fileID: 8272839718325411334} - component: {fileID: 5770331367975928816} - component: {fileID: 4112854993683970537} + - component: {fileID: 7573101351210360154} m_Layer: 0 m_Name: xNPC m_TagString: Untagged @@ -161,23 +162,15 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: Assembly-CSharp::EnemyAI player: {fileID: 0} - viewAngle: 90 - viewRadius: 20 - targetLayerMask: - serializedVersion: 2 - m_Bits: 0 - obstacleLayerMask: - serializedVersion: 2 - m_Bits: 0 - patrolPoints: [] moveSpeed: 3 - chaseSpeed: 5 + rotateSpeed: 50 + patrolPoints: [] + patrolWaitTime: 2 playerHasArtifact: 0 laserPrefab: {fileID: 3965388737199864462, guid: fbec2b501d70daa4c9cb481ba53fc0b8, type: 3} firePoint: {fileID: 5863061020199015852} minShootDelay: 1 maxShootDelay: 3 - rotateSpeed: 50 dodgeForce: 8 dodgeDuration: 0.5 dodgeCooldown: 3 @@ -230,6 +223,28 @@ Rigidbody: m_Interpolate: 0 m_Constraints: 112 m_CollisionDetection: 0 +--- !u!114 &7573101351210360154 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7522161431095319480} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 210b37cfe4a84a34a91d0a9e58856a60, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::FieldOfView + viewAngle: 90 + viewRadius: 20 + obstacleLayerMask: + serializedVersion: 2 + m_Bits: 128 + targetLayerMask: + serializedVersion: 2 + m_Bits: 256 + canSeePlayer: 0 + lastKnownPlayerPosition: {x: 0, y: 0, z: 0} --- !u!1001 &7561534673732472622 PrefabInstance: m_ObjectHideFlags: 0 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 d7190db5..8fa226f5 100644 --- a/Assets/Scripts/AI NPC/EnemyAI.cs +++ b/Assets/Scripts/AI NPC/EnemyAI.cs @@ -1,80 +1,75 @@ 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; - - [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; + + [Header("Dodge Settings (Rigidbody)")] + public float dodgeForce = 8f; + public float dodgeDuration = 0.25f; + public float dodgeCooldown = 1.5f; + private bool isDodging = false; + private float nextDodgeTime; + + // Gốc của Cây hành vi 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(); - } + fov = GetComponent(); - nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay); + agent.speed = moveSpeed; + // Tự động tìm tất cả điểm PatrolPoint trong Map giống RobotBrain + 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(); } @@ -84,83 +79,56 @@ 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. Không có gì -> Tuần tra theo điểm + // Ưu tiên số 4: 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, 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 CheckHasArtifact() { @@ -169,45 +137,70 @@ public class EnemyAI : MonoBehaviour private NodeState CheckCanSeePlayer() { - return canSeePlayer ? NodeState.Success : NodeState.Failure; + 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 - private NodeState ActionPatrol() + // 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; @@ -215,29 +208,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) @@ -246,7 +236,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(); @@ -260,67 +250,7 @@ public class EnemyAI : MonoBehaviour { 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)); - } -} \ 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