Merge branch 'main' of https://scove-vault.duckdns.org/scove/HALLUCINATION
This commit is contained in:
16
Assets/Scripts/AI NPC/AnimatorAI.cs
Normal file
16
Assets/Scripts/AI NPC/AnimatorAI.cs
Normal file
@@ -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()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/AI NPC/AnimatorAI.cs.meta
Normal file
2
Assets/Scripts/AI NPC/AnimatorAI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 35bba55c2a743d042ab1fff35e29db50
|
||||||
@@ -1,41 +1,39 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.AI;
|
using UnityEngine.AI;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
[RequireComponent(typeof(NavMeshAgent))]
|
[RequireComponent(typeof(NavMeshAgent))]
|
||||||
[RequireComponent(typeof(Rigidbody))]
|
[RequireComponent(typeof(Rigidbody))]
|
||||||
|
[RequireComponent(typeof(FieldOfView))]
|
||||||
public class EnemyAI : MonoBehaviour
|
public class EnemyAI : MonoBehaviour
|
||||||
{
|
{
|
||||||
[Header("References")]
|
[Header("References")]
|
||||||
public Transform player;
|
public Transform player;
|
||||||
|
private NavMeshAgent agent;
|
||||||
|
private Rigidbody rb;
|
||||||
|
private FieldOfView fov;
|
||||||
|
|
||||||
[Header("Field of View")]
|
[Header("Movement & Rotation")]
|
||||||
[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 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;
|
public bool playerHasArtifact;
|
||||||
|
|
||||||
[Header("Laser")]
|
[Header("Laser Weapon")]
|
||||||
public GameObject laserPrefab;
|
public GameObject laserPrefab;
|
||||||
public Transform firePoint;
|
public Transform firePoint;
|
||||||
public float minShootDelay = 1f;
|
public float minShootDelay = 1f;
|
||||||
public float maxShootDelay = 3f;
|
public float maxShootDelay = 3f;
|
||||||
public float rotateSpeed = 50f;
|
private float nextShootTime;
|
||||||
|
|
||||||
[Header("Conversation")]
|
[Header("Conversation")]
|
||||||
public string npcName = "Guard";
|
public string npcName = "Guard";
|
||||||
@@ -47,45 +45,42 @@ public class EnemyAI : MonoBehaviour
|
|||||||
private EnemyAI talkingPartner;
|
private EnemyAI talkingPartner;
|
||||||
private Hallucinate.UI.ChatBubble chatBubble;
|
private Hallucinate.UI.ChatBubble chatBubble;
|
||||||
|
|
||||||
[Header("Dodge Mechanics")]
|
[Header("Dodge Settings (Rigidbody)")]
|
||||||
public float dodgeForce = 8f; // Lực đẩy văng đi
|
public float dodgeForce = 8f;
|
||||||
public float dodgeDuration = 0.5f; // Thời gian nhào lộn/né
|
public float dodgeDuration = 0.25f;
|
||||||
public float dodgeCooldown = 3f; // Thời gian chờ giữa 2 lần né
|
public float dodgeCooldown = 1.5f;
|
||||||
|
|
||||||
private float nextDodgeTime;
|
|
||||||
private bool isDodging = false;
|
private bool isDodging = false;
|
||||||
private Rigidbody rb;
|
private float nextDodgeTime;
|
||||||
|
|
||||||
private float nextShootTime;
|
// Gốc của Cây hành vi
|
||||||
private NavMeshAgent agent;
|
|
||||||
public Node behaviorTreeRoot;
|
public Node behaviorTreeRoot;
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
agent = GetComponent<NavMeshAgent>();
|
agent = GetComponent<NavMeshAgent>();
|
||||||
rb = GetComponent<Rigidbody>();
|
rb = GetComponent<Rigidbody>();
|
||||||
|
fov = GetComponent<FieldOfView>();
|
||||||
chatBubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
|
chatBubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(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();
|
InitBehaviorTree();
|
||||||
StartCoroutine(FindTargetWithDelay(0.1f)); // Chạy FOV quét mục tiêu
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (player == null) FindPlayer();
|
if (player == null) FindPlayer();
|
||||||
if (Input.GetMouseButtonDown(0) && canSeePlayer && !isDodging && Time.time >= nextDodgeTime)
|
|
||||||
{
|
// Thực thi cây hành vi liên tục mỗi khung hình
|
||||||
StartCoroutine(DodgeRoutine());
|
|
||||||
}
|
|
||||||
if (isDodging) return;
|
|
||||||
behaviorTreeRoot?.Evaluate();
|
behaviorTreeRoot?.Evaluate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,95 +90,68 @@ public class EnemyAI : MonoBehaviour
|
|||||||
if (playerObj != null) player = playerObj.transform;
|
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()
|
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<Node>
|
var laserSequence = new Sequence(new List<Node>
|
||||||
{
|
{
|
||||||
new TaskNode(CheckHasArtifact),
|
new TaskNode(CheckHasArtifact),
|
||||||
new TaskNode(ActionFocusAndShoot)
|
new TaskNode(ActionFocusAndShoot)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Thấy Player -> Đuổi theo
|
// Ư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 chaseSequence = new Sequence(new List<Node>
|
var trackingSelector = new Selector(new List<Node>
|
||||||
{
|
{
|
||||||
new TaskNode(CheckCanSeePlayer),
|
// Nhìn thấy trực tiếp -> dí theo
|
||||||
new TaskNode(ActionMoveToPlayer)
|
new Sequence(new List<Node> { 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<Node> { new TaskNode(CheckHasInvestigateTarget), new TaskNode(ActionInvestigate) })
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Mất dấu Player -> Đi tới vị trí cuối cùng để điều tra
|
// Ưu tiên số 4: Gần NPC khác -> nói chuyện (Mới)
|
||||||
var investigateSequence = new Sequence(new List<Node>
|
|
||||||
{
|
|
||||||
new TaskNode(CheckShouldInvestigate),
|
|
||||||
new TaskNode(ActionInvestigate)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Gần NPC khác -> nói chuyện (Mới)
|
|
||||||
var talkSequence = new Sequence(new List<Node>
|
var talkSequence = new Sequence(new List<Node>
|
||||||
{
|
{
|
||||||
new TaskNode(CheckCanTalkToNPC),
|
new TaskNode(CheckCanTalkToNPC),
|
||||||
new TaskNode(ActionTalk)
|
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);
|
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<Node>
|
behaviorTreeRoot = new Selector(new List<Node>
|
||||||
{
|
{
|
||||||
|
dodgeNode,
|
||||||
laserSequence,
|
laserSequence,
|
||||||
chaseSequence,
|
trackingSelector,
|
||||||
investigateSequence,
|
|
||||||
talkSequence,
|
talkSequence,
|
||||||
patrolNode
|
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()
|
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 (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure;
|
||||||
if (isTalking) return NodeState.Success;
|
if (isTalking) return NodeState.Success;
|
||||||
|
|
||||||
@@ -213,46 +181,72 @@ public class EnemyAI : MonoBehaviour
|
|||||||
|
|
||||||
private NodeState CheckCanSeePlayer()
|
private NodeState CheckCanSeePlayer()
|
||||||
{
|
{
|
||||||
if (canSeePlayer) StopConversation();
|
if (fov.canSeePlayer) StopConversation();
|
||||||
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
|
#endregion
|
||||||
|
|
||||||
#region ACTIONS
|
#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()
|
private NodeState ActionPatrol()
|
||||||
{
|
{
|
||||||
if (patrolPoints.Length == 0) return NodeState.Failure;
|
if (patrolPoints.Length == 0) return NodeState.Failure;
|
||||||
|
|
||||||
Debug.Log("Patrolling...");
|
Debug.Log("Patrolling...");
|
||||||
agent.isStopped = false;
|
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);
|
agent.SetDestination(patrolPoints[currentPatrolIndex].position);
|
||||||
|
|
||||||
// Nếu đã tới nơi, chuyển sang điểm tiếp theo
|
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
|
||||||
if (agent.remainingDistance <= agent.stoppingDistance && !agent.pathPending)
|
|
||||||
{
|
{
|
||||||
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
|
currentWaitTime += Time.deltaTime;
|
||||||
|
if (currentWaitTime >= patrolWaitTime)
|
||||||
|
{
|
||||||
|
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
|
||||||
|
currentWaitTime = 0f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NodeState.Running;
|
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.isStopped = false;
|
||||||
agent.speed = chaseSpeed;
|
agent.speed = moveSpeed; // Chạy nhanh hết tốc lực
|
||||||
agent.SetDestination(player.position);
|
agent.SetDestination(player.position);
|
||||||
|
|
||||||
return NodeState.Running;
|
return NodeState.Running;
|
||||||
@@ -260,29 +254,26 @@ public class EnemyAI : MonoBehaviour
|
|||||||
|
|
||||||
private NodeState ActionInvestigate()
|
private NodeState ActionInvestigate()
|
||||||
{
|
{
|
||||||
Debug.Log("Investigating last known position...");
|
Debug.Log("Investigating Last Position...");
|
||||||
agent.isStopped = false;
|
agent.isStopped = false;
|
||||||
agent.speed = moveSpeed;
|
agent.speed = moveSpeed * 0.8f;
|
||||||
|
agent.SetDestination(fov.lastKnownPlayerPosition);
|
||||||
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.pathPending && agent.remainingDistance <= agent.stoppingDistance)
|
||||||
if (agent.remainingDistance <= agent.stoppingDistance && !agent.pathPending)
|
|
||||||
{
|
{
|
||||||
isInvestigating = false;
|
// Đế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
|
||||||
return NodeState.Success;
|
fov.lastKnownPlayerPosition = Vector3.zero;
|
||||||
|
return NodeState.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
return NodeState.Running;
|
return NodeState.Running;
|
||||||
}
|
}
|
||||||
|
|
||||||
private NodeState ActionFocusAndShoot()
|
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
|
// Tự xoay người hướng thẳng về phía Player
|
||||||
|
|
||||||
// Xoay người về phía player
|
|
||||||
Vector3 dir = player.position - transform.position;
|
Vector3 dir = player.position - transform.position;
|
||||||
dir.y = 0f;
|
dir.y = 0f;
|
||||||
if (dir != Vector3.zero)
|
if (dir != Vector3.zero)
|
||||||
@@ -291,7 +282,7 @@ public class EnemyAI : MonoBehaviour
|
|||||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime);
|
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)
|
if (Time.time >= nextShootTime)
|
||||||
{
|
{
|
||||||
ShootLaser();
|
ShootLaser();
|
||||||
@@ -305,7 +296,6 @@ public class EnemyAI : MonoBehaviour
|
|||||||
{
|
{
|
||||||
if (laserPrefab == null || firePoint == null) return;
|
if (laserPrefab == null || firePoint == null) return;
|
||||||
Instantiate(laserPrefab, firePoint.position, firePoint.rotation);
|
Instantiate(laserPrefab, firePoint.position, firePoint.rotation);
|
||||||
Debug.Log("Laser Shot!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private NodeState ActionTalk()
|
private NodeState ActionTalk()
|
||||||
@@ -381,63 +371,4 @@ public class EnemyAI : MonoBehaviour
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
52
Assets/Scripts/AI NPC/FieldOfView.cs
Normal file
52
Assets/Scripts/AI NPC/FieldOfView.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/AI NPC/FieldOfView.cs.meta
Normal file
2
Assets/Scripts/AI NPC/FieldOfView.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 210b37cfe4a84a34a91d0a9e58856a60
|
||||||
Reference in New Issue
Block a user