FSM有限状态机在游戏中的运用

思考并回答以下问题:

本章涵盖:

  • FSM基类设计
  • 子类设计
  • 实体类设计
  • 技能子类
  • 游戏案例分享
  • 小结

FSM全称是Finite State Machine,中文称为有限状态机,在游戏开发中应用非常广泛。它的修饰词Finite是有限的意思,运用FSM解决问题的前提条件是“有限”个状态。在游戏中经常使用它处理状态转化程序逻辑,尤其在动作游戏中,比如在ARPG游戏和运动类休闲游戏中,角色的动作会有不同的动作状态变换,比如idle->walk->run->attack->idle等。在使用FSM编程之前,先把FSM架构设计图介绍一下,如图1所示。

图1 FSM架构设计图

图1架构的最顶部FSM模块是所有状态的基类,所有子类都需要继承该类,FSMState类是定义状态的枚举,这些枚举变量与子类的有限状态机是一一对应的,也就是最底层所具体实现状态实体的逻辑。下面根据FSM架构图逐一实现它们的类编写工作,首先设计一下FSM基类。

FSM基类设计

有限状态机一定要有状态的定义。状态定义可以使用枚举或者是字符串,在二选一的情况下,我倾向于选择枚举,因为这样程序操作起来更方便,当然程序员各有所爱。表示状态变化的枚举代码如下所示。

FsmState.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace Game.FSM
{
public enum FsmState
{
FSM_STATE_FREE,
FSM_STATE_RUN,
FSM_STATE_SING,
FSM_STATE_RELEASE,
FSM_STATE_LEADING,
FSM_STATE_LASTING,
FSM_STATE_DEAD,
FSM_STATE_ADMOVE,
FSM_STATE_FORCEMOVE,
FSM_STATE_RELIVE,
FSM_STATE_IDLE,
}
}

定义了枚举FsmState,枚举值包括了游戏中定义角色的大部分状态,比如:FSM_STATE_RUN跑步状态、FSM_STATE_DEAD死亡状态等,还可以继续添加所需要的角色状态。下面开始设计基类代码。

EntityFSM.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace Game.FSM
{
using UnityEngine;
using Game.GameEntity;
using System.Collections;

public interface EntityFSM
{
bool CanNotStateChange{set;get;}
FsmState State { get; }
void Enter(Ientity entity , float stateLast);
bool StateChange(Ientity entity , EntityFSM state);
void Execute(Ientity entity);
void Exit(Ientity Ientity);
}
}

上面的代码相对来说比较简单,代码量非常少。在FSM基类中,定义Enter函数表示的是进入状态,StateChange函数表示的是状态改变,Execute函数表示的是状态执行,Exit函数表示的是状态停止。眼尖的读者注意到它们的参数是Ientity。Ientity类非常重要,后面会给大家详细介绍。现在父类已经设计完成了,下面开始设计子类。

子类设计

有限状态机子类是根据具体的动作实体设计的,拆分每个动作单独作为一个状态实体类。从前面的FSM架构图中可以看出,每个子类都有自己的逻辑方法和属性,子类的共性在父类中已经定义了,在子类中只需实现而已,下面把子类的代码给大家展现一下。

EntityIdleFSM.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
namespace Game.FSM
{
using Game.GameEntity;
using UnityEngine;

public class EntityIdleFSM : EntityFSM
{
public static readonly EntityFSM Instance = new EntityIdleFSM();

public FsmState State
{
get
{
return FsmState.FSM_STATE_IDLE;
}
}

public bool CanNotStateChange{get;set; }

public bool StateChange(Ientity entity , EntityFSM fsm)
{
return CanNotStateChange;
}

public void Enter(Ientity entity , float last)
{
entity.OnEnterIdle();
}

public void Execute(Ientity entity)
{
if (EntityStrategyHelper.IsTick(entity, 3.0f))
{
entity.OnFSMStateChange(EntityFreeFSM.Instance);
}
}

public void Exit(Ientity entity)
{

}
}
}

在子类文件中,它继承了父类的Enter、Execute、Exit函数,这些都是FSM通用的方法。这些函数的参数都有Ientity类,设计Ientity类的目的是对角色状态的切换做统一接口处理。它不仅实现了FSM的状态切换接口,同时也包含实体的一些基本功能函数,比如重生、复活、技能释放等,Ientity类是FSM的驱动类,由于该类包含的功能比较多,下面把Ientity类的实现给大家展示一下。

实体类设计

Ientity实体类是每个FSM子类都需要使用的,实体状态之间的转换需要通过实体类Ientity类去操作。为了统一管理,将FSM的状态转换放到了Ientity类中,当然也可以单独拿出来进行处理。Ientity类的核心代码是FSM的状态转换,它封装了统一的转换接口供角色不同的动作状态切换,Ientity类核心代码如下所示。

Ientity.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class Ientity
{
/// <summary>
/// 状态改变
/// </summary>
/// <param name="fsm"></param>
/// <param name="last"></param>
public void OnFSMStateChange(EntityFSM fsm, float last)
{
if (this.FSM != null && this.FSM.StateChange(this, fsm))
{
return;
}

if (this.FSM == fsm && this.FSM != null && this.FSM.State == FsmState.FSM_STATE_DEAD)
{
return;
}

if (this.FSM != null)
{
this.FSM.Exit(this);
}

if (this.FSM != null)
this.RealEntity.FSMStateName = fsm.ToString();

this.FSM = fsm;
StrategyTick = Time.time;
this.FSM.Enter(this, last);
}

public void OnFSMStateChange(EntityFSM fsm)
{
if (this.FSM != null && this.FSM.StateChange(this, fsm))
{
return;
}

if (this.FSM == fsm && this.FSM != null && (this.FSM.State == FsmState.FSM_STATE_DEAD))
{
return;
}

if (this.FSM != null)
{
this.FSM.Exit(this);
}

this.FSM = fsm;
if (this.FSM != null)
this.RealEntity.FSMStateName = fsm.ToString();

StrategyTick = Time.time;
this.FSM.Enter(this, 0.0f);
}
}

在Ientity类中实现了两个重载函数,一个函数是public void OnFSMStateChange(EntityFSM fsm,float last),另一个函数是public void OnFSMStateChange(EntityFSM fsm)。

如果需要做状态变换,直接调用这两个函数中的一个即可实现状态切换。此外,在开发中也将技能作为状态变换的一个子类处理。下面给大家介绍一下技能子类,当然技能子类也可以作为技能系统去单独处理,这里介绍的目的是可以将FSM完全用于游戏玩法的架构设计。

技能子类

游戏中的玩家或者NPC都会释放技能,技能对于FSM来说也是一种状态的改变。游戏中的实时同步也是与状态有关系的。注意,架构没有好坏之分,开发者用着方便就是一个好的架构设计。用着方便包括两方面:一是根据需求可以随意扩展,二是代码模块之间的耦合性比较低,技能子类的代码如下所示。

EntityReleaseSkillFSM.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Game.GameData;
using MO.MOBA.Tools;

namespace Game.FSM
{
using Game.GameEntity;

public class EntityReleaseSkillFSM : EntityFSM
{
public static readonly EntityFSM Instance = new EntityReleaseSkillFSM();

public FsmState State
{
get
{
return FsmState.FSM_STATE_RELEASE;
}
}

public bool CanNotStateChange{get;set;}

public bool StateChange(Ientity entity , EntityFSM fsm)
{
return CanNotStateChange;
}

public void Enter(Ientity entity , float last)
{
entity.OnEntityReleaseSkill();
}

public void Execute(Ientity entity)
{
entity.OnEntityPrepareAttack ();
}

public void Exit(Ientity entity)
{

}
}
}

技能子类的处理方式跟上面实现的实体子类类似,在这里介绍一下函数的功能,Enter函数执行的是技能释放,Execute函数执行的是攻击准备。这样FSM整个系统就完成了,其他子类读者按照这个模式照猫画虎就可以了,下面通过案例的方式讲解一下如何在游戏中使用FSM有限状态机。

游戏案例分享

前几节实现了FSM有限状态机的架构设计,同时利用该思想设计了很多的实体FSM,用于不同动作或者不同技能之间的转换,在这里限于篇幅就不给大家一一列举定义子类的FSM了,它们的书写方式都是类似的。下面给大家展示一下游戏中使用FSM设计的类,首先将编写的FSM代码统一放在Unity项目下的FSM文件夹下面,效果如图2所示。

图2 FSM脚本

在使用时,可以通过调用Ientity类中的函数进行状态切换。调用函数举例如下。

1
OnFSMStateChange(Game.FSM.EntityIdleFSM.Instance);

该函数已在封装的Ientity类中给出,在这里提供了不同的动作状态,通过配置文件去配置操作,这也是策划的需求。游戏开发是数据驱动的,程序的设计架构、文本文件的读取也要在设计时考虑到,这样策划可以根据需求配置文件内容。程序加载读取文件内容并将它们显示出来,配置文件的XML文件内容如图3所示。

图3 配置动作文本文件

在XML文件中有n2RandomAttack字段,这个字段的内容表示的是角色动作名字,配置表配置的三个动作名字分别是:attack、attack2、attack3。本案例使用的是老动画系统,如果是新的动画系统可以用触发条件表示不同状态切换,它们的原理是一样的。老动画系统的参考设置如图4所示。

图4 老动画系统的参数设置

小结

以前做游戏架构设计时,开发MMORPG网络游戏时没有使用FSM有限状态机,所有的战斗技能都是在一个类里面封装的,导致后期扩展非常麻烦,所以我痛定思痛把架构重新调成FSM有限状态机架构,虽然花费了一些时间,但是后期开发非常快。一个好的游戏架构是非常重要的。

0%