code review - basic state system

Started by
3 comments, last by dmatter 10 years, 7 months ago

I am writing a simple board game where the players place pieces on a board. (theres more to it than that, but you get the idea)

I needed a system where, on one screen the player can view the board and select tiles, and on the other they can select from a menu the type of tile they want to place.

For this I needed two 'states', each one handling input and output differently. So I created these as classes, both inheriting from the 'state' class. The thing I have had difficulty with is switching between these classes, since they cant see each other, and how the current state pointer switches from one state to the next will depend on the original state. What I have done is given the state class a 'states' static vector, which stores pointers to all the states that have been created, and a non-static 'keys' vector which stores the input needed to access that state from the this state (so the two vectors sort of line-up).

Anyway this seemed kind of messy and there is probably a reason why you shouldn't do this and i'm probably doing everything wrong anyway. So I'm posting this on here to ask for some feedback before I make it even worse.

thanks!

Here is the program. (its not actually a board game here. I've reduced it to just the basic state system)

Advertisement

Your two states should be completely isolated from one another. The thing that you pass between them is a tile so one should return a tile and the other should accept a tile. Beyond that the two sides of your game do not need to interact with one another.

For example in the game of Scrabble, you have a bag of tiles and you have a board containing tiles. You could implement it this way.


std::vector<Tile*> _bagOfTiles;
std::vector<std::vector<Tile*>> _gameBoard;

Tile* getTile() {
   //bag is shuffled already or what ever before this is called
   //don't forget to add error checking when your bag is empty!
   Tile* pTile = _bagOfTiles.back();
   _bagOfTiles.pop_back();
   return pTile;
}

void placeTile( int x, int y, Tile* pTile ) {
   _gameBoard[x][y] = pTile;

   //game logic based on placement of tile goes here
}

The _bagOfTiles and _gameBoard would be members inside their own classes used to deal with the game logic

I don't think there is any particular right or wrong with these systems.

As a general opinion though, I think that global (including static) state is to be avoided; it's not thread-safe and pretty much assumes/requires that you only have one instance of the system overall. Generally it's not a good idea to solve problems by reaching for global state out of the programmer's toolbox without a good reason.

I would move the management of all states more towards your "currentState" pointer and whatever it is that owns that pointer: "the state manager/machine", in your case it's main.cpp.

In fact you can kind of simplify things, what you've implementing with two vectors and a few loops is basically a map. So just use a map!

Each state has a local (non-static!) member, which is a map that maps the key to the subsequent state:

std::map<char, State*> keysToStates;

Then you just use the map to lookup the state based on the input key.


I wrote a similar system recently for a game. It was similar to yours in a few ways, but different also. Described thusly:

Each state had a render() function which was my (graphical) equivalent to your giveOutput() function.

There was also a process() function which returns a State pointer; this is the pointer to the next state. So my process() function acts as a combination of your nextState() and getInput() functions. You could even make the return optional (using boost::optional, or by returning null if you use raw pointers) and then the absence of a returned state can be interpreted as a quit, so it could also subsume your quit function.

My process() function was responsible for gathering input (like your getInput) and doing the main 'thinking' that the state needs to do.

Another difference between my states and yours is the way that states are driven to transition. Your transitions are based on key inputs - there is absolutely nothing wrong with this so long as state transitions are _always_ driven by key inputs. That's a decision you need to make. This was not the case for me however, for example a gameover state is reached when the player loses all their health, in this case clearly it is not based on an input key but on some internal game logic. Therefore I couldn't just directly map key inputs to the state transitions.

To achieve this I wire states together a little differently. Each state knows about the states that it can transition to in an abstract way. For example the main gameplay state knows that when the main character dies it needs to move to a gameover state. So it has a member function on it, something like this: setGameoverState(State * gameover);
Now, the transition occurs when the process() function decides the transition should take place. It could be looking at the key input (in which case it works similarly to your system), or in this case it is looking at the remaining health and once it hits zero it returns the gameover state from the process() function.

Note however that the main gameplay state doesn't *really* know anything about the game-over state. It simply knows that some kind of gameover state exists, but it doesn't care what that actually is. So states are still isolated. You might wire in a GameoverScreenState or HighscoresScreenState, or a DeathSceneState, whatever you like.

Perhaps, generally speaking, states would be driven by input, in my system I might create a convenient AbstractKeyDrivenState, which holds the map of keys-to-states that was discussed above.

One final point. My states also have abstract functions for entering and leaving a state. If the nextState != currentState then these functions would be called. They allow the states to setup/init and de-init themselves (releasing resources, etc). For example when entering (or re-entering) the main gameplay state it has to (re)setup the world, load all the resources, etc and unload them all at the end as well. The enter() function was actually an enter(State*) function, where the previous state was passed into the next state. This allowed states to implement a backup feature where they can return you back to the last state you came from without having to explicitly wire them up beforehand (and explicitly wiring them wouldn't work if a state can be entered from multiple other states as you wouldn't know which state was the correct one return to).

Thanks for the replies!

I don't think there is any particular right or wrong with these systems.

As a general opinion though, I think that global (including static) state is to be avoided; it's not thread-safe and pretty much assumes/requires that you only have one instance of the system overall. Generally it's not a good idea to solve problems by reaching for global state out of the programmer's toolbox without a good reason.

I would move the management of all states more towards your "currentState" pointer and whatever it is that owns that pointer: "the state manager/machine", in your case it's main.cpp.

In fact you can kind of simplify things, what you've implementing with two vectors and a few loops is basically a map. So just use a map!

Each state has a local (non-static!) member, which is a map that maps the key to the subsequent state:

std::map<char, State*> keysToStates;

Then you just use the map to lookup the state based on the input key.


I wrote a similar system recently for a game. It was similar to yours in a few ways, but different also. Described thusly:

Each state had a render() function which was my (graphical) equivalent to your giveOutput() function.

There was also a process() function which returns a State pointer; this is the pointer to the next state. So my process() function acts as a combination of your nextState() and getInput() functions. You could even make the return optional (using boost::optional, or by returning null if you use raw pointers) and then the absence of a returned state can be interpreted as a quit, so it could also subsume your quit function.

My process() function was responsible for gathering input (like your getInput) and doing the main 'thinking' that the state needs to do.

Another difference between my states and yours is the way that states are driven to transition. Your transitions are based on key inputs - there is absolutely nothing wrong with this so long as state transitions are _always_ driven by key inputs. That's a decision you need to make. This was not the case for me however, for example a gameover state is reached when the player loses all their health, in this case clearly it is not based on an input key but on some internal game logic. Therefore I couldn't just directly map key inputs to the state transitions.

To achieve this I wire states together a little differently. Each state knows about the states that it can transition to in an abstract way. For example the main gameplay state knows that when the main character dies it needs to move to a gameover state. So it has a member function on it, something like this: setGameoverState(State * gameover);
Now, the transition occurs when the process() function decides the transition should take place. It could be looking at the key input (in which case it works similarly to your system), or in this case it is looking at the remaining health and once it hits zero it returns the gameover state from the process() function.

Note however that the main gameplay state doesn't *really* know anything about the game-over state. It simply knows that some kind of gameover state exists, but it doesn't care what that actually is. So states are still isolated. You might wire in a GameoverScreenState or HighscoresScreenState, or a DeathSceneState, whatever you like.

Perhaps, generally speaking, states would be driven by input, in my system I might create a convenient AbstractKeyDrivenState, which holds the map of keys-to-states that was discussed above.

One final point. My states also have abstract functions for entering and leaving a state. If the nextState != currentState then these functions would be called. They allow the states to setup/init and de-init themselves (releasing resources, etc). For example when entering (or re-entering) the main gameplay state it has to (re)setup the world, load all the resources, etc and unload them all at the end as well. The enter() function was actually an enter(State*) function, where the previous state was passed into the next state. This allowed states to implement a backup feature where they can return you back to the last state you came from without having to explicitly wire them up beforehand (and explicitly wiring them wouldn't work if a state can be entered from multiple other states as you wouldn't know which state was the correct one return to).

Ok, I have made a few changes to the program. It is now closer to yours, in that there is no global/static 'currentState' pointer (I understand thread safety a bit better now) and instead of a 'getInput()' function there is a 'process()' function which returns a state pointer which works just as you described.
Although, how would you not change state after a process()? Since, returning NULL is used for quit. I returned 'this', so it sets the current state to the same as it was before. I don't know if there is a way of returning something else, so it knows not to bother changing the state.
I also decided to change state transitions so they don’t have to be driven by input. For my simple program I wasn’t thinking I would need this, but I might as well just in case I want to later on. I still used the map of keys-to-states idea, but instead of keys, it uses state indexes (I used an enum). So, like you described, states know about the other states in an abstract way, but you need to 'setTransition(stateIndex index, state* const state)' so that 'process()' can return a pointer to that state, using the map and index.
here is the updated version, if you want to see what i mean.
So yeah, thanks for the help! I think it works much better now (:

Although, how would you not change state after a process()? Since, returning NULL is used for quit. I returned 'this', so it sets the current state to the same as it was before. I don't know if there is a way of returning something else, so it knows not to bother changing the state.

Yeah I just return the this pointer.

You could switch things about and have a special quit state that you return for quitting and then you can free up NULL as another way to remain in the same state.

One idea is to define a quitState() function in your abstract State class that returns the appropriate pointer value (even if that is the null pointer). Then you just do: return quitState(); and you don't have to care about whether that's returning null or some special state.

Edit: Or define it as a constant somewhere: const QUIT_STATE = null; then even you main loop can compare against that constant too.

This topic is closed to new replies.

Advertisement