Kẻ địch biết đuổi và đánh trả
Assets/
Scenes/
Arena.unity
Scripts/
Prefabs/
thiết lập dự án
đã có sẵnbài này thêmbài này thay thế
Con Dummy ở bài trước biết nhận sát thương và chết, chỉ có vậy. Nó không di chuyển, không đánh trả, không bao giờ làm bạn thua. Hiện tại bạn không thể thua — nên chưa có game nào cả.
Bài này cho nó đứng dậy. Vẫn capsule đó, vẫn CharacterStats đó, thêm ba hành vi: thấy bạn, đi tới chỗ bạn, đánh bạn.
Khoảng cách quyết định tất cả: ở xa thì đi tuần, vào trong vòng thì đuổi, tới tầm tay thì vung.
Bước 1 — ba trạng thái, một con số
Với sân đấu cỡ này bạn chưa cần hệ thống tìm đường. Một phép đo khoảng cách chọn ra trạng thái, trạng thái chọn ra hành vi. Bộ não chỉ có vậy.
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class EnemyBrain : MonoBehaviour
{
public Transform target; //bắt đầu đuổi
public float aggroRange = 9f;
public float attackRange = 1.8f; // dừng lại và vung
public float speed = 2.6f;
public float damage = 12f;
public float attackEvery = 1.2f;
CharacterController cc;
float nextAttack;
void Awake() => cc = GetComponent<CharacterController>();
void Update()
{
if (target == null) return;
// Bỏ trục Y: mục tiêu đứng trên khối vẫn tính là "gần" theo phương ngang.
Vector3 flat = target.position - transform.position;
flat.y = 0f;
float distance = flat.magnitude;
if (distance > aggroRange) return; // đi tuần: tạm chưa làm gì
transform.rotation = Quaternion.LookRotation(flat);
if (distance > attackRange) // đuổi
{
cc.Move(flat.normalized * speed * Time.deltaTime);
}
else if (Time.time >= nextAttack) // đánh
{
nextAttack = Time.time + attackEvery;
target.GetComponent<CharacterStats>().TakeDamage(damage);
}
cc.Move(Vector3.down * 9.8f * Time.deltaTime); // bám sàn
}
}Dòng flat.y = 0f mới là dòng quan trọng. Thiếu nó, Knight đứng trên khối sẽ bị tính là ở xa, và kẻ địch mất hứng ngay khi bạn nhảy lên.
nextAttack chính là kiểu hồi chiêu giống skill của người chơi — kẻ địch chơi theo đúng luật của bạn.
CharacterStats là script bạn đã viết rồi. Kẻ địch gọi TakeDamage lên bạn; skill của bạn gọi nó lên kẻ địch. Một script, hai chiều.
Bước 2 — nối vào cảnh
- 1
Chọn Dummy. Nó đã có CharacterStats và layer Enemy từ bài trước — giữ nguyên cả hai.
- 2
Thêm Character Controller, rồi thêm EnemyBrain. Kéo Knight vào ô Target.
- 3
Đổi tên thành Enemy rồi kéo vào Assets/Prefabs. Từ giờ bạn đặt kẻ địch, không phải bù nhìn.
- 4
Rải ba con quanh lưới khối, ở các khoảng cách khác nhau. Bấm Play rồi đi lại gần một con.
Bước 3 — cho kẻ địch chết được
Skill của bạn vốn đã gây sát thương lên mọi thứ thuộc layer Enemy, nên nửa này gần như miễn phí. Phần mới duy nhất là chuyện gì xảy ra khi máu về 0 — và CharacterStats đã báo sẵn chuyện đó: bạn đã thêm OnDied cùng lúc với thanh máu. Kẻ địch chỉ cần lắng nghe rồi tự xoá mình.
CharacterStats stats;
void Awake()
{
cc = GetComponent<CharacterController>();
stats = GetComponent<CharacterStats>();
// Knight cũng có CharacterStats, nên script đó không bao giờ được tự huỷ
// thứ gì. Thay vào đó, kẻ địch tự lắng nghe cái chết của chính nó.
stats.OnDied += () => Destroy(gameObject);
}Bước 4 — khối phải chặn được đòn đánh
Hiện tại sân đấu có khối để nhảy lên, mà không thứ gì trong trận đánh biết là chúng tồn tại. Dòng flat.y = 0f đã vứt bỏ độ cao, nên kẻ địch đứng dưới sàn vẫn vung trúng Knight đứng cao hơn nó một mét, xuyên qua khối đặc.
Hai câu hỏi sửa được chuyện đó, và tách riêng là có chủ ý: nó có vung cao tới đó không, và có gì chắn ở giữa không. Một bức tường làm hỏng câu thứ hai ngay cả khi cả hai đứng cùng mặt sàn.
- 1
Layer Obstacle đã có sẵn: bạn tạo nó lúc chặn skill bắn xuyên khối, và P_Cube cũng đã nằm trong đó. Kẻ địch đọc đúng layer ấy.
- 2
Trên prefab Enemy, chuột phải vào tên → Create Empty, đặt tên Eye, set Position 0, 1.4, 0. Đó là chỗ kẻ địch nhìn ra.
- 3
Thêm đoạn code dưới vào EnemyBrain, rồi trong Inspector kéo Eye vào ô Eye và chỉ tick Obstacle trong mask Obstacles.
[Header("Cover & height")]
public LayerMask obstacles; // layer Obstacle: khối, tường
public float reachHeight = 1.2f; // cao hơn nó bao nhiêu thì còn vung tới
public Transform eye; // một Empty đặt ngang tầm mắt
// Một đường thẳng từ mắt nó tới ngực bạn. Có thứ gì thuộc layer Obstacle
// nằm chắn giữa nghĩa là nó không có đường đánh.
bool CanSee()
{
Vector3 chest = target.position + Vector3.up;
return !Physics.Linecast(eye.position, chest, obstacles);
}
// Bạn đứng trên khối là ngoài tầm vung, kể cả khi khoảng cách đã làm
// phẳng nói rằng bạn đang đứng ngay cạnh nó.
bool CanReach()
{
return target.position.y - transform.position.y <= reachHeight;
}Rồi dòng tấn công hỏi cả hai câu trước khi vung. Không đổi gì khác trong Update.
else if (Time.time >= nextAttack && CanReach() && CanSee())
{
nextAttack = Time.time + attackEvery;
target.GetComponent<CharacterStats>().TakeDamage(damage);
}Bước 5 — bộ não biết tự tìm đường
Phần đuổi vẫn là một đường thẳng. Đặt một khối giữa kẻ địch và Knight là nó đâm vào sườn khối rồi đứng lì đó, cứ ẩn tới trước vào bức tường nó không bao giờ qua được. Nó không đi vòng, vì chẳng ai bảo nó rằng có đường vòng.
Unity tính hộ nó đường đi được. Bake một lần phần sàn đi được, rồi NavMeshAgent cứ theo tấm bản đồ ấy: vòng qua khối, lách qua khe, tới chỗ bạn.
- 1
Mở . Không thấy tab đó thì cài gói AI Navigation trong .
- 2
Chọn Floor và object cha Blocks, tick Navigation Static ở góc trên bên phải Inspector. Đó là cách nói 'cái này thuộc về bản đồ'.
- 3
Trong cửa sổ Navigation, tab Bake → Bake. Một lớp xanh phủ lên sàn và dừng lại ở các khối — chỗ xanh đó là mọi nơi kẻ địch được phép đi.
- 4
Trên prefab Enemy, gỡ Character Controller và thêm Nav Mesh Agent. Giờ agent lo việc di chuyển, nên mấy dòng cc.Move cũ đi theo luôn.
- 5
Đặt Radius của agent khoảng 0.4 và Height 2 để nó lách vừa giữa hai khối thay vì từ chối cái khe đó.
using UnityEngine;
using UnityEngine.AI; // NavMeshAgent nằm ở đây
[RequireComponent(typeof(NavMeshAgent))]
public class EnemyBrain : MonoBehaviour
{
public Transform target;
public float aggroRange = 9f;
public float attackRange = 1.8f;
public float speed = 2.6f;
NavMeshAgent agent;
void Awake()
{
agent = GetComponent<NavMeshAgent>();
agent.speed = speed;
agent.stoppingDistance = attackRange * 0.9f; // dừng trước khi dí sát bạn
}
void Update()
{
if (target == null) return;
Vector3 flat = target.position - transform.position;
flat.y = 0f;
float distance = flat.magnitude;
if (distance > aggroRange)
{
agent.ResetPath(); // hết hứng
return;
}
if (distance > attackRange)
agent.SetDestination(target.position); // nó tự tính đường đi
else
agent.ResetPath(); // đủ gần rồi: đứng lại mà vung
}
}SetDestination là toàn bộ yêu cầu: bạn đưa một điểm, nó tìm đường. Bạn không bao giờ phải tự viết thuật toán tìm đường.
stoppingDistance giữ cho agent không dí sát vào người bạn rồi mới vung, và nó thay cho phép đo khoảng cách từng dùng để dừng việc đuổi.
Trọng lực biến mất khỏi script vì agent tự bám lấy mặt đã bake. Agent không thể rơi khỏi bản đồ, và đó cũng là lý do nó không leo theo bạn lên khối.
Giữ nguyên CanSee và CanReach. Tìm đường quyết định nó tới chỗ bạn bằng cách nào; còn hai hàm kia vẫn quyết định nó có được phép đánh trúng bạn hay không.
✖ Chasing with transform.position +=
Đuổi bằng transform.position += · đi xuyên khối và tường
✓ Chasing with CharacterController.Move
Đuổi bằng CharacterController.Move · va chạm như người chơi