2017年7月22日 星期六

scm framework 教學(StateChart Machine)

今天,我們來學習怎麼使用scm library。

首先,你可以在這裏下載scm。

不知道為什麼要用scm的請看之前blog的討論。

scm裏面有幾個測試程式,其中一個是糖果販賣機,原始碼如下:

#include <StateMachineManager.h>
#include <iostream>

using namespace std;
using namespace SCM;

std::string cm_scxml = "\
   <scxml> \
        <state id='idle'> \
            <transition event='empty' target='disabled'/> \
            <transition event='coin' target='active'/> \
        </state> \
        <state id='active'> \
            <transition event='release-candy' ontransit='releaseCandy' target='releasing'/> \
            <transition event='withdraw-coin' ontransit='withdrawCoins' target='idle'/> \
        </state> \
        <state id='releasing'> \
            <transition event='candy-released' cond='condNoCandy' target='disabled'/> \
            <transition event='candy-released' target='idle'/> \
        </state> \
        <state id='disabled'> \
            <transition event='add-candy' cond='condNoCredit' target='idle'/> \
            <transition event='add-candy' target='active'/> \
        </state> \
    </scxml> \
";

class TheCandyMachine : public Uncopyable
{
    StateMachine *mach_;
    int           credit_; // 
    int           num_of_candy_stored_;
    
public:
    TheCandyMachine()
    : credit_(0)
    , num_of_candy_stored_(0)
    {
        mach_ = StateMachineManager::instance()->getMach("cm_scxml");
        REGISTER_STATE_SLOT (mach_, "idle", &TheCandyMachine::onentry_idle, &TheCandyMachine::onexit_idle, this);
        REGISTER_STATE_SLOT (mach_, "active", &TheCandyMachine::onentry_active, &TheCandyMachine::onexit_active, this);
        REGISTER_STATE_SLOT (mach_, "releasing", &TheCandyMachine::onentry_releasing, &TheCandyMachine::onexit_releasing, this);
        REGISTER_STATE_SLOT (mach_, "disabled", &TheCandyMachine::onentry_disabled, &TheCandyMachine::onexit_disabled, this);

        //boost::function<bool() const> cond_slot;
        REGISTER_COND_SLOT(mach_, "condNoCandy", &TheCandyMachine::condNoCandy, this);
        REGISTER_COND_SLOT(mach_, "condNoCredit", &TheCandyMachine::condNoCredit, this);
        
        // boost::function<void()> 
        REGISTER_ACTION_SLOT(mach_, "releaseCandy", &TheCandyMachine::releaseCandy, this);
        REGISTER_ACTION_SLOT(mach_, "withdrawCoins", &TheCandyMachine::withdrawCoins, this);
        
        mach_->StartEngine();
} ~TheCandyMachine () { mach_->ShutDownEngine(true); } void store_candy (int num) { num_of_candy_stored_ += num; mach_->enqueEvent("add-candy"); cout << "store " << num << " gumballs, now machine has " << num_of_candy_stored_ << " gumballs." << endl; } void insertQuater () { insert_coin(25); cout << "you insert a quarter, now credit = " << credit_ << endl; } void ejectQuater () { mach_->enqueEvent("withdraw-coin"); cout << "you pulled the eject crank" << endl; } void turnCrank () { mach_->enqueEvent("release-candy"); cout << "you turned release crank" << endl; } protected: void insert_coin (int credit) { credit_ += credit; mach_->enqueEvent("coin"); } void onentry_idle () { cout << "onentry_idle" << endl; cout << "Machine is waiting for quarter" << endl; if (num_of_candy_stored_ == 0) { mach_->enqueEvent ("empty"); } } void onexit_idle () { cout << "onexit_idle" << endl; } void onentry_active () { cout << "onentry_active" << endl; } void onexit_active () { cout << "onexit_active" << endl; } void onentry_releasing () { cout << "onentry_releasing" << endl; //WorkerManager::instance()->perform_work_after(1.0, boost::bind(&TheCandyMachine::candy_released, this), false); candy_released (); } void onexit_releasing () { cout << "onexit_releasing" << endl; } void onentry_disabled () { cout << "onentry_disabled" << endl; } void onexit_disabled () { cout << "onexit_disabled" << endl; } bool condNoCandy () const { return num_of_candy_stored_ == 0; } bool condNoCredit () const { return credit_ == 0; } void releaseCandy () { int num_to_release = credit_ / 25; if (num_to_release > num_of_candy_stored_) { num_to_release = num_of_candy_stored_; } cout << "release " << num_to_release << " gumballs" << endl; num_of_candy_stored_ -= num_to_release; credit_ -= num_to_release * 25; } void withdrawCoins () { cout << "there you go, the money, " << credit_ << endl; credit_ = 0; cout << "Quarter returned" << endl; } void candy_released () { mach_->enqueEvent("candy-released"); } public: void report () { cout << "\nA Candy Selling Machine\n"; cout << "Inventory: " << num_of_candy_stored_ << " gumballs\n"; cout << "Credit: " << credit_ << endl << endl; } void init () { mach_->frame_move(0); assert (mach_->inState("disabled")); this->store_candy(5); mach_->frame_move(0); assert (mach_->inState("idle")); report (); } void frame_move () { mach_->frame_move(0); } void test () { this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); report (); this->insertQuater(); frame_move(); this->ejectQuater(); frame_move(); report (); this->turnCrank(); frame_move(); report (); this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); this->ejectQuater(); frame_move(); report(); this->insertQuater(); frame_move(); this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); this->insertQuater(); frame_move(); this->ejectQuater(); frame_move(); report(); this->store_candy(5); frame_move(); this->turnCrank(); frame_move(); report(); } }; int main(int argc, char* argv[]) { AutoReleasePool apool; StateMachineManager::instance()->set_scxml("cm_scxml", cm_scxml); { TheCandyMachine mach; mach.init (); mach.test (); } return 0; }


先用cmake編譯起來吧,你需要boost與expat這兩個library。如果你使用Linux,系統一定有提供package。如果是使用Windows,可能要自己build expat。boost方面只要header only的boost signals與boost function,設定一下include路徑就行了。不知道怎麼用cmake嗎?大概像這樣,先到clone出來的目錄,然後

mkdir build
cd build
cmake ../scm
make

就行了。build完執行看看吧,你應該可以見到如下的輸出:
=========================================================
onentry_idle
Machine is waiting for quarter
onexit_idle
onentry_disabled
store 5 gumballs, now machine has 5 gumballs.
onexit_disabled
onentry_idle
Machine is waiting for quarter

A Candy Selling Machine
Inventory: 5 gumballs
Credit: 0

you insert a quarter, now credit = 25
onexit_idle
onentry_active
you turned release crank
onexit_active
release 1 gumballs
onentry_releasing
onexit_releasing
onentry_idle
Machine is waiting for quarter

A Candy Selling Machine
Inventory: 4 gumballs
Credit: 0

you insert a quarter, now credit = 25
onexit_idle
onentry_active
you pulled the eject crank
onexit_active
there you go, the money, 25
Quarter returned                                                                                                                                                            
onentry_idle
Machine is waiting for quarter

A Candy Selling Machine
Inventory: 4 gumballs
Credit: 0

you turned release crank

A Candy Selling Machine
Inventory: 4 gumballs
Credit: 0

you insert a quarter, now credit = 25
onexit_idle
onentry_active
you turned release crank
onexit_active
release 1 gumballs
onentry_releasing
onexit_releasing
onentry_idle
Machine is waiting for quarter
you insert a quarter, now credit = 25
onexit_idle
onentry_active
you turned release crank
onexit_active
release 1 gumballs
onentry_releasing
onexit_releasing
onentry_idle
Machine is waiting for quarter
you pulled the eject crank

A Candy Selling Machine
Inventory: 2 gumballs
Credit: 0

you insert a quarter, now credit = 25
onexit_idle
onentry_active
you insert a quarter, now credit = 50
you turned release crank
onexit_active
release 2 gumballs
onentry_releasing
onexit_releasing
onentry_disabled
you insert a quarter, now credit = 25
you turned release crank
you insert a quarter, now credit = 50
you pulled the eject crank

A Candy Selling Machine
Inventory: 0 gumballs
Credit: 50

store 5 gumballs, now machine has 5 gumballs.
onexit_disabled
onentry_active
you turned release crank
onexit_active
release 2 gumballs
onentry_releasing
onexit_releasing
onentry_idle
Machine is waiting for quarter

A Candy Selling Machine
Inventory: 3 gumballs
Credit: 0

onexit_idle
===============================================================


我們從 main()開始看看怎麼使用吧。

首先你需要生出一個AutoReleasePool 的object。scm學Objective-C的方式管理記憶體,但我們畢竟不是一個Objective-C的runtime,而是C++,所以至少要自己生成一個 auto release pool物件。你呼叫autorelease()的物件其實會被放在這邊等待被release。

scm是為遊戲類的即時應用開發的,這類的即時應用通常會有一個main loop,你需要在main loop的每個frame呼叫我們FrameMover物件的frame_move(),參數是經過了多少時間。

接下來這行

    StateMachineManager::instance()->set_scxml("cm_scxml", cm_scxml);

將一個scxml id與scxml內容關聯起來。你的應用裏不會只有一個state machine,通常你會在程式開始時將state machine的定義load進來。這樣的話當要生成一個state machine時只要像這樣

    mach_ = StateMachineManager::instance()->getMach("cm_scxml");

呼叫getMach()就行。cm_scxml是定義在原始碼裏的字串,你可能會想把這些state machine的定義存在外部空間,需要時再load進來。

接下來,在生成新state machine之後,在呼叫它的StartEngine之前,你會先設定一些名字與slot的對應(改天再來談signal/slot吧)。scm裏分三種slot: action slot, condition slot, 與 frame move slot。你的scxml裏設定的onentry, onexit, ontransit等action就是呼叫你設定的slot。為方便使用,scm提供REGISTER_STATE_SLOT(), REGISTER_COND_SLOT()等巨集。設定好之後就可以StartEngine()了。

接下來你只要用enqueEvent()來告訴mach發生了什麼事件,之後mach在 frame_move時就會處理這些事件,進行狀態轉換,並呼叫你設定好的slot,就像我們在範例看到的。

是不是很簡單易用呢。:D

下週我們再來談scm與SCXML標準不同的地方和使用時的一些注意事項吧。

沒有留言:

張貼留言

ftps.space released!

  I started doing web site development some time ago.   After fiddled with Flask, nginx, javascript, html, css, websocket, etc, etc... I h...