1. 程式人生 > >Unity簡單有限狀態機實現

Unity簡單有限狀態機實現

前言】 本篇來說一下關於簡單有限狀態機的演算法實現,當然我們的幾個狀態會比較簡單,而且本身我也是處於入門狀態,所以當成一個簡單的知識積累。有限狀態機,顧名思義,狀態是有限個的,而且狀態之間是關聯的,本篇寫的狀態機,其實是類似寫遊戲裡面的AI機器人,就是那些遊戲裡面的怪啊,npc啊,本篇也是針對幾個行為或者是狀態,進行設計,編寫相關指令碼。
1.新建·一個專案,再資源商店搜尋Zombie,匯入敵人的模型和動畫,搜尋Yurowm匯入玩家的模型和動畫,至於場景,搜尋_Barking_Dog,匯入場景 在這裡插入圖片描述
2.新建之後,先把這個放一邊,首先我們來分析怪物狀態以及過渡條件
如下圖:
在這裡插入圖片描述
3.根據以上簡單的集中關係組合,我們來進行設計。首先新建一個資料夾,取名Animators,建立一個動畫控制器,把Zombie資料夾下面的動畫idle,attack,walk拉進動畫控制器,設定以下關係以及新建引數。至於動畫之間的過渡,idle過渡到walk和attack,就設定相應的為true就可以了,反之回到idle設定為false,而walk到attack或者是attack到walk,則設定目標為true,自身為false。這樣,簡單的控制器就完成了。
在這裡插入圖片描述


4.分析敵人獲取主角的演算法,這裡我採用的是獲取角度獲取視野的方式,當然這個不限於這種簡單的方式,我這裡就簡單用一下這種演算法。獲取主角的原理如下,很簡單:
在這裡插入圖片描述
判斷角度a即可,然後判斷距離,獲取是否在攻擊範圍之內。
5。還有一個,在移動的方式上,我選擇了unity自帶的AI尋路移動,所以不可避免地要在Zombie的模型上新增一個Nav Mesh Agent元件,到時候直接呼叫元件移動即可。把_Barking_Dog檔案下的Test_Map拉進場景,然後把_Level和攝像機拉進我們自己的Scene,當然原來的攝像機和燈光要刪除。檢查一下_Level下面的子物體Floor_01是否都設定為靜態,如果是就可以開始烘焙尋路了。
6.為Zombie新增Animator元件,把控制器賦上,然後在資源庫裡面找到Yurowm的模型,拉進場景裡面,取名Player,設定標籤為Player。
7.接下來上指令碼,新建一個指令碼為FsmDemo,賦給敵人,解釋在腳本里面:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public enum EnemyState//狀態列舉
{
Patrol,//巡邏
Attack,//攻擊
Rest,//休息
Chase//追擊

}
public class FsmDemo : MonoBehaviour {

    public EnemyState enemyState;
    private Transform Player;//主角

    public float EnemyRestTime = 3;//控制休息時間
    private float RestTime;//休息時間

    private Animator EnemyAnimator;//動畫控制器

    public float MaxSightDistance = 10;//最大的視野範圍
    public float CanAttackDistance = 1;//攻擊範圍

    public Transform MoveTarget;//最終要移動到的目標
    public Transform point;//路點父物體
    public List<Transform> Points = new List<Transform>();//路點的連結串列陣列

    private NavMeshAgent m_navmeshment;//尋路元件

    private int index = 0;//路點索引
    public bool canmove = true;//是否可以移動
    void Start() {
        m_navmeshment = GetComponent<NavMeshAgent>();//獲取尋路元件
        RestTime = EnemyRestTime;//初始化休息時間
        EnemyAnimator = GetComponent<Animator>();//獲取動畫控制器
        Player = GameObject.FindWithTag("Player").transform;//獲取主角
        enemyState = EnemyState.Rest;//初始化狀態

        for (int i = 0; i < point.childCount; i++)//獲取父物體下面的路點
        {
            Points.Add(point.GetChild(i));
        }
        MoveTarget = Points[0];//初始化移動目標
        if (m_navmeshment == null || Player == null || EnemyAnimator == null)//以防出錯
        {
            Debug.LogError("未新增完整元件或者是標籤");
            enabled = false;
            return;
        }
      
	}
	
	// Update is called once per frame
	void Update () {

        DealWithEnemyState();
        //Debug.DrawLine(transform.position, transform.forward, Color.red);
        //Debug.DrawLine(transform.position, Player.position - transform.position, Color.red);
	}
    private void DealWithEnemyState()//處理狀態的方法
    {
        switch (enemyState)
        {
            case EnemyState.Patrol://巡邏
            
                OnPatrolState();
                ChangePatrolToOtherState();

                break;
            case EnemyState.Attack://攻擊
                OnAttackState();
                ChangeAttackToOtherState();

                break;
            case EnemyState.Rest://休息
                OnRestState();
                ChangeRestToOtherState();
                break;
            case EnemyState.Chase://追擊
                OnChaseState();
                ChangeChaseToOtherState();
                break;
            default:
                break;
        }
    }
    private void OnRestState()//處理休息
    {
        RestTime -= Time.deltaTime;//休息時間倒計時
        EnemyAnimator.SetBool("walk", false);
        EnemyAnimator.SetBool("attack", false);//播放動畫
        m_navmeshment.speed = 0;//移動速度設定為0
      
    }
    private void OnAttackState()//處理攻擊
    {
        
        m_navmeshment.speed = 0;//速度設定為0
        EnemyAnimator.SetBool("attack", true);
        EnemyAnimator.SetBool("walk",false);//播放動畫
        
    }
    private void OnChaseState()//處理追擊
    {
      
        EnemyAnimator.SetBool("walk", true);
        EnemyAnimator.SetBool("attack", false);//播放動畫
        MoveTarget = Player;//設定目標位Player
        m_navmeshment.speed = 0.1f;//設定移動速度
        MoveToTarget(MoveTarget);//開始移動追擊
    }

 
    private void OnPatrolState()//處理巡邏
    {
        //移動到目標點,進行位移
        m_navmeshment.speed = 0.1f;
       
        if (canmove)
        {
            MoveToTarget(MoveTarget);
        }
        EnemyAnimator.SetBool("walk", true);
        EnemyAnimator.SetBool("attack", false);
        //播放移動動畫
    }
    private void ChangeRestToOtherState()//休息狀態到別的狀態
    {
        
        if (IsOnSightOfEnemy())//在視野範圍內
        {
            MoveTarget = Player;   //設定目標MoveToTarget
            print("在視野內");
            if (IsOnAttackDistance())//在攻擊範圍內
            {
                print("在攻擊範圍內");
                enemyState = EnemyState.Attack;//改變狀態
            }
            else
            {
                print("不在攻擊範圍內");
                enemyState = EnemyState.Chase;
            }
        }
        else//不在視野範圍內
        //判斷倒計時,且目標點還是巡邏點
        {
            //設定目標巡邏點
            if (RestTime <= 0)//倒計時結束
            {
                RestTime = EnemyRestTime;//重置倒計時時間
                canmove = true;//可以移動
                if (index == Points.Count - 1)//迴圈路點
                {
                    index = 0;
                }
                else
                {
                    index++;
                }
                MoveTarget = Points[index];
                enemyState = EnemyState.Patrol;//改變狀態

            }
            else//倒計時未結束,不能移動
            {
                canmove = false;
            }
        }
    }
    private void ChangeAttackToOtherState()
    {
        //時刻監視狀態
        if (IsOnSightOfEnemy())
        {
            MoveTarget = Player;   //設定目標MoveToTarget
            print("在視野內");
            if (IsOnAttackDistance())
            {
                print("在攻擊範圍內");
                enemyState = EnemyState.Attack;
            }
            else
            {
                print("不在攻擊範圍內");
                enemyState = EnemyState.Chase;
            }
        }
        else
        {
            enemyState = EnemyState.Patrol;//不在視野內,也就是說主角逃走了,設定為巡邏
        }
        //判斷血量
        //判斷目標
    }//攻擊到別的狀態
    private void ChangeChaseToOtherState()
    {
        if (IsOnSightOfEnemy())//在視野內
        {
            MoveTarget = Player;   //設定目標MoveToTarget
            print("在視野內");
            if (IsOnAttackDistance())
            {
                print("在攻擊範圍內");
                enemyState = EnemyState.Attack;
            }
            else
            {
                print("不在攻擊範圍內");
                enemyState = EnemyState.Chase;
            }
        }
        else
        {
            MoveTarget = Points[index];
            enemyState = EnemyState.Patrol;

        }
    }//追擊到別的狀態
    private void ChangePatrolToOtherState()
    {
        //監測狀態
        if (IsOnSightOfEnemy())
        {
            MoveTarget = Player;   //設定目標MoveToTarget
            print("在視野內");
            if (IsOnAttackDistance())
            {
                print("在攻擊範圍內");
                enemyState = EnemyState.Attack;
            }
            else
            {
                print("不在攻擊範圍內");
                enemyState = EnemyState.Chase;
            }
        }
        else
        {
        
            float dis = Vector3.Distance(transform.position, MoveTarget.position);
            if (dis < 0.5f)
            {
                enemyState = EnemyState.Rest;
            }
        }
        //如果不在視野內,目標還是巡邏點
    }//巡邏到別的狀態
    private bool IsOnSightOfEnemy()//是否在可視範圍內
    {
        bool onsight=false;
        float dis = Vector3.Distance(Player.position, transform.position);
        float angle = Vector3.Angle(transform.forward, Player.position - transform.position);
        if (dis < MaxSightDistance)
        {
            if (angle < 70)
            {
                onsight = true;
            }
        }
        return onsight;
          
      
    }
    private bool IsOnAttackDistance()//判斷是否在攻擊範圍內的方法
    {
        if (IsOnSightOfEnemy())
        {
            float dis = Vector3.Distance(Player.position, transform.position);
            if (dis < CanAttackDistance)
            {
                return true;
            }
        }
        return false;
    }
    private void MoveToTarget(Transform target)//移動的方法
    {
        m_navmeshment.SetDestination(target.transform.position);
    }

}

8.回到Unity中,設定巡邏路點,我這裡設定3個點,其實設定多少個都可以,演算法裡會進行迴圈執行,如下圖
在這裡插入圖片描述
9.把父路點賦給FsmDemo腳本里面的point,調整Zombie的位置,執行。主角在這裡沒有進行指令碼控制,所以你可以在Scene面板中移動主角進行測試。
10.總結:其實總結下來,這種簡單的有限狀態機實現起來很簡單,但是遇到複雜一點的狀態機,就要考慮優化的問題了。所以說,學習的時候要有很強的邏輯性,也要研究一下自己的實現方式,是用什麼模式實現,畢竟如果真是做專案,需要團隊合作,這一點還是要考慮的。