Unity通用有限状态机的从零搭建手册(一):阵前磨枪
Unity通用有限状态机的从零搭建手册(二):凿地筑基
Unity通用有限状态机的从零搭建手册(三):雏形初见
Unity通用有限状态机的从零搭建手册(四):优化漫漫(1)
Unity通用有限状态机的从零搭建手册(五):优化漫漫(2)
Unity通用有限状态机的从零搭建手册(六):优化漫漫(3)
Unity通用有限状态机的从零搭建手册(七):新的挑战
Unity通用有限状态机的从零搭建手册(八):最终试炼
在上一章我们完成了框架的搭建,但是并没有提及如何创建一个完整的状态机以及如何初始化它,这一章我们就来尝试解决这一问题。
上一章中提到状态机如果脱离主体将失去意义,所以我们状态机的构造函数需要一个主体的参数,并且我们不希望外部可以改动状态机的主体,我们将set访问器改为private:
////// 状态机 /// public class FiniteStateMachine: IFSM , IUpdater { public T Subject { get; private set; } public FiniteStateMachine(T subject) { Subject = subject; } }
结构上我们认为状态类从属于状态机,并且使用时也是无法脱离状态机而单独发挥作用的,所以在构造方法中传入状态机实例,主体就可以直接读取状态机的主体而不需要额外存储了:
////// 状态类 /// ///主体 public abstract class State{ public FiniteStateMachine StateMachine { get; private set; } public State(FiniteStateMachine stateMachine) { StateMachine = stateMachine; } //状态主体 public T Subject => StateMachine.Subject; }
初始化方法我们效仿Unity的Awake()和Start()方法来定义状态和状态机初始化和开始运行的行为
////// 状态类 /// ///主体 public abstract class State{ //状态初始化 public virtual void Awake() { } }
////// 状态机 /// public class FiniteStateMachine: IFSM , IUpdater { //默认状态 public State DefaultState { get; set; } /// /// 状态机初始化 /// public void Awake() { foreach (var state in _states) { //遍历状态列表初始化 state.Awake(); } } ////// 状态机开始运行 /// public void Start() { if (DefaultState == null) { Debug.LogError("状态加没有设置默认状态"); return; } _currentState = DefaultState; _currentState.Enter(); } }
设计上我们不允许状态为空的情况出现,所以需要在状态机开始运行之前设置一下默认状态(DefaultState)以确保状态机一旦运行就有当前状态,同时也便于实现状态机的重置功能。
条件类之前介绍过通过传参的方式免去了初始化的步骤,不需要做改动。OK到此我们已经完成了状态机的基本功能搭建,开始尝试做一个小Demo吧!
第一个实例我们测试状态机功能的第一个Demo内容很简单,创建一个小方块响应鼠标的悬浮、点击等操作改变自己的状态,状态的转移通过颜色的变化来体现,下面是状态关系示意图:
三个颜色表示三种状态,箭头上的文字表示转移条件,箭头则指向转移的目标状态。根据图示,我们需要创建三个具体状态类和四个具体条件类,当然主体Cube类也是不能少的:
Cube类:
我们的小方块需要响应鼠标事件,我们写一下Unity自带的几个鼠标事件方法,用两个布尔值储存一下鼠标和物体的交互关系,使用之前记得检查一下创建的方块是否带有碰撞体。
public class Cube : MonoBehaviour
{
//获取材质
public Material material => GetComponent().material;
//鼠标是否悬浮
public bool mouseOver = false;
//鼠标是否按下
public bool mouseDown = false;
private void OnMouseEnter()
{
mouseOver = true;
Debug.Log("MouseOver");
}
private void OnMouseExit()
{
mouseOver = false;
Debug.Log("MouseExit");
}
private void OnMouseDown()
{
mouseDown = true;
Debug.Log("MouseDown");
}
private void OnMouseUp()
{
mouseDown = false;
Debug.Log("MouseUp");
}
//TODO 状态机的初始化
}
后面几个状态类内容比较简单直接贴代码了
状态类:
//默认状态 public class Cube_State_Default : State{ public Cube_State_Default(FiniteStateMachine stateMachine) : base(stateMachine) { } public override void Enter() { base.Enter(); Subject.material.color = Color.white; } }
//鼠标悬浮状态 public class Cube_State_MouseDown : State{ public Cube_State_MouseDown(FiniteStateMachine stateMachine) : base(stateMachine) { } public override void Enter() { base.Enter(); Subject.material.color = Color.green; } }
//鼠标按下状态 public class Cube_State_MouseDown : State{ public Cube_State_MouseDown(FiniteStateMachine stateMachine) : base(stateMachine) { } public override void Enter() { base.Enter(); Subject.material.color = Color.green; } }
条件类:
//鼠标是否悬浮 public class Cube_Condition_MouseOver : Condition{ public override bool ConditionCheck(Cube subject) { return subject.mouseOver; } }
//鼠标是否移开 public class Cube_Condition_MouseExit : Condition{ public override bool ConditionCheck(Cube subject) { return !subject.mouseOver; } }
//鼠标是否按下 public class Cube_Condition_MouseDown : Condition{ public override bool ConditionCheck(Cube subject) { return subject.mouseDown; } }
//鼠标是否松开 public class Cube_Condition_MouseUp : Condition{ public override bool ConditionCheck(Cube subject) { return !subject.mouseDown; } }
状态机配置:
状态和条件都准备好之后,我们需要在主体类当中把状态机配置起来,先在类中新建一个私有变量存储状态机实例。
public class Cube : MonoBehaviour
{
private FiniteStateMachine _stateMachine;
//...
}
在Awake方法中配置并初始化状态机:
public class Cube : MonoBehaviour
{
private void Awake()
{
_stateMachine = new FiniteStateMachine(this);
//状态创建
var stateDefault = new Cube_State_Default(_stateMachine);
var stateMouseDown = new Cube_State_MouseDown(_stateMachine);
var stateMouseOver = new Cube_State_MouseOver(_stateMachine);
//条件创建
var conditionMouseOver = new Cube_Condition_MouseOver();
var conditionMouseExit = new Cube_Condition_MouseExit();
var conditionMouseDown = new Cube_Condition_MouseDown();
var conditionMouseUp = new Cube_Condition_MouseUp();
//条件添加
stateDefault.AddCondition(conditionMouseOver, stateMouseOver);
stateMouseOver.AddCondition(conditionMouseExit, stateDefault).AddCondition(conditionMouseDown, stateMouseDown);
stateMouseDown.AddCondition(conditionMouseUp, stateMouseOver);
//状态添加
_stateMachine.AddState(stateDefault).AddState(stateMouseDown).AddState(stateMouseOver).DefaultState = stateDefault;
_stateMachine.Awake();
}
}
为了编写方便,状态和条件添加的时候我们使用了链式编程格式,相应地我们需要在AddState方法中将this作为返回值:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ja4FcueM-1647355518721)(C:UsersAdministratorAppDataRoamingTyporatypora-user-imagesimage-20220301154539959.png)]
最后在Start中启动状态机,Update中更新状态机:
public class Cube : MonoBehaviour
{
private void Start()
{
_stateMachine.Start();
}
private void Update()
{
_stateMachine.Update();
}
}
运行,效果达成!
欸这时有人就会说了,哎呀你这这么简单的逻辑又是配置又是初始化的,还新建了一堆的类,new了一堆实例,也太麻烦了。啊确实,为一个简单的逻辑去做一个状态机有时的确是吃力不讨好的行为,不过要注意到的是,我们有两个每一个状态下只需要判断一个bool值就可以确定状态的转化,而单纯使用if-else语句需要在任何情况下同时判断两个bool值:
public class Cube : MonoBehaviour
{
public void StateUpdate()
{
if (!mouseOver && !mouseDown)
{
material.color = Color.white;
}
else if (mouseOver && !mouseDown)
{
material.color = Color.yellow;
}
else if(mouseOver && mouseDown)
{
material.color = Color.red;
}
}
private void Update()
{
StateUpdate();
}
}
这样的写法显然也能达成一样的效果,但是如果我希望在没有点击的情况下移入移出鼠标打印一条信息呢,这一串判断语句可能有需要进行破坏式的改动;如果状态的转化与更多属性有关,分支语句很有可能会指数增长;随着项目的迭代,状态的数量将变得庞大,状态之间的转换关系也会变得更为复杂,这时候你就会发现离开状态机将寸步难行。当然我们的状态机也有不少的优化空间,可以让配置的过程变得更为简洁,之后的章节我们来尝试用C#的反射特性对状态机的配置和繁杂的实例创建过程进行优化,实现一定程度的自动化,同时也会有更加复杂的案例让我们感受到状态机的优势所在。
Unity通用有限状态机的从零搭建手册(一):阵前磨枪
Unity通用有限状态机的从零搭建手册(二):凿地筑基
Unity通用有限状态机的从零搭建手册(三):雏形初见
Unity通用有限状态机的从零搭建手册(四):优化漫漫(1)
Unity通用有限状态机的从零搭建手册(五):优化漫漫(2)
Unity通用有限状态机的从零搭建手册(六):优化漫漫(3)
Unity通用有限状态机的从零搭建手册(七):新的挑战
Unity通用有限状态机的从零搭建手册(八):最终试炼



