GameFramework解析:有限状态机(FSM)

[复制链接]
查看7519 | 回复10 | 2022-5-20 20:57:03 | 显示全部楼层 |阅读模式
GameFramework解析:有限状态机(FSM)
, n* d3 p9 S# i  b& g  X( J$ _  l) e( {0 X0 ~  C# u5 X$ A7 j
什么是有限状态机
( |* C2 H* D' r3 i: e4 o3 ~2 D# [7 `7 M3 j1 H% |4 w5 P
有限状态机的概念相信很多同学都清楚了,不清楚的可以参考一下书籍《游戏编程模式》中状态模式一节,里面讲得十分清楚。FSM在游戏中常用于玩家控制、怪物AI、UI状态、游戏流程控制等。! h2 Z% B" Y5 }; _0 I
有限状态机的实现# {1 ^1 z8 M! s5 A, H. J

. m3 w9 H! @6 H: K' L7 {' e结构
6 W; ^9 b8 v* a6 F2 T+ _
# x+ R0 s$ r5 F: ~1 z# ]) H9 D  l( o6 V" z4 |( z

9 j% N& ~1 Y+ L; o  t& z0 Z GameFramework解析:有限状态机(FSM)-1.jpg : U, }+ ?: V3 u, C( w3 v: I. _
有限状态机的实现我们可以把他分成3部分,上图中从上到下每一行就是一部分,分别是状态部分(FsmState),状态机部分(FsmBase、IFsm、Fsm)以及状态机管理器部分(IFsmManager、FsmManager)。7 E; E0 n" ~; n/ g! n, s
状态类FsmState
# ?. S/ f' q/ P7 Q- m! R9 M' V- \% M; T4 C* c

    / q( m' K* G- a, A, \& I/ d, U3 Y  A
  • FsmState为有限状态机状态基类,所有用于有限状态机的状态都需要继承自此类,泛型参数T需要传入状态持有者类型。+ i1 T6 P0 p& x# w6 }
  • OnInit、OnEnter、OnUpdate、OnLeave、OnDestroy为状态的生命周期方法,其中OnInit和OnDestroy分别在状态创建和销毁时调用,只会调用一次,而OnEnter、OnLeave分别在进入状态和离开状态时调用,可能会调用多次,而OnUpdate则是在进入该状态后每帧调用。( a: D  u% w( h# b' I& X
  • ChangeState用于切换到下一状态。ChangeState实际是用该方法传入的FSM对象调用FSM类里的ChangeState方法,正式执行状态切换逻辑。
    8 R; o. b! l6 x+ x8 j
状态机类Fsm
% P( |9 U9 B4 ?5 ~8 M: a# A5 W
    6 R4 w  p$ Y: C& u
  • Fsm对象通过Create方法创建,需要传入状态机拥有者类型、状态机名字、状态列表3个参数,Create方法为静态方法,由FsmManager调用。参数状态列表将会保存在字段m_States中,并调用所有状态的OnInit方法。/ j% R% @" {! Q  X& s- m+ k" G0 o3 K/ j
  • 状态机通过Start方法启动,传入初始状态类型作为参数,方法内部会调用该状态的OnEnter。* s- S- W" d# @/ d' n0 `, S% A
  • Update方法会每帧调用当前状态的Update方法,且会计算当前状态机进行了的累计时间,可通过CurrentStateTime获取。( }3 l/ z, \% k; t" l; C  z; l
  • GetAllState和GetState方法可以获取注册进这个状态机的状态对象。4 N9 `& B" D, ~7 d( U. W
  • 状态机内通常不同状态之间是需要有数据交互的,GetData,SetData,HasData,RemoveData这四个接口则提供了不同状态间数据交互的功能,分别对应获取数据、设置数据、是否有数据、移除数据,数据以key-value形式存在于字典m_Datas中。
    % n7 N3 B) `% q* `7 Z% e4 f- a
  • Shutdown方法会回收FSM对象,此方法由FsmManager的DestroyFsm方法调用。
    * a! L" ]& {- O! j5 w
状态机管理器FsmManager+ {. U/ R6 B) m

' n8 U# B9 b, Y8 M7 @' f& ^
    5 d5 v( C- O( J; o
  • 外部创建新的状态机统一通过FsmManager的CreateFsm接口创建,参数同FSM类中的静态方法Create,此方法会调用Fsm类的Create创建Fsm对象,然后以key-value的形式储存在字段m_Fsms中,注意m_Fsms是Dictionary类型,以TypeNamePair为Key,TypeNamePair对象是结合状态机持有者类型和状态机名字字符串类型参数组成,为了保证Key的唯一性,对于同样类型的而不同实例的持有者,应该传入不同的状态机名字。
    - I" ]& h4 e8 e) P" E
  • GetFsm、GetAllFsm、HasFsm,向外部提供某个状态机的查询、获取,需要传入持有者类型和状态机名字两个参数。
    : [  ?* F& L5 w+ U; u5 W& J
  • DestroyFsm可销毁特定状态机,会调用对应Fsm对象的Shutdown方法,并在FsmManager的m_Fsms字段中移除该状态机。
    / y. m  o" i5 B9 u5 s0 I0 i
internal override void Update(float elapseSeconds, float realElapseSeconds)4 C, I7 ~9 J7 h- A3 n( ~
{
! x) p6 e  l- e3 @    m_TempFsms.Clear();; S8 t- F1 D& V. g6 R' m9 q
    if (m_Fsms.Count <= 0)* f9 Q- [- n$ B+ `/ \
    {
; e: Q9 I0 F5 j& L. {  ?" R5 H        return;
) ~& ^  Q% j0 H  ~  [/ s7 c    }
$ O1 ?8 V6 P+ Z- D" D$ {( G7 W; c" F& E' ?8 o8 i& P" V" S3 f; M5 `' R
    foreach (KeyValuePair<TypeNamePair, FsmBase> fsm in m_Fsms)+ D0 h( y7 ^3 d  b5 Z- @
    {
& \* k" t" |2 S  ?1 _+ z5 i        m_TempFsms.Add(fsm.Value);
3 y0 M/ n' D0 X( W- O    }$ @' R2 ]8 n5 Z, F' F6 k

# C/ w5 m4 A& w    foreach (FsmBase fsm in m_TempFsms)# [- y$ p. k3 U$ q/ D' `
    {& W5 \: b; \  s$ X" {
        if (fsm.IsDestroyed)
$ x% R5 \0 t+ Q( d- v0 M+ t' a        {; c4 ~9 N6 U- `$ t
            continue;
, X0 e4 ^6 D$ D        }8 Z/ G! b! ~% E$ x- c+ z

9 l+ M2 H& R( f' h1 O& q        fsm.Update(elapseSeconds, realElapseSeconds);
: b/ H0 Q8 \( T) i. D0 P    }
7 \& j! F  {+ N0 s; Z0 ~}# f) z2 i0 u2 Q

    1 n2 u) Z1 m- ]: h
  • Update方法中会调用m_Fsms中的所有状态机的Update方法,值得注意的是这里并没有直接对m_Fsms进行foreach,而是添加到一个临时的列表中再进行循环调用,这样可以防止在迭代过程中,外部销毁某个状态机而从m_Fsms移除状态机对象时,造成迭代器失效。# ~  G) ]* B3 F) |
示例
$ S2 q) i9 W4 c, N/ ^" j$ U1 d
5 y# t- [" N$ c- v+ z- }% f假设我们现在需要用状态来实现玩家的控制,其中包括空闲和移动状态,处于空闲状态下的玩家当检测到方向键按下时,会切换到移动状态,且根据方向键向某个方向进行移动,移动过程持续一秒。$ Q( Q$ U7 q7 v  g1 {
我们需要3个类去实现这一需求,其中IdleState、MoveState两个类分别对应空闲状态、移动状态,Player则为状态机的持有者,也是状态机要控制的主体。5 q* R% M) p( `' n
空闲状态类
6 l6 b1 c6 [+ W) r  ]  T: A
$ x8 f4 c' r% p( musing UnityEngine;3 Y6 H: X6 V; z
using GameFramework.Fsm;
. c3 X# u: T3 }* `8 _4 uusing ProcedureOwner = GameFramework.Fsm.IFsm<Player>;
) k5 ~) ^& m7 b' kusing UnityGameFramework.Runtime;' j/ o# k/ p. v, s# [

* b1 W7 T6 g: a1 o4 Cpublic class IdleState : FsmState<Player>
+ ~9 s4 z3 d$ I' c{
9 v- O2 @. X. p    //触发移动的指令列表
% c, `; }/ n8 F8 [& A    private static KeyCode[] MOVE_COMMANDS = { KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow };
, p. V& B7 q7 I* W- N" r+ W3 I
1 M% X1 w# R, o( w  B3 i" A  l) Z    protected override void OnInit(ProcedureOwner fsm)
4 w/ U& J( B  h% @/ ]  I    {
3 ]1 g% `1 e# \# `6 _! V2 ]        base.OnInit(fsm);
! G  ~+ U6 X3 e& l; R- N7 f0 C; p    }
: q2 W6 U4 U( d) b# D: j+ U$ o
7 P4 I( l2 m0 O# {" c    protected override void OnEnter(ProcedureOwner fsm)
: N! o# N3 E2 n% _2 f. y- b    {
2 i# ]+ n+ g  u- h        base.OnEnter(fsm);
# n* N1 K$ L4 l  ]/ {; q    }% Z6 R9 z* G/ G% |
: |! i. R1 e+ A9 Q! f0 i: W
    protected override void OnUpdate(ProcedureOwner fsm, float elapseSeconds, float realElapseSeconds)# G- E# l/ V3 e8 Z8 B6 n) N
    {
4 I1 T- o' p" D3 Z        base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);+ H; D: w: x5 f8 N
; {' f( t8 G: {* k5 o6 n
        foreach (var command in MOVE_COMMANDS)7 V/ J. U, |9 J3 r
        {
0 T% c1 ~% A7 i$ N3 D! `            //触发任何一个移动指令时2 k) P! ]& ^+ w5 W/ u$ O: r# d+ O' T4 ~
            if (Input.GetKeyDown(command))
" A$ G9 B" e# t' b8 O+ x# L/ [            {
: j8 y9 d2 s- J% H$ S                //记录这个移动指令
' c+ w4 n( u  j5 W; n                fsm.SetData<VarInt32>("MoveCommand", (int)command);5 g  y+ C  y& d" V/ n
                //切换到移动状态; }2 H0 d6 L- K5 c" I+ L
                ChangeState<MoveState>(fsm);$ Z' L4 o+ C; I" k3 J
            }& \; d2 w- d; p+ |+ Z' z+ d
        }
; S# x: H# E& f/ q1 c5 K    }
( j) M; b' K8 x: u7 j7 T2 Q* n+ ?; m( r3 o" p; c; f8 B
    protected override void OnLeave(ProcedureOwner fsm, bool isShutdown)) o2 P/ z$ H) f( B
    {, Z( o8 U- {9 V1 F3 z' h/ o1 t
        base.OnLeave(fsm, isShutdown);
! q: ~) E- i- r2 ]/ a) L7 v5 C    }
* Z/ M, G8 y% b3 ?8 {! |. W
3 C( {) P7 C2 Q2 ]# w    protected override void OnDestroy(ProcedureOwner fsm)- D5 \* W! z2 M; P2 e& W
    {  z& J9 w6 a2 ]4 [" j; v+ P
        base.OnDestroy(fsm);
( J4 ~+ r. L* c" j    }
, U+ \+ @: s& M  Z}
! \* l5 ~. A% m, Q& f移动状态类( g. E5 V6 U; L! Y' {
* ~6 m% x2 R3 G# W
using UnityEngine;
7 J" Q1 R+ c/ S, ]5 T9 k. Uusing GameFramework.Fsm;
- y2 X4 O9 |4 y% _4 H8 z) c+ {1 Pusing ProcedureOwner = GameFramework.Fsm.IFsm<Player>;3 `8 C( E# e( z, M
using UnityGameFramework.Runtime;* A3 e' q( J( i7 ]6 n3 t
7 K" p6 F( X! K8 S# z) l8 U. D
public class MoveState : FsmState<Player>. M+ c) L$ s# \' w0 N) u; P/ T8 z3 I
{- Z6 g$ b# r! p9 C' A
    private static readonly float EXIT_TIME = 1f;" I9 ]4 h- X& j! L8 z! s& L
    private float exitTimer;
6 i$ i- ^! G3 M" p7 S    private KeyCode moveCommand;
2 ]" C: Z: r3 L' G1 S( `; e* j. j
    protected override void OnInit(ProcedureOwner fsm)$ ^, j4 U! m. r. w2 b; S  N  W
    {) P' z& l  l7 n0 N  c2 q& E
        base.OnInit(fsm);
5 b! l4 b) N- v9 \    }- H/ k) ]# _4 e
- t$ x3 p9 q7 h: E, R
    protected override void OnEnter(ProcedureOwner fsm)  [3 J/ t# L. b
    {
2 Q& t3 ?; b* h" k: H        base.OnEnter(fsm);7 H/ T* ]( [5 f+ r; R4 x

( l+ `' s+ e0 K        //进入移动状态时,获取移动指令数据2 l. t9 p) u; Z* ^0 a" g: Z  w: J
        moveCommand = (KeyCode)(int)fsm.GetData<VarInt32>("MoveCommand");
5 x+ U6 @' y1 w7 z    }
1 ^( e- t' @2 C# B1 G! {
6 p! f. e1 e, a% ?% ~, g    protected override void OnUpdate(ProcedureOwner fsm, float elapseSeconds, float realElapseSeconds)$ ^" Y6 D1 d/ K* |* }6 ?5 C
    {
$ X8 x8 U7 B1 x1 l3 t# l8 K        base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);
6 P7 V) P0 ^: Y( E/ c# Y3 K( U+ H
. E8 m, l2 _( S( z        //计时器累计时间
9 u3 v( v, R# F, H2 y        exitTimer += elapseSeconds;$ l& @6 ~9 v- M
* Y8 e1 W% F* l" @) k: A
        //switch(moveCommand)
! x, }% ]1 P% F" I; B% C7 k        //{
: A7 k) B0 M: Z/ x4 V        //根据移动方向指令向对应方向移动
! O7 q: @$ q: \$ q3 f6 j        //}
9 {% c# n# c7 L0 X. r2 ?$ _# y8 S  s  |8 Z; J6 a+ Q9 q+ V
        //达到指定时间后
8 S4 ?1 }4 y) [! g3 S& T        if (exitTimer > EXIT_TIME)
8 D, U; y! R) ]9 E        {
. W' w: ^/ R& Z& L1 v6 O7 r) m            //切换回空闲状态) `' d& h# M$ E0 t/ v
            ChangeState<IdleState>(fsm);
0 H! e8 ?, T! s) ?$ x        }
: l% {0 T5 H5 t- H' W/ s2 z    }8 l9 k) r3 ]" b7 p
9 T! u; C) L3 q( K- s" e  [9 {
    protected override void OnLeave(ProcedureOwner fsm, bool isShutdown)3 F# [8 o) O* Z7 B8 d, z
    {+ j# b. M6 q+ ?$ @
        base.OnLeave(fsm, isShutdown);" {' P) w- r& R! n

2 R) a3 J' y, T        //推出移动状态时,把计时器清零3 g: {5 u; S9 J
        exitTimer = 0;- s  R% r$ `8 w
        //清空移动指令
$ S! }4 s6 N1 L/ B; g8 C        moveCommand = KeyCode.None;
/ {  J. F: M. [. e3 A% e7 |+ C        fsm.RemoveData("MoveCommand");
5 a2 P1 R* n# F8 ^5 T8 n3 m6 ^    }
9 d+ p  Z7 k9 g8 h, F4 `4 R+ E: U( ?# q2 L) h
    protected override void OnDestroy(ProcedureOwner fsm)/ _# U+ E, |1 ]' L# j7 C
    {: V$ p1 v4 [1 E( i3 K
        base.OnDestroy(fsm);
$ b" f1 P! S1 z. \+ Z    }& G( n# C6 w- m4 M
}
: K. b& g9 G/ h% @% T1 }( a玩家类/ |/ G- }( e. v- ]7 v! H& Y
# x) ]" {% v5 j7 x
using System.Collections.Generic;
& Y' x7 A3 x/ @) M0 Z; lusing UnityEngine;: J: F$ R! [+ l- ^3 j
using GameFramework.Fsm;
1 b( K5 `: u2 ]8 tusing StarForce;
) C8 |2 C* B6 [! q$ [: h
# ]/ ?' \: Z+ |/ ]3 P- m2 Ipublic class Player : MonoBehaviour
6 p; u. |% H6 ]1 b" E+ R{8 k# D% P& d9 D4 m5 q- C
    //Player对象自增Id
3 F+ T+ U2 x' N; k4 I3 y    private static int SERIAL_ID = 0;& N* \2 A3 U' J$ @
9 M; y  Q, x3 W  g+ r+ O3 h
    private IFsm<Player> fsm;
& r' f3 |5 L$ ^& q9 [9 P& D+ S% L
) Z3 A2 t0 o  E9 Z. i    // Start is called before the first frame update% I1 o' D. K' W: w, ^$ O4 o' c
    void Start()
! P. P+ e. W3 Z* i    {
6 A5 w! ~0 n  F1 _        //创建状态列表3 y) G9 N1 z% a- P
        List<FsmState<Player>> stateList = new List<FsmState<Player>>() { new IdleState(), new MoveState() };- [% U! u6 a; r/ g" T& v
        //创建状态机,注意,对于所有持有者为Player类型的状态机的名字参数不能重复,这里用自增ID避免重复) I) k* g5 z" h( Y2 I. I. Q9 @
        fsm = GameEntry.Fsm.CreateFsm<Player>((SERIAL_ID++).ToString(), this, stateList);# {' {$ [2 w$ b2 B5 O4 I9 A. {
        //以IdleState为初始状态,启动状态机
8 F6 I1 H. u# I! k9 H0 @' K        fsm.Start<IdleState>();
6 {0 }. D7 B9 }& [3 l2 f    }4 E( y' \3 g( F, Z  t  k0 B

8 O6 E" _* \- m. O    // Update is called once per frame
# X( \3 D( B% J7 B* C; Q% T    void Update()# s6 @! \1 G7 _" z3 g3 O. D$ R
    {
3 Z7 g. i1 F& Z# }, E% ~+ u( \- x! g, ?2 p6 \4 M' c
    }
0 F8 d: Y4 d" V: e/ s
% w% v. K9 D7 g1 T% H" J; S8 h/ K( f! ^    private void OnDestroy()
4 H* B) l; ?" D    {
; X! x' f% a: G& N        //销毁状态机5 s, |) V' _& K9 `  s
        GameEntry.Fsm.DestroyFsm(fsm);
+ ~7 t# p9 y5 |( B7 I0 Z    }
$ ]( _- n$ e1 _' f% i( ]6 Y" X- f- j}
2 _/ N& o. c$ W1 f! mInspector面板
! P, ]9 l; Q8 s( M. L! t; N- u! c8 ~1 @) c1 D. u" {* O

! q( i6 V9 m& |- K: ~
5 v/ @. P/ g& W: O* @ GameFramework解析:有限状态机(FSM)-2.jpg
) W1 m# L, L- x
; v& M7 I( G3 c; O, g4 {FSM组件的Inspector面板可以实时看到所有正在运行的状态机,以及这些状态机当前处于的状态、运行时间。+ D+ n" _# |9 B
最后
, {" h' j8 U- C( n' v* Q/ U, j
, v1 v- K, G2 [4 a5 s& HGameFramework解析 系列目录:GameFramework解析:开篇: N7 g$ H8 ^6 {$ G/ e3 v
个人原创,未经授权,谢绝转载!
悠然296 | 2022-5-21 00:10:59 | 显示全部楼层
好![赞]
万灵1 | 2022-5-21 05:20:21 | 显示全部楼层
花卷高产似母猪
hbbx616 | 2022-5-21 06:38:31 | 显示全部楼层
外贸快车_方才 | 2022-5-21 12:55:59 | 显示全部楼层
熬夜
夜雨萧声裂 | 2022-5-21 17:48:22 | 显示全部楼层
[赞] 那个临时的m_Fsms当时真的很疑惑为什么要这么干
恶魔法修 | 2022-5-21 21:57:48 | 显示全部楼层
[感谢]
右脑工厂诓 | 2022-5-22 03:55:50 | 显示全部楼层
太棒了,敲碗等更新
+ w5 @- q9 E. A3 k$ |笔芯
杏花微雨2017 | 2022-5-22 13:01:10 | 显示全部楼层
感谢分享~
123471661 | 2022-5-23 00:43:39 | 显示全部楼层
超认真
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

295

金钱

0

收听

0

听众
性别

新手上路

金钱
295 元