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
* 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>(&#34;MoveCommand&#34;, (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>(&#34;MoveCommand&#34;);
/ 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(&#34;MoveCommand&#34;);
' 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
- 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 ]个人原创,未经授权,谢绝转载! |