U3D-FSM有限状态机的简单设计

http://coder.beitown.com/archives/592

在之前的文章里介绍了一个基础U3D状态机框架(Unity3D游戏开发之状态流框架)即大Switch的枚举状态控制。这种方法虽然容易理解,编程方法也相对简单,但是弊端是当状态变得复杂之后,或需要添加一种新的状态时,会显得非常混乱并且难以下手。故我们需要引进一种更高级的状态机技术来避免这些问题。网上有一些讲述U3D-FSM状态机的文章,但都不针对基础讲解,而且大多带有冗余的与状态机不相关的代码,基础不好的读者容易看不清FSM状态机的核心所在。这里针对网上的一些文章和代码做了一个整理,意图使之简单易懂。

这里关于FSM有限状态机这类名词的解释这里就不再说明了,感兴趣的朋友可以自己去百度下(度娘链接),本文只说重点。

首先是状态机基类State.cs

/*** 状态基类 */
public class State[entity_type>
{public entity_type Target;//Enter state  public virtual void Enter (entity_type entityType){}//Execute statepublic virtual void Execute (entity_type entityType){}//Exit statepublic virtual void Exit (entity_type entityType){}}

基类之所以设计成含有3个小的状态方法是因为,通常在游戏中有些行为都只是在进入或退出某个状态时出现的,并不会发生在通常的更新步骤中。这样设计就可以有效的将持续性调用语句和一次性调用语句有效的区分开来。(举例:发送技能时的特效,有些是持续性而有些又是一次性的)

接下来我们编写状态机代码,来使直接的这个基类的各个方法运作起来:

using UnityEngine;
using System.Collections;public class StateMachine[entity_type>
{private entity_type m_pOwner;private State[entity_type> m_pCurrentState;//当前状态private State[entity_type> m_pPreviousState;//上一个状态private State[entity_type> m_pGlobalState;//全局状态/*状态机构造函数*/public StateMachine (entity_type owner){m_pOwner = owner;m_pCurrentState = null;m_pPreviousState = null;m_pGlobalState = null;}/*进入全局状态*/public void GlobalStateEnter(){m_pGlobalState.Enter(m_pOwner);}/*设置全局状态*/public void SetGlobalStateState(State[entity_type> GlobalState){m_pGlobalState = GlobalState;m_pGlobalState.Target = m_pOwner;m_pGlobalState.Enter(m_pOwner);}/*设置当前状态*/public void SetCurrentState(State[entity_type> CurrentState){m_pCurrentState = CurrentState;m_pCurrentState.Target = m_pOwner;m_pCurrentState.Enter(m_pOwner);}/*Update*/public void SMUpdate (){if (m_pGlobalState != null)m_pGlobalState.Execute (m_pOwner);if (m_pCurrentState != null)m_pCurrentState.Execute (m_pOwner);}/*状态改变*/public void ChangeState (State[entity_type> pNewState){if (pNewState == null) {Debug.LogError ("can't find this state");}//触发退出状态调用Exit方法
        m_pCurrentState.Exit(m_pOwner);//保存上一个状态 m_pPreviousState = m_pCurrentState;//设置新状态为当前状态m_pCurrentState = pNewState;m_pCurrentState.Target = m_pOwner;//进入当前状态调用Enter方法
        m_pCurrentState.Enter (m_pOwner);}public void RevertToPreviousState (){//切换到前一个状态
        ChangeState (m_pPreviousState);}public State[entity_type> CurrentState (){//返回当前状态return m_pCurrentState;}public State[entity_type> GlobalState (){//返回全局状态return m_pGlobalState;}public State[entity_type> PreviousState (){//返回前一个状态return m_pPreviousState;}}

这个状态机其实还不是最简的,全局和上一个状态的相关部分都可以去掉,但同时功能上就会被削减,故这里将其保留。

现在状态基类和状态机类都有了,我们可以开始编写游戏对象的独立状态类,先编写游戏的总流程状态类,这里命名为MainState.cs

/*** 全局状态*/
public class MainState : State[Main>
{public static MainState instance;/*构造函数单例化*/public static MainState Instance(){if (instance == null)instance = new MainState();return instance;}public override void Enter(Main Entity){//这里添加进入此状态时执行的代码
    }public override void Execute(Main Entity){//这里添加持续此状态刷新代码
    }public override void Exit(Main Entity){//这里添加离开此状态时执行代码
    }}/*** Ready状态*/
public class MainState_Ready : State[Main>
{public static MainState_Ready instance;/*构造函数单例化*/public static MainState_Ready Instance(){if (instance == null)instance = new MainState_Ready();return instance;}public override void Enter(Main Entity){//这里添加进入此状态时执行的代码
    }public override void Execute(Main Entity){//这里添加持续此状态刷新代码//这里是重点 当满足某条件后 我们可以进行状态切换 执行如下代码 切换到 Run状态
        Entity.GetFSM().ChangeState(MainState_Run.Instance());  }public override void Exit(Main Entity){//这里添加离开此状态时执行代码
    }
}/*** Run状态*/
public class MainState_Run : State[Main>
{public static MainState_Run instance;/*构造函数单例化*/public static MainState_Run Instance(){if (instance == null)instance = new MainState_Run();return instance;}public override void Enter(Main Entity){//这里添加进入此状态时执行的代码
    }public override void Execute(Main Entity){//这里添加持续此状态刷新代码//当满足某条件后 我们可以继续进行状态切换 执行如下代码 切换到 Over状态
        Entity.GetFSM().ChangeState(MainState_Over.Instance()); }public override void Exit(Main Entity){//这里添加离开此状态时执行代码
    }
}/*** Over状态*/
public class MainState_Over : State[Main>
{public static MainState_Over instance;/*构造函数单例化*/public static MainState_Over Instance(){if (instance == null)instance = new MainState_Over();return instance;}public override void Enter(Main Entity){//这里添加进入此状态时执行的代码
    }public override void Execute(Main Entity){//这里添加持续此状态刷新代码//如之前两个状态类一样 同理 当满足一定状态后 可以切换回Ready状态
       Entity.GetFSM().ChangeState(MainState_Ready.Instance()); }public override void Exit(Main Entity){//这里添加离开此状态时执行代码
    }
}

代码有点长,主要是为了让大家能够看清楚如何进行一个状态的编写,其实基类都是一样的,都是重复内容。 这里我们看到,除了定义一个全局的状态类之外,我们还添加了Ready、Run、Over三个状态。重点注意一下Execute函数,这里是状态切换的关键,当带此状态绑定的对象Update时就在不停的执行Execute里的代码段,当满足一定条件后,即达成状态的切换。

这里我们看一下之前的状态机代码里的ChangeState方法,就知道整个状态切换是如何工作的了:

/*状态改变*/public void ChangeState (State[entity_type> pNewState){if (pNewState == null) {Debug.LogError ("can't find this state");}//触发退出状态调用Exit方法
        m_pCurrentState.Exit(m_pOwner);//保存上一个状态 m_pPreviousState = m_pCurrentState;//设置新状态为当前状态m_pCurrentState = pNewState;m_pCurrentState.Target = m_pOwner;//进入当前状态调用Enter方法
        m_pCurrentState.Enter (m_pOwner);}

可以看到当状态切换时,会自动触发当前状态的Exit方法和目标状态的Enter方法。这样就完成了一整个状态的切换过程。

到这里整个有限状态机体系基本就算完工了,剩下的是如何在Main里进行MainState类的创建及使用,Main.cs代码如下:

using UnityEngine;
using System.Collections;public class Main : MonoBehaviour{StateMachine[Main> m_pStateMachine;//定义一个状态机void Start () {m_pStateMachine = new StateMachine[Main>(this);//初始化状态机m_pStateMachine.SetCurrentState(MainState_Ready.Instance()); //设置一个当前状态m_pStateMachine.SetGlobalStateState(MainState.Instance());//设置全局状态
    }void Update (){   m_pStateMachine.SMUpdate();}/*返回状态机*/public StateMachine[Main> GetFSM (){return m_pStateMachine;}}

写到这里我们整个状态机的框架及使用流程就基本结束了,这里要注意几个问题: ①不要在SetCurrentState()方法调用前,调用ChangeState()方法,否则会出现null对象错误,具体原因很简单,看一下ChangeState()里的代码调用了哪些变量就知道了。 ②状态间的通信,这个状态机其实还是有未完善的地方的,目前状态间的通知是通过直接调用其他状态机的ChangeState()方法实现的,这样势必要先获取该对象的脚本,这个功能待完善吧。 ③在U3D里每个游戏对象初始化并调用Start()方法的时机是不一样的,所以要注意,开始游戏时不要直接进入开始状态,而是要有一个等待态来让所有的游戏对象完成Start()方法后再调用这些对象的状态机。

另外,多个状态机间的通信,就像上文②中所述那样,仅仅是通过调用ChangeState()方法来实现,并不是非常完善,所以暂时不做讲解,以免误导大家,待日后有较好解决方案再另行开篇。 此FSM状态机仅为一个雏形,还有很多功能及优化要做,但对于入门FSM有限状态机来说,已经实现了其最主要的功能。不足之处欢迎大家提出讨论,并帮助加以完善。

谢谢关注。

转载于:https://www.cnblogs.com/softimagewht/p/4042431.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/273549.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《Java 高并发》04 线程的基本操作

新建线程 新建线程很简单。只要使用new 关键字创建一个线程对象,并且调用 start 方法启动线程。 Thread t new Thread(); t.start();注意:run 方法不是用来启动线程。如果调用 run 方法它只会作为普通方法来执行,而不会开启线程执行。 终止…

Dispatch 方法简介

后台执行 dispatch_async(dispatch_get_global_queue(0, 0), ^{ //后台程执行 something; }); 主线程执行 dispatch_async(dispatch_get_main_queue(), ^{// 主线程执行something; }); 一次性执行 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 主…

linux杂七杂八整理

64系统里执行32位程序: 1、在64系统里执行32位程序如果出现/lib/ld-linux.so.2: bad ELF interpreter: No such file or directory,安装下glic即可sudo yum install glibc.i6862、error while loading shared libraries: libz.so.1: cannot …

《Java 高并发》05 线程的基本操作

volatile 与 Java 内存模型 Java 内存模型都是围绕着原子性、有序性和可见性展开的。为了在适当的场合,确保线程间的原子性、有序性和可见性。Java 使用了一些特许的操作或者关键字来申明、告诉虚拟机,在这个地方,要尤其注意,不能…

mybatis 2 -常用数据操作

1、写入数据并获取自增ID XML配置&#xff1a; <!-- 写入数据获取自增ID --><insert id"insertLog" parameterType"com.mamaguwen.entity.sys_loginlog" useGeneratedKeys"true" keyProperty"logid">insert into sys_…

Spring常用的的注解对应xml配置详解

Component(value"")注解&#xff1a;组件 标记在类上&#xff0c;也可以放在接口上注解作用&#xff1a;把AccountDao实现类对象交由Spring IOC容器管理 相当于XML配置文件中的Bean标签 <bean id"userAnnonMapper" class"com.spring.mapper.User…

安卓模拟器bluestacks mac地址修改教程

http://szmars2008.blog.163.com/blog/static/118893702201373181349348/ 转载于:https://www.cnblogs.com/prayer521/p/4069037.html

Docker 搭建 ELK 日志系统,并通过 Kibana 查看日志

Docker 搭建 ELK 日志系统,并通过 Kibana 查看日志 docker-compose.yml version: 3 services:elasticsearch:image: elasticsearch:7.7.0 #镜像container_name: elasticsearch #定义容器名称restart: always #开机启动&#xff0c;失败也会一直重启environment:- "cl…

蟠桃记

Problem Description 喜欢西游记的同学肯定都知道悟空偷吃蟠桃的故事&#xff0c;你们一定都觉得这猴子太闹腾了&#xff0c;其实你们是有所不知&#xff1a;悟空是在研究一个数学问题&#xff01; 什么问题&#xff1f;他研究的问题是蟠桃一共有多少个&#xff01; 不过&#…

Spring 定时任务动态管理

管理 Spring 中定时任务 pom.xml <properties><hutool.version>5.6.6</hutool.version><lombok.version>1.18.20</lombok.version><spring-boot.web.version>2.2.10.RELEASE</spring-boot.web.version> </properties><de…

高效率Oracle SQL语句

1、Where子句中的连接顺序&#xff1a; ORACLE采用自下而上的顺序解析WHERE子句。 根据这个原理&#xff0c;表之间的连接必须写在其他WHERE条件之前&#xff0c; 那些可以过滤掉最大数量记录的条件必须写在WHERE子句的末尾。 举例&#xff1a; (低效) select ... from table1…

RabbitMQ Management:Management API returned status code 500

错误显示&#xff1a; 解决方案&#xff1a; 因为是使用docker 容器安装的&#xff0c;所有需要进入容器 docker exec -it rabbitmq /bin/bash进入目录 cd /etc/rabbitmq/conf.d/执行命令 echo management_agent.disable_metrics_collector false > management_agent.dis…

Android JNI和NDK学习(5)--JNI分析API

Java类型和本地类型对应 在如下情况下&#xff0c;需要在本地方法中应用java对象的引用&#xff0c;就会用到类型之间的转换&#xff1a; java方法里面将参数传入本地方法&#xff1b;在本地方法里面创建java对象&#xff1b;在本地方法里面return结果给java程序。Java基本类型…

RabbitMq 消费失败,重试机制

方案一&#xff1a; 本地消息表 定时任务 本地消息表&#xff1a;主要用于存储 业务数据、交换机、队列、路由、次数 定时任务&#xff1a;定时扫描本地消息表&#xff0c;重新给业务队列投递消息。 具体思路&#xff1a;业务队列消费失败时&#xff0c;把 业务数据、交换机、…

Android常用的工具类

主要介绍总结的Android开发中常用的工具类&#xff0c;大部分同样适用于Java。目前包括HttpUtils、DownloadManagerPro、ShellUtils、PackageUtils、 PreferencesUtils、JSONUtils、FileUtils、ResourceUtils、StringUtils、 ParcelUtils、RandomUtils、ArrayUtils、ImageUtils…

0. Spring 基础

BeanDefinition BeanDefinition 表示 Bean 定义&#xff1a; Spring根据BeanDefinition来创建Bean对象&#xff1b;BeanDefinition有很多的属性用来描述Bean&#xff1b;BeanDefiniton是Spring中非常核心的概念。BeanDefiniton中重要的属性&#xff1a; a. beanClass&#xf…

1. Spring 源码:Spring 解析XML 配置文件,获得 Bena 的定义信息

通过 Debug 运行 XmlBeanDefinitionReaderTests 类的 withFreshInputStream() 的方法&#xff0c;调试 Spring 解析 XML 配置文件&#xff0c;获得 Bean 的定义。 大体流程可根据序号查看&#xff0c;xml 配置文件随便看一眼&#xff0c;不用过多在意。 <?xml version&qu…

c++ 读取文件 最后一行读取了两次

用ifstream的eof()&#xff0c;竟然读到文件最后了&#xff0c;判断eof还为false。网上查找资料后&#xff0c;终于解决这个问题。 参照文件&#xff1a;http://tuhao.blogbus.com/logs/21306687.html 在使用C/C读文件的时候&#xff0c;一定都使用过eof&#xff08;&#xff0…

java中的io系统详解(转)

Java 流在处理上分为字符流和字节流。字符流处理的单元为 2 个字节的 Unicode 字符&#xff0c;分别操作字符、字符数组或字符串&#xff0c;而字节流处理单元为 1 个字节&#xff0c;操作字节和字节数组。 Java 内用 Unicode 编码存储字符&#xff0c;字符流处理类负责将外部的…

js获取字符串最后一个字符代码

方法一&#xff1a;运用String对象下的charAt方法 charAt() 方法可返回指定位置的字符。 代码如下 复制代码 str.charAt(str.length – 1) 请注意&#xff0c;JavaScript 并没有一种有别于字符串类型的字符数据类型&#xff0c;所以返回的字符是长度为 1 的字符串 方法二&#…