2017年7月8日 星期六

關於狀態(states) part II

今天我們來繼續「狀態」的話題。

不過我們先岔開一下話題談一下strategy pattern。有進行design pattern研究的朋友應該都發現了,我們的例子看起來更像strategy pattern。是的,它們在精神上是差不多的。strategy pattern簡單說就是在不同的情景、條件、選擇下,某個行為的執行方法可能不同,所以我們把這些方法變成可依照時機使用/切換的策略物件,可以在執行時期變換。狀態切換時,你同時切換某個行為的策略物件也是很合理的。因此,說state pattern是一種strategy pattern也不為過。其實不用拘泥於這些術語。我們目的一樣是避免一堆重複的「if..else...」。只是在strategy pattern原本可能是拿使用者選擇的策略來做判斷,而在state pattern則是用執行時的狀態做判斷。

ok, 回到state pattern。我們先把上次的範例做完整一點吧。你可以把下面的程式碼編譯起來跑看看。

//GameEvent.h
#ifndef Game_Event_H
#define Game_Event_H

enum {
    StartKey,
    OptionKey,
    UpKey,
    DownKey,
    LeftKey,
    RightKey,
    GameOverEvent,
};

struct InputEvent
{
    int event_;
    InputEvent (int e)
    : event_(e)
    {}  
};

#endif

//GameState.h
#ifndef Game_State_H
#define Game_State_H

#include <string>
#include "GameEvent.h"

class GameApp;

class GameState
{
public:
    virtual ~GameState() {}
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_system_event(GameApp &app, const InputEvent &e) = 0;
    virtual std::string name () const = 0;
};


#endif
//GameApp.h
#ifndef Game_App_H
#define Game_App_H

#include "GameEvent.h"
#include "GameState.h"

enum eDifficulty {
    EasyLevel,
    NormalLevel,
    HardLevel,
};

class StateTitleScreen: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e);
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e);
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e);
    virtual void handle_system_event(GameApp &app, const InputEvent &e);

    virtual std::string name () const { return "TitleScreen"; }
};

class StateOptionScreen: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e);
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e);
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e);
    virtual void handle_system_event(GameApp &app, const InputEvent &e);

    virtual std::string name () const { return "OptionScreen"; }
};

class StatePlaying: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e);
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e);
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e);
    virtual void handle_system_event(GameApp &app, const InputEvent &e);

    virtual std::string name () const { return "Playing"; }
};


class StateCountingScore: public GameState
{
    
public:
    StateCountingScore ();
    ~StateCountingScore ();
    
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e);
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e);
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e);
    virtual void handle_system_event(GameApp &app, const InputEvent &e);

    virtual std::string name () const { return "CountingScore"; }
};

class GameApp
{
    GameState *game_state_; // 目前遊戲狀態物件
    eDifficulty difficulty_;

    friend class StateTitleScreen;
    friend class StateOptionScreen;
    friend class StatePlaying;
    friend class StateCountingScore;

    void set_state (GameState *state);
    
public:
    GameApp();
    virtual ~GameApp() {}

    void handle_keyboard_input(const InputEvent &e);
    void handle_mouse_input(const InputEvent &e);
    void handle_gamepad_input(const InputEvent &e);
    void handle_system_event(const InputEvent &e);
};
#endif

// main.cpp
#include <iostream>
#include <string>
#include "GameApp.h"

using namespace std;

//class StateTitleScreen
void StateTitleScreen::handle_keyboard_input(GameApp &app, const  InputEvent &e)
{
    if (e.event_ == StartKey) {
        app.set_state(new StatePlaying);
    } else if (e.event_ == OptionKey) {
        app.set_state(new StateOptionScreen);
    }
}

void StateTitleScreen::handle_mouse_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateTitleScreen::handle_gamepad_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateTitleScreen::handle_system_event(GameApp &app, const  InputEvent &e)
{
    //...
}

//class StateOptionScreen
void StateOptionScreen::handle_keyboard_input(GameApp &app, const  InputEvent &e)
{
    // logic to navigate option screen, set game difficulty, screen resolution, etc.
}

void StateOptionScreen::handle_mouse_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateOptionScreen::handle_gamepad_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateOptionScreen::handle_system_event(GameApp &app, const  InputEvent &e)
{
    //...
}

//class StatePlaying
void StatePlaying::handle_keyboard_input(GameApp &app, const  InputEvent &e)
{
    if (e.event_ == GameOverEvent) {
        app.set_state(new StateCountingScore);
    }
}

void StatePlaying::handle_mouse_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StatePlaying::handle_gamepad_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StatePlaying::handle_system_event(GameApp &app, const  InputEvent &e)
{
    //...
}


// for simplfication, we use a global variable to set if score counting is done.
bool g_counting_done;

//class StateCountingScore
StateCountingScore::StateCountingScore ()
{
    cout << "\t\tgame over!" << endl;
}

StateCountingScore::~StateCountingScore ()
{
    g_counting_done = false;
}

void StateCountingScore::handle_keyboard_input(GameApp &app, const  InputEvent &e)
{
    // handle input only after score counting done.
    if (g_counting_done) { // any input is ok
        app.set_state(new StateTitleScreen);
    }
}

void StateCountingScore::handle_mouse_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateCountingScore::handle_gamepad_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateCountingScore::handle_system_event(GameApp &app, const  InputEvent &e)
{
    //...
}


GameApp::GameApp()
: game_state_(NULL)
, difficulty_(EasyLevel)
{
    this->set_state(new StateTitleScreen);
}


void GameApp::set_state(GameState* state)
{
    delete game_state_;
    game_state_ = state;
    cout << "change state to " << state->name() << endl;
}


void GameApp::handle_keyboard_input(const  InputEvent &e)
{
    game_state_->handle_keyboard_input (*this, e);
}

void GameApp::handle_mouse_input(const  InputEvent &e)
{
    game_state_->handle_mouse_input (*this, e);
}

void GameApp::handle_gamepad_input(const  InputEvent &e)
{
    game_state_->handle_gamepad_input (*this, e);
}

void GameApp::handle_system_event(const  InputEvent &e)
{
    game_state_->handle_system_event(*this, e);
}


int main()
{
    GameApp game_app;
    cout << "\tpress up key" << endl;
    game_app.handle_keyboard_input(InputEvent(UpKey));
    cout << "\tpress up key" << endl;
    game_app.handle_keyboard_input(InputEvent(UpKey));
    cout << "\tpress down key" << endl;
    game_app.handle_keyboard_input(InputEvent(DownKey));
    cout << "\tpress down key" << endl;
    game_app.handle_keyboard_input(InputEvent(DownKey));
    cout << "\tpress start key" << endl;
    game_app.handle_keyboard_input(InputEvent(StartKey));
    cout << "\t\tplaying..." << endl;
    cout << "\t\tthe Boss..." << endl;
    game_app.handle_keyboard_input(InputEvent(GameOverEvent));
    cout << "\tpress start key" << endl;
    game_app.handle_keyboard_input(InputEvent(StartKey));
    cout << "\t\tscore counting done" << endl;
    g_counting_done = true;
    cout << "\tpress start key" << endl;
    game_app.handle_keyboard_input(InputEvent(StartKey));
return 0; }
你應該可以得到下面的output:

change state to TitleScreen

        press up key

        press up key

        press down key

        press down key

        press start key

change state to Playing

                playing...

                the Boss...

                game over!

change state to CountingScore

        press start key

                score counting done

        press start key

change state to TitleScreen
我們可以看到,使用者的輸入會轉交給目前的state object處理,需要改變state時,呼叫app的GameApp::set_state(GameState* state)。你可以做些變化使符合執行時的要求。比如說使用shared pointer來管理記憶體而不是raw pointer。或者我們不介意多用些記憶體,使用預先建立好的物件而不是每次切換state時重新new一個物件。

好的,管理簡單的狀態變化這樣就夠了。但是在開發實際的程式時,相對於簡單的範例,我們很可能會希望在進入一個狀態時執行某個動作,在離開一個狀態時執行另一個動作,在狀態轉換時執行另另一個動作。 如果你使用你這邊的範例的做法,像StateCountingScore這樣,我們可以把進入狀態時做的事寫在constructor,離開狀態時做的事寫在destructor。不過這樣做顯然不夠理想,除了在construct/destruct時執行複雜的動作可能帶來不可遇期的麻煩之外,如果你使用預先產生好的物件,這招基本上就行不通了。顯然比較好的做法是,在base class定義一組onentry(), onexit()方法。在轉換state時在依序呼舊state的onexit(),新state的onentry()。另外,你可以定義一個轉換時的ontransit()方法,在兩個狀態轉換中間呼叫。像下面的code

class GameState
{
public:
    virtual ~GameState() {}
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_system_event(GameApp &app, const InputEvent &e) = 0;
    virtual std::string name () const = 0;
    virtual void onentry(GameApp &app)=0;
    virtual void onexit(GameApp &app)=0;
    virtual void ontransit(GameApp &app, std::string const&target_state)=0;
};
void GameApp::set_state(GameState* state)
{
    if (game_state_) {
        game_state_->onexit(*this);
        game_state_->ontransit(*this, state->name());
        delete game_state_;
    }
    game_state_ = state;
    game_state_->onentry(*this);
}

而這邊,顯然在ontransit()中我們可能會針對轉換的目標狀態來決定要執行的動作,這似乎會造成將來維護上的困難,我們之後將回來討論這一部分。在那之前,我們再想想,除了onentry/onexit/ontransit這些東西之外,在使用狀態控制程式時還會有什麼實際的需求?一般來說,當我們想到狀態圖時,心裏浮現的圖大概是長這樣


每個狀態之間涇渭分明,互相獨立。然而,每一種東西都有狀態的概念在裏頭,就拿我們身為開發者來說,開發程式中(developing)也是一種狀態,而開發程式可以再細分成coding,debugging,testing等子狀態,你在這些子狀態中難道不都叫開發中嗎?所以,狀態常常是階層式的。再來,就像一個物體可能由幾個部分組成,這些部分都是構成該物體的必要成分一樣,一個狀態可能由幾個同時存在的次狀態構成。使用state pattern無法有效的模仿這些實際的狀況。以這個developer的例子來說,使用state pattern來模擬developer的狀態時,我們可能只能定義coding, debugging, testing與其它的終端狀態(睡眠、覓食、休息...)。developing只是一個概念上的狀態,所以實際的程式裏每當進入這三個狀態的其中之時,我們要記得我們也進入了developing的狀態,要執行developing的onentry,離開時也一樣不能忘了developing的onexit。要檢查developer是不是在developing的狀態則是看看developer是不是在coding, deugging,或testing中。面對比較實際的問題時,state pattern顯得力不從心,該怎麼辦呢?我們下次將繼續這個話題。敬請期待...


沒有留言:

張貼留言

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...