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方法接受两个参数,第一个是触发器,第二个是触发以后要变成谁。
例如空闲在触发了下的情况下会变成下蹲。
Permit(Trigger.下, State.下蹲)
如果有Dynamic,那么第二个参数会是一个委托,你可以动态的决定要变成谁。
PermitDynamic(Trigger.下, () => Guid.NewGuid() > Guid.NewGuid() ? State.下蹲 : State.空闲)
例如这句代码中,有50%概率变成下蹲,有50%概率变成空闲。
如果有If,那么还有第三个参数。也是一个委托,表示条件,需要你返回bool。
PermitIf(Trigger.下, State.下蹲, () => Guid.NewGuid() > Guid.NewGuid())
只有条件满足的时候,这个转化才能成功。
当一次触发时,会判断所有的转化。如果有多个方案可以通过(即便目标相同),会报错。
按照目标分组有以下分类
Permit
基础的转换,自选目标。
PermitReentry
重新进入自己,意义是触发一次自己的退出和进入方法。
和直接Permit填自己是一样的。
InternalTransition
不会发生切换,也不会触发退出和进入。
取而代之的是给定一个动作委托,触发你的动作。
使得看起来像因为切换状态发生了什么事情。
.InternalTransition(Trigger.上, () => { Console.WriteLine("在蹲下的时候不能跳"); })
InitialTransition
指定一个子状态,表示这种状态的默认情况。
指定了以后就不会直接停留在这个状态身上了,一旦回来就会切换走。
例如走路和奔跑是移动的子状态。
为移动注册初始子状态为走路,那么以任何方式切换到移动都会变为走路。
包括从走路切到移动。
Ignore
方法可以忽略这个触发器,什么也不发生。
那如果声明忽略而调用这个转换会怎么样呢?会报一个错,表示这个转换没有注册。
你可以对状态机(不是配置)注册一个委托,来表示有未注册的转换时什么也不做。
stateMachine.OnUnhandledTrigger((s, t) => { });
有时候,你从外部获取了输入,例如鼠标的位置。这种情况下不能用枚举来覆盖所有情况。
你需要对状态机调用SetTriggerParameters方法,然后保存这个返回值,之后要用到他。
var upTrigger = stateMachine.SetTriggerParameters
只能在有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,他是直接比较的。
带有Async的方法可以把注册的委托改为异步形式。
StateMachine<string, int> st = new StateMachine<string, int>("1");
st.Configure("1")
.Permit(2, "2")
.Permit(3, "3")
;
st.Configure("2")
.OnEntryFromAsync(2, async () =>
{
await Task.Delay(100);
await Console.Out.WriteLineAsync("进入2的第一个异步");
await Task.Delay(100);
await Console.Out.WriteLineAsync("进入2的第二个异步");
})
.OnEntryFrom(3, () => Console.WriteLine("进入2的同步"))
.Permit(1, "1")
.Permit(3, "3")
.OnExit(() => Console.WriteLine("离开2"))
;
st.Configure("3")
.OnEntry(() => Console.WriteLine("进入3"))
.Permit(1, "1")
.Permit(3, "2")
;
var t = st.FireAsync(2);
_ = st.FireAsync(3);
_ = st.FireAsync(1);
await t;
Console.WriteLine(st.State);
会有以下几个影响
FireAsync
OnEntryFromAsync
携带条件,并且没有通过此条件,也会报错。Fire
的转换,可以使用FireAsync
而不会报错。FireAsync
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;
})
;
}
}