Hiển thị các bài đăng có nhãn Chương trình AI. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn Chương trình AI. Hiển thị tất cả bài đăng

Thứ Hai, 28 tháng 4, 2014

AI 2.3 - FSM FRAMEWORK


  2.3 - FSM FRAMEWORK - Khuôn mẫu FSM

Trong bài viết cuối của AI 2, chúng ta sẽ tìm hiểu về 2 FSM khác nhau đã có sẵn trong thư mục Scenes là AdvanceFSM và SimpleFSM để hiểu rõ hơn về cấu trúc cũng như sự khác biệt nhau.


Lớp AdvanceFSM

Lớp AdvanceFSM cơ bản dùng để quản lý tất cả các FSMState đã được cung cấp và giữ quá trình cập nhật các chuyển đổi với trạng thái hiện tại. Vì vậy, việc đầu tiên trước khi sử dụng sườn máy trạng thái có sẵn chính là khai báo các chuyển đổi và trạng thái mà chúng ta thực hiện cho các xe tăng AI.

Code trong file C# AdvancedFSM được viết như sau:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public enum Transition
{
    None = 0,
    SawPlayer,
    ReachPlayer,
    LostPlayer,
    NoHealth,
}

public enum FSMStateID
{
    None = 0,
    Patrolling,
    Chasing,
    Attacking,
    Dead,
}


Ngoài ra còn có danh sách các đối tượng FSMState và 2 biến cục bộ để lưu trữ ID hiện tại của lớp FSMState cũng như trạng thái hiện tại của FSMState:

private List<FSMState> fsmStates;

    //The fsmStates are not changing directly but updated by using transitions
    private FSMStateID currentStateID;
    public FSMStateID CurrentStateID {
        get {
           return currentStateID;
        }
    }


Hàm AddFSMState và DeleteState có công dụng thêm và xóa tình trạng của lớp FSM trong danh sách riêng biệt của chúng ta. Khi hàm PerformTransition được gọi, biến CurrentState sẽ được cập nhật với trạng thái mới tùy theo sự chuyển đổi.


Lớp FSMState

Lớp FSMState quản lý các chuyển đổi sang các trạng thái khác. Nó có từ điển được gọi là map để lưu trữ các cặp giá trị khóa chính của các sự chuyển đổi và các trạng thái. Vi dụ: SawPlayer vạch ra những chuyển đổi đến trạng thái rượt đuổi - Chasing, còn LostPlayer liên kết với trạng thái đang tuần tra - Patrolling và vân vân...

Code trong file C# FSMState như sau:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public abstract class FSMState
{
        protected Dictionary<Transition, FSMStateID> map = new
        Dictionary<Transition, FSMStateID>();
...


Hai hàm AddTransitiona và DeleteTransition thực hiện công việc thêm và xóa các chuyển đổi từ từ điển trạng thái chuyển đổi đối tượng - map. Hàm GetOutputState tìm các đối tượng trong map và trả về trạng thái dựa theo các chuyển đổi được nhận.

Lớp FSMState còn 2 hàm cơ sở để các lớp con cần thực hiện. Các hàm đó như sau:

...
public abstract void Reason(Transform player, Transform npc);
public abstract void Act(Transform player, Transform npc);
...


Hàm Reason kiểm tra khi trạng thái cần phải chuyển đổi đến trạng thái khác. Và hàm Act thực hiện nhiệm vụ thi hành những công việc cho biến currentState như di chuyển tới trước điểm mốc và sau đó rượt và tấn công người chơi. Cả hai hàm yêu cầu di chuyển dữ liệu của người chơi và NPC (Non Player Character - Nhân vật máy điều khiển), tất cả đều được lớp này xử lý.


Các lớp trạng thái

Không giống như trong ví dụ về SimpleFSM, các trạng thái của xe tăng AI được viết thành các lớp (file script) riêng biệt nhau liên kết đến lớp FSMState như AttackState, ChaseState, DeadState, và PatrolState, mỗi cái với sự thực hiện của 2 hàm Reason và Act. Hãy xem qua file C# PatrolState như một ví dụ điển hình.


Lớp PatrolState

Lớp này bao gồm 3 phần chính: Hàm tạo (Constructor), hàm Reason và hàm Act.

Code trong file C# PatrolState như sau:

using UnityEngine;
using System.Collections;

public class PatrolState : FSMState
{
    public PatrolState(Transform[] wp)
    {
        waypoints = wp;
        stateID = FSMStateID.Patrolling;

        curRotSpeed = 1.0f;
        curSpeed = 100.0f;
    }

    public override void Reason(Transform player, Transform npc)
    {
        //Check the distance with player tank
        //When the distance is near, transition to chase state
        if (Vector3.Distance(npc.position, player.position) <= 300.0f)
        {
            Debug.Log("Switch to Chase State");
            npc.GetComponent<NPCTankController>().SetTransition(Transition.SawPlayer);
        }
    }

    public override void Act(Transform player, Transform npc)
    {
        //Find another random patrol point if the current point is reached
       
        if (Vector3.Distance(npc.position, destPos) <= 100.0f)
        {
            Debug.Log("Reached to the destination point\ncalculating the next point");
            FindNextPoint();
        }

        //Rotate to the target point
        Quaternion targetRotation = Quaternion.LookRotation(destPos - npc.position);
        npc.rotation = Quaternion.Slerp(npc.rotation, targetRotation, Time.deltaTime * curRotSpeed);

        //Go Forward
        npc.Translate(Vector3.forward * Time.deltaTime * curSpeed);
    }
}


Hàm tạo lấy giá trị từ mảng waypoints và lưu chúng vào mảng cục bộ và khi đó nó được khởi tạo các thuộc tính như di chuyển và tốc độ xoay. Hàm Reason kiểm tra khoảng cách của xe tăng AI với xe tăng của người chơi. Nếu xe tăng của người chơi trong tầm, nó sẽ thiết lập transition ID thành SawPlayer bằng cách sử dụng hàm SetTransition của lớp NPCTankController theo code trong file C# NPCTankController như sau:

  public void SetTransition(Transition t)
  {
        PerformTransition(t);
  }


Nó được bao bởi hàm PerformTransition của lớp AdvanceFSM. Hàm này sẽ cập nhật biến trạng thái hiện hành - CurrentState, với hàm nhận trách nhiệm chuyển đổi, sử dụng đối tượng Transition, và từ điển trạng thái chuyển đổi đối tượng - map từ lớp FSMState. Hàm Act cập nhật điểm mốc của xe tăng AI, xoay xe tăng và di chuyển thẳng đến điểm mốc đó. Các lớp trạng thái khác cũng dựa theo mẫu này với các reason và act khác nhau. Chúng ta đã được thấy chúng qua bài viết trước Máy trạng thái - FSM đơn giản, vì vậy mình sẽ không nói lại ở bài này.


Lớp NPCTankController

Xe tăng AI, lớp NPCTankController sẽ liên kết đến lớp AdvanceFSM. Đây là cách chúng ta thiết lập các trạng thái cho các xe tăng máy.

  private void ConstructFSM()
  {
        ...


        PatrolState patrol = new PatrolState(waypoints);
        patrol.AddTransition(Transition.SawPlayer, FSMStateID.Chasing);
        patrol.AddTransition(Transition.NoHealth, FSMStateID.Dead);

        ChaseState chase = new ChaseState(waypoints);
        chase.AddTransition(Transition.LostPlayer, FSMStateID.Patrolling);
        chase.AddTransition(Transition.ReachPlayer, FSMStateID.Attacking);
        chase.AddTransition(Transition.NoHealth, FSMStateID.Dead);

        AttackState attack = new AttackState(waypoints);
        attack.AddTransition(Transition.LostPlayer, FSMStateID.Patrolling);
        attack.AddTransition(Transition.SawPlayer, FSMStateID.Chasing);
        attack.AddTransition(Transition.NoHealth, FSMStateID.Dead);

        DeadState dead = new DeadState();
        dead.AddTransition(Transition.NoHealth, FSMStateID.Dead);

        AddFSMState(patrol);
        AddFSMState(chase);
        AddFSMState(attack);
        AddFSMState(dead);
  }


Đây là cái đẹp của việc sử dụng FSM framework. Khi các trạng thái tự quản lý mà không cần các lớp riêng, lớp NPCTankController chỉ cần gọi hàm Reaso và Act của trạng thái hiện tại đang được kích hoạt. Điều này loại trừ việc bạn phải viết một danh sách dài về các câu lệnh điều kiện if / else và switch. Thay vào đó, giờ đây các trạng thái được gói gọn đẹp đẽ vào các lớp riêng biệt, làm cho code dễ quản lý hơn về số lượng trạng thái để thực hiện cũng như các chuyển đổi giữa chúng dù là các project lớn ngày càng phức tạp hơn.

  protected override void FSMFixedUpdate()
  {
        CurrentState.Reason(playerTransform, transform);
        CurrentState.Act(playerTransform, transform);
  }



Và đó là các công việc của khuôn mẫu FSM mà chúng ta vừa tham khảo. Trong phần tổng quát, các bước chính để sử dụng khuôn mẫu này như sau:
  1. Khai báo các chuyển đổi và các trạng thái trong lớp AdvanceFSM.
  2. Viết các lớp trạng thái liên kết đến lớp FSMState, và bổ sung các hàm Reason và Act.
  3. Viết lớp tùy chỉnh NPC AI liên kết đến lớp AdvanceFSM.
  4. Tạo các lớp trạng thái, và thêm các chuyển đổi và cặp trạng thái sử dụng hàm AddTransition của lớp FSMState.
  5. Thêm các trạng thái đã tạo vào danh sách trong lớp AdvanceFSM bằng cách sử dụng hàm AddFSMState.
  6. Gọi hàm Reason và Act của biến CurrentState trong mỗi chu trình chạy game.

Bạn có thể vọc thêm scene AdvancedFSM. Nó sẽ vận hành tương tự như SimpleFSM. Nhưng code và các lớp sẽ có tổ chức và dễ quản lý hơn.


Tổng Kết AI 2.

Trong phần 2 này, chúng ta đã học được cách vận hành của máy trạng thái trong Unity3D dựa vào game bắn xe tăng đơn giản. Chúng ta đã hiểu sơ qua cách làm việc của FSM bằng cách đơn giản nhất, sử dụng hàm switch. Và chúng ta đã học cách để sử dụng khuôn mẫu để làm AI vận hành một cách dễ quản lý và dễ mở rộng hơn.

Trong phần tiếp theo là AI 3., chúng ta sẽ tìm hiểu về xác xuất ngẫu nhiên và cách chúng ta sẽ sử dụng nó để làm cho game của chúng ta trở nên không thể đoán trước được.

Chủ Nhật, 9 tháng 3, 2014

AI 2.2 - MÁY TRẠNG THÁI - FSM ĐƠN GIẢN

  2.2 - Máy trạng thái - FSM đơn giản

Bài viết này, chúng ta sẽ cùng nghiên cứu các phương thức hoạt động cũng như tính năng chiến đấu của các xe tăng AI trong scene SimpleFSM. Bạn có thể tìm thấy scene này trong source code đã cung cấp ở bài viết AI 2.1.

Thiết lập waypoint

Tiếp theo, chúng ta sẽ đặt bốn object Cube vào các vị trí ngẫu nhiên, đó chính là các điểm mốc mà xe của AI sẽ chạy đến theo thứ tự ngẫu nhiên, tên của những điểm này được gom chung lại thành WandarPoints.

WanderPoints
Đây là thuộc tính của đối tượng WanderPoint :

WanderPoint properties
Một điều cần lưu ý ở đây, chính là việc cần đặt tag cho những điểm đó với tên là WandarPoint. Chúng ta sẽ liên kết các tag này lại khi chúng ta thiết lập các điểm mốc cho xe tăng AI. Như bạn có thể thấy các thuộc tính của nó, một điểm mốc ở đây chỉ là một Cube Object đã bị vô hiệu hóa Mesh Renderer, và đối tượng Box Collider bị loại bỏ. Thậm chí chúng ta có thể dùng một empty object cũng được, tất cả những gì chúng ta cần từ một điểm mốc chính là vị trí cùng dữ liệu về sự biến đổi của nó. Nhưng chúng ta đang dùng Cube objects ở đây, để chúng ta có thể dễ nhìn thấy các điểm mốc khi làm việc.

Lớp FSM ảo

Tiếp theo, chúng ta sẽ thực thi một lớp ảo, định nghĩa các phương thức xe tăng AI của địch phải thực thi.

Code trong file FSM.cs như sau :

using UnityEngine;
using System.Collections;
public class FSM : MonoBehaviour
{
    //Player Transform
    protected Transform playerTransform;
    //Next destination position of the NPC Tank
    protected Vector3 destPos;
    //List of points for patrolling
    protected GameObject[] pointList;
    //Bullet shooting rate
    protected float shootRate;
    protected float elapsedTime;
    //Tank Turret
    public Transform turret { get; set; }
    public Transform bulletSpawnPoint { get; set; }
    protected virtual void Initialize() { }
    protected virtual void FSMUpdate() { }
    protected virtual void FSMFixedUpdate() { }
    // Use this for initialization
    void Start ()
    {
      Initialize();
    }
   
    // Update is called once per frame
    void Update ()
    {
      FSMUpdate();
    }
    void FixedUpdate()
    {
      FSMFixedUpdate();
    }
}

Tất cả các xe tăng của địch cần biết vị trí xe tăng của người chơi, điểm đến tiếp theo của chúng, và danh sách của các điểm mốc để chọn lựa hướng đi, khi chúng đang tuần tra. Một khi xe tăng của người chơi bước vào phạm vi, chúng sẽ xoay đối tượng turret (tháp pháo) và rồi bắt đầu bắn từ điểm viên đạn xuất phát với tốc độ bắn của chúng.

Các lớp kế thừa cũng cần thực thi ba hàm : Initialize, FSMUpdate, và FSMFixedUpdate. Đó chính là các lớp ảo, mà xe tăng AI của chúng ta đang thực thi.

Xe tăng AI của địch

Bây giờ hãy nhìn vào đoạn code thực sự cho các xe tăng AI của chúng ta. Lớp SimpleFSM kế thừa từ lớp ảo FSM.

Đoạn code nằm trong file SimpleFSM.cs như sau:

using UnityEngine;
using System.Collections;
public class SimpleFSM : FSM
{
    public enum FSMState
    {
      None,
      Patrol,
      Chase,
      Attack,
      Dead,
    }
    //Current state that the NPC is reaching
    public FSMState curState;
    //Speed of the tank
    private float curSpeed;
    //Tank Rotation Speed
    private float curRotSpeed;
    //Bullet
    public GameObject Bullet;
    //Whether the NPC is destroyed or not
    private bool bDead;
    private int health;

Ở đây, chúng ta đang khai báo vài biến mới. Xe tăng AI của chúng ta sẽ có bốn trạng thái khác nhau : Patrol, Chase, Attack, và Dead. Cơ bản chúng ta đang thực thi FSM được miêu tả như trong ví dụ ở AI 1.1 - Giới thiệu về AI.


FSM của xe tăng AI của địch

Trong hàm Initialize, chúng ta thiết lập các thuộc tính của xe tăng AI bằng các giá trị mặc định. Rồi chúng ta lưu lại các vị trí của các điểm mốc trong các biến cục bộ. Chúng ta lấy các điểm mốc đó từ scene bằng cách dùng hàm FindGameObjectsWithTag, cố tìm các đối tượng đó bằng tag WandarPoint.

    //Initialize the Finite state machine for the NPC tank
    protected override void Initialize ()
    {
      curState = FSMState.Patrol;
      curSpeed = 150.0f;
      curRotSpeed = 2.0f;
      bDead = false;
      elapsedTime = 0.0f;
      shootRate = 3.0f;
      health = 100;
      //Get the list of points
      pointList =
      GameObject.FindGameObjectsWithTag("WandarPoint");
      //Set Random destination point first
      FindNextPoint();
      //Get the target enemy(Player)
      GameObject objPlayer =
      GameObject.FindGameObjectWithTag("Player");
      playerTransform = objPlayer.transform;
      if (!playerTransform)
        print("Player doesn't exist.. Please add one "+
        "with Tag named 'Player'");
        //Get the turret of the tank
        turret = gameObject.transform.GetChild(0).transform;
        bulletSpawnPoint = turret.GetChild(0).transform;
      }

Hàm update của chúng ta được thực thi mỗi khung hình như sau :

    //Update each frame
    protected override void FSMUpdate()
    {
      switch (curState)
      {
        case FSMState.Patrol: UpdatePatrolState(); break;
        case FSMState.Chase: UpdateChaseState(); break;
        case FSMState.Attack: UpdateAttackState(); break;
        case FSMState.Dead: UpdateDeadState(); break;
      }
     //Update the time
     elapsedTime += Time.deltaTime;
     //Go to dead state is no health left
     if (health <= 0)
      curState = FSMState.Dead;
    }

Chúng ta kiểm tra tình trạng hiện hành, và rồi gọi hàm trạng thái thích hợp. Một khi đối tượng health trở về 0 hoặc âm, chúng ta cho xe tăng trở về trạng thái Dead.

Trạng thái tuần tra (Patrol)


Khi xe tăng của chúng ta đang ở trạng thái Patrol, chúng ta kiểm tra nó có hay không đã đến được điểm đích. Nếu đúng, nó sẽ tìm ra điểm đích tiếp theo. Cơ bản phương thức FindNextPoint chọn điểm đích tiếp theo một cách ngẫu nhiên từ các điểm mốc xác định. Nếu vẫn nằm trên con đường đến điểm đích hiện tại, nó sẽ kiểm tra khoảng cách đến xe tăng của người chơi. Nếu xe tăng người chơi vẫn còn ở trong phạm vị (ở đây là 300), nó sẽ đổi sang trạng thái Chase. Các đoạn code còn lại chỉ là việc chuyển hướng và di chuyển về trước của xe tăng.

    protected void UpdatePatrolState()
    {
      //Find another random patrol point if the current
      //point is reached
      if (Vector3.Distance(transform.position, destPos) <=
      100.0f)
      {
        print("Reached to the destination point\n"+
        "calculating the next point");
        FindNextPoint();
      }
      //Check the distance with player tank
      //When the distance is near, transition to chase state
      else if (Vector3.Distance(transform.position,
      playerTransform.position) <= 300.0f)
      {
        print("Switch to Chase Position");
        curState = FSMState.Chase;
      }
      //Rotate to the target point
      Quaternion targetRotation =
      Quaternion.LookRotation(destPos
      - transform.position);
      transform.rotation =
      Quaternion.Slerp(transform.rotation,
      targetRotation, Time.deltaTime * curRotSpeed);
      //Go Forward
      transform.Translate(Vector3.forward * Time.deltaTime *
      curSpeed);
    }
    protected void FindNextPoint()
    {
      print("Finding next point");
      int rndIndex = Random.Range(0, pointList.Length);
      float rndRadius = 10.0f;
      Vector3 rndPosition = Vector3.zero;
      destPos = pointList[rndIndex].transform.position +
      rndPosition;
      //Check Range to decide the random point
      //as the same as before
      if (IsInCurrentRange(destPos))
      {
        rndPosition = new Vector3(Random.Range(-rndRadius,
        rndRadius), 0.0f, Random.Range(-rndRadius,
        rndRadius));
        destPos = pointList[rndIndex].transform.position +
        rndPosition;
      }
    }
    protected bool IsInCurrentRange(Vector3 pos)
    {
      float xPos = Mathf.Abs(pos.x - transform.position.x);
      float zPos = Mathf.Abs(pos.z - transform.position.z);
      if (xPos <= 50 && zPos <= 50)
        return true;
        return false;
    }

Trạng thái rượt đuổi (Chase)


Cũng tương tự, đầu tiên xe AI sẽ kiểm tra khoảng cách đến xe tăng người chơi. Nếu đủ gần, nó sẽ chuyển sang trạng thái Attack. Nếu xe tăng người chơi đã đi xa quá, nó sẽ trở lại trạng thái Patrol.

    protected void UpdateChaseState()
    {
      //Set the target position as the player position
      destPos = playerTransform.position;
      //Check the distance with player tank When
      //the distance is near, transition to attack state
      float dist = Vector3.Distance(transform.position,
      playerTransform.position);
      if (dist <= 200.0f)
      {
        curState = FSMState.Attack;
      }
      //Go back to patrol is it become too far
      else if (dist >= 300.0f)
      {
        curState = FSMState.Patrol;
      }
      //Go Forward
      transform.Translate(Vector3.forward * Time.deltaTime *
      curSpeed);
    }

Trạng thái tấn công (Attack)


Nếu xe tăng người chơi đủ gần để tấn công xe tăng AI của chúng ta, chúng sẽ xoay đối tượng tháp pháo turret về phía xe tăng của người chơi, và rồi nổ súng. Nó sẽ trở lại trạng thái Patrol, nếu xe tăng người chơi đã ra khỏi phạm vi.

    protected void UpdateAttackState()
    {
      //Set the target position as the player position
      destPos = playerTransform.position;
      //Check the distance with the player tank
      float dist = Vector3.Distance(transform.position,
      playerTransform.position);
      if (dist >= 200.0f && dist < 300.0f)
      {
        //Rotate to the target point
        Quaternion targetRotation =
        Quaternion.LookRotation(destPos -
        transform.position);
        transform.rotation = Quaternion.Slerp(
        transform.rotation, targetRotation,
        Time.deltaTime * curRotSpeed); 
        //Go Forward
        transform.Translate(Vector3.forward *
        Time.deltaTime * curSpeed);
        curState = FSMState.Attack;
      }
      //Transition to patrol is the tank become too far
      else if (dist >= 300.0f)
      {
        curState = FSMState.Patrol;
      }
      //Always Turn the turret towards the player
      Quaternion turretRotation =
      Quaternion.LookRotation(destPos
      - turret.position);
      turret.rotation =
      Quaternion.Slerp(turret.rotation, turretRotation,
      Time.deltaTime * curRotSpeed);
      //Shoot the bullets
      ShootBullet();
    }
    private void ShootBullet()
    {
      if (elapsedTime >= shootRate)
      {
        //Shoot the bullet
        Instantiate(Bullet, bulletSpawnPoint.position,
        bulletSpawnPoint.rotation);
        elapsedTime = 0.0f;
      }
    }

Trạng thái chết (Dead)

Nếu xe tăng đã đến trạng thái Dead, chúng ta sẽ bật trạng thái chết và làm nó nổ tung.

    protected void UpdateDeadState()
    {
      //Show the dead animation with some physics effects
      if (!bDead)
      {
        bDead = true;
        Explode();
      }
    }

Nổ tung (Explode)



Đây là một hàm sẽ mang đến một hiệu ứng nổ đẹp. Chúng ta chỉ áp dụng một ExplosionForce vào thành phần rigidbody với các hướng ngẫu nhiên, có trong đoạn code sau đây:

    protected void Explode()
    {
      float rndX = Random.Range(10.0f, 30.0f);
      float rndZ = Random.Range(10.0f, 30.0f);
      for (int i = 0; i < 3; i++)
      {
        rigidbody.AddExplosionForce(10000.0f,
        transform.position - new Vector3(rndX, 10.0f,
        rndZ), 40.0f, 10.0f);
        rigidbody.velocity = transform.TransformDirection(
        new Vector3(rndX, 20.0f, rndZ));
      }
      Destroy(gameObject, 1.5f);

    }