340 lines
10 KiB
C#
340 lines
10 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.AI; // Cần thiết để dùng NavMesh
|
|
using Hallucinate.Audio;
|
|
|
|
[RequireComponent(typeof(NavMeshAgent))] // Tự động thêm component này nếu chưa có
|
|
public class EnemyAI : MonoBehaviour
|
|
{
|
|
[Header("References")]
|
|
public Transform player;
|
|
|
|
[Header("Detection")]
|
|
public float detectRange = 10f;
|
|
public float moveSpeed = 3f;
|
|
public float rotateSpeed = 50f;
|
|
|
|
[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;
|
|
|
|
[Header("Artifact")]
|
|
public bool playerHasArtifact;
|
|
|
|
[Header("Laser")]
|
|
public GameObject laserPrefab;
|
|
public Transform firePoint;
|
|
public float minShootDelay = 1f;
|
|
public float maxShootDelay = 3f;
|
|
|
|
[Header("Audio")]
|
|
public string alertSound = "Enemy_Alert";
|
|
public string shootSound = "Enemy_Shoot";
|
|
private bool hasSpottedPlayer; // Để chỉ kêu alert 1 lần
|
|
|
|
[Header("Conversation")]
|
|
public string npcName = "Guard";
|
|
public string persona = "You are a grumpy guard protecting gold.";
|
|
public float talkRange = 4f;
|
|
public float talkCooldown = 30f;
|
|
private float lastTalkTime;
|
|
private bool isTalking;
|
|
private EnemyAI talkingPartner;
|
|
private Hallucinate.UI.ChatBubble chatBubble;
|
|
|
|
private float nextShootTime;
|
|
private NavMeshAgent agent;
|
|
|
|
public Node behaviorTreeRoot;
|
|
|
|
private void Start()
|
|
{
|
|
agent = GetComponent<NavMeshAgent>();
|
|
chatBubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
|
|
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;
|
|
|
|
nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay);
|
|
InitBehaviorTree();
|
|
FindPlayer();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
// 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)
|
|
behaviorTreeRoot?.Evaluate();
|
|
}
|
|
|
|
private void FindPlayer()
|
|
{
|
|
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
|
|
if (playerObj != null)
|
|
{
|
|
player = playerObj.transform;
|
|
}
|
|
}
|
|
|
|
private void InitBehaviorTree()
|
|
{
|
|
// Ưu tiên 1: Player có artifact -> focus + shoot (Cao nhất)
|
|
var laserSequence = new Sequence(new List<Node>
|
|
{
|
|
new TaskNode(CheckHasArtifact),
|
|
new TaskNode(ActionFocusAndShoot)
|
|
});
|
|
|
|
// Ưu tiên 2: Thấy player -> rượt đuổi
|
|
var chaseSequence = new Sequence(new List<Node>
|
|
{
|
|
new TaskNode(CheckCanSeePlayer),
|
|
new TaskNode(ActionMoveToPlayer)
|
|
});
|
|
|
|
// Ưu tiên 3: Gần NPC khác -> nói chuyện (Mới)
|
|
var talkSequence = new Sequence(new List<Node>
|
|
{
|
|
new TaskNode(CheckCanTalkToNPC),
|
|
new TaskNode(ActionTalk)
|
|
});
|
|
|
|
// Ưu tiên cuối: Tuần tra
|
|
var patrolNode = new TaskNode(ActionPatrol);
|
|
|
|
behaviorTreeRoot = new Selector(new List<Node>
|
|
{
|
|
laserSequence,
|
|
chaseSequence,
|
|
talkSequence,
|
|
patrolNode
|
|
});
|
|
}
|
|
|
|
#region CONDITIONS
|
|
|
|
private NodeState CheckCanTalkToNPC()
|
|
{
|
|
if (playerHasArtifact || hasSpottedPlayer) return NodeState.Failure;
|
|
if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure;
|
|
if (isTalking) return NodeState.Success;
|
|
|
|
// Tìm NPC gần nhất
|
|
Collider[] hitColliders = Physics.OverlapSphere(transform.position, talkRange);
|
|
foreach (var hit in hitColliders)
|
|
{
|
|
if (hit.gameObject != gameObject && hit.CompareTag("Enemy"))
|
|
{
|
|
EnemyAI other = hit.GetComponent<EnemyAI>();
|
|
if (other != null && !other.isTalking && Time.time >= other.lastTalkTime + talkCooldown)
|
|
{
|
|
talkingPartner = other;
|
|
return NodeState.Success;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NodeState.Failure;
|
|
}
|
|
|
|
private NodeState CheckHasArtifact()
|
|
{
|
|
// Khi bị phát hiện hoặc player có artifact, ngắt lời ngay
|
|
if (playerHasArtifact || hasSpottedPlayer) StopConversation();
|
|
return playerHasArtifact ? NodeState.Success : NodeState.Failure;
|
|
}
|
|
|
|
private NodeState CheckCanSeePlayer()
|
|
{
|
|
if (player == null) return NodeState.Failure;
|
|
|
|
float distance = Vector3.Distance(transform.position, player.position);
|
|
|
|
if (distance <= detectRange)
|
|
{
|
|
if (!hasSpottedPlayer)
|
|
{
|
|
hasSpottedPlayer = true;
|
|
AudioManager.Instance?.Play(alertSound, position: transform.position);
|
|
StopConversation(); // Ngắt hội thoại khi thấy player
|
|
}
|
|
return NodeState.Success;
|
|
}
|
|
|
|
hasSpottedPlayer = false;
|
|
return NodeState.Failure;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ACTIONS
|
|
|
|
private NodeState ActionTalk()
|
|
{
|
|
if (talkingPartner == null) return NodeState.Failure;
|
|
|
|
if (!isTalking)
|
|
{
|
|
isTalking = true;
|
|
agent.isStopped = true;
|
|
|
|
// Xoay về phía bạn
|
|
FaceTarget(talkingPartner.transform.position);
|
|
|
|
// Bắt đầu hội thoại qua Gemini
|
|
StartNPCConversation();
|
|
}
|
|
|
|
return NodeState.Running;
|
|
}
|
|
|
|
private void StartNPCConversation()
|
|
{
|
|
string prompt = $"You are {npcName}. You are talking to your fellow guard {talkingPartner.npcName}. " +
|
|
"Keep it short (1 sentence). Topic: gold security or complaining about work.";
|
|
|
|
Hallucinate.AI.GeminiService.Instance.GetResponse(persona, prompt, (response) => {
|
|
if (chatBubble != null) chatBubble.Show(response);
|
|
|
|
// Hẹn giờ kết thúc hội thoại
|
|
Invoke(nameof(EndConversation), 5f);
|
|
});
|
|
|
|
// Thông báo cho bạn diễn cũng dừng lại để "nghe"
|
|
talkingPartner.OnPartnerTalked(this);
|
|
}
|
|
|
|
public void OnPartnerTalked(EnemyAI partner)
|
|
{
|
|
isTalking = true;
|
|
talkingPartner = partner;
|
|
agent.isStopped = true;
|
|
FaceTarget(partner.transform.position);
|
|
|
|
// Chờ bạn nói xong mới phản hồi (Tùy chọn: có thể thêm logic phản hồi ở đây)
|
|
Invoke(nameof(EndConversation), 6f);
|
|
}
|
|
|
|
private void EndConversation()
|
|
{
|
|
isTalking = false;
|
|
lastTalkTime = Time.time;
|
|
if (agent != null) agent.isStopped = false;
|
|
talkingPartner = null;
|
|
}
|
|
|
|
private void StopConversation()
|
|
{
|
|
if (!isTalking) return;
|
|
CancelInvoke(nameof(EndConversation));
|
|
EndConversation();
|
|
if (chatBubble != null) chatBubble.Show("Suỵt! Có gì đó không ổn...", 2f);
|
|
}
|
|
|
|
private void FaceTarget(Vector3 targetPos)
|
|
{
|
|
Vector3 dir = targetPos - transform.position;
|
|
dir.y = 0;
|
|
if (dir != Vector3.zero)
|
|
{
|
|
transform.rotation = Quaternion.LookRotation(dir);
|
|
}
|
|
}
|
|
|
|
private NodeState ActionPatrol()
|
|
{
|
|
// Debug.Log("Patrolling...");
|
|
if (!agent.isActiveAndEnabled || !agent.isOnNavMesh) return NodeState.Failure;
|
|
|
|
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
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
return NodeState.Running;
|
|
}
|
|
|
|
private NodeState ActionMoveToPlayer()
|
|
{
|
|
if (player == null) return NodeState.Failure;
|
|
|
|
// Debug.Log("Chasing Player");
|
|
|
|
if (!agent.isActiveAndEnabled || !agent.isOnNavMesh) return NodeState.Failure;
|
|
|
|
agent.isStopped = false;
|
|
agent.speed = moveSpeed; // Phục hồi tốc độ rượt đuổi
|
|
agent.SetDestination(player.position);
|
|
|
|
return NodeState.Running;
|
|
}
|
|
|
|
private NodeState ActionFocusAndShoot()
|
|
{
|
|
if (player == null) return NodeState.Failure;
|
|
|
|
// Debug.Log("Focus and Shoot!");
|
|
|
|
if (!agent.isActiveAndEnabled || !agent.isOnNavMesh) return NodeState.Failure;
|
|
|
|
// Dừng NavMeshAgent lại để đứng bắn, tránh bị trượt
|
|
agent.isStopped = true;
|
|
|
|
// Focus 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);
|
|
}
|
|
|
|
// Shoot with random delay
|
|
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);
|
|
AudioManager.Instance?.Play(shootSound, position: transform.position);
|
|
// Debug.Log("Laser Shot!");
|
|
}
|
|
|
|
#endregion
|
|
} |