I've spent a lot of time thinking about how to do state management in a flexible, generalized way. My current state system is (obviously) the best solution I've come up with so far, and I am fairly pleased with it.
Each State is separated out into its own class (MainMenuState, PlayState, PauseMenuState, etc.), which inherits from the State virtual class. The State virtual class defines a common interface for a State--handling events, updating, and rendering. A State also has an associated StateType, which can be one of: Primary, Overlay, and Popup. More on these later.
Code: Select all
enum StateType {
kStateTypePrimary, // Blocks everything
kStateTypeOverlay, // Blocks events and updates
kStateTypePopup // Blocks nothing
};
class State {
public:
State(StateType type) : type_(type) {}
virtual ~State() {}
virtual void handleEvent(ALLEGRO_EVENT& /*ev*/) {}
virtual void update(float /*ft*/) {}
virtual void render() {}
StateType type() const { return type_; }
private:
StateType type_;
};
States can stand on their own, or they can be inserted into a StateManager. The StateManager maintains a two-dimensional collection of States, arranged into Layers. The first dimension of the StateManager acts like a stack: only the top Layer can be accessed, and it can only be changed via push or pop. Layers act like weakly-ordered lists, but they cannot be mutated directly. There are two operations defined for a Layer: insert and remove. The insert operation checks the State's StateType. If the StateType is Primary, the insert ensures that the first State in the Layer is non-Primary and acts as a prepend; otherwise, it acts as an append.
The StateManager defines the same methods as declared in the State interface (handle event, update, and render). These methods call the corresponding method on each State in the topmost Layer. The handle event and update methods start at the first Primary or Overlay State (or simply the first State if the Layer doesn't have a Primary or Overlay) and continue to the end. The render method starts at the first State.
Code: Select all
class StateManager {
public:
void push();
void push(State* state);
void pop();
void insert(State* state);
void remove(State* state);
void handleEvent(ALLEGRO_EVENT& ev);
void update(float ft);
void render();
private:
std::stack<std::vector<std::unique_ptr<State>>> layers_;
};
Be careful in how the StateManager is implemented; if you mutate the Layer in any way during a State method call (including popping it), you may invalidate the current State iterator and cause shit to go crazy. I avoid this by also maintaining an ordered list of Jobs; the push, pop, insert, and remove methods are dummies that simply add a Job for the corresponding operation. The Jobs are then carried out by another method, process, that is called at the beginning of each game tick, before handleEvent, update, and render.
The benefit of using a State system like this is that you can use it for many things, including (to some degree) window management--you could have an InventoryState, a JournalState, and an AbilitiesState all open at once as Popups on top of a Primary PlayState. Then if the player pauses, you could insert a PauseMenuState as an Overlay that blocks events from the lower States until it is dismissed, while still rendering them.
I haven't found it to be much slower than conventional enum-based State "management" (the overhead is mostly noticeable during State changes, but it's still negligible), but I do think it's
significantly more powerful and usable.
If anyone has any suggestions for my design, I'd be very interested in hearing them. Hope this helps!