• 讲给自己听:gems中的一个状态机实现


    打算做一个基于策略的状态机,不知道直觉准不准.先准备前期的知识.以下是<游戏编程精粹3>中实现的状态机.

    状态机 = 状态 + 机. 

    machine驱动state.

    一般的,对于类似state概念的判断,我们简单的使用if-else或者switch来判断.那当我们可以有很多状态时,if-else或switch搭成的那一大坨代码中的每个判断分支里,就会看到很多相似的代码,比如这些状态所依附的对象,或者相似的计算语句等.很熟悉的:

     1 if(state == a) 
    2 {
    3 if(event_c) { do_c(); state = c; }
    4 if(event_b) { do_b(); state = b; }
    5 }
    6 else if(state == b)
    7 {
    8 if(event_a) { do_a(); state = a; }
    9 if(event_b) { do_b(); state = b; }
    10 }
    11 else if(state_c)
    12 {
    if(event_a) { do_a(); state = a; }
    13 if(event_c) { do_c(); state =c; }
    14 } else {
    15 //do nothing
    16 }

    在程序的每个逻辑循环中这段代码都会被调用.

    这就是个简单的状态机,这里的判断分支已经包含了对进入每个状态后的处理,然后再根据结果或者其他规则把当前状态设为某一个可行值.于是状态就可以根据逻辑保持跳转,每次跳转都意味着我们处理了一些事情.所以这整段if-else就像一台machine,驱动了state的跳转.不过一个术语而已,很贴切.

    想起术语就想起微软一些奇怪的东西,比如网络编程的iocp,说白了就是个线程池,干嘛扯了这么长的一串单词让人发怵.其他的什么敏捷编程,驱动开发,结对编程, 我看了一两段已经要嗑药了,他们的口气明明在搞基,不是在编程啊.太把自己当一回事生活就没意思了.发发牢骚,也许是我的层次还没到.

    当状态和事件很多时,这段if-else明显会变成一大坨东西.每次要加什么状态事件都得在这里改.然后if-else判断到某一个具体状态,平均需要花费n/2的时间,n是状态个数.

    c里可以通过定义一些宏和利用其他机制(消息或回调函数等)把过程简化,<游戏编程精粹1>有c的一个状态机实现.

    这里是用c++来实现的,面向对象方式.

    首先上面的c代码,state是全局的,至少也是某个模块全局可见的.state往往依附于对象,每个对象都维护自己的state应该是蛮清楚的,所以我们会将state组合到对象中.显然state可以直接声明为对象内的简单枚举值,整型值,那么有没有必要将state实现为一个类呢?看看第3行:

     if(event_c) { do_c(); state = c; }

    { do_c(); state = c; }内的2个语句必然是联系在一起的.那么do_c()是否可以和state_c挂钩呢?即是说state_c内有一个do_c()函数.

    c代码中的do_c()是由if-else这个语句调用.我们实现面向对象的状态机的话,这段if-else结构显然要被我们抽象为一个machine类了.machine肯定是至少要模拟if-else的控制结构的类了,但是具体的do_c()是否应该在machine内实现呢?

    state内实现do_c(),语义不明确.因为do_c()是实际的宿主要做的事情,那这个state必然要知道很多关于宿主的细节才能do_c()吧.这个解决办法是给state传入一个函数指针.当状态切换时,回调这个函数即可.而machine内实现do_c(),取决于我们是怎么看待machine和宿主关系的.

    照我理解,machine应该只是实现一个控制结构,而具体do_c()要干的事情应该跟他没关系,如果machine作为一个构件组合在宿主对象中,确实是这样.跟state情况一样,machine也要知道很多关于do_c()的细节才行.更关键的是,machine要实现很多个类似do_c()的函数,而machine本身的类型也会有很多,会有负责机关状态的machine,也会有负责怪物状态的machine...它们要实现一系列do_c()函数族,每个系列当然依据各自的状态都有自己的一堆do_c().我们是可以把do_c()的函数指针传入machine的,但问题来了,显然我们需要传入多个函数指针,因为machine内有多个状态,为了判断何时该使用哪些函数指针,if-else又来了,而这正是我们需要解决的问题.我觉得machine内使用do_c()的思路杂,可能还是state内调用do_c比较明了,因为一个state只可能对应一个do_c().

    前面说过在c代码的每个逻辑循环中if-else代码都会被调用.machine取代if-else后,也应该有个update()之类的函数,供循环中调用.当状态跳转设定好后,我们将在update()中启用这个状态,如state_a->go(),go()函数内将会调用do_c().是的,machine需要知道state,而前面我们已经决定不采用把machine组合到宿主内的手法.事实上,if-else结构确实是应该发生在宿主内部的事.因为在每个逻辑循环中,应该是每个对象都得运行if-else一次,宿主本身确实就是一个状态机machine,当外部事件触发时,宿主自己决定状态怎么跳转,然后怎么处理对应的更新.以面向对象的方式看,当一个事件发生后,大家(各个对象)最好各管各的,不要都得让外部处理,那样我们要处理很多交集,分支,变成一锅粥了.这就是前面所说的看待machine和宿主关系的用意,宿主如果继承自machine,那就省事了.确定后,流程是这样的:

    [event触发] --> [machine根据规则设定当前state] --> [外部循环调用machine->update()] --> [调用state->go()] --> [state回调do_c()]

    当我们需要添加一个新的state进去时,只需要在machine(即宿主对象)内添加该state,再给该state实现相应的处理函数do_x(),然后将do_x()函数指针传入新state中即可.利用模板,我们甚至不必写新的state类.最后,在machine中添加跳转逻辑就行了.这里第一步[event触发]可能会用到if-else结构,比如简单的按键检查,但event事件大家都可以做成观察者模式或回调函数实现,所以这一步同样也是可以避免多级判断的.

    需要注意的是,以上分析我们是建立在这么一个假设上的:宿主内的state可以根据需要添加,而宿主本身的machine固定的.如果宿主需要面对选择多个状态机之一的情况,那采取继承自machine的手法就不行了.但多状态机的行为我认为是可以用单状态机模拟的.究竟是否需要及复杂性比较是个难题.

    首先抽象出了状态类.state只需保存函数指针即可,因为c++成员函数指针需要搭配类实例,所以还需要保存一个类指针.使用模板是必然的,回避多个类的成员函数指针所对应的类实例的问题.state的实现:

    <state.h>

    View Code
    #ifndef _fsm_state_h
    #define _fsm_state_h

    // System Includes
    #include <assert.h>

    //==================================================================================================
    // CState

    // CState Class
    class CState
    {
    public:
    // Destructor
    virtual ~CState() {}

    // State Functions
    virtual void ExecuteBeginState()=0;
    virtual void ExecuteState()=0;
    virtual void ExecuteEndState()=0;
    };

    //==================================================================================================
    // CStateTemplate

    // CStateTemplate Class
    template <class T>
    class CStateTemplate : public CState
    {
    protected:
    typedef void (T::*PFNSTATE)(void);
    T *m_pInstance; // Instance Pointer
    PFNSTATE m_pfnBeginState; // State Function Pointer
    PFNSTATE m_pfnState; // State Function Pointer
    PFNSTATE m_pfnEndState; // State Function Pointer

    public:
    // Constructor
    CStateTemplate() : m_pInstance(0),m_pfnBeginState(0),m_pfnState(0),m_pfnEndState(0) {}

    // Initialize Functions
    void Set(T *pInstance,PFNSTATE pfnBeginState,PFNSTATE pfnState,PFNSTATE pfnEndState)
    {
    // Set Instance
    assert(pInstance);
    m_pInstance=pInstance;
    // Set Function Pointers
    assert(pfnBeginState);
    m_pfnBeginState=pfnBeginState;
    assert(pfnBeginState);
    m_pfnState=pfnState;
    assert(pfnBeginState);
    m_pfnEndState=pfnEndState;
    }

    // State Functions
    virtual void ExecuteBeginState()
    {
    // Begin State
    assert(m_pInstance && m_pfnBeginState);
    (m_pInstance->*m_pfnBeginState)();
    }
    virtual void ExecuteState()
    {
    // State
    assert(m_pInstance && m_pfnState);
    (m_pInstance->*m_pfnState)();
    }
    virtual void ExecuteEndState()
    {
    // End State
    assert(m_pInstance && m_pfnEndState);
    (m_pInstance->*m_pfnEndState)();
    }
    };

    #endif

    每个状态需在machine初始化好时设置好(传入实例及成员函数指针),对应上述代码中的Set()函数.我们想传入多少个函数指针依你的喜好了.一般来说从跳转到一个状态直到离开时,我们会有3个阶段:初始化-->维持阶段-->离开处理.为这3个阶段各准备一个函数传入既可.

    以下将machine成为FSM.这是FSM的实现:

    <fsm.h>

    View Code
    #ifndef _fsm_fsm_h
    #define _fsm_fsm_h

    // Local Includes
    #include "State.h"

    // FSM Class
    class CFSM
    {
    protected:
    CState *m_pCurrentState; // Current State
    CState *m_pNewState; // New State
    CStateTemplate<CFSM> m_StateInitial; // Initial State

    public:
    // Constructor
    CFSM();

    // Destructor
    virtual ~CFSM() {}

    // Global Functions
    virtual void Update();

    // State Functions
    bool IsState(CState &State);
    bool GotoState(CState &NewState);

    virtual void BeginStateInitial() {}
    virtual void StateInitial() {}
    virtual void EndStateInitial() {}
    };

    #endif

    <fsm.cpp>

    View Code
    // Local Includes
    #include "FSM.h"

    // Constructor
    CFSM::CFSM()
    {
    // Initialize States
    m_StateInitial.Set(this,BeginStateInitial,StateInitial,EndStateInitial);
    // Initialize State Machine
    m_pCurrentState=static_cast<CState*>(&m_StateInitial);
    m_pNewState=0;
    }

    //======================================================================================================
    // Global Functions

    // Update
    void CFSM::Update()
    {
    // Check New State
    if (m_pNewState)
    {
    // Execute End State
    m_pCurrentState->ExecuteEndState();
    // Set New State
    m_pCurrentState=m_pNewState;
    m_pNewState=0;
    // Execute Begin State
    m_pCurrentState->ExecuteBeginState();
    }
    // Execute State
    m_pCurrentState->ExecuteState();
    }

    //======================================================================================================
    // State Functions

    // Is State
    bool CFSM::IsState(CState &State)
    {
    return (m_pCurrentState==&State);
    }

    // Goto State
    bool CFSM::GotoState(CState &NewState)
    {
    // Set New State
    m_pNewState=&NewState;
    return true;
    }

    原始的FSM自带了3个状态m_pCurrentState,m_pNewState, m_StateInitial.m_pCurrentState记录当前状态,每次逻辑循环若无跳转则执行之,若有新状态则执行旧状态的离开函数和新状态的初始化函数,并设置m_pCurrentState为新状态. m_StateInitial则作为全局初始化用,重载那3个函数即可,当然你也可以不用.                                                                                                                             



  • 相关阅读:
    SQL SERVER数据库索引(学习笔记)
    下载word文档
    Asp.net MVC 3+ Jquery UI Autocomplete实现百度效果
    利用C#实现分布式数据库查询
    SQL2005 存储过程通用分页
    发送电子邮件
    查看索引碎片,并生成重建索引代码
    数学趣题——哥德巴赫猜想的近似证明
    数学趣题——判断回文数字
    数学趣题——填数字游戏
  • 原文地址:https://www.cnblogs.com/flytrace/p/2201084.html
Copyright © 2020-2023  润新知