Unity進階開發-FSM有限狀態機

来源:https://www.cnblogs.com/CloudAndMist/archive/2023/10/28/17794640.html
-Advertisement-
Play Games

# Unity進階開發-FSM有限狀態機 前言 我們在進行開發時,到了一定程度上,會遇到數十種狀態,繼續使用Unity的Animator控制器會出現大量的bool,float類型的變數,而這些錯綜複雜的變數與Animatator控制器如同迷宮版連線相結合會變得極其的複雜且無法良好維護擴展,出現一個B ...


# Unity進階開發-FSM有限狀態機

前言

我們在進行開發時,到了一定程度上,會遇到數十種狀態,繼續使用Unity的Animator控制器會出現大量的bool,float類型的變數,而這些錯綜複雜的變數與Animatator控制器如同迷宮版連線相結合會變得極其的複雜且無法良好維護擴展,出現一個BUG會導致開發過程中開發者承受極大的精神力,而這時候,使用有限狀態機或者AI行為樹便成為了一個極佳的選擇,本文只記錄了有限狀態機的開發

使用有限狀態機進行狀態管理與切換可以大幅度的減少開發時候的難度,在開發過程中只需要關註各個狀態間的切換即可

圖示FSM工作過程:

我們可以看到,FSM的腳本一共分為兩大塊兒,一塊兒繼承自IState,這裡面保存著狀態內執行的行為,例如進入狀態,離開狀態,邏輯切換(用於判斷是否切換到其他狀態),PlayerState實現這個介面,並繼承ScriptableObject類,看到ScriptableObject我們就知道了PlayerState的作用了,ScriptableObject類的對象不依賴場景中的對象可,是獨立存在的數據文件。而在這裡,PlayState保存的是狀態的行為邏輯函數,這些行為邏輯函數的本質作用就是滿足條件就去執行相關行為。當然,在FSM中用一個例子來說明,當玩家處於跑步狀態時,PlayState文件發現滿足執行跑步行為的邏輯,就需要去播放跑步動畫。我們知道,ScriptableObject的數據是不會自己變化的,那麼所有判斷邏輯只能使用一遍,我們該如何解決這個問題,這就需要FSM第二的板塊兒的類來實現了

我們不難發現,StateMachine是繼承MonoBehaviour的,這就需要掛載到場景中了,並且也可以使用Update函數來時刻改變檢測到的信息,那麼,上文已經說了,PlayerState的數據不能主動變化,而StateMachine的數據可以進行變化,所以我們就可以通過一個函數來講StateMachine中檢測到數據來發給PlayerState,以此來實現PlayerState創建的數據文件可以連續進行邏輯判斷

好了,現在開始上代碼,用代碼繼續說

No.1 IState

public interface IState
{
    //進入狀態
    void Enter() { }
    //退出狀態
    void Exit() { }
    //狀態邏輯更新
    void LogicUpdate() { }
}

在這裡就是狀態內部需要執行的行為函數

No.2 StateMachine

// <summary>
/// 持有所有狀態類,並對他們進行管理和切換
/// 負責當前狀態的更新
/// </summary>
public class StateMachine : MonoBehaviour
{
    IState currentState;                                           //當前狀態
    public Dictionary<System.Type, IState> stateTable;             //字典,用於保存狀態以及查詢狀態,方便狀態切換時                                                                    //進行查找
    public void Update()
    {
        currentState.LogicUpdate();                                //執行每個狀態的邏輯切換函數,可以使狀態實現檢測                                                                    //變換,類似於Update內的函數實時接收信息
    }
    //切換狀態
    protected void SwitchOn(IState newState)
    {
        //當前狀態變為新狀態
        currentState = newState;
        //進入新狀態
        currentState.Enter();
    }
    //
    public void SwitchState(IState newState)
    {
        //退出狀態
        currentState.Exit();
        //進入新狀態
        SwitchOn(newState);
    }
    //切換狀態函數重載
    public void SwitchState(System.Type newStateType)
    {
        SwitchState(stateTable[newStateType]);                    //將字典內的狀態傳入
    }

}

No.3 PlayetState

public class PlayerState : ScriptableObject, IState
{
    /*************物理檢測*************/
    protected bool isGround;                          //是否在地面
    /*************基礎信息*************/
    protected bool isRun;                             //是否跑步
    protected bool isJump;                            //是否跳躍
    protected bool isIdle;                            //是否靜止
    /*************相關組件*************/
    protected Rigidbody2D my_Body2D;                  //剛體組件,用於獲取物體剛體屬性
    protected Animator animator;                      //動畫組件,用來播放動畫
    protected PlayerStateMachine stateMachine;        //PlayerStateMachine,玩家狀態機類,執行狀態間的切換
    public void Initiatize(Animator animator, Rigidbody2D my_Body2D, PlayerStateMachine stateMachine)
    {//獲取PlayerStateMachine傳遞進來的 動畫,剛體,狀態機類
        this.animator = animator;
        this.my_Body2D = my_Body2D;
        this.stateMachine = stateMachine;
    }
    public void PhysicalDetection(bool isGround)
    {
        this.isGround = isGround;
    }
    /// <summary>
    /// 狀態信息傳遞
    /// </summary>
    public void BasicInformation(bool isRun,bool isJump,bool isIdle)
    {
        this.isRun = isRun;
        this.isJump = isJump;
        this.isIdle = isIdle;
    }
    //進入狀態
    public virtual void Enter() { }
    //離開狀態
    public virtual void Exit() { }
    //邏輯切換
    public virtual void LogicUpdate() { }
}

No.4 PlayerStateMachine

/// <summary>
/// 玩家狀態機類
/// </summary>
public class PlayerStateMachine : StateMachine
{
    /*************************檢測信息***************************/
    PlayerPhysicalDetection playerPhysicalDetection;                      //物理檢測組件,這個是繼承                                                                                       //MonoBehaviour,掛載到玩家身上來檢測                                                                           //玩家的物理信息,例如是否位於地面
    /*************************狀態信息***************************/
    //PlayerState資源文件
    [SerializeField] PlayerState[] states;
    Animator animator;                            //獲取動畫組件
    Rigidbody2D my_Body2D;                        //獲取剛體組件
    void Awake()
    {
        /*************************物理檢測信息***************************/
        playerPhysicalDetection=GetComponent<PlayerPhysicalDetection>();            //獲取物理檢測組件

        /*************************狀態信息組件***************************/
        stateTable = new Dictionary<System.Type, IState>(states.Length);            //初始化字典
        animator = GetComponent<Animator>();                                        //獲取動畫組件
        my_Body2D = GetComponent<Rigidbody2D>();                                    //獲取剛體組件
        //迭代器迴圈獲取狀態
        foreach (PlayerState state in states)
        {
            state.Initiatize(animator, my_Body2D, this);//將動畫組件,剛體組件以及PlayerStateMachine傳入進去
            //狀態存入字典
            stateTable.Add(state.GetType(), state);
        }
    }
    private void Start()
    {//在開始時執行Idle,進入Idle狀態
        SwitchOn(stateTable[typeof(PlayerState_Idle)]);
    }
    private new void Update()
    {
        base.Update();//執行父類StateMachine的Update函數
        foreach (PlayerState state in states)
        {//將檢測信息傳入進去
            state.PhysicalDetection(playerPhysicalDetection.isGround);
            state.BasicInformation( isRun, isJump, isIdle);
        }
    }
}

No.5 PlayerState_Idle

[CreateAssetMenu(menuName = "StateMachine/PlayerState/Idle", fileName = "PlayerState_Idle")]//創建文件
public class PlayerState_Idle : PlayerState
{
    /*****************物理檢測*******************/
    public override void Enter()
    {
        //執行該狀態數據文件,首先執行進入狀態函數,在進入狀態函數執行相關的行為
        //進入狀態,預設播放Idle動畫
        animator.Play("PlayerIdle");
    }
    
    //邏輯切換函數,當檢測到處於某種狀態,立刻執行該狀態的數據文件
    public override void LogicUpdate()
    {
        if(isRun)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Run)]);
        }
        if(isJump)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Jump)]);
        }
        if(my_Body2D.velocity.y<0&&!isGround)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Fall)]);
        }
    }
}

No.6 PlayerState_Jump

[CreateAssetMenu(menuName = "StateMachine/PlayerState/Jump", fileName = "PlayerState_Jump")]//創建文件
public class PlayerState_Jump : PlayerState
{
    /*****************物理檢測*******************/
    public override void Enter()
    {
        //執行該狀態數據文件,首先執行進入狀態函數,在進入狀態函數執行相關的行為
        //進入狀態,預設播放Idle動畫
        animator.Play("PlayerJump");
    }
    
    //邏輯切換函數,當檢測到處於某種狀態,立刻執行該狀態的數據文件
    //該腳本中,跳躍後無法執行其它行為,若有需要可以添加判斷
    public override void LogicUpdate()
    {
        if(my_Body2D.velocity.y<0&&!isGround)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Fall)]);
        }
    }
}

No.7 PlayerState_Fall

[CreateAssetMenu(menuName = "StateMachine/PlayerState/Fall", fileName = "PlayerState_Fall")]//創建文件
public class PlayerState_Jump : PlayerState
{
    /*****************物理檢測*******************/
    public override void Enter()
    {
        //執行該狀態數據文件,首先執行進入狀態函數,在進入狀態函數執行相關的行為
        //進入狀態,預設播放Fall動畫
        animator.Play("PlayerFall");
    }
    
    //邏輯切換函數,當檢測到處於某種狀態,立刻執行該狀態的數據文件
    //該腳本中,掉落時無法執行其它行為,若有需要可以添加判斷
    public override void LogicUpdate()
    {
        if(isGround)
        {//落地進入Idle狀態
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Idle)]);
        }
    }
}

No.7 PlayerState_Run

[CreateAssetMenu(menuName = "StateMachine/PlayerState/Run", fileName = "PlayerState_Run")]//創建文件
public class PlayerState_Idle : PlayerState
{
    /*****************物理檢測*******************/
    public override void Enter()
    {
        //執行該狀態數據文件,首先執行進入狀態函數,在進入狀態函數執行相關的行為
        //進入狀態,預設播放Run動畫
        animator.Play("PlayerRun");
    }
    
    //邏輯切換函數,當檢測到處於某種狀態,立刻執行該狀態的數據文件
    public override void LogicUpdate()
    {
        if(isIdle)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Idle)]);
        }
        if(isJump)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Jump)]);
        }
        if(my_Body2D.velocity.y<0&&!isGround)
        {
            stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Fall)]);
        }
    }
}

以上便是一個簡單的狀態機

總結與拓展延伸

FSM可以大幅減少玩家動畫間的判斷和切換,只需要關註一個狀態到另一個狀態的切換條件,滿足條件就切換,不滿足就繼續執行

FSM在玩家上的使用並不明顯,因為在這上面,進入某種狀態後,我們只播放了動畫,並沒有執行其他行為。當我們用在怪物AI中會更加的方便,例如怪物處於巡邏狀態,播放巡邏動畫,執行巡邏的腳本文件(建立一個類,專門用來存怪物AI個狀態行為函數,通過傳遞的方式將該類對象傳遞給EnemyState,由EnemyState進行操作),怪物處於追擊玩家狀態,播放追擊動畫,並執行追擊玩家的行為函數,在怪物AI中,FSM的應用會更加輕鬆的管理怪物,怪物可以放技能了,就進入技能狀態,發現玩家就進入追擊玩家,一個條件一個行為,更加有利於FSM進行管理


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一.數組 數組的定義:相同類型的數據集合 go語言中數組的索引從0開始 沒有賦值的數值型數組,預設值為0 數組一旦被創建,它的大小就是不可改變的 (1)聲明數組與列印 var 變數名 [大小]變數類型 //數組的聲明 var nums [4]int //數組的賦值 nums[0] = 1 nums[ ...
  • 對於日常開發者來講很少會使用到本章節的內容,但是對框架作者等是必備知識,同時也是高頻的面試常見問題。 1.線程安全 線程安全是多線程或多進程編程中的一個概念,在擁有共用數據的多條線程並行執行的程式中,線程安全的代碼會通過同步機制保證各個線程都可以正常且正確的執行,不會出現數據污染等意外情況。 線程安 ...
  • 在寫代碼的時候,免不了要使用變數。但程式中的一個變數並不一定是在哪裡都可以被使用,根據情況不同,會有不同的“有效範圍”。 看這樣一段代碼: def func(x): print ('X in the beginning of func(x): ', x) x = 2 print ('X in the ...
  • 用go封裝一下臨時token 本篇為用go設計開發一個自己的輕量級登錄庫/框架吧 的臨時token篇,會講講臨時token的實現,給庫/框架增加新的功能。 Github:https://github.com/weloe/token-go 臨時token也算是比較常見的業務,例如登錄驗證碼信息,邀請鏈 ...
  • 操作系統 :CentOS 7.6_x64 FreeSWITCH版本 :1.10.9 之前寫過FreeSWITCH添加自定義endpoint的文章,今天整理下api及app開發的筆記。歷史文章可參考如下鏈接: FreeSWITCH添加自定義endpointFreeSWITCH添加自定義endpoint ...
  • 哈嘍大家好,我是鹹魚 之前鹹魚寫過幾篇關於知網爬蟲的文章,後臺反響都很不錯。雖然但是,鹹魚還是忍不住想訴苦一下 有些小伙伴文章甚至代碼看都沒看完,就問我 ”為什麼只能爬這麼多條文獻信息?“(看過代碼的會發現我代碼裡面定義了 papers_need 變數來設置爬取篇數),”為什麼爬其他文獻不行?我想爬 ...
  • 未來可期,靜水流深 畢業到現在都已經快 6 年,是從 2011 年 8 月份從江西贛州信豐火車站匆匆忙忙踏上北上的火車。 這是逃避還是幸運,從小到大都沒出過這麼遠的路程。現在回到南方,工作生 活急急忙忙,又好像無聲無息。 高中老師說高考是指揮棒,揮舞著千軍萬馬,初中老師說中考決定著前腳是否能夠踏進大 ...
  • OpenSSL 是一種開源的加密庫,提供了一組用於加密和解密數據、驗證數字證書以及實現各種安全協議的函數和工具。它可以用於創建和管理公鑰和私鑰、數字證書和其他安全憑據,還支持`SSL/TLS`、`SSH`、`S/MIME`、`PKCS`等常見的加密協議和標準。OpenSSL 的功能非常強大,可以用於... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...