本文是最近写的一个简单的 C++ 下推自动机 pdfsm.h 的开发笔记。
出发点:
- 可以用在一个
tick
主循环之中。 - 数据和行为分离。
关于第二点,主要考虑下面的场景:
- 如果我们存在大量的实体、每个实体有自身的状态。
同一类的实体有同一套行为。
比如游戏中的士兵可能会有许多个实例,但是它们的状态和行为是同一套逻辑。
- 那么,实体的数据最好放在一起,比如说在一块连续内存上、以使得性能更优。
这样就面临一个问题:高度使用的状态机模式如何把行为和实体状态分离。
下推自动机 ¶
下推自动机其实就是用一个栈来保存当前状态的有限状态机。
如果我们用数字标号来表示一个状态的话, 那么下推自动机就是只包含一个栈的一个结构体,其中栈顶表示的是当前激活中的状态:
struct StateMachine {
int stack[N], top = -1;
};
下推自动机相比一般的有限状态机来说,保存了下推路径,也就是说,可以恢复到之前的状态,这样就可以方便的实现「暂停」和「恢复」功能:
Push(ctx, RobotState::Dancing); // 下推到新状态 RobotState::Dancing
Pop(ctx); // 恢复到上一个状态
这里采用了静态的、固定大小的数组来存储,原因在于:
方便把
StateMachine
直接嵌入实体中存储,大小是固定的,跟随实体本身的存储。实体在栈上,它的状态机就存栈上;实体在堆上,它的状态机就存堆上。
没有动态内存申请。
并不预期一个状态机的状态数目会很多,否则就不要用状态机了,所以这个
stack
的大小会很有限。
实现 ¶
状态就是一个枚举:
enum class RobotState { Idle, Moving, Dancing, N };
这里末尾的 N
是必要的,在 C++ 中,枚举值的数字这样会递增,这个末尾的 N
就自然表示了这个枚举值的个数。
然后,我们需要一套「跳转表」,如果跳转违背这个跳转表,会在运行时报错:
// 来源 => {可到达的目标状态列表...}
pdfsm::TransitionTable<RobotState> transitions{
{RobotState::Idle, {RobotState::Moving, RobotState::Dancing}},
{RobotState::Moving, {RobotState::Idle, RobotState::Dancing}},
{RobotState::Dancing, {RobotState::Idle}},
};
可以把各个状态彻底解耦吗?
出于「分离关注点」的设计考虑,最最最好的情况是,一个状态只关心自身的实现、而不关心其他状态,然后用「跳转」来连接各个独立的状态。
就是说,每个状态的行为相当于一个模块,然后依赖跳转表把模块搭建成最终的状态机。
但是这个设计似乎太过美好,触发状态跳转的逻辑往往是复杂的、而且依赖于运行时的情况,这会让「跳转」关系成为新的逻辑点。
我还没有找到干净解耦的方式。
我们要实现一个状态的行为逻辑,只需要继承 pdfsm::B<State>
即可:
class RobotMovingBehavior : public pdfsm::B<RobotState::Moving> {
public:
void OnSetup() override {}
void Update(const pdfsm::Context& ctx) override {}
void OnEnter(const pdfsm::Context& ctx) override {}
void OnTerminate(const pdfsm::Context& ctx) override { }
void OnPause(const pdfsm::Context& ctx) override {}
void OnResume(const pdfsm::Context& ctx) override {}
};
可以选择性地实现一些钩子函数,不必赘述。
需要注意的是,状态类里面要只包括行为逻辑、不应该存储任何和实体相关的状态和数据。
如果要访问实体数据、或者环境数据,可以通过 ctx
来完成,此上下文结构可以持有一份外部世界的指针。
对于一个下推自动机,可以有如下几个接口,它们实现在一个叫做 StateMachineHandler
的结构内:
在操作进行的同时,会同步执行对应状态的钩子函数。
那么,最终「数据和行为的分离」体现在哪里呢?即 handler
来绑定和解绑一个实体的状态机,然后再操作:
pdfsm::StateMachineHandler<RobotState> handler(behaviors, transitions);
while (...) {
for (auto& e : entities) {
// 先绑定要操作的实体的状态机结构,再操作
handler.SetHandlingFsm(e.fsm, ctx);
handler.Update(ctx);
handler.ClearHandlingFsm();
}
}
在状态类实现中,也可以通过 GetHandler()
方法获取当前的 handler 的一个引用:
// 在一个状态的 Update 方法中
void Update(const pdfsm::Context& ctx) override {
auto world = std::any_cast<std::shared_ptr<Blackboard>>(ctx.data);
if (world.target) GetHandler().Jump(ctx, RobotState::Moving);
}
状态机善于对事件做出反应,这个库也可以很方便的和 blinker.h 集成。
最后,代码地址:https://github.com/hit9/pdfsm.h,欢迎给小星星。
(完)
本文原始链接地址: https://writings.sh/post/pushdown-fsm