【状态模式】

状态模式主要解决的是当控制一个对象状态转换的条件判断过于复杂时,把判断逻辑转移到表示不同状态的一系列类中,从而简化复杂的逻辑判断。(如果状态转换条件判断很简单,就没必要用状态模式了)

【FSM的相关概念】

输入:在上一篇文章中,我们并没有显式给状态机输入,在这个实现中给显式的输入,我们暂且认为所有输入都是string类型的。在实际运用中,可以写一个输入类。

状态:表示对象的某种形态,在当前形态下可能会拥有不同的行为和属性。

状态机:每一个时刻只能处于一个状态;拥有对象所有状态的集合;会接受输入,并根据对象当前的状态转换到下一个状态

输出:确切的说,输出指转换到某个状态后,在这个状态内对象所有的行为、表现等都是输出。

动作:指状态机的行为,包括进入状态动作,离开状态动作

【FSM的实现】

using System.Collections.Generic;public class FSM //不要继承MonoBehaviour
{//状态机运行时状态参数,当前状态,上一个状态,下一个状态。//复杂的状态机状态关联多,需要这三个参数,简单的状态机不需要那么多public StateBase curState { get; private set; }public StateBase lastState { get; private set; }public StateBase nextState { get; private set; }//状态机内所有对象集合,简单的状态机不需要private Dictionary<RoleState, StateBase> states = new Dictionary<RoleState, StateBase>();//状态机整体的参数,简单的状态机不需要private StateBase defaultState;//状态机默认状态,也即第一个状态private StateBase exitState;//状态机退出状态,也即最后一个状态public void Init(StateBase defaultState)//状态机初始化{this.defaultState = defaultState;curState = defaultState;curState.Enter();}public void Update(string input)//状态机的执行,即切换状态{if(curState == null)return;nextState = curState.HandleInput(input);//处理当前输入,获取下一状态if (nextState != curState && nextState != null){curState.Exit();lastState = curState;curState = nextState;//状态切换curState.Enter();}curState.Update();}#region 状态机对外提供的方法public void AddState(RoleState stateId,StateBase state){states.Add(stateId,state);}public bool Remove(RoleState stateId){if (states.ContainsKey(stateId)){states.Remove(stateId);return true;}return false;}public StateBase GetState(RoleState stateId){if (states.ContainsKey(stateId))return states[stateId];return null;}#endregion
}public class StateBase//可以是接口,也可以是基类,具体使用哪个看情况
{public RoleState stateId { get;private set; }//状态IDpublic FSM parent { get;private set; }//状态归属哪个状态机public StateBase(FSM parent, RoleState stateId){this.parent = parent;this.stateId = stateId;}  public virtual void Enter(){}//进入动作,可以对状态做初始化public virtual void Exit(){}//退出动作,可以对状态对重置//在这个状态内要执行的操作,可以有多个public virtual void Update(){}//在状态内执行相关输出public virtual StateBase HandleInput(string input)//在当前状态内处理输入,状态转换条件判断{return null;}
}

在这个实现中,我们只在状态机内进行状态切换,这正是状态机的单一职责。将状态转换的条件判断和状态内的输出移出了,并将这两者移入到表示不同状态的一系列类中。

回顾我们之前的最简单的状态机的实现,先是将状态转换、状态转换的条件判断、状态内的输出都放在状态机内;随后将状态转换的条件判断移出,可以在任何类中进行状态转换条件判断。

【FSM的使用】

首先,我们要将对象的所有状态类写出来。

