FMS 状态机-用这个能干啥?
尤记得刚开始写代码之时初见状态机感受到的代码逻辑设计的简洁精妙。
FMS 状态机初识
在游戏行业的基本都见过 UNITY3D 的动画状态机,即一个简单的有限状态机。状态机主要玩法处于当前状态下满足某条件进入另一个状态。
FMS 适应情景分析
上面图示貌似没法表达出状态机的优势,但是里面 状态/事件/处理事件这些概念基本都在写代码的过程中接触过,于是我们尝试下通过手写日常代码理解下不同写法里的不同思考方式。
解决一个简单场景:进行开关门
用 if else 编写状态的逻辑
//-----------------伪代码---------------------------
//门开关状态
var beOpen =false
//打开门
let openDoor=()=>beOpen=true
//关闭门
let closeDoor=()=>beOpen=false
这个代码看起来没问题,接着我们让情况变复杂点,给门加个锁,我们锁门怎么锁,关上门后用钥匙锁上门/用钥匙解锁后打开门,如果接着 if else 写
//-----------------伪代码---------------------------
//门锁状态
var beLock=false
//门开关状态
var beOpen =false
//开锁
let unlockDoor=()=>{
if(!beLock){
console.log("门都开锁了,不需要开锁")
return
}
//如果需要处理门开了情况去开锁,思考下真的需要吗,不需要
{
if(!beOpen){...}
}
//如果需要处理门关了情况去开锁逻辑,思考下真的需要吗,貌似有可能
{
if(!beOpen){...}
}
beLock=false
}
//打开门
//let openDoor=()=>beOpen=true
let openDoor=()=>{
if(!beLock){
console.log("门还没解锁,无法开门")
return
}
//如果需要处理门开了情况去开门逻辑,思考下真的需要吗,貌似有可能
{
if(!beOpen){...}
}
beOpen=true
}
//关闭门
let closeDoor=()=>{
//如果需要处理门锁了情况去关门逻辑,貌似不需要
{
if(beLock){...}
}
beOpen=false
}
//锁门
let lockDoor=()=>{
if(beOpen){
console.log("门还没关,无法上锁")
return
}
//如果需要处理门开了情况去锁门逻辑,思考下真的需要吗,貌似二傻子
{
if(!beOpen){...}
}
beLock=true
}
当我们写以上代码考虑两个状态(门开关状态、门锁状态)的各种情况时是不是已经感到疲惫,没感觉的话再加一个红外感应开关锁状态试试
用 Switch Case 改写下
由上面的 if else 的逻辑就知道我们总是在考虑各种门状态和该状态下进行操作的情况,于是我们试着用 swith case 分解下情况
//-----------------伪代码---------------------------
Switch(doorState){
case "open":
//开门状态下,可以关门
if(action=="closeDoor"){
//doorState进入 close
}else{
//没有其他动作
}
break
case "close":
//关门状态,可以开门,可以锁门
if(action=="openDoor"){
//doorState进入 open
}else if(action=="lockDoor"){
//doorState进入locked
}else{
//没有其他动作
}
break
case "locked":
//门锁了状态,可以开锁
if(action=="unlockDoor"){
//doorState进入close
}
break
}
按照状态思维可以将现实情况进行简化分类(开门状态、关门状态、锁门状态)后可以发现:
- 在开门状态下,我们只需要处理开门状态下的情况(关门)
- 在关门状态下我们只需要关门状态下的情况(开门、上锁)
- 在锁门状态下只需要锁门状态的情况(开门)
这样分解下来我们的代码逻辑应该变清晰了一点吧,没有被几个状态搞迷糊掉,试想一下如果在这个基础上再加一个半开门状态,应该也是很容易处理,就是增加一个 case 就行了,是不是体会到了状态分解的妙处。
这里接着延伸思考状态切换的问题:如果从关门变为半开门则需要侧身进入,如果从关门变为全开门则直身进入,上述代码怎么修改,浅分析一下,可以在 case 状态里根据不同 nextState 做不同处理。
用 FMS 改写下
fms 实际就是干的就是 switch case 这些事情,只是将 switch case 这种耦合强的代码分成一个个小 class,一个 class 即一个 case,将状态的钩子(进入钩子、离开钩子、初始化钩子、处理事件钩子(可选)、tick 钩子(可选))开放出来(abstract function) 进行自定义实现,这样我们就只需要思考单个状态下的各种处理逻辑.
//-----------------伪代码---------------------------
class baseState{
OnEnter(preState)
OnLeave(nextState)
HandleAction(action)
}
class OpenState extends BaseState{
OnEnter(preState){
//进入钩子函数,可以处理进入逻辑
console.log(`我从${prestate}过来的`)
}
OnLeave(nextState){
//离开钩子函数,可以处理离开逻辑
console.log(`我要去${nextState}了`)
}
HandleAction(action){
//开门状态下,可以关门
if(action=="closeDoor"){
//doorState进入 close
}else{
//没有其他动作
}
}
}
//以此类推,其他 状态class
TIP:文章题外话,是否看到了 React 和 Redux 的影子
FMS 延伸
当情况越来越复杂,可能就需要下列更适合的状态管理模式
- 并发状态机:同时多个状态
- 分层状态机:父子状态嵌套,构成类似状态树结构
- 行为树
启蒙文档: