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

[复制链接]
查看7590 | 回复10 | 2022-5-20 20:57:03 | 显示全部楼层 |阅读模式
GameFramework解析:有限状态机(FSM)& W8 u! q4 ]8 s
+ \5 k& J1 l% z7 s; z
什么是有限状态机
- t- R+ P1 @) S$ `7 X7 @9 V& ]8 x) t- {) F
有限状态机的概念相信很多同学都清楚了,不清楚的可以参考一下书籍《游戏编程模式》中状态模式一节,里面讲得十分清楚。FSM在游戏中常用于玩家控制、怪物AI、UI状态、游戏流程控制等。
$ `) P8 j* W/ O' ?有限状态机的实现" i: [7 ]9 {6 T$ S6 B& Q; t
1 h% Y3 b# F1 j, l- O: Y! c
结构$ q; A% i+ E; N$ c/ Q

: x2 _% _5 J( H) {3 [$ P
7 J( r9 C8 G6 q2 N  F2 @$ h: Q+ c7 C9 ~$ n) K$ p
GameFramework解析:有限状态机(FSM)-1.jpg * e' ?5 w" x& Z* X) a8 r- M8 w
有限状态机的实现我们可以把他分成3部分,上图中从上到下每一行就是一部分,分别是状态部分(FsmState),状态机部分(FsmBase、IFsm、Fsm)以及状态机管理器部分(IFsmManager、FsmManager)。
0 F& S1 T+ ]+ a$ X状态类FsmState
. i8 H; p- o: P& j  Z% `$ W' K5 w+ {/ W+ o! i9 J( H
    2 t# f- {$ i% @# _: t" G
  • FsmState为有限状态机状态基类,所有用于有限状态机的状态都需要继承自此类,泛型参数T需要传入状态持有者类型。" D/ p6 _1 v3 d7 s- w9 J
  • OnInit、OnEnter、OnUpdate、OnLeave、OnDestroy为状态的生命周期方法,其中OnInit和OnDestroy分别在状态创建和销毁时调用,只会调用一次,而OnEnter、OnLeave分别在进入状态和离开状态时调用,可能会调用多次,而OnUpdate则是在进入该状态后每帧调用。- y, T. M7 z# [; `  m& V+ d/ s
  • ChangeState用于切换到下一状态。ChangeState实际是用该方法传入的FSM对象调用FSM类里的ChangeState方法,正式执行状态切换逻辑。
    9 F4 y* A' R/ `! y7 j5 ?6 J; K- G6 k9 O
状态机类Fsm
7 H( d2 V' p7 b5 m$ w. V" i  f( C7 U# c9 I, u

    ; m+ n% a6 J0 z7 n) [
  • Fsm对象通过Create方法创建,需要传入状态机拥有者类型、状态机名字、状态列表3个参数,Create方法为静态方法,由FsmManager调用。参数状态列表将会保存在字段m_States中,并调用所有状态的OnInit方法。7 }: w1 H  s+ h
  • 状态机通过Start方法启动,传入初始状态类型作为参数,方法内部会调用该状态的OnEnter。. E3 u, {9 G4 d) N- J
  • Update方法会每帧调用当前状态的Update方法,且会计算当前状态机进行了的累计时间,可通过CurrentStateTime获取。
    " n& ~3 P- M/ d3 K
  • GetAllState和GetState方法可以获取注册进这个状态机的状态对象。
    / q1 l$ T( v' a$ w, \
  • 状态机内通常不同状态之间是需要有数据交互的,GetData,SetData,HasData,RemoveData这四个接口则提供了不同状态间数据交互的功能,分别对应获取数据、设置数据、是否有数据、移除数据,数据以key-value形式存在于字典m_Datas中。
    4 l( K3 y1 [5 p
  • Shutdown方法会回收FSM对象,此方法由FsmManager的DestroyFsm方法调用。7 X% K# f) B& q" r
状态机管理器FsmManager
+ Q5 l2 V3 `5 u& Z, s' o6 i2 v7 }! v3 l! I. c) l1 ]

    6 v8 z3 }- l$ b1 A3 {( K
  • 外部创建新的状态机统一通过FsmManager的CreateFsm接口创建,参数同FSM类中的静态方法Create,此方法会调用Fsm类的Create创建Fsm对象,然后以key-value的形式储存在字段m_Fsms中,注意m_Fsms是Dictionary类型,以TypeNamePair为Key,TypeNamePair对象是结合状态机持有者类型和状态机名字字符串类型参数组成,为了保证Key的唯一性,对于同样类型的而不同实例的持有者,应该传入不同的状态机名字。5 z, n! D( W" w
  • GetFsm、GetAllFsm、HasFsm,向外部提供某个状态机的查询、获取,需要传入持有者类型和状态机名字两个参数。; S" _) ]; j: b* P
  • DestroyFsm可销毁特定状态机,会调用对应Fsm对象的Shutdown方法,并在FsmManager的m_Fsms字段中移除该状态机。
    + P) p" G- ^9 I" W
internal override void Update(float elapseSeconds, float realElapseSeconds). }0 d5 ~0 c" T' g4 Y& n! f  \& ^
{/ X: R2 q* _+ e  N
    m_TempFsms.Clear();
* h" M& u* k9 g( i8 R; m    if (m_Fsms.Count <= 0)* Y- n- a2 ?3 a2 K  l: s- c4 i
    {, a% B: u! L! j0 Y0 I* i! E
        return;5 \) G0 _4 Z% k  }
    }
# j2 [% ]/ t( W* b  R: \; o; K! ?+ I1 I/ p
    foreach (KeyValuePair<TypeNamePair, FsmBase> fsm in m_Fsms)# S$ C: _3 j8 a# h! \
    {
/ T* A7 z) M$ \' ~        m_TempFsms.Add(fsm.Value);+ M+ ]7 ]+ ^! B
    }9 A& p* Q% U  O
$ x* B; G( J  [# J2 m. I- o
    foreach (FsmBase fsm in m_TempFsms)* }) P1 h+ f8 D% \+ O; l
    {
4 U/ c) |/ a, b' ~$ D  t! y4 s        if (fsm.IsDestroyed)
0 k2 z' F# a, w0 k: U        {$ o4 \4 ^7 ~1 M! c, I
            continue;/ Z8 V+ ~6 u; n7 Q( B9 [  ^9 r3 Q9 K
        }
* ?, b1 R1 ~. A9 b: r1 I) d0 i/ l0 p: W
        fsm.Update(elapseSeconds, realElapseSeconds);
( S( s& u9 U) G3 T0 _5 }9 G    }1 r) B7 a! {2 p+ K# [# X
}+ r% r. p% o" V8 P6 l/ j& [

    ; a5 V1 J- G$ M1 }, ^
  • Update方法中会调用m_Fsms中的所有状态机的Update方法,值得注意的是这里并没有直接对m_Fsms进行foreach,而是添加到一个临时的列表中再进行循环调用,这样可以防止在迭代过程中,外部销毁某个状态机而从m_Fsms移除状态机对象时,造成迭代器失效。
    ( ]) W' a* n6 a( O6 p
示例
5 Q+ X2 I$ G" A# c
3 C# F1 \( J, n% R假设我们现在需要用状态来实现玩家的控制,其中包括空闲和移动状态,处于空闲状态下的玩家当检测到方向键按下时,会切换到移动状态,且根据方向键向某个方向进行移动,移动过程持续一秒。
  C. L$ y& e$ M# x8 ^ 我们需要3个类去实现这一需求,其中IdleState、MoveState两个类分别对应空闲状态、移动状态,Player则为状态机的持有者,也是状态机要控制的主体。
" ^. ^9 V; c( M( ?) F空闲状态类8 _! ~# n3 S4 p) W
% W2 M' ?9 v* a5 I+ r
using UnityEngine;" a% T! q) d1 T+ }" a+ Q% d, J
using GameFramework.Fsm;
' o7 s% e5 H% g* busing ProcedureOwner = GameFramework.Fsm.IFsm<Player>;- L: ?  m' f- F% S$ _. g- P0 y5 h8 n( {
using UnityGameFramework.Runtime;* x' x! n! c- t5 _, D  |# w% }
, }8 H3 ~0 I% i0 v6 ~
public class IdleState : FsmState<Player>
3 ~+ ^# \0 n/ L2 y0 F, r5 |. h{9 c; {; a) Z' J% b6 ]- u1 M
    //触发移动的指令列表
2 |  T+ [0 L- j( k4 U    private static KeyCode[] MOVE_COMMANDS = { KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow };. h9 k# j# c- ~- h# j

1 C5 U. {# T6 ^# T    protected override void OnInit(ProcedureOwner fsm)
4 ^) S, `- Y. [% `2 N% Y, V    {
9 y. A7 K6 g$ h/ N; F2 P1 p4 h        base.OnInit(fsm);- i$ l( I! U/ f; S  I
    }; r8 K8 a( v9 Q$ O+ A  O% m$ ?/ ]
  t/ x+ M' W* o7 d) D
    protected override void OnEnter(ProcedureOwner fsm)
5 N$ Z, _' E( ?2 r! A    {
% v2 x9 \& I. k        base.OnEnter(fsm);
0 q# g6 \3 W! X- l* _3 v* j    }  b' E2 ]- N3 C/ C+ g! `4 Q& F
0 l. w2 x( J1 _" h" p7 I- S: ?
    protected override void OnUpdate(ProcedureOwner fsm, float elapseSeconds, float realElapseSeconds)) L6 J" r# I$ W7 Q, K
    {: M) T8 a2 g$ ^. f: j
        base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);8 B# w  I6 F7 P6 y: X& R
" I, H* q! ^- w" c; }. Y. L2 f& b8 r
        foreach (var command in MOVE_COMMANDS)) X8 s: k: T8 A3 A3 H
        {
$ j4 H3 F* {! c            //触发任何一个移动指令时) S* Y7 v6 b8 d( |
            if (Input.GetKeyDown(command))  Z$ X8 R; ]% \
            {+ I1 _+ A0 x$ p9 E) ~# K
                //记录这个移动指令
& o1 _9 d# t& K9 I! L                fsm.SetData<VarInt32>("MoveCommand", (int)command);
5 V( P6 x4 L) ^0 x! @$ \                //切换到移动状态
' W3 K# _! z- K$ Q                ChangeState<MoveState>(fsm);. V# x- r, o8 z- H$ ~
            }- Z6 R4 w7 p+ t0 G5 e( z
        }- X! Q6 K6 d) @# M1 w8 Q) g
    }
9 k- y4 x  p" E5 l& o! F; \) P0 k: Z+ k$ Q4 f$ ~
    protected override void OnLeave(ProcedureOwner fsm, bool isShutdown)7 |$ D4 X0 X$ d$ P# T5 V$ U
    {4 U: k( K9 c& F% V) a% {! g
        base.OnLeave(fsm, isShutdown);
- P' \9 O1 J9 P& p5 v5 @8 T    }
% R* j2 e9 r( u$ V2 U4 K
- G8 f& [5 j7 w7 P9 W* e9 Q7 h+ B    protected override void OnDestroy(ProcedureOwner fsm)
8 B1 D5 c9 h5 T( ^    {
5 d! l$ Z& }* b1 t        base.OnDestroy(fsm);1 {0 N( G6 T6 Z( i  o
    }
1 v8 u3 f+ m- s+ Y( S}
! d6 V4 h+ h7 z' I9 K: q6 q移动状态类
: V" v" _3 V( u, O3 u; ^
5 {" o' G, b5 u  _% w$ N: K& {using UnityEngine;
" i7 l3 ~; {  a/ Lusing GameFramework.Fsm;% j4 Z* h, A: A5 `  F
using ProcedureOwner = GameFramework.Fsm.IFsm<Player>;
) e+ P1 X1 V( M' Q7 h9 ], f; Rusing UnityGameFramework.Runtime;5 x  \2 t) n' y! `

4 ]3 Q1 Q/ N. m* mpublic class MoveState : FsmState<Player>
- C# j9 s) S# Q% w" X3 Z{* B+ y" Q& {; n7 D' ]  h
    private static readonly float EXIT_TIME = 1f;
  N7 R8 r3 S4 ~6 G7 t    private float exitTimer;. f5 P9 C! b1 {4 {  s  ?* @) O
    private KeyCode moveCommand;/ D; r9 n9 g/ ~" k# M" o6 r
% _9 B; O, ^- n( m: m
    protected override void OnInit(ProcedureOwner fsm)# d; l. X, T% E2 Q' Q% a4 M
    {
. k& a, {3 C  J5 f- q        base.OnInit(fsm);
+ B+ O  ~; j+ i2 m' r    }
1 `, t1 s) y; m* u. Z2 }% i8 b
/ m- x+ ~! G! T. _% ?% R6 ^( m& @# W    protected override void OnEnter(ProcedureOwner fsm)4 @. x, o3 [' h! d
    {
' L3 E: ~' Y; E( p        base.OnEnter(fsm);9 ^7 t% a+ O; s  b, F
. _, {( E& N" t- h. Y
        //进入移动状态时,获取移动指令数据
4 l  Q. |. M# l& b        moveCommand = (KeyCode)(int)fsm.GetData<VarInt32>("MoveCommand");
/ k' c2 A' O- S- p    }
( V* a  z6 W- n. [# A$ @+ t4 U6 T; F" o1 v1 h( @  i+ L
    protected override void OnUpdate(ProcedureOwner fsm, float elapseSeconds, float realElapseSeconds)
9 k5 z$ }# N* `) p& W/ e    {+ x5 K' V. U% L, w- l' Z; n
        base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);: r) t5 `% C7 j" b5 Y# G! @; R. L
- H& S) j) M5 g' c7 r! [
        //计时器累计时间+ |1 U' @! o8 G( L3 g
        exitTimer += elapseSeconds;
* I& Y! n6 g. R" @
" Y& P* W9 V! a5 j( d1 W        //switch(moveCommand)
5 o4 _# f5 T. l$ a' L+ q% b: l% w        //{
. a: O1 d( t4 u9 p        //根据移动方向指令向对应方向移动
" i" f& h8 S: w: w9 d        //}
2 p4 Y1 [) O3 b! x6 e6 d  I8 W  d, p) t
        //达到指定时间后( K; {+ H7 d* q) c7 m: C
        if (exitTimer > EXIT_TIME)6 @# s5 r' n/ [2 \" m
        {
. ^- h5 G4 j+ A! Y            //切换回空闲状态
  ^4 |* y. S7 i% _/ X: Y; p0 w            ChangeState<IdleState>(fsm);
7 G8 q  d: D) @1 P, {* P% |        }- `( W9 E7 \* G
    }
- r# \# G: j+ q
6 \; K3 V+ P4 S' O    protected override void OnLeave(ProcedureOwner fsm, bool isShutdown)+ H* w8 y/ W  [$ e3 b  y
    {& ?8 u+ V+ b* p! P4 e. Z3 V) l" t
        base.OnLeave(fsm, isShutdown);
: s0 c2 z& B' F) w/ a- e
, a( c. j; F  f/ E* {2 w6 c        //推出移动状态时,把计时器清零
, _8 ~. r3 ~( y% U; ~: k        exitTimer = 0;
6 O8 X; G1 w! H7 v/ E4 R" D: Z        //清空移动指令
7 G- a. m( D6 n3 l        moveCommand = KeyCode.None;
; |) g- k& h  U) t4 @        fsm.RemoveData("MoveCommand");
' O3 U+ a+ A9 g, v' K1 V    }: I3 b! b+ A% f) ^3 j+ d' @

% ?+ I: V) p" ?" V. u' a5 \' [    protected override void OnDestroy(ProcedureOwner fsm)* q% v2 o8 M# m4 [7 h, f
    {
: `3 w, H+ Z3 n$ ~: H8 x        base.OnDestroy(fsm);9 T( @- o& c( s5 v
    }5 a9 D! t. g$ g" c2 E
}% T. J+ Y( N0 L& R0 m
玩家类
, ?+ Y. x; n8 l+ e: N
0 c, h+ @% l. J7 E# V2 `* _' dusing System.Collections.Generic;
7 k2 j) q4 u2 |/ jusing UnityEngine;6 K4 L# |. b7 ]8 p8 z
using GameFramework.Fsm;
) `+ P/ `& l% E8 z. g8 C- |8 Yusing StarForce;3 C( l4 g' ]! L3 ]( Y$ a6 r

: S% d% w; k! x; j% Wpublic class Player : MonoBehaviour
4 x2 l. Q7 W: A) C" M& _) G' O" H. W{' e. `2 f# a6 p  M( x+ l
    //Player对象自增Id* O% w* @0 H) G# d: Y: N
    private static int SERIAL_ID = 0;+ B: H8 A. G2 P1 f/ t, `

# k+ X* k2 n8 W3 a9 L    private IFsm<Player> fsm;
- W; |6 n9 L; r$ `0 j% }1 i2 F. l3 _% i, B
    // Start is called before the first frame update
2 S+ q3 ?3 `/ l" s( N9 v- V    void Start()) G- G1 Q& t6 M$ A6 i) u4 k
    {
& g& [# q  |, {0 v        //创建状态列表
& W  m+ W  H$ A4 ~        List<FsmState<Player>> stateList = new List<FsmState<Player>>() { new IdleState(), new MoveState() };
4 T, q. P* G6 p9 J; r% h. X3 y        //创建状态机,注意,对于所有持有者为Player类型的状态机的名字参数不能重复,这里用自增ID避免重复
) j! h* N" s' _* G6 S2 ?        fsm = GameEntry.Fsm.CreateFsm<Player>((SERIAL_ID++).ToString(), this, stateList);
/ F) ?$ k8 U9 s0 T# w9 O        //以IdleState为初始状态,启动状态机
6 J/ X6 S- j) U! v3 ^        fsm.Start<IdleState>();! b+ ?/ m1 f# a( U' a/ T$ r( O
    }
% C2 A+ \# g. p0 s# W$ ?# a/ [
    // Update is called once per frame
$ l7 S8 E9 C$ x6 ^- R    void Update()
( L. l" G2 R/ [3 r    {
8 A# I- r1 d; e/ {! k; o& i; C. ~/ r: H9 B6 P
    }3 B0 D( W5 g! m
6 t  u& u. y; `' t& F( y
    private void OnDestroy()
1 C. J: r. O9 j( D* N/ ]3 o1 B& Y    {
- H' s( V& E2 M9 E5 b. V        //销毁状态机
) I- L6 x; h- ]  r1 p  H, g        GameEntry.Fsm.DestroyFsm(fsm);
. s0 N# m5 R; c6 w/ B5 T    }0 x8 S8 X8 a2 }4 Q* K! N6 D3 J( ]' l
}0 Y7 r' {3 G$ W  c1 A
Inspector面板
* \; i/ w/ V, X0 G/ T! z
6 C& y2 _) O$ e; @. ~& Z- n" H; u3 M9 k) V  {/ X
  Y# \: X+ \" O. b
GameFramework解析:有限状态机(FSM)-2.jpg
- c) c) ]& Z9 W+ O! V% ?6 G6 H0 h5 j" n2 v+ l  T
FSM组件的Inspector面板可以实时看到所有正在运行的状态机,以及这些状态机当前处于的状态、运行时间。
5 r/ Z% n5 H$ q& y* Q  |/ g) d最后
+ ~  |. }  _& M  V2 X- D. v
! @( u" i0 m6 ^4 uGameFramework解析 系列目录:GameFramework解析:开篇
. F! [7 o. {# w2 ]个人原创,未经授权,谢绝转载!
悠然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 | 显示全部楼层
太棒了,敲碗等更新
5 ~8 ~' k1 D( }! f笔芯
杏花微雨2017 | 2022-5-22 13:01:10 | 显示全部楼层
感谢分享~
123471661 | 2022-5-23 00:43:39 | 显示全部楼层
超认真
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

295

金钱

0

收听

0

听众
性别

新手上路

金钱
295 元