序言介绍
有限状态机(finite state machine)简称FSM,表示有限个状态及在这些状态之间的转移和动作等行为的数学模型,FSM是一种逻辑单元内部的一种高效编程方法。使得程序逻辑清晰易懂。
用处:各种通信协议发送方和接受方传递数据对消息处理,游戏AI等都有应用场景。
主要分为两种实现方法:
一、if,switch条件语句实现
这是看到问题后最直观的解决办法。
这种方法实现的状态机,在系统较小(状态少)时,简单易懂,条理清晰,但在系统复杂(状态多)时,则有着难以扩展和维护的缺点。这里不做代码分析了。
二、映射实现
映射分为 方法映射 和 表映射。
程序设计思路大致如下:
- 使用状态转换图描述FSM
- 状态转换图中的结点对应不同的状态对象
- 每个状态对象通过满足某个事件(表现为环境中的一个具体条件)转换到另一个状态上,或者保持原状态不变。
a.方法映射
方法映射即状态满足转换条件后,返回某一特定方法,也称为函数指针。
将每个状态写成类,在不同的满足条件下返回不同的函数指针,也执行不同的方法。
可以参考https://blog.csdn.net/qq_22337119/article/details/103310353 糖果机的案例。
而方法映射一般用于这种糖果机这种类单机式的设计。
b.表映射
更准确地说,是表中数据映射,在每个状态中都使用一张转换表来表示映射关系,转换表的索引使用输入字符来表示。此外,由于通过转换表就可以描述不同状态之间的变化,那么就没有必要将每种状态定义为一个类了,即不需要多余的继承和虚函数了,仅使用一个State即可。(也符合少用继承,多用组合的原则)
在项目中,更多的是用此方法,如游戏中的 挑战副本流程, 战斗过程, 角色行为等。
进阶 - 分层状态机
(引自https://www.cnblogs.com/chencheng/archive/2012/06/28/2564336.html)
对于状态较多的状态机,通常的设计会维护一个庞大的二维矩阵,所有状态耦合在一起,这往往导致维护困难,由于可能存在许多公共的特性,也会导致许多状态具有相同的处理函数。针对这些问题我们可以通过设计分层状态机来解决,主要的思想就是根据不同的功能模块设计出多个状态机,各个状态机分布在不同的层次上。上层状态机调用下层状态机时,上层状态机入栈,下层状态机变为当前处理状态机。通常我们使用堆栈来保存当前状态机的上层状态机信息。
分层状态机图解:
类似上图所示,可以进行多个状态机的嵌套。
下面介绍 副本流程状态机battleSM,战斗状态机fightSM,角色状态机roleSM 三层状态机的使用。
lua语言实现,此处只展示可运行代码作为示例,以免代码太多导致部分读者昏迷。。。
代码讲解
状态枚举和表数据
-------------------战斗流程fsm---------------------
-- 战斗状态枚举
TABLE_STATE_BATTLE = {
STATE_INIT = 1, -- 初始状态
STATE_APPEAR = 2, -- 出场状态
STATE_DIALOG = 3, -- 剧情状态 跑步遇敌+对话
STATE_FIGHT_RULE = 4, -- 战斗开始之前说明
STATE_FIGHT = 5, -- 战斗状态
STATE_SETTLE = 6, -- 战斗结算状态
STATE_END = 7, -- 结束退出
}
-- 战斗状态对应执行函数枚举 与战斗状态枚举TABLE_STATE_BATTLE索引一一对应
STATE_FUNC_BATTLE = {
"ProcFunc_Init",
"ProcFunc_Appear",
"ProcFunc_Dialog",
"ProcFunc_FightRule",
"ProcFunc_Fight",
"ProcFunc_Settle",
"ProcFunc_End",
}
-- 状态转换表 简单的顺序执行
STATE_TRANS_TABLE = {
Chapter1_1 = {
battleSeq = {1, 2, 3, 4, 5, 6, 7}, -- TABLE_STATE_BATTLE表
dialog = {10001}, -- 对话表
},
Chapter1_2 = {
battleSeq = {1, 2, 3, 4, 5, 3, 5, 6, 7},
dialog = {10002, 10003},
},
}
状态机实现
class方法为lua实现 c++类的一种实现,具体参照lua元表实现面向对象。
BattleStateMachine = class("BattleStateMachine", nil)
-- 创建 Battle状态机 - 战斗流程
function BattleStateMachine:Ctor(stateTbl)
-- body
print("-----创建状态机-----\n")
self.curState = TABLE_STATE_BATTLE.STATE_INIT
-- 数据有修改 深拷贝
self.stateSeq = table.deepcopy(stateTbl.battleSeq)
self.dialogSeq = table.deepcopy(stateTbl.dialog)
self.bFightEnd = false -- Fighting是否结束
self.bFightPause = false -- Fighting是否暂停
self.bFightWin = true -- Fighting是否暂停
end
-- 执行状态机
function BattleStateMachine:Execute()
-- body
print("-----状态机开始运行-----\n")
while self.curState do
self[STATE_FUNC_BATTLE[self.curState]](self) -- 相当于.调用
self:ChangeState()
end
print("-----状态机结束运行-----")
end
-- 初始状态
function BattleStateMachine:ProcFunc_Init()
-- body
-- 加载数据 角色信息,地图,spine等
-- 加载完毕
print("初始\n-----加载数据 ... 完毕-----\n")
end
-- 出场状态
function BattleStateMachine:ProcFunc_Appear()
-- body
-- 出场动画,round提示等
-- 播放完毕
-- RoleSM = RoleStateMachine.new()
-- RoleSM:ProcFunc_Appear()
print("出场\n-----动画播放 ... 完毕-----\n")
end
-- 剧情状态 跑步遇敌+对话
function BattleStateMachine:ProcFunc_Dialog()
-- body
-- 跑步遇敌、对话
-- 对话完毕
if next(self.dialogSeq) then
-- dialog
end
print("剧情\n-----对话".. self.dialogSeq[1] .. " ... 完毕-----\n")
self:ChangeDialog()
end
-- 战斗开始之前说明
function BattleStateMachine:ProcFunc_FightRule()
-- body
-- 说明
-- 说明完毕 时间制or玩家点击确定
print("说明\n-----说明 ... 完毕-----\n")
end
-- 战斗状态
function BattleStateMachine:ProcFunc_Fight()
-- body
-- 创建 fighting状态机
-- 战斗完毕 根据:分胜负或时间制
-- self.fightSM = FightStateMachine.new()
local update_fighting = function()
while not self.bFightEnd do
-- 战斗未结束
if self.bFightPause then
-- nothing
else
-- 启动 fighting状态机 类似于 battle状态机 顺序循环执行
-- 更新战斗时间
-- 检测战斗结果 有结果跳出
-- 更新角色信息 死亡、移除
-- 更新角色速度:出手顺序
-- 更新角色对象 状态机 RoleSM
-- RoleSM:Execute()
-- 处理特效碰撞
end
end
end
-- self:setTimmer(update_fighting, 0.3)
print("战斗\n-----战斗中 ... 结束-----\n")
end
-- 战斗结算状态
function BattleStateMachine:ProcFunc_Settle()
-- body
-- 战斗结算界面
-- 时间制or玩家点击确定
print("结算\n-----结算 ... 完毕-----\n")
end
-- 结束退出
function BattleStateMachine:ProcFunc_End()
-- body
-- 释放资源 releaseData
-- 退出
print("结束\n-----释放 ... 完毕-----\n")
end
-- 处理 转换状态
function BattleStateMachine:ChangeState()
-- body
self.preState = self.curState
table.remove(self.stateSeq, 1)
self.curState = self.stateSeq[1]
end
-- 处理 对话
function BattleStateMachine:ChangeDialog()
-- body
table.remove(self.dialogSeq, 1)
end
local battleSM = BattleStateMachine.new(STATE_TRANS_TABLE.Chapter1_2)
battleSM:Execute()
输出
-----创建状态机-----
-----状态机开始运行-----
初始
-----加载数据 ... 完毕-----
出场
-----动画播放 ... 完毕-----
剧情
-----对话10002 ... 完毕-----
说明
-----说明 ... 完毕-----
战斗
-----战斗中 ... 结束-----
剧情
-----对话10003 ... 完毕-----
战斗
-----战斗中 ... 结束-----
结算
-----结算 ... 完毕-----
结束
-----释放 ... 完毕-----
-----状态机结束运行-----
总结
可读性 逻辑清晰易懂 - 易于他人维护
实用性 使用便捷 - 调用方便
强壮性 维护方便 - 增加、修改容易
使代码更加 高内聚 低耦合
本篇只实现了副本流程状态机battleSM,其余两个并未实现,但用法相同,读者可以自行书写,欢迎留言讨论。