public class JumpState : StateBase
{public JumpState(FSM parent, RoleState stateId) : base(parent, stateId) { }public override void Enter(){base.Enter();//该状态的初始化行为}public override void Exit(){base.Exit();//离开状态时重置初始化参数}public override void Update(){base.Update();//状态内执行相关输出}public override StateBase HandleInput(string input){return base.HandleInput(input);}}public class AttackState : StateBase
{public AttackState(FSM parent, RoleState stateId) : base(parent, stateId){}public override void Enter(){base.Enter();//该状态的初始化行为}public override void Exit(){base.Exit();//离开状态时重置初始化参数}public override void Update(){base.Update();//状态内执行相关输出}public override StateBase HandleInput(string input){return base.HandleInput(input);}}//剩下的不写了,对象有多少个状态就要写多少个状态类

随后,列出状态转换图/表,按照这个图/表写好每个状态内的HandleInput和Update类,Enter和Exit去决定于具体的逻辑,这里不作表述。(代码以跳跃状态和行为状态为例)

public class WalkState : StateBase
{public WalkState(FSM parent, RoleState stateId) : base(parent, stateId){}public override void Enter(){base.Enter();//该状态的初始化行为}public override void Exit(){base.Exit();//离开状态时重置初始化参数}public override void Update(){base.Update();//状态内执行相关输出Debug.Log("开始行走");}public override StateBase HandleInput(string input){base.HandleInput(input);if (input.Equals("按下I键")){return parent.GetState(RoleState.Idle);}else if(input.Equals("按下J键")){return parent.GetState(RoleState.Jump);}else if(input.Equals("按下A键")){return parent.GetState(RoleState.Attack);}else if(input.Equals("按下D键")||input.Equals("受到攻击")){return parent.GetState(RoleState.Dead);}return null;}
}public class JumpState : StateBase
{public JumpState(FSM parent, RoleState stateId) : base(parent, stateId) { }public override void Enter(){base.Enter();//该状态的初始化行为}public override void Exit(){base.Exit();//离开状态时重置初始化参数}public override void Update(){base.Update();//状态内执行相关输出Debug.Log("开始跳跃");}public override StateBase HandleInput(string input){base.HandleInput(input);if (input.Equals("按下A键")){return parent.GetState(RoleState.Attack);}if (input.Equals("按下D键") || input.Equals("受到攻击")){return parent.GetState(RoleState.Dead);}return null;}}

之后,我们可以在对象内使用状态机,并给状态机输入。

public class Player:MonoBehaviour
{private FSM fsm;public Player(){//生成状态类实例并添加到状态机fsm.AddState(RoleState.Attack,new AttackState(fsm,RoleState.Attack));fsm.AddState(RoleState.Idle, new IdleState(fsm, RoleState.Idle));fsm.AddState(RoleState.Jump, new JumpState(fsm, RoleState.Jump));fsm.AddState(RoleState.Walk, new WalkState(fsm, RoleState.Walk));fsm.AddState(RoleState.Dead, new DeadState(fsm, RoleState.Dead));//状态机初始化fsm.Init(fsm.GetState(RoleState.Idle));}public void Update(){fsm.Update(input);}private string input;public void SetInput(string input){this.input = input;}
}

当我们增加拾取状态时,需要修改状态转移图,并实现新的拾取状态类,这就是再做扩展。对拾取状态类中的HandleInput方法,我们需要重新写,但不需要更改状态机的Update方法。

【功能完善FSM】

上面实现的FSM只具有基本的功能,我们需要对外提供状态改变回调、强制状态转换、运行时间等,下面是实现。

using System.Collections.Generic;
using UnityEngine;public class FSM //不要继承MonoBehaviour
{//状态机运行时状态参数,当前状态,上一个状态,下一个状态。//复杂的状态机状态关联多,需要这三个参数,简单的状态机不需要那么多public StateBase curState { get; private set; }public StateBase lastState { get; private set; }public StateBase nextState { get; private set; }private List<RoleState> history = new List<RoleState>();//状态切换历史记录//状态机内所有对象集合,简单的状态机不需要private Dictionary<RoleState, StateBase> states = new Dictionary<RoleState, StateBase>();//状态机整体的参数,简单的状态机不需要private StateBase defaultState;//状态机默认状态,也即第一个状态private StateBase exitState;//状态机退出状态,也即最后一个状态//状态机回调public delegate void OnStateChange(StateBase curState,StateBase lastState);//是否带参数/返回值,带什么参数/返回值,根据项目自己确定private OnStateChange OnPreStateChange;//状态改变前private OnStateChange OnPostStateChange;//状态改变后//状态机时间public float startTime { get; private set; }public float runTime { get; private set; }public void Init(StateBase defaultState)//状态机初始化{this.defaultState = defaultState;curState = defaultState;curState.Enter();history.Add(curState.stateId);startTime = Time.time;runTime = 0f;}public void Update(string input)//状态机的执行,即切换状态{if(curState == null)return;nextState = curState.HandleInput(input);//处理当前输入,获取下一状态if (nextState != curState && nextState != null){ChangeState(curState,nextState);}curState.Update();runTime += Time.deltaTime;}#region 状态机对外提供的方法public void AddState(RoleState stateId,StateBase state){states.Add(stateId,state);}public bool Remove(RoleState stateId){if (states.ContainsKey(stateId)){states.Remove(stateId);return true;}return false;}public StateBase GetState(RoleState stateId){if (states.ContainsKey(stateId))return states[stateId];return null;}public void ForceChangeCurState(StateBase state)//强制转换状态{ChangeState(curState,state);}public void ForceStop()//强制状态机退出{ForceChangeCurState(exitState);}public bool isStop => curState == exitState;//状态机是否停止//状态注册public void RegisterPostStateChange(OnStateChange onPostStateChange){OnPostStateChange += onPostStateChange;}public void UnRegisterPostStateChange(OnStateChange onPostStateChange){OnPostStateChange -= onPostStateChange;}public void RegisterPreStateChange(OnStateChange onPreStateChange){OnPreStateChange += onPreStateChange;}public void UnRegisterPreStateChange(OnStateChange onPreStateChange){OnPreStateChange -= onPreStateChange;}public void Reset()//状态机重置{OnPreStateChange = null;OnPostStateChange = null;curState = defaultState;lastState = null;nextState = null;startTime = Time.time;runTime = 0f;}#endregion#region 状态机的私有方法private void ChangeState(StateBase curState,StateBase nextState){curState.Exit();OnPreStateChange?.Invoke(curState, lastState);lastState = curState;curState = nextState;//状态切换OnPostStateChange?.Invoke(curState, lastState);curState.Enter();}#endregion
}public class StateBase//可以是接口,也可以是基类,具体使用哪个看情况
{public RoleState stateId { get;private set; }//状态IDpublic FSM parent { get;private set; }//状态归属哪个状态机//状态时间public float startTime { get; private set; }public float runTime { get; private set; }//状态可以不用回调,因为状态机的状态改变回调给了两个状态参数public bool active { get; set; } = true;//状态是否激活public StateBase(FSM parent, RoleState stateId){this.parent = parent;this.stateId = stateId;}public virtual void Enter(){//进入动作,可以对状态做初始化;初始化也可以在生成状态类实例时做//前者初始化的方式可能与运行时参数有关,后者无关startTime = Time.time;}public virtual void Exit()//退出动作,可以对状态对重置{runTime = 0;}//在这个状态内要执行的操作,可以有多个public virtual void Update()//在状态内执行相关输出{runTime += Time.deltaTime;}public virtual StateBase HandleInput(string input)//在当前状态内处理输入{if (!active)return null;return null;}
}

【添加转移的FSM】

可以发现,当增加拾取状态时,需要修改行走状态的HandleInput方法,这仍需要修改。因此,这种FSM只适合状态关联弱的对象。如果状态关联强,每添加一个新的状态,都会使得原有状态类的HandleInput要修改,那么我们添加转移类,将状态转换条件判断从状态类中移出。

public class StateBase//可以是接口,也可以是基类,具体使用哪个看情况
{public RoleState stateId { get;private set; }//状态IDpublic FSM parent { get;private set; }//状态归属哪个状态机//状态时间public float startTime { get; private set; }public float runTime { get; private set; }//状态可以不用回调,因为状态机的状态改变回调给了两个状态参数public bool active { get; set; } = true;//状态是否激活//状态的全部转换,需要在Player对象类中初始化生成并添加进对应的状态中public List<TransitionBase> transitions = new List<TransitionBase>();public void AddTransition(TransitionBase transition){transitions.Add(transition);}public StateBase(FSM parent, RoleState stateId){this.parent = parent;this.stateId = stateId;}public virtual void Enter(){//进入动作,可以对状态做初始化startTime = Time.time;}public virtual void Exit()//退出动作,可以对状态对重置{runTime = 0;}//在这个状态内要执行的操作,可以有多个public virtual void Update()//在状态内执行相关输出{runTime += Time.deltaTime;}public virtual StateBase HandleInput(string input)//在当前状态内处理输入{if (!active)return null;foreach (TransitionBase item in transitions)//判断该输入是否满足某个转移,轮询所有转移{if (item.fromState != this){continue;}if(item.CanTransition(input)){return item.toState;}}return null;}
}public class TransitionBase
{//从某个状态转换到另一个状态public StateBase fromState;public StateBase toState;public TransitionBase(StateBase fromState, StateBase toState){this.fromState = fromState;this.toState = toState;}public virtual bool CanTransition(string input){//从某个状态转换到另一个状态的逻辑判断return true;}}

添加转移前,某个状态到其他所有状态的转换条件判断都是该在状态类中完成的;添加转移后,某个状态到其他所有状态的转换条件判断在一个新的转移类中,如果一个状态A可以转换到另外五个状态,那么需要写五个转移类,如果新增加了一个状态B,只需要新写一个状态A到状态B的转移即可。使用时,在Player类中,还需要实例化这些转移类并添加到对应的状态类当中。

其缺点是,如果有很多状态都可以转换到状态B,那么要写很多个转移类。也即新增一个状态,要写一个状态类和很多的转移类。

值得注意的是,添加转移类并没有解决,从一个状态转换到另一个状态的条件判断复杂的问题。这意味着我们需要修改转移类的CanTransition(string input)方法,同样会违背开放封闭原则。

在实际的项目中,如果我们的转移条件简单,采用这种方式是OK的。

如果转移条件复杂,我们需要采用更好的实现方式。

有限状态机FSM详解(2)——采用状态模式的FSM相关推荐

  1. 详解设计模式:状态模式

    状态模式(State Pattern)也被称为状态机模式(State Machine Pattern),是在 GoF 23 种设计模式中定义了的行为型模式. 在状态模式 类的行为是基于它的状态改变的. ...

  2. python模式匹配算法_详解Python 最短匹配模式

    问题 你正在试着用正则表达式匹配某个文本模式,但是它找到的是模式的最长可能匹配. 而你想修改它变成查找最短的可能匹配. 解决方案 这个问题一般出现在需要匹配一对分隔符之间的文本的时候(比如引号包含的字 ...

  3. VMware虚拟机三种网络模式详解--Bridged(桥接模式)

    VMware虚拟机三种网络模式详解--Bridged(桥接模式) 简介: 由于Linux目前很热门,越来越多的人在学习Linux,但是买一台服务器放家里来学习,实在是很浪费. 那么如何解决这个问题?虚 ...

  4. 2021-08-03 VMware虚拟机三种网络模式详解 Bridged(桥接模式)

    VMware虚拟机三种网络模式详解 Bridged(桥接模式) 参考连接:VMware虚拟机三种网络模式详解 Bridged(桥接模式)

  5. 《设计模式详解》行为型模式 - 状态模式

    状态模式 6.5 状态模式 6.5.1 反例 6.5.2 结构 6.5.3 案例实现 6.5.4 优缺点 6.5.5 使用场景 完整的笔记目录:<设计模式详解>笔记目录,欢迎指点! 行为型 ...

  6. 有限状态机FSM详解(1)——最简单的FSM

    [基本概念] 有限状态机(finite-state machine,FSM)可以解决有限个状态转换的问题.具体的定义和相关的概念先不说,我们直接通过例子来理解. 我们知道一个角色通常会至少有idle, ...

  7. 有限状态机FSM详解(一)

    有限状态机在维基百科中的解释是: 有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模 ...

  8. 《设计模式详解》行为型模式 - 解释器模式

    解释器模式 6.11 解释器模式 6.11.1 概述 6.11.2 结构 6.11.3 案例实现 6.11.4 优缺点 6.11.5 使用场景 完整的笔记目录:<设计模式详解>笔记目录,欢 ...

  9. 《设计模式详解》行为型模式 - 备忘录模式

    备忘录模式 6.10 备忘录模式 6.10.1 概述 6.10.2 结构 6.10.3 案例实现 "白箱" 备忘录模式 "黑箱" 备忘录模式 6.10.4 优缺 ...

最新文章

  1. CentOs6.5中安装和配置vsftp简明教程
  2. 【十大经典排序算法】java实现--插入排序(3)
  3. nginx中configure脚本支持的常用选项,拍摄自《Nginx高性能Web服务器详解》
  4. Oralce的内存结构
  5. Bootstrap源代码多行代码
  6. [Python] L1-023. 输出GPLT-PAT团体程序设计天梯赛GPLT
  7. ubuntu linux 14.04 apache,在 Ubuntu 14.04 中Apache从2.2迁移到2.4的问题
  8. http工作原理和机制
  9. 20.5 Shell脚本中的逻辑判断;20.6 文件目录属性判断;20.7 if特殊用法;20.8 20.9 cace判断(上下)...
  10. 使用微信实现查卷返利机器人功能
  11. 基于Python的作业自动批改系统
  12. 互联网协议 — 802.1q VLAN 虚拟局域网协议
  13. let , const , var , 的区别
  14. windows ios良心软件推荐
  15. Mac 下终端运行C++
  16. 大数据联姻“互联网+”驱动绿色变革
  17. 【pyqt】自制的图片裁剪分割器
  18. 2021春哈工大计算机系统大作业
  19. java实现rar格式,java解压缩文件的实现示例,支持rar和zip格式
  20. 表格图片加载不出来,破图,加载失败

热门文章

  1. 解决bootstrap popover首次显示位置有偏差的问题
  2. 手把手教你用MindSpore训练一个AI模型!
  3. HTML表单标签 input
  4. linux rename 批量修改文件名
  5. python 切割和拼接图片
  6. mysql数据库--表中数据的基本操作
  7. 4.1 城市路径规划
  8. html网页设计思路,极简主义网页设计思路与原则
  9. NFS挂载参数详解及使用建议
  10. linux设置设备MAC