c#扩展包-Stateless

2023-09-20 10:44:11

准备

Stateless是一个有限状态机扩展包。在c#项目中可以直接通过NuGet安装。

使用他需要先用枚举写好你所有可能的状态和子状态。
例如移动,下蹲,空闲,跳跃,游泳,奔跑,走路。
其中,奔跑和走路是移动的子状态。

然后需要写触发器。所有状态转换必须要一个触发器。
所以你需要把所有的时机都精确描述,并且哪怕只有一个地方用到也要描写。

class Player
{
	enum State { 移动, 下蹲, 空闲, 空中, 二段跳, 游泳, 奔跑, 走路 }
	enum Trigger {,,,, 松开下, 落地, 落水, 出水, 跌落 }

	private StateMachine<State, Trigger> stateMachine = new StateMachine<State, Trigger>(State.空闲); 
}

配置状态机

对状态机调用Configure会启用对这个状态下的配置。
从这个配置上调用的配置方法全部都会再返回这个状态的配置。
需要以此法对所有的状态都进行配置。

	public Player()
	{
		stateMachine.Configure(State.空闲)
			.Permit(Trigger., State.下蹲)
			.Permit(Trigger., State.走路)
			.Permit(Trigger., State.走路);

		stateMachine.Configure(State.下蹲)
			.Permit(Trigger.松开下, State.空闲);

	}

触发器,目标,条件。

配置的方法有很多排列组合出来的版本,例如

  • Permit
  • PermitIf
  • PermitDynamic
  • PermitDynamicIf

一个基本的Permit方法接受两个参数,第一个是触发器,第二个是触发以后要变成谁。
例如空闲在触发了下的情况下会变成下蹲。
Permit(Trigger.下, State.下蹲)

Dynamic

如果有Dynamic,那么第二个参数会是一个委托,你可以动态的决定要变成谁。
PermitDynamic(Trigger.下, () => Guid.NewGuid() > Guid.NewGuid() ? State.下蹲 : State.空闲)
例如这句代码中,有50%概率变成下蹲,有50%概率变成空闲。

If

如果有If,那么还有第三个参数。也是一个委托,表示条件,需要你返回bool。
PermitIf(Trigger.下, State.下蹲, () => Guid.NewGuid() > Guid.NewGuid())
只有条件满足的时候,这个转化才能成功。

当一次触发时,会判断所有的转化。如果有多个方案可以通过(即便目标相同),会报错。

  • 有多个相同的方案
  • 一个方案存在的时候注册了他的If版本,并且If通过。
  • 有多个If版本的方案种都通过了。

切换的目标

按照目标排列组合有以下分类

  • Permit
  • PermitReentry
  • InternalTransition

Permit基础的转换,自选目标。

PermitReentry重新进入自己,意义是触发一次自己的退出和进入方法。
和直接Permit填自己是一样的。

InternalTransition不会发生切换,也不会触发退出和进入。
取而代之的是给定一个动作委托,触发你的动作。
使得看起来像因为切换状态发生了什么事情。
.InternalTransition(Trigger.上, () => { Console.WriteLine("在蹲下的时候不能跳"); })

忽略和未定义的触发

Ignore方法可以忽略这个触发器,什么也不发生。
那如果声明忽略而调用这个转换会怎么样呢?会报一个错,表示这个转换没有注册。

你可以对状态机(不是配置)注册一个委托,来表示有未注册的转换时什么也不做。
stateMachine.OnUnhandledTrigger((s, t) => { });

有参数的转换

有时候,你从外部获取了输入,例如鼠标的位置。这种情况下不能用枚举来覆盖所有情况。
你需要对状态机调用SetTriggerParameters方法,然后保存这个返回值,之后要用到他。
var upTrigger = stateMachine.SetTriggerParameters<int>(Trigger.上);

只能在有If的情况下使用这个东西进行配置。
因为如果你没有条件,那么这个参数是没有意义的。
.PermitIf(upTrigger, State.下蹲, i => i > 4);
使用刚刚获得的东西作为触发器,那么你的条件委托就能使用他的参数。

给状态机触发器

使用Fire方法传递给状态机触发器。
他会根据配置进行转换状态。

如果你使用注册了参数的触发器,你还可以传递参数。

stateMachine.Fire(Trigger.出水);
stateMachine.Fire(upTrigger, 10086);

注册进入和退出

OnEntry方法注册进入这个状态时会发生什么事情。
OnExit方法注册退出这个状态时会发生什么事情。

OnEntryFrom版本的方法。表示从xx触发器来,使用注册了参数的触发器就能读取参数。

.OnEntry(() => { Console.WriteLine("进入到下蹲"); })
.OnExit(() => { Console.WriteLine("从下蹲退出"); }) 
.OnEntryFrom(Trigger.出水, () => { })
.OnEntryFrom(upTrigger, i => Console.WriteLine("携带的参数是" + i));

子级状态

一个状态可以声明为另一个状态的子状态(只能声明一个直接父状态)。

		stateMachine.Configure(State.二段跳)
			.SubstateOf(State.空中);

		stateMachine.Configure(State.奔跑)
			.SubstateOf(State.移动);

		stateMachine.Configure(State.走路)
			.SubstateOf(State.移动);

如果只涉及子状态改变,状态是同一个,那么不会执行父状态的进入和退出。
例如说,从走路切换到奔跑,就移动而言是没有变化的。

如果从子状态越过父状态切换到了其他状态,那么他们的退出都会执行。
例如从走路切换到空闲。那么此时也不能算作移动状态,所以移动也会一起结束。

bool b1 = stateMachine.IsInState(State.移动);
bool b2 = stateMachine.State == State.移动;

可以使用IsInState方法带父子级进行状态检测。
例如如果当前是走路,那么b1是true,因为走路是移动。
b2是false,他是直接比较的。

先后顺序

  1. 从子级到父级,依次经过所有Exit。
  2. 执行转换间隙。
  3. 从父级到子级,依次执行Entry,带参数的Entry,进入下一级。
  4. 完成切换后执行转换完成。
StateMachine<string, int> st = new StateMachine<string, int>("1.1");

st.Configure("1")
	.OnExit(() => Console.WriteLine("从1退出"));

st.Configure("1.1")
   .SubstateOf("1")
   .Permit(0, "2.1")
   .OnExit(() => Console.WriteLine("从1.1退出"));

st.Configure("2")
	.OnEntry(() => Console.WriteLine("进入2"))
	.OnEntryFrom(0, () => Console.WriteLine("通过0进入2"));

st.Configure("2.1")
	.OnEntry(() => Console.WriteLine("进入2.1"))
	.OnEntryFrom(0, () => Console.WriteLine("通过0进入2.2"))
	.SubstateOf("2");

st.OnTransitioned(st => Console.WriteLine("完成所有退出"));
st.OnTransitionCompleted(st => Console.WriteLine("进入到目标状态"));

st.Fire(0);
/*
从1.1退出
从1退出
完成所有退出
进入2
通过0进入2
进入2.1
通过0进入2.2
进入到目标状态
*/

示例

class Player
{
	enum State { 移动, 下蹲, 空闲, 空中, 二段跳, 游泳, 奔跑, 走路 }
	enum Trigger { 键盘输入, 落地, 落水, 出水, 跌落, 超时 }

	DateTime TimeOut;
	StateMachine<State, Trigger> stateMachine;
	StateMachine<State, Trigger>.TriggerWithParameters<Vector2> InputTrigger;

	public Player()
	{
		stateMachine = new StateMachine<State, Trigger>(State.空闲);
		InputTrigger = stateMachine.SetTriggerParameters<Vector2>(Trigger.键盘输入);
		stateMachine.OnUnhandledTrigger((s, t) => { });

		stateMachine.Configure(State.移动)
			.PermitIf(InputTrigger, State.空闲, vec => vec == Vector2.Zero)
			.PermitIf(InputTrigger, State.空中, vec => vec == Vector2.UnitY)
			.PermitIf(InputTrigger, State.下蹲, vec => vec == -Vector2.UnitY)
			.Permit(Trigger.跌落, State.空中)
			.Permit(Trigger.落水, State.游泳)
			;

		stateMachine.Configure(State.下蹲)
			.PermitIf(InputTrigger, State.空闲, vec => vec == Vector2.Zero)
			.PermitIf(InputTrigger, State.空中, vec => vec == Vector2.UnitY)
			;

		stateMachine.Configure(State.空闲)
			.PermitIf(InputTrigger, State.走路, vec => vec.Y == 0 && vec.X != 0)
			.PermitIf(InputTrigger, State.空中, vec => vec == Vector2.UnitY)
			.PermitIf(InputTrigger, State.下蹲, vec => vec == -Vector2.UnitY)
			;

		stateMachine.Configure(State.空中)
			.Permit(Trigger.落地, State.空闲)
			.PermitIf(InputTrigger, State.二段跳, vec => vec == Vector2.UnitY)
			;

		stateMachine.Configure(State.二段跳)
			.SubstateOf(State.空中)
			.IgnoreIf(InputTrigger, vec => vec == Vector2.UnitY)
			;

		stateMachine.Configure(State.游泳)
			.Permit(Trigger.出水, State.空中)
			;

		stateMachine.Configure(State.奔跑)
			.SubstateOf(State.移动)
			;

		stateMachine.Configure(State.走路)
			.SubstateOf(State.移动)
			.PermitIf(Trigger.超时, State.奔跑, () => DateTime.Now - TimeOut > TimeSpan.FromMicroseconds(500))//如果最后更新时间超过500秒则说明纯按住了走路0.5秒
			.OnEntry(async () =>
			{
				TimeOut = DateTime.Now;
				await Task.Delay(500);//在0.5秒延迟后尝试改为奔跑
				stateMachine.Fire(Trigger.超时);
			})
			.OnExit(() =>
			{
				TimeOut = DateTime.Now;
			})
			;

	}
}
更多推荐

逻辑漏洞挖掘之XSS漏洞原理分析及实战演练 | 京东物流技术团队

一、前言2月份的1.2亿条用户地址信息泄露再次给各大公司敲响了警钟,数据安全的重要性愈加凸显,这也更加坚定了我们推行安全测试常态化的决心。随着测试组安全测试常态化的推进,有更多的同事对逻辑漏洞产生了兴趣,本系列文章旨在揭秘逻辑漏洞的范围、原理及预防措施,逐步提升大家的安全意识。作为开篇第一章,本文选取了广为熟知的XSS

vue3 封装公共弹窗函数

前言:博主封装了一个公共弹窗函数接收四个参数,(title:弹窗标题,ContentComponent:弹窗中显示的组件内容,opt:接收弹窗本身的属性和props,beforeSure:点击确定做的操作(请求后端接口))封装的公共函数:import{defineComponent,h,ref,getCurrentIn

提升预算管控精度,助力保险资管协会财务管理数字化转型

数字化转型是当前中国经济社会发展的重要趋势和根本方向。中国保险资产管理业协会(以下称“协会”)是专门履行保险资产管理自律职能的全国性金融自律组织。过去几年,协会一直在积极探索应用信息化手段,加强预算管理。近期,协会与百望云合作,重构了预算项目,整合了核算、报销、OA系统,通过智能管票、系统控制,搭建全流程自动化、智能化

为什么我们总是被赶着走

最近发生了一些事情,让shigen不禁的思考:为什么我们总是被各种事情赶着走。一第一件事情就是工作上的任务,接触的是一个老系统ERP,听说是2018年就在线上运行的,现在出现问题了,需要我去修改一下。在这里,我需要记录一下技术背景:ERP系统背景后端采用的是jfinal框架,让我觉得很奇葩的地方有:接受前端的参数采用的

HDFS的Shell操作

1、进程启停管理1.1、一键启停脚本HadoopHDFS组件内置了HDFS集群的一键启停脚本。1.1.1、一键启动HDFS集群$HADOOP_HOME/sbin/start-dfs.sh,一键启动HDFS集群start-dfs.sh执行原理:在执行此脚本的机器上,启动SecondaryNameNode。读取core-s

GIT常用命令

GIT常用命令1、版本控制!什么是版本控制版本迭代,新的版本!版本管理器版本控制(Revisioncontrol)是一种在开发的过程中用于管理我们对文件、目录或工程等内容的修改历史,方便查看更改历史记录,备份以便恢复以前的版本的软件工程技术。实现跨区域多人协同开发追踪和记载一个或者多个文件的历史记录组织和保护你的源代码

聊聊编程是什么

前言前言不看没关系,不影响。半夜睡不着,想写点啥,浅聊下我理解的编程的,我认为编程就是解决问题,就像互联网是依附于实体业,是处理解决实际问题的。刚学编程的时候总是很恐慌的,天赋不够,我这么认为的原因,一是当时流行一种说法叫不是热爱编程的是坚持不下去或者没什么发展的,后来发现是一些阴谋家为了减少竞争的言论;二是确实遇上了

Axios笔记

1、Axios介绍Axios基于promise网络请求库,作用于node.js和浏览器中(即同一套代码可以运行在node.js和浏览器中),在服务器中他使用原生node.jshttp,在浏览器端则使用XMLHttpRequest。特性:(1)、支持PromiseAPI(2)、拦截请求和响应、转换请求和响应数据、取消请求

Java手写启发式搜索算法和启发式搜索算法应用拓展案例

Java手写启发式搜索算法和启发式搜索算法应用拓展案例1.算法思维导图以下是使用Mermanid代码表示的启发式搜索算法的实现原理:#mermaid-svg-3nox60fbtHCDRqqD{font-family:"trebuchetms",verdana,arial,sans-serif;font-size:16p

免费好用bpm平台,实现生产管理系统

1.什么是生产管理系统生产制造管理系统主要是以生产制造管理为核心,其管理功能包括产品结构设置(BOM)生产计划、加工、领料、质检、库存、成本核算等。通过信息化解决行业管理问题,满足各企业科学管理的需求,并为制造管理提供较完善的解决方案。2.为什么选择天翎市面上不乏各种成品软件可供企业选择,但成品软件多数为通用固定的功能

Shell 正则表达式及综合案例及文本处理工具

目录一、常规匹配二、常用特殊字符三、匹配手机号四、案例之归档文件五、案例之定时归档文件六、Shell文本处理工具1.cut工具2.awk工具一、常规匹配一串不包含特殊字符的正则表达式匹配它自己例子,比如说想要查看密码包含root字符串的,可以这样写cat/etc/passwd|greproot二、常用特殊字符特殊字符作

热文推荐