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

[复制链接]
查看7616 | 回复10 | 2022-5-20 20:57:03 | 显示全部楼层 |阅读模式
GameFramework解析:有限状态机(FSM)
) y3 n: \$ N, {+ \% b7 \3 W/ f8 F
什么是有限状态机
! r- M$ O$ C. k! ^% S9 @& o" V" ]
3 t9 D6 ?, h- [9 ^有限状态机的概念相信很多同学都清楚了,不清楚的可以参考一下书籍《游戏编程模式》中状态模式一节,里面讲得十分清楚。FSM在游戏中常用于玩家控制、怪物AI、UI状态、游戏流程控制等。
3 k' {7 u* O! l. m( I  a, C5 r. M有限状态机的实现9 d, |; c6 U: h5 {6 ^* P/ O$ T! y8 `

- g+ O- t# R; x) }" D结构
' |9 Q  _( r5 t, _( @
. g5 ?1 Q7 E+ Q; O/ B% {
. O. i# N3 n; n" ^
# f5 G& A3 D2 M" o% z4 f0 q1 x4 g GameFramework解析:有限状态机(FSM)-1.jpg ) B# q* X! _3 ]6 V
有限状态机的实现我们可以把他分成3部分,上图中从上到下每一行就是一部分,分别是状态部分(FsmState),状态机部分(FsmBase、IFsm、Fsm)以及状态机管理器部分(IFsmManager、FsmManager)。
# I5 T8 l8 s9 P( ?5 g' v! Y4 X状态类FsmState( e4 ]2 J5 B) ?( i8 l3 @

8 X+ e3 ^( n2 m6 U+ ^

    $ f7 u6 \1 G+ r; I7 }3 [0 `& W
  • FsmState为有限状态机状态基类,所有用于有限状态机的状态都需要继承自此类,泛型参数T需要传入状态持有者类型。# \* ~) t8 O$ t8 f. B& w
  • OnInit、OnEnter、OnUpdate、OnLeave、OnDestroy为状态的生命周期方法,其中OnInit和OnDestroy分别在状态创建和销毁时调用,只会调用一次,而OnEnter、OnLeave分别在进入状态和离开状态时调用,可能会调用多次,而OnUpdate则是在进入该状态后每帧调用。( ^( B3 |8 `$ q: Z0 F) |
  • ChangeState用于切换到下一状态。ChangeState实际是用该方法传入的FSM对象调用FSM类里的ChangeState方法,正式执行状态切换逻辑。9 R! Q1 a# n" w, W
状态机类Fsm- q0 E6 l) \3 [& u3 x( m

4 K. y+ f( U' K( F( N* v$ B7 N
    # y4 V. I; m5 N% E
  • Fsm对象通过Create方法创建,需要传入状态机拥有者类型、状态机名字、状态列表3个参数,Create方法为静态方法,由FsmManager调用。参数状态列表将会保存在字段m_States中,并调用所有状态的OnInit方法。5 L' I0 x7 V* F& K8 Q3 m, |, e
  • 状态机通过Start方法启动,传入初始状态类型作为参数,方法内部会调用该状态的OnEnter。7 P5 {; r& [% T0 e& m- ]
  • Update方法会每帧调用当前状态的Update方法,且会计算当前状态机进行了的累计时间,可通过CurrentStateTime获取。7 p2 D% V, [8 d) z6 ~: Y
  • GetAllState和GetState方法可以获取注册进这个状态机的状态对象。3 I4 }: e0 Z) w; q
  • 状态机内通常不同状态之间是需要有数据交互的,GetData,SetData,HasData,RemoveData这四个接口则提供了不同状态间数据交互的功能,分别对应获取数据、设置数据、是否有数据、移除数据,数据以key-value形式存在于字典m_Datas中。
    2 z, F, O7 W0 @5 [, K
  • Shutdown方法会回收FSM对象,此方法由FsmManager的DestroyFsm方法调用。) H4 N- s( E# _
状态机管理器FsmManager5 f6 M; G- r* Z, W

8 ?- ~% C, Z6 w- g; t% e

    4 i2 r5 N4 X4 p3 o+ R2 W7 x, m3 F
  • 外部创建新的状态机统一通过FsmManager的CreateFsm接口创建,参数同FSM类中的静态方法Create,此方法会调用Fsm类的Create创建Fsm对象,然后以key-value的形式储存在字段m_Fsms中,注意m_Fsms是Dictionary类型,以TypeNamePair为Key,TypeNamePair对象是结合状态机持有者类型和状态机名字字符串类型参数组成,为了保证Key的唯一性,对于同样类型的而不同实例的持有者,应该传入不同的状态机名字。8 B' ^$ L$ f, _
  • GetFsm、GetAllFsm、HasFsm,向外部提供某个状态机的查询、获取,需要传入持有者类型和状态机名字两个参数。
    ( M* `+ U& J9 N- w
  • DestroyFsm可销毁特定状态机,会调用对应Fsm对象的Shutdown方法,并在FsmManager的m_Fsms字段中移除该状态机。
    : E' J4 o* H$ [) Q
internal override void Update(float elapseSeconds, float realElapseSeconds)
8 {" p* g. P5 C& d# t( ?% [! K{% R0 `+ T, q! V' w5 N) O$ q
    m_TempFsms.Clear();
. g( x" U5 r- c/ c* A7 [; g$ h5 C' g8 v    if (m_Fsms.Count <= 0)
  |) E, s- o/ b1 d* M    {2 E+ E1 b( W" L
        return;
& Y- h8 b5 _6 ]5 C4 y4 M; d% {" S: h    }, r3 i: v' }: j6 f5 [
+ Q/ {1 C8 j( v# b# l( M9 H" N9 m
    foreach (KeyValuePair<TypeNamePair, FsmBase> fsm in m_Fsms)3 c- B6 J3 g; j- P
    {
& K. Z9 Z' Y6 Q) p        m_TempFsms.Add(fsm.Value);! i$ L$ m% R7 n3 @4 u
    }) `1 x( n& W2 _4 \

/ H& w0 L7 x+ f/ V' Q8 {7 E( Z$ n8 n5 t    foreach (FsmBase fsm in m_TempFsms)
. M- l. w5 P: M1 P: y, ?# y    {
; c6 ~. R0 B+ A* J0 r        if (fsm.IsDestroyed)! J0 i8 J, e3 A8 g7 z
        {! g, f; G( v( y# h, S8 _
            continue;' b2 X4 h/ X  T. X) }6 R0 I% W
        }0 o4 x( T- b) y, M8 `* g% X$ N

! z; M7 ?3 p6 o" o        fsm.Update(elapseSeconds, realElapseSeconds);
- U, N( V6 m  N! M4 {    }
, e" k" u* x# J! v( W}8 d9 N, S: J2 e: u) h0 G

    ) s9 j/ }  @4 g0 W) F
  • Update方法中会调用m_Fsms中的所有状态机的Update方法,值得注意的是这里并没有直接对m_Fsms进行foreach,而是添加到一个临时的列表中再进行循环调用,这样可以防止在迭代过程中,外部销毁某个状态机而从m_Fsms移除状态机对象时,造成迭代器失效。
    ) Y+ o! E& `: C* l  P1 K
示例
0 l/ G" w: p' S4 O# _2 v" S) Z$ j6 q" ?; A( u% Z
假设我们现在需要用状态来实现玩家的控制,其中包括空闲和移动状态,处于空闲状态下的玩家当检测到方向键按下时,会切换到移动状态,且根据方向键向某个方向进行移动,移动过程持续一秒。% Q# |$ l4 x- @0 W  R2 k" [
我们需要3个类去实现这一需求,其中IdleState、MoveState两个类分别对应空闲状态、移动状态,Player则为状态机的持有者,也是状态机要控制的主体。
. E9 {- d  y% n  ^# J空闲状态类0 d+ G' `9 |" `# a* z$ O

& |- B& z( ?3 O7 Yusing UnityEngine;
& c5 x7 P3 \& [& k) Q& B! r* ?0 cusing GameFramework.Fsm;' B* X9 U: {- X  }2 b) m; O0 y( m5 Z; m
using ProcedureOwner = GameFramework.Fsm.IFsm<Player>;
9 t* h1 f5 _7 ]3 l- zusing UnityGameFramework.Runtime;
, l% M, J5 `$ y0 g, u! P2 m1 J/ r2 r( P! v, z0 n
public class IdleState : FsmState<Player>
- t5 l$ j- S8 e3 v6 `0 t{
% P& v& W1 Y4 `4 Y& `- q' r8 P    //触发移动的指令列表
0 H" q: B0 A( c6 z2 M8 {+ F2 e  T    private static KeyCode[] MOVE_COMMANDS = { KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow };
( A9 b# n( w4 C% A; i) {
4 j6 ~$ ]1 o9 @( _( {    protected override void OnInit(ProcedureOwner fsm)2 {/ {/ m4 h1 m; ~' a: D
    {. N: N; f' a& }  L6 k+ L
        base.OnInit(fsm);8 H+ V" b  T7 [& R5 W' G& ]4 Q; O
    }
9 Q8 ^% t* I, q0 |9 W, s
0 c7 P/ _6 [. l, t( o6 e7 e. A+ i    protected override void OnEnter(ProcedureOwner fsm)
6 Z2 B6 b) X9 t; }; z    {! p" {. U7 w- k+ \2 i# F/ a- r  I
        base.OnEnter(fsm);
7 V7 O5 W( K% |! X  r! z    }
* {5 N! }- n/ E% M% T
- g* W$ _3 d+ J+ t7 `    protected override void OnUpdate(ProcedureOwner fsm, float elapseSeconds, float realElapseSeconds)
. Z0 I0 ]! T8 ~' R    {& u4 ]3 h3 U5 j
        base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);$ H/ N' p3 K# H2 j
8 O: X) ~6 w' C( E$ O
        foreach (var command in MOVE_COMMANDS)5 P" o  c1 ~$ z
        {
! Z! c! h  Q: z9 V            //触发任何一个移动指令时* g* j; q  X0 |
            if (Input.GetKeyDown(command))
7 q6 J0 j- Z- ^            {
" M3 |. [1 ]) l: r/ |' m                //记录这个移动指令
8 p2 P. R9 }# K/ J# c1 F                fsm.SetData<VarInt32>("MoveCommand", (int)command);* o+ H6 B' a# H* @
                //切换到移动状态  ^$ W4 @7 i8 R8 V; u$ G
                ChangeState<MoveState>(fsm);
# ^8 b) D$ m5 x& L            }! X% K1 L, ~( ^
        }
4 @% U! I* F+ `9 x    }6 x# }# b. F) Y' j& e

$ P5 q2 |" s; t/ w6 J# Q    protected override void OnLeave(ProcedureOwner fsm, bool isShutdown)
$ c; B9 B: j7 s+ p3 G  u; [  c  x    {
& q: @- N8 N+ R1 P- k: Y; G        base.OnLeave(fsm, isShutdown);
2 Q0 ~) ]3 U# S, b/ m: m6 o    }3 }0 E: J9 m1 G8 M% ^! `
( S; O# ~7 ~9 s3 G, V. q% W
    protected override void OnDestroy(ProcedureOwner fsm)5 r9 L/ Y" c1 x6 j
    {
$ X3 }1 W5 k/ X& z4 A' @        base.OnDestroy(fsm);
; x. A; `5 U3 C; g. A0 _/ G    }2 o2 e% g* _3 S. m
}
' }! W& B! C. |- i! O+ q9 {移动状态类. q* O' X9 S- A8 E" Z

$ g  r" c( g- |( d. y4 _% N/ w6 dusing UnityEngine;
4 F/ |- g, k1 D5 Gusing GameFramework.Fsm;, [) j8 S+ J$ o$ Y
using ProcedureOwner = GameFramework.Fsm.IFsm<Player>;1 }% n9 r3 ~" D  a  G6 P3 h5 A7 x2 ~
using UnityGameFramework.Runtime;0 T& h6 j$ G: k1 a8 I6 F* a/ B
9 `/ P* d2 }# _6 Z5 i
public class MoveState : FsmState<Player>: {0 p' t& j- r) h& ]
{
* G2 `4 g6 C0 a6 ]    private static readonly float EXIT_TIME = 1f;
, A8 l  |0 m" \    private float exitTimer;' g1 \" I* F0 {+ s8 ^
    private KeyCode moveCommand;6 g8 O- V# o" E5 C/ n

( Y1 |! t' ^: u. a$ M4 T0 @    protected override void OnInit(ProcedureOwner fsm)
/ D6 A2 Q! M) C( P: I) V$ ~    {
# M1 [2 D/ E) q0 j6 _        base.OnInit(fsm);
% l! f7 s5 X% U( n) L( @8 u9 `- o    }( y. j  z7 [5 u8 P: ^8 l8 s! [- J

+ z4 J/ r7 f( G) A* I    protected override void OnEnter(ProcedureOwner fsm)
- g8 h2 [, i' W0 i  q) G- N    {
; }" P; S; B5 [( Z        base.OnEnter(fsm);: u0 q+ A+ a0 I& t

% u  n* o# ?6 o        //进入移动状态时,获取移动指令数据+ n( h& T5 u- u& t  I$ K2 D
        moveCommand = (KeyCode)(int)fsm.GetData<VarInt32>("MoveCommand");
3 F- g4 d- N& c# G  p5 i" d% a. [    }
# G. I; ?. d- t# m2 d$ c% J# N
( N: W& n8 L$ {5 Y; w$ V    protected override void OnUpdate(ProcedureOwner fsm, float elapseSeconds, float realElapseSeconds)
. J, |1 }( y% \9 _& [6 u  |    {
' B, f6 Q* k# g7 J' m' Q        base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);! P* ^$ y( m1 z1 i2 G) F
& Y6 r/ a) y+ [. h, `/ D2 }
        //计时器累计时间3 D: k8 l/ Q: {% j+ d1 p5 K
        exitTimer += elapseSeconds;
# N2 o3 [) }4 x7 n; f: N$ D& \; p/ a3 i0 q
        //switch(moveCommand)
, a4 ]% w! ?% I/ {% L' k3 |1 T5 p        //{4 U2 L8 H  o# V; F
        //根据移动方向指令向对应方向移动: R0 b. H- T2 ~  m
        //}
- n( r/ }  B" r7 k7 ?- V5 M- t0 {" k/ \
5 \' Z. a: m& p+ L( O; B$ V) u, u        //达到指定时间后
. z1 k4 D( v- ]' L& E/ c2 r" g  B! w        if (exitTimer > EXIT_TIME), \4 O" P6 [. A5 i& B
        {0 u7 j$ ?0 j' Q3 C% d6 O9 m' q
            //切换回空闲状态) F4 Z* K/ V8 _) p
            ChangeState<IdleState>(fsm);
6 z! j/ Y7 N+ z1 H0 \4 X  z4 [6 Y        }
4 Z. {! Q1 ^. P# s. i4 `    }
' p: k# m( n; H9 e. o9 V, c
% R4 W/ ^; B! c5 _8 f2 @    protected override void OnLeave(ProcedureOwner fsm, bool isShutdown)& I% J4 W- m! k" c; P9 b
    {$ }, o' |' ?, p. u! z) u
        base.OnLeave(fsm, isShutdown);
+ D% Y0 x+ o: Z" a0 Z& O+ C0 o6 ]  n* d. ^/ N
        //推出移动状态时,把计时器清零+ i9 f7 r7 Y  P0 H, h* x0 X. L% |
        exitTimer = 0;! G/ ^7 n8 F  T/ m% U
        //清空移动指令! q: C! Q; ^$ _0 f" i
        moveCommand = KeyCode.None;6 D3 [% f" Y) s: {2 z
        fsm.RemoveData("MoveCommand");
- Y3 `- i! d. E0 O5 z5 u$ @/ F: k    }5 i: S, N6 v8 O9 K0 Z% o2 ?- l

; Q+ Z$ }  g9 j1 t( I. g    protected override void OnDestroy(ProcedureOwner fsm)$ \+ k7 Y5 w( W' N8 d# D1 r
    {
' y. H7 ^. t  i1 y" F+ l        base.OnDestroy(fsm);3 W+ p( H8 K. I) {" Y
    }
& z3 a: ~" Y# g7 W3 m: e}
: z) ?, w9 P  }- u3 h1 E' ~0 X- k+ ?9 E玩家类
: ?( G2 n) T) Q! {- ^! z4 d( R$ K! D( {
using System.Collections.Generic;. H' \! k9 N  T& z1 {0 Z
using UnityEngine;
7 \+ o0 I3 a+ A6 gusing GameFramework.Fsm;
+ {% [5 D4 U- H' A2 qusing StarForce;& O6 k( v0 s: x# P* c* C
$ x! ]7 B$ B; i- I7 z' A
public class Player : MonoBehaviour4 d7 n% N7 R% G4 d* |- `" t
{) M, Y+ T! e; V% d& g4 s4 A0 L
    //Player对象自增Id
+ Q; d' T6 o- U# [$ d0 g! {    private static int SERIAL_ID = 0;4 x% r" B8 C. R) i( Y5 n0 s
0 x3 F1 }& m# l
    private IFsm<Player> fsm;
" F1 g4 ~. B# t0 H5 \  n5 b
2 l% ^5 v* Q0 X& i' _/ y$ F) k& y    // Start is called before the first frame update
* l& B5 x0 U  G* i3 X9 J    void Start()
: z9 K  m. u# i2 K3 t" F$ R    {
& q7 [8 ^7 w! x% M& l! S. q# u9 t        //创建状态列表2 U1 K$ t; f7 u7 H/ n
        List<FsmState<Player>> stateList = new List<FsmState<Player>>() { new IdleState(), new MoveState() };' P4 R. J& S/ ^1 I' C& ?/ r- S
        //创建状态机,注意,对于所有持有者为Player类型的状态机的名字参数不能重复,这里用自增ID避免重复2 h8 G/ [% _! X4 m
        fsm = GameEntry.Fsm.CreateFsm<Player>((SERIAL_ID++).ToString(), this, stateList);1 h# e" L/ H1 X# y
        //以IdleState为初始状态,启动状态机  X6 Y. C1 H) Q$ e4 i
        fsm.Start<IdleState>();2 f" w+ N: s' [+ ^
    }* z4 u) b4 _# e$ b

( c" s- T: B' H7 Z5 a8 v    // Update is called once per frame. J. W& h# g1 {/ V
    void Update()1 _2 {) T8 \' u% C# U: {! u  \6 W* x
    {
* [' N. |! J5 G3 G& X1 U0 t$ X1 c$ @$ [
    }( C* B8 x6 s7 B! d6 b: ^6 @  G6 q

* c2 ~7 d0 I; m, h3 |) O2 e- J4 ]: K    private void OnDestroy()' W3 \+ D7 r- Q  t
    {# d: U% e' \# t5 z  T
        //销毁状态机
# L& ~* N7 N! U        GameEntry.Fsm.DestroyFsm(fsm);
) m+ R8 z) _' b" H8 S% S    }9 j# M% s/ O& k  Q; S
}4 y4 e0 n/ S: p5 w9 Q& h+ M# G  \
Inspector面板+ y& |# y$ C" `
, W) H8 k# a5 r
) j# I! j, b- z. G

: a. O! ^/ a' J0 |/ Z" d4 m GameFramework解析:有限状态机(FSM)-2.jpg 6 q8 ?2 W$ j4 G! H$ x* v

$ Z! ^9 i0 B$ H) g, Y6 c- {4 KFSM组件的Inspector面板可以实时看到所有正在运行的状态机,以及这些状态机当前处于的状态、运行时间。, @7 e5 h& |1 X& A, I' T4 G
最后5 N3 ^; {. [8 {5 S/ c0 i4 n& z
: N* D" z+ R6 c, ]
GameFramework解析 系列目录:GameFramework解析:开篇( K# k$ A2 q1 v. O
个人原创,未经授权,谢绝转载!
悠然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 | 显示全部楼层
太棒了,敲碗等更新
3 M" y% h+ l% i' I笔芯
杏花微雨2017 | 2022-5-22 13:01:10 | 显示全部楼层
感谢分享~
123471661 | 2022-5-23 00:43:39 | 显示全部楼层
超认真
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

295

金钱

0

收听

0

听众
性别

新手上路

金钱
295 元