设计心得——状态机

一、状态机

在设计一些与硬件交互或者游戏等开发中,经常会听到状态机(State Machines)这个字眼,而在设计模式(GoF)中,又经常听到状态模式这个概念,它们之间有什么联系和不同呢?通常,状态机是一种计算模式,它描述在不同的状态下系统的行为及状态间的转换。状态机一般包括状态、事件、转换和动作几个部分。
状态模式是一种设计模式,状态模式下每个状态都是一个类,通过状态的变化操作不同的类对象可以更好的描述状态的工作状态。
这里只关注有限状态机,在有限状态机中,状态机一般有两种常见的模型:
1、输出只和当前状态有关而与输入无关,则称为摩尔(Moore)状态机,即动作与状态强相关;
2、输出不仅和当前状态有关而且和输入有关,则称为米利(Mealy)状态机,即动作与转换强相关;
对于前者大家都非常熟悉,其实后者也比较常见,比如在某些回合制游戏中,当角色的血量到达一个危险的域值(危险状态)时,游戏会停止,然后弹出一个框,问你是继续血战,有死亡风险,还是撤退。这时,就就是Mealy状态机,客户选择的不同(输入不同)结果也不相同。也就是说,这种情况下,状态机受状态和事件触发的同时影响。

二、状态机设计层面实现

状态机是一个模型,是一种抽象的存在;而状态模式是一种状态机的实现方式,是一种具体的实现方式(当然,它也存在着一定的抽象,但这种抽象已经更接近于实现)。状态机重点在状态的转换造成不同状况(即逻辑和事件),而状态模式更注重在状态转换的整个过程中的实现(即整个状态转换期间的行为、状态实现的结果)。
状态模式是状态机的一种具体的实现方式,但不是唯一。最简单的状态机是可以用if…else来实现,也可以用switch或表驱动等来实现。而状态模式则更强调在面向对象设计中的实现。所以大家一定明白状态机和状态模式的关系。
需要说明的是,在Qt和Boost等框架库中,都封装实现了相关的状态机实现,如果有需要的大家可以参考其实进行一些状态机的控制和实现,相对来说要简单一些。

三、应用场景

状态机的应用场景非常广泛,比如在驱动硬件的情况下,按一下按键,状态进行运行状态,再按一下,进入加速状态,再按一下,恢复正常…这就是最简单的状态机的处理场景。另外在游戏开发中,一个角色在正常的情况下被剑砍后是什么状态,中毒后再被砍是什么状态,同时又被冰冻后呢?这种情况大家就非常好理解了吧。
另外,在编译器的实现上,状态机也是应用的非常丰富,如果大家学习过编译原理自然就明白,不明白的也没关系,毕竟搞编译器开发的人少之又少。其它的情况,如协议实现上也广泛采用状态机的处理。

四、实例分析

看前面Mealy例子:

#include <iostream>
#include <map>
#include <functional>
#include <string>

enum class State { Figthting, dangerous, End };
enum class Event { Start, Keep, Retreat, Timeout };


using execAction = std::function<std::string()>;

struct TransState {
    State nextState;
    execAction action;
};

class MealyMachine {
public:
    MealyMachine() : curState(State::Figthting) {

        TransStates = {

            {State::Figthting, {
                {Event::Start, {State::dangerous, []{ return "start fighting."; }}
            }},
            {State::dangerous, {
                {Event::Keep, {State::End, []{ return "go on..."; }},
                {Event::Retreat, {State::Figthting, []{ return "retreat and running"; }},
                {Event::Timeout, {State::Figthting, []{ return "Timeout. go on..."; }}
            }},
            {State::End, {
                {Event::Timeout, {State::Figthting, []{ return "End"; }}
            }}
        };
    }

    std::string notify(Event event) {
        auto& stateTransStates = TransStates[curState];
        if (auto it = stateTransStates.find(event); it != stateTransStates.end()) {
            TransState TransState = it->second;
            curState = TransState.nextState;
            return TransState.action();
        } else {
            return "error event!";
        }
    }

private:
    State curState;
    std::map<State, std::map<Event, TransState>> TransStates;
};

int main() {
    MealyMachine machine;

    std::cout << machine.notify(Event::Start) << std::endl;  
    std::cout << machine.notify(Event::Keep) << std::endl;
    std::cout << machine.notify(Event::Timeout) << std::endl;    

    std::cout << machine.notify(Event::Start) << std::endl;  
    std::cout << machine.notify(Event::Retreat) << std::endl;    

    return 0;
}

这里没有使用状态模式实现,主要是为了更简单的体现Mealy这种状态机。
下面再看一个使用Qt中的状态机实现的相关例程:

#include <QCoreApplication>
#include <QStateMachine>
#include <QState>
#include <QDebug>
int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);
 
    QStateMachine machine;
 
    QState *s1 = new QState();
    QState *s2 = new QState();
    s1->assignProperty(&a, "state", "State 1");
    s2->assignProperty(&a, "state", "State 2");
 
    QEventTransition *transition = new QEventTransition(&machine, QEvent::Type(QEvent::User));
    transition->setTargetState(s2);
    s1->addTransition(transition);
 
    machine.addState(s1);
    machine.addState(s2);
    machine.setInitialState(s1);
 
    machine.start();
 
    QCoreApplication::postEvent(&machine, new QEvent((QEvent::Type)QEvent::User));
 
    return a.exec();
}

代码非常简单,但可以看出使用起来要比自己实现简单很多。

五、总结

一定要明白状态机的目的和作用,要学会在不同的场景和条件灵活的使用状态机。一般情况下,状态机的设计不要过于复杂,这样不利用状态机的维护。同样过于简单的情况也不一定非得要用状态机。至于如何避免复杂,就记住一句话,拆分状态逻辑,利用中间层处理。
状态机和其实现的方式,其实都是重点,重点在于如何将状态机与实际的实现逻辑严格的对应起来,保证状态的稳定和准确,特别是不能忽视某些特定条件的状态转换及行为的细微差异。只要能把握完整的状态流程,状态机的实现也就是一种代码的展开罢了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值