GameFramework解析:有限状态机(FSM)* ?+ Q8 c, M4 K: k1 _
! }$ U6 \2 k z) z" c6 l# R什么是有限状态机; M! _7 {% {5 K1 w( M! h
$ m/ b% |9 D( M" I; |+ C有限状态机的概念相信很多同学都清楚了,不清楚的可以参考一下书籍《游戏编程模式》中状态模式一节,里面讲得十分清楚。FSM在游戏中常用于玩家控制、怪物AI、UI状态、游戏流程控制等。
2 n3 @" h# L. n( B$ W: G+ G/ W有限状态机的实现
9 `4 U* X" j& l* _$ Q9 ^' W
9 T+ l' M: K4 C+ D结构
% ]$ L2 _# P5 p
+ c; d; }8 a ^+ @
" T! w! {& P3 E, ^3 w* v0 N# R3 k
0 m. x) E% v1 e" o
5 l' L+ R1 j9 o' u, [有限状态机的实现我们可以把他分成3部分,上图中从上到下每一行就是一部分,分别是状态部分(FsmState),状态机部分(FsmBase、IFsm、Fsm)以及状态机管理器部分(IFsmManager、FsmManager)。* d c; w; ~1 ]$ T4 h$ n
状态类FsmState8 P9 v8 X* N F* t5 E4 S/ W0 n8 n) T
3 k8 ?5 l5 G" S# {' m. C& L' a1 E% j, h$ f( D- p1 |' @; q/ Y! a
- FsmState为有限状态机状态基类,所有用于有限状态机的状态都需要继承自此类,泛型参数T需要传入状态持有者类型。# y# G0 [) I" S8 y3 a2 }- T
- OnInit、OnEnter、OnUpdate、OnLeave、OnDestroy为状态的生命周期方法,其中OnInit和OnDestroy分别在状态创建和销毁时调用,只会调用一次,而OnEnter、OnLeave分别在进入状态和离开状态时调用,可能会调用多次,而OnUpdate则是在进入该状态后每帧调用。& V/ Z( k( R2 T
- ChangeState用于切换到下一状态。ChangeState实际是用该方法传入的FSM对象调用FSM类里的ChangeState方法,正式执行状态切换逻辑。3 n: n: D7 ]- Q# ^6 X$ h% K' N
状态机类Fsm
! Q# ~) m0 J6 ]: F! v" \, X9 U! ^* G: e
4 _ {( B. Q& S: ~* O
- Fsm对象通过Create方法创建,需要传入状态机拥有者类型、状态机名字、状态列表3个参数,Create方法为静态方法,由FsmManager调用。参数状态列表将会保存在字段m_States中,并调用所有状态的OnInit方法。% u* U5 i; l7 H+ ~0 {' A% m' _
- 状态机通过Start方法启动,传入初始状态类型作为参数,方法内部会调用该状态的OnEnter。! V% f9 J5 M. q
- Update方法会每帧调用当前状态的Update方法,且会计算当前状态机进行了的累计时间,可通过CurrentStateTime获取。# `: D8 k6 B6 M5 d! B& W
- GetAllState和GetState方法可以获取注册进这个状态机的状态对象。
) A% b2 \! O; o$ ]& g; F - 状态机内通常不同状态之间是需要有数据交互的,GetData,SetData,HasData,RemoveData这四个接口则提供了不同状态间数据交互的功能,分别对应获取数据、设置数据、是否有数据、移除数据,数据以key-value形式存在于字典m_Datas中。
9 |6 E% n" i% g - Shutdown方法会回收FSM对象,此方法由FsmManager的DestroyFsm方法调用。
6 b, C& ^8 u" q5 e4 u 状态机管理器FsmManager
# |5 W% U9 g0 B/ {8 U! l( \1 A# C, ^9 @: O
- X0 }, y- Q# [# O
- 外部创建新的状态机统一通过FsmManager的CreateFsm接口创建,参数同FSM类中的静态方法Create,此方法会调用Fsm类的Create创建Fsm对象,然后以key-value的形式储存在字段m_Fsms中,注意m_Fsms是Dictionary类型,以TypeNamePair为Key,TypeNamePair对象是结合状态机持有者类型和状态机名字字符串类型参数组成,为了保证Key的唯一性,对于同样类型的而不同实例的持有者,应该传入不同的状态机名字。
7 w5 M( U& \4 v# X) F - GetFsm、GetAllFsm、HasFsm,向外部提供某个状态机的查询、获取,需要传入持有者类型和状态机名字两个参数。+ i4 |: {9 s3 }( a6 q; H
- DestroyFsm可销毁特定状态机,会调用对应Fsm对象的Shutdown方法,并在FsmManager的m_Fsms字段中移除该状态机。* q2 d6 M: j) N7 n
internal override void Update(float elapseSeconds, float realElapseSeconds)
( J# x% e9 c6 F* s4 F. O, R{1 }3 \0 o6 l% |2 X
m_TempFsms.Clear();
2 M) Z7 _1 z+ T2 S7 q t3 A if (m_Fsms.Count <= 0)
! ^2 i0 ~ M' _3 ~* i( } {4 a3 s0 z7 h) @% ^5 U
return;" q% M! v; A% U0 J4 Z% `
}% ]( E* u; H2 j; |3 u+ { }4 M
) n. J- u5 m! [/ N$ H- \' B
foreach (KeyValuePair<TypeNamePair, FsmBase> fsm in m_Fsms)/ \/ C+ D5 b- o* b G7 ^
{8 B) B- |" n2 ~4 d
m_TempFsms.Add(fsm.Value);( k. z9 U0 Y o/ m
}4 p7 `% M; O7 j" S
. n1 w @6 E8 p
foreach (FsmBase fsm in m_TempFsms)
/ P' I9 F" A5 U* v0 v {
8 K( t3 E$ |0 e. L6 z. U# a% X if (fsm.IsDestroyed)2 b' a% Z$ r; b7 d
{
+ q5 j& p% {9 A5 [0 t7 j continue;+ W+ G5 c! \) G) `* ]$ e M
}
" W! P: C, {3 i5 `) A' r9 u& _8 `
% ~& C3 k/ p! H: c* ^0 c fsm.Update(elapseSeconds, realElapseSeconds);
: B1 k8 Q7 o8 n4 ~ }. s7 j" n0 X: N5 j1 X, `$ ^5 ]& F
}
0 @' K1 [% W/ K
" t5 l. [. p: h4 Z+ {' i z. @1 F- Update方法中会调用m_Fsms中的所有状态机的Update方法,值得注意的是这里并没有直接对m_Fsms进行foreach,而是添加到一个临时的列表中再进行循环调用,这样可以防止在迭代过程中,外部销毁某个状态机而从m_Fsms移除状态机对象时,造成迭代器失效。
3 ^7 E* g; c3 C* r: X5 Q 示例
$ O) r! P" ]9 }; X
+ a) j' X: ]0 l* J假设我们现在需要用状态来实现玩家的控制,其中包括空闲和移动状态,处于空闲状态下的玩家当检测到方向键按下时,会切换到移动状态,且根据方向键向某个方向进行移动,移动过程持续一秒。
1 ~: u8 r( Y$ W4 ^ 我们需要3个类去实现这一需求,其中IdleState、MoveState两个类分别对应空闲状态、移动状态,Player则为状态机的持有者,也是状态机要控制的主体。6 C: k4 G: v4 j; `9 F
空闲状态类
2 J9 [+ w' ]- A
6 y7 c, e/ t4 r- N# z1 U$ Ousing UnityEngine;
- C1 T8 V w) T5 d' e* ]using GameFramework.Fsm;
d. r' h2 F: ^, r: Yusing ProcedureOwner = GameFramework.Fsm.IFsm<Player>;# X! H: @% L; D
using UnityGameFramework.Runtime;' ]. q6 w, _4 f
0 ]- H' s0 A7 |) w/ P; a! l( _( f
public class IdleState : FsmState<Player>
4 K4 [( t: @, @{
/ R* e9 [7 g% ]% z H //触发移动的指令列表8 l8 E! c5 U) p5 M; ^% c! \6 h
private static KeyCode[] MOVE_COMMANDS = { KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow };
: ~- ]9 h( A9 l. m& e/ ~8 w8 B2 G& O4 K- N" c8 c& i
protected override void OnInit(ProcedureOwner fsm)
9 c( R& p( b4 Q/ I, ] {
2 F" t8 r+ h2 J base.OnInit(fsm);
4 b8 i& s7 d- H0 Q, Q6 O, C }
+ W+ T3 Q) o0 P2 N& J6 i1 f
; m F O; s0 ~5 J" h4 l. U* |" Y2 H } protected override void OnEnter(ProcedureOwner fsm)
$ P- K2 z; m- F- C {
9 g, D$ Z2 Q* T base.OnEnter(fsm);
; t3 Y+ m# R: w3 u m! D9 } }
+ {" z: ?' q0 ]$ |; a! J0 N8 ]% [5 T! T, ]( @, l8 C M) s
protected override void OnUpdate(ProcedureOwner fsm, float elapseSeconds, float realElapseSeconds)
2 ~3 E+ a! U6 v2 `; m {, Y7 Q9 \& o$ f' g1 q
base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);
/ _" h8 B+ J5 ~
5 G- @0 V" F2 _4 n0 U, K foreach (var command in MOVE_COMMANDS)
; D, c4 o# ~$ ?6 k! V- p6 O {6 Z* R5 P8 P7 M _- c) F& Q
//触发任何一个移动指令时3 ^4 l1 L G; e9 N# O$ P
if (Input.GetKeyDown(command))# w( E# h( t4 n2 m
{. k( u7 H% p% Y5 e. a
//记录这个移动指令& O* l \% B& C) w
fsm.SetData<VarInt32>(&#34;MoveCommand&#34;, (int)command);3 q- y2 d- m4 j3 s# z, D
//切换到移动状态/ n; r% Q) Z" H5 H8 p6 i$ G# @/ M
ChangeState<MoveState>(fsm);
1 I& s: H2 M7 c' H& o6 N6 l4 ]0 J8 B }
# e* F' ^. t& E: B9 z }- {1 V% I( f2 M( ^# y6 h2 ^# x# d
}
( D6 s; t; D% M5 n' j# ~3 W8 ~
; U3 d7 b4 r* Y$ L3 s. x protected override void OnLeave(ProcedureOwner fsm, bool isShutdown)
3 F2 H" u4 f) Z2 \ U" w R {
; T7 F; f$ `$ C base.OnLeave(fsm, isShutdown);6 q+ @) k( f4 M3 P' a8 c
}) K' E* o9 H7 H! b1 @+ ^
, t' G" l: z# b' [- h- h7 [ protected override void OnDestroy(ProcedureOwner fsm) `" x' x- }3 d Z; S8 O1 \
{0 ]" d$ I- r6 y) T) I# ?
base.OnDestroy(fsm);! t2 Z$ N: q i7 f: \4 T
}( c0 d ]7 w! \. ~( n( m/ ~% g
}
+ H! `, K2 R5 o7 h7 Y移动状态类4 }+ H; U7 m4 I
# o( p- f3 i" Z F* P; b$ k! ^
using UnityEngine;
9 U+ }* S1 ~8 t' ousing GameFramework.Fsm;
4 T4 P1 X# w7 u0 u/ g/ Wusing ProcedureOwner = GameFramework.Fsm.IFsm<Player>;
+ _9 c6 R8 z9 J* Xusing UnityGameFramework.Runtime;
3 v% ?, v% B: z
0 c# Z+ a+ O3 t, spublic class MoveState : FsmState<Player>4 ]+ H& j8 V( j+ _
{
4 \$ k# {8 `, U2 y+ } private static readonly float EXIT_TIME = 1f;
+ q$ o7 J& k$ d7 c( U4 I private float exitTimer;
( Q7 t' c0 L: k+ ]: H% v private KeyCode moveCommand;5 ^, f1 `/ J% G% g- U# \
; o. `* U a, l z protected override void OnInit(ProcedureOwner fsm)
' Y! H; Y: q: N, }' } {9 `: l! Y s% V( X& o0 N' g
base.OnInit(fsm);
. W8 H n. I1 p1 t }8 x& s5 N% e$ U& v
% G$ T2 L9 Y2 u) n protected override void OnEnter(ProcedureOwner fsm)
3 J; A1 R9 d7 i8 @. y2 G" F {! l% |! N: m4 r* z% j. Z
base.OnEnter(fsm);, e" ]* o0 u9 s% q' L, e& V
* s2 T1 L$ S" {# ^5 o
//进入移动状态时,获取移动指令数据
' Z8 b9 O. _2 ^+ l) d8 `+ B e' `0 ` moveCommand = (KeyCode)(int)fsm.GetData<VarInt32>(&#34;MoveCommand&#34;);# R1 {* D- Z8 B9 h
}; v( z! ~" t1 u; m! f- W# n' \8 v
3 t5 F! d) n% ` z' ^ ?0 s% ~" H7 q protected override void OnUpdate(ProcedureOwner fsm, float elapseSeconds, float realElapseSeconds)
( h* Q7 j8 Y& R& H, p# x {8 p8 @# g% L5 b2 n, _$ g
base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);
1 ?( g1 q7 T: I$ P q7 z4 K/ p, `' R' ^
//计时器累计时间! u4 d4 l* i) |3 T9 [: N
exitTimer += elapseSeconds;
) q4 j1 | j5 K' p8 i. c+ }& M/ O4 l* T# h) e% i
//switch(moveCommand)% W. w6 c/ D- g2 |6 B
//{1 u n* f5 z& `, ?" y; v
//根据移动方向指令向对应方向移动
: ]1 @+ r6 ^8 Q! L3 ] B //}' [; b N$ I: w* b0 D8 N
5 a7 x- U- {( N2 n
//达到指定时间后
3 s' z/ L; a! u+ ~8 f* P if (exitTimer > EXIT_TIME)( b1 x }1 k1 Q5 s/ q% e% a5 ~
{
2 E9 h( k, s5 U+ C* w //切换回空闲状态
% n5 |- {; w h5 D- c0 v9 G ChangeState<IdleState>(fsm);. M$ P) L3 u3 W, a% v" v# C4 X
}$ p' _! `8 d! D0 p
}
. P4 P' o! a1 t- ]5 i* b! c g, Y) F' G; H: i' y' V
protected override void OnLeave(ProcedureOwner fsm, bool isShutdown)7 P4 g* J, Z0 m. X0 i" z& z, L& ?
{
7 D- j, i: }2 T( x8 }' m4 e, }' i base.OnLeave(fsm, isShutdown);2 g* V' i5 h7 x: |: g: S/ j
3 [1 u1 {6 k8 k% n4 [ //推出移动状态时,把计时器清零2 a* O: E i. I4 J- u
exitTimer = 0;" C; k5 S$ P: ~0 P# X) n
//清空移动指令
8 U3 ?( n9 S6 {2 {7 [: i: i moveCommand = KeyCode.None;' A" g* h0 W5 O4 y f+ L1 `0 x( U
fsm.RemoveData(&#34;MoveCommand&#34;);
; b% a) c( u! G. J, y0 M7 g% C }
/ W+ _& ]/ ]$ y5 }! _. E: h; O' E
. T1 M3 I' @8 x" C! b) |3 o protected override void OnDestroy(ProcedureOwner fsm)
9 A: i" u$ X+ v# g {
3 G; ]" l* \. l base.OnDestroy(fsm);# U( A/ `1 Y( l
}% a" g# D o# u, o5 j
}2 J7 y' v6 `% F6 [! |! {
玩家类
: U& J/ S8 R+ H: |. ~6 `) t9 o2 S6 S! d$ @+ n4 o
using System.Collections.Generic;# R/ P8 E6 f8 _4 P; H& d6 a
using UnityEngine;" f; S, K/ l z7 X# _1 L8 \
using GameFramework.Fsm;
1 J; b% q1 @3 ?9 D7 [3 C/ d' lusing StarForce;
7 S# u! X' x; n
& d; z2 u3 r4 j7 V2 Vpublic class Player : MonoBehaviour
1 q1 X* {7 \6 r+ K: {, D" n{) ?, R0 e' C6 Q/ k. s
//Player对象自增Id. T# W+ t$ W% {$ b' e
private static int SERIAL_ID = 0;/ k1 w: q( f' l Y( a) z
" @+ ?8 c. J5 P private IFsm<Player> fsm;# s5 P8 y3 Z7 k" a
3 `3 j+ B4 ]0 m: r' h; u; { // Start is called before the first frame update
( B7 b: o# o. t. i' D/ ~# i& K void Start()' @, a9 y. D T; W* G5 q( [' P
{
4 k W( Y/ _7 N; n //创建状态列表7 q0 P) Q# z) x( [1 ]9 v, ?" n" {8 @
List<FsmState<Player>> stateList = new List<FsmState<Player>>() { new IdleState(), new MoveState() };
& ]& W# y$ G. k r //创建状态机,注意,对于所有持有者为Player类型的状态机的名字参数不能重复,这里用自增ID避免重复" T6 U! O7 j& j) c* g0 D
fsm = GameEntry.Fsm.CreateFsm<Player>((SERIAL_ID++).ToString(), this, stateList);
/ @, D. H4 n8 L4 T //以IdleState为初始状态,启动状态机
2 @" U, A' h( W: E fsm.Start<IdleState>();. M3 q! Z8 D: v( o# A, c
}
3 n& o& l2 a: o9 j( f3 G1 ]3 n& Y0 O
// Update is called once per frame
H. T4 I: n9 T void Update(). W5 Z3 w- |' L0 f$ Q6 g# Y
{! @4 M3 H: M3 h0 g0 Q, p
3 d: w1 \/ F0 |; r9 Y. x }
) H5 I" r& ^* x/ _2 D$ @
, @ w6 ]4 T0 b7 s private void OnDestroy()4 s4 q$ r; O2 [3 l% K0 S! r5 ^
{/ n, { P+ W- ~1 W3 s$ ]9 E
//销毁状态机
; J0 {0 o% C2 }2 W8 i2 M+ c, O1 L GameEntry.Fsm.DestroyFsm(fsm);: b2 B! T6 T- b7 Q( P, t
}* Q9 }4 r- w5 d6 e& V- d
}
2 H/ p' Q G6 Z! E/ k" ~Inspector面板9 M0 R; w2 j4 M6 y1 I8 ~/ v
0 B& @7 d5 m$ h U" e
/ r* D: f ]; d9 J7 {4 M
9 _. Y# a1 z( s" r; S
( `# F9 O2 I6 _3 j, I- k1 v# v
. C8 ?: n- a* J* o: y) @0 Z, {# k. x
FSM组件的Inspector面板可以实时看到所有正在运行的状态机,以及这些状态机当前处于的状态、运行时间。$ v: Q- [/ C7 D/ g F9 ^- i
最后, H5 Q5 m6 k5 L' |
2 \2 q5 N) C: C. l0 O: ^/ V
GameFramework解析 系列目录:GameFramework解析:开篇8 D% k- n$ L; j8 \+ z
个人原创,未经授权,谢绝转载! |