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