Make AI with State Design Pattern
Hôm nay đang trong thời gian chạy deadline nhập môn kỹ thuật phần mềm , cụ thể là con game "The Walker" của team tụi mình , mình cần phải làm một con AI cụ thể là con zombie với các chức năng : tìm đường , tự động tấn công trong phạm vi cố định , chuyển sang trạng thái đứng yên khi ông có người chơi đứng gần , vân vân mây mây.
Đây có lẽ là một tính năng rất bình thường , mọi chuyện vẫn suôn sẻ cho đến khi mình thêm 1 state mới và cứ như vậy code của mình hầu như phải sửa đi sửa lại nhìn rất không được clean theo phong cách OOP mà mình hướng đến
Trong phạm vi bài viết này mình sẽ nói cách làm ban đầu của mình và sự cải tiến khi dùng State Design Pattern
Cách giải quyết ban đầu
Mình nghĩ mọi người đều sẽ nghĩ rằng : tạo 1 enum cho zombie , và dùng if else để thay đổi trạng thái của nó ví dụ :
enum Status
{
Idle,
Chase,
}
IEnumerator AIZombie()
{
float dis = Vector3.Distance(transform.position, player.transform.position);
switch (WalkerStatus)
{
case Status.Idle:
agent.SetDestination(transform.position);
if (dis < DistanceToChase)
{
WalkerStatus = Status.Chase;
anim.SetBool("Chasing", true);
}
break;
case Status.Chase:
agent.SetDestination(player.transform.position);
if (dis > DistanceToChase)
{
WalkerStatus = Status.Idle;
anim.SetBool("Chasing", false);
}
break;
}
yield return new WaitForSeconds(0.5f);
StartCoroutine(AIZombie());
}
Với ví dụ trên , để đơn giản , mình chỉ tao 1 enum gồm 2 trạng thái Idle và Chase , DistanceToChase là khoảng cách lớn nhất từ nhân vật -> zombie để zombie bắt đầu rượt đuổi , agent là NavMeshAgent component tự tìm đường.
sau đó tạo một IEnumerator để update mỗi 0.5s . Nếu khoảng cách từ nhân vật -> zombie quá xa(vượt DistanceToChase) thì zombie sẽ đổi sang trạng thái Idle và ngược lại sẽ đi săn.
Thật ra cách ở trên cũng không phải quá tệ nhưng chuyện gì xảy ra nếu mình thêm nhiều hơn 2 state? Tưởng tượng ra những dòng code nhìn chắc chắc rất kém sang :)) , ngoài ra nếu bạn muốn add vài animation , add thêm sound , music thì những dòng code đó thực sự rất khó maintain và sau này đọc bạn lại sẽ nghĩ : "Mình đang viết cái đéo gì vậy?" . Mọi thứ chỉ diễn ra trong 1 hàm duy nhất nhìn cực kì xấu
State Design Pattern
Những vấn đề trên dẫn chúng ta đến với State Design Pattern (SDP) , SDP là một giải pháp giúp chúng ta đóng gói hành vi của đối tượng thông qua các interface. Hiểu nôm na là vậy bây giờ là cách thực hiện của chúng ta.
Ban đầu ta sẽ tạo 1 interface đại diện cho mỗi State và có hành động là DoSate().
public interface IEnemyState
{
IEnemyState DoState(Enemy enemy, Player player);
}
Với 2 tham số lưu các thuộc tính của enemy(zombie) và player(nhân vật của ta)
Sau đó , ta tạo các đối tượng đại diện cho mỗi state và implement interface IEnemyState
public class AttackState : IEnemyState
{
public IEnemyState DoState(Enemy enemy, Player player)
{
enemy.DoAttack();
float dis = Vector3.Distance(enemy.transform.position, player.transform.position);
if (dis < enemy.DistanceToChase)
{
return enemy.chase;
}
else
{
return enemy.idle;
}
}
public class IdleState : IEnemyState
{
public IEnemyState DoState(Enemy enemy, Player player)
{
enemy.DoIdle();
float dis = Vector3.Distance(enemy.transform.position, player.transform.position);
if (dis < enemy.DistanceToChase)
{
enemy.anim.SetBool("Chasing", true);
return enemy.chase;
}
else
{
enemy.anim.SetBool("Chasing", false);
return enemy.idle;
}
}
}
public class ChaseState : IEnemyState
{
public IEnemyState DoState(Enemy enemy, Player player)
{
enemy.DoChase();
float dis = Vector3.Distance(enemy.transform.position, player.transform.position);
if (dis > enemy.DistanceToChase)
{
enemy.anim.SetBool("Chasing", false);
return enemy.idle;
}
else
{
enemy.anim.SetBool("Chasing", true);
return enemy.chase;
}
}
}
Đồng thời ở script Enemy(Script chính quản lý các zombie) khai báo các đối tượng state:
public AttackState attack = new AttackState();
public ChaseState chase = new ChaseState();
public IdleState idle = new IdleState();
public IEnemyState currentState;
IEnumerator ZombieAI()
{
currentState = currentState.DoState(this , player);
yield return new WaitForSeconds(1f);
StartCoroutine(Couroutine_PathFiding);
}
Đoạn script trên có currentState là nơi ta sẽ lưu trạng thái hiện tại , tại mỗi trạng thái , nếu thay đổi sẽ thay đổi currentState và gọi hàm DoState trong mỗi state được tham chiếu đến
Hãy so sánh 2 hàm ZombieAI ban đầu và hàm ZombieAI sau khi dùng SDP :))
Final
State Design Pattern giúp bạn đóng gói code mình , dễ chỉnh sửa và duy trì nhưng không có nghĩa nó giúp bạn làm giảm số dòng code của mình :)) (tùy trường hợp) , nhiều lúc thậm chí nó dài hơn nhưng hãy nhìn về mục đích của chúng ta là dễ chỉnh sửa và bảo trì , đối với một lập trinh game , việc bảo trì hay update liên tục là điều cực kì quan trọng
Cuối cùng , mình hy vọng các bạn hiểu những gì mình truyền đạt trong bài viết này và khá tò mò những gì bạn nghĩ trong bài viết này , nếu bạn có cách để cái tiến điều này tốt hơn , đừng do dự mà hãy comment bên dưới bài viết.
Thanks for reading
Nhận xét
Đăng nhận xét