2017年7月1日 星期六

關於狀態(state)

程式設計中最重要的問題之一就是狀態的處理。

系統的狀態,物件的狀態,連線的狀態,等等等等... 目前的狀態決定我們什麼可以做,什麼不能做,可以的話又是怎麼做呢?狀態的管理一直以來都是個棘手的問題,今天我就來分享一下我的一些經驗與心得吧。

最開始的時候,我們可能都是這樣管理狀態的: 使用一個數值來代表。
假設我們有一個遊戲,它有四種狀態

enum eGameState{
    eLoadingGame,
    eTitleScreen,
    eOptionScreen,
    ePlaying,
    eScoreDisplay,
};

我們可能有一個叫GameApp的class,define成這樣

class GameApp {
    eGameState game_state_; // 目前遊戲狀態

public:
    GameApp()
    : game_state_(eLoadingGame)
    {
    }

    void handle_keyboard_input(InputEvent &e); 
};

在handle_keyboard_input()中用一串if else來做對應目前遊戲狀態的工作

void GameApp::handle_keyboard_input(InputEvent &e)
{
    if (game_state_ == eLoadingGame) {
        return; // 遊戲載入中,不管鍵盤輸入
    } else if (game_state_ == eTitleScreen) {
        ... // 選擇開始遊戲、遊戲設定、離開等
    } else if (game_state_ == eOptionScreen) {
        ...
    } else if (game_state_ == ePlaying) {
        ...
} 

一切都很合理。但假設我們還有 handle_mouse_input(), handle_gamepad_input()等等,程式可能變成這樣

void GameApp::handle_keyboard_input(InputEvent &e)
{
    if (game_state_ == eLoadingGame) {
        return;
    } else if (game_state_ == eTitleScreen) {
        ...
    } else if (game_state_ == eOptionScreen) {
        ...
    } else if (game_state_ == ePlaying) {
        ...
}

void GameApp::handle_mouse_input(InputEvent &e)
{
    if (game_state_ == eLoadingGame) {
        return;
    } else if (game_state_ == eTitleScreen) {
        ...
    } else if (game_state_ == eOptionScreen) {
        ...
    } else if (game_state_ == ePlaying) {
        ...
}

void GameApp::handle_gamepad_input(InputEvent &e)
{
    if (game_state_ == eLoadingGame) {
        return;
    } else if (game_state_ == eTitleScreen) {
        ...
    } else if (game_state_ == eOptionScreen) {
        ...
    } else if (game_state_ == ePlaying) {
        ...
}

光加三個method應該就夠你覺得煩了,每加一個需要依目前的game_state_來決定採取的行動的地方都要加這一串 if .. else ...。這還不是最煩的,頂多copy/paste就好了嘛,麻煩的是,如果有一天我們要加一個新的狀態,我們就必須找到所有使用到game_state_的地方,加上新的判斷,要是有漏掉的地方,程式依然在表面上可以正常執行,但實際上卻可能埋了一個未爆彈等著那天出其不意的嚇死你...或讓你一頭霧水。

避免重複的程式碼是寫出可維護的程式的最基本步驟。

要避免用一堆if...else判斷來做狀態的對應動作,人們發明了一個叫state pattern的東西。簡單的說,就是把每個狀態都變成一個物件,根據狀態的變化切換目前的狀態物件,那些原本的if...else改成只要呼叫目前狀態物件的method就行了。

我們的範例可能就變成下面這樣

class GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, InputEvent &e) = 0;
    virtual void handle_mouse_input(GameApp &app, InputEvent &e) = 0;
    virtual void handle_gamepad_input(GameApp &app, InputEvent &e) = 0;
};

class StateLoadingGame: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, InputEvent &e)
    {    }

    virtual void handle_mouse_input(GameApp &app, InputEvent &e)
    {    }

    virtual void handle_gamepad_input(GameApp &app, InputEvent &e)
    {    }
};

class StateTitleScreen: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, InputEvent &e)
    {
        ...
    }

    virtual void handle_mouse_input(GameApp &app, InputEvent &e)
    {
        ...
    }

    virtual void handle_gamepad_input(GameApp &app, InputEvent &e)
    {
        ...
    }

};

class StateOptionScreen: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, InputEvent &e)
    {
        ...
    }

    virtual void handle_mouse_input(GameApp &app, InputEvent &e)
    {
        ...
    }

    virtual void handle_gamepad_input(GameApp &app, InputEvent &e)
    {
        ...
    }

};

class StatePlaying: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, InputEvent &e)
    {
        ...
    }

    virtual void handle_mouse_input(GameApp &app, InputEvent &e)
    {
        ...
    }

    virtual void handle_gamepad_input(GameApp &app, InputEvent &e)
    {
        ...
    }

};

class StateScoreDisplay: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, InputEvent &e)
    {
        ...
    }

    virtual void handle_mouse_input(GameApp &app, InputEvent &e)
    {
        ...
    }

    virtual void handle_gamepad_input(GameApp &app, InputEvent &e)
    {
        ...
    }

};

GameApp class變成

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

    friend class StateLoadingGame;
    friend class StateTitleScreen;
    friend class StateOptionScreen;
    friend class StatePlaying;
    friend class StateScoreDisplay;

public:
    GameApp()
    : game_state_(new StateLoadingGame)
    {}

    void handle_keyboard_input(InputEvent &e);
    void handle_mouse_input(InputEvent &e);
    void handle_gamepad_input(InputEvent &e);
};

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

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

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

有沒有?程式變得很乾淨?真是太棒了!

如果你要給GameApp加新的與狀態相關的method,就去每個state class中加對應的method。如果你要加新的state,那就建立新的state class。再也不用維護一堆if...else...了,一切都變成簡單而美好,是不是?

然而,如果事情這麼簡單就好了。

使用雖簡單,但是你怎麼進行與維護狀態的轉換?如果你的物件狀態不像GameApp只有五個,而是五十個呢?

關於狀態的話題我們將在下一話中繼續...


有興趣的朋友去買本design patterns的書來充實一下吧。(雖然這本我沒看過 XD)

沒有留言:

張貼留言

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