About Me

Website

Role

Twitter

Github

Twitch

Steam

Hello!
As far as I understand, the traditional approach to the architecture of a game with different states or "screens" (such as a menu screen, a screen where you fly your ship in space, another screen where you walk around on the surface of a planet etc.) is to make some sort of FSM with virtual update/render methods in the state classes, which in turn are called in the game loop; something similar to this:
struct State {
virtual void update()=0;
virtual void render()=0;
virtual ~State() {}
};
struct MenuState:State {
void update() override { /*...*/ }
void render() override { /*...*/ }
};
struct FreeSpaceState:State {
void update() override { /*...*/ }
void render() override { /*...*/ }
};
struct PlanetSurfaceState:State {
void update() override { /*...*/ }
void render() override { /*...*/ }
};
MenuState menu;
FreeSpaceState freespace;
PlanetSurfaceState planet;
State * states[] = {&menu, &freespace, &planet};
int currentState = 0;
void loop() {
while (!exiting) {
/* Handle input, time etc. here */
states[currentState]->update();
states[currentState]->render();
}
}
int main() {
loop();
}
My problem here is that if the state changes only rarely, like every couple of minutes, then the very same update/render method will be called several times for that time period, about 100 times per second in case of a 100FPS game. This seems a bit to make dynamic dispatch, which has some performance penalty, pointless. Of course, one may argue that a couple hundred virtual function calls per second is nothing for even a not so modern computer, and especially nothing compared to the complexity of the render/update function in a real life scenario. But I am not quite sure. Anyway, I might have become a bit too paranoid about virtual functions, so I wanted to somehow "move out" the virtual function calls from the game loop, so that the only time a virtual function is called is when the game enters a new state. This is what I had in mind:
template<class TState>
void loop(TState * state) {
while (!exiting && !stateChanged) {
/* Handle input, time etc. here */
state->update();
state->render();
}
}
struct State {
/* No update or render function declared here! */
virtual void run()=0;
virtual ~State() {}
};
struct MenuState:State {
void update() { /*...*/ }
void render() { /*...*/ }
void run() override { loop<MenuState>(this); }
};
struct FreeSpaceState:State {
void update() { /*...*/ }
void render() { /*...*/ }
void run() override { loop<FreeSpaceState>(this); }
};
struct PlanetSurfaceState:State {
void update() { /*...*/ }
void render() { /*...*/ }
void run() override { loop<PlanetSurfaceState>(this); }
};
MenuState menu;
FreeSpaceState freespace;
PlanetSurfaceState planet;
State * states[] = {&menu, &freespace, &planet};
void run() {
while (!exiting) {
stateChanged = false;
states[currentState]->run(); /* Runs until next state change */
}
}
int main() {
run();
}
The game loop is basically the same as the one before, except that it now exits in case of a state change as well, and the containing loop() function has become a function template.
Instead of loop() being called directly by main(), it is now called by the run() method of the concrete state subclasses, each instantiating the function template with the appropriate type. The loop runs until the state changes, in which case the run() method shall be called again for the new state. This is the task of the global run() function, called by main().
There are two negative consequences. First, it has become slightly more complicated and harder to maintain than the one above; but only SLIGHTLY, as far as I can tell based on this simple example. Second, code for the game loop will be duplicated for each concrete state; but it should not be a big problem as a game loop in a real game should not be much more complicated than in this example.
My question: Is this a good idea at all? Does anybody else do anything like this, either in a scenario like this, or for completely different purposes? Any feedback is appreciated!