2017年7月2日 星期日

About states

One of the most important issues in programming is handling states.

The state of the system, the state of the object, the state of the connection, and so on ... the current state determines what we can do, what we can not do. And if we can, how to do it? State management has always been a thorny problem, today I will share with you some of my experience and thought about it.

At the beginning, we probably all do it this way: use an integer value to represent state. Suppose we are writing a game that has four states

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

We may have a class called GameApp, define it like this

class GameApp {
    eGameState game_state_; // current game status

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

    void handle_keyboard_input (InputEvent & e); 
};

In handle_keyboard_input () we use a series of if...else... to do the work corresponding to the current state of the game, like this

void GameApp :: handle_keyboard_input (InputEvent & e)
{
    if (game_state_ == eLoadingGame) {
        return; // game loading, discard keyboard input
    } else if (game_state_ == eTitleScreen) {
        ... // choose starting the game, set game option, or quit, etc.
    } else if (game_state_ == eOptionScreen) {
        ...
    } else if (game_state_ == ePlaying) {
        ...
}

Everything is plausible. But assuming we have additional methods like handle_mouse_input (), handle_gamepad_input (), etc., the program may be like this

void GameApp :: handle_keyboard_input (InputEvent & e)
{
    ff (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) {
        ...
}

Just adding three methods should be enough for you to feel troublesome. For every new method that need to take actions depend on current game_state_, we have to write a series of "if... else...". And yet this is not the most annoying thing, since we can simply copy/paste the code and make change. The most annoying is that if one day we have to add a new state, we must find all places that use game_state_ to make amendments. If there is a missing place, the program will still run normally on the surface, but in fact there may be some bomb buried there waiting for the day to explode in front of your face ... or let you confused.

Avoiding code duplicates is the most basic step in writing maintainable programs.

To avoid using a pile of "if...else..." to determine the state and take the corresponding action, people invented something called "state pattern". Simply put, that is, make each state an object, according to the current state switching the state object. All the "if...else..." changed to simply call the corresponding method in the state object.

Our example may become the following

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)
    {
        // use app to achieve its function
        ...
    }

    virtual void handle_mouse_input (GameApp & app, InputEvent & e)
    {
        // use app to achieve its function
        ...
    }

    virtual void handle_gamepad_input (GameApp & app, InputEvent & e)
    {
        // use app to achieve its function
        ...
    }

};

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 becomes following

class GameApp {
    GameState * game_state_; // The current game state object
    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);
}

The program becomes very clean, isn't it? It was fantastic!

If you want to give GameApp a new state-related method, go to each state class to add the corresponding method. If you want to add a new state, then create a new state class. No longer we need to maintain a bunch of "if ... else ..." fragment and everything becomes simple and beautiful, isn't it?

However, if only things are so simple and easy.

The usage is simple, but how do you manage the state transition? What to do if your object had not only five states like GameApp here, but fifty?

We will continue the discussion in next part, till then, please be safe.

(For those interested, design patterns worth you time and money to study, buy one if you like.)

沒有留言:

張貼留言

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