As i collected some basic information and even tried to cathegorize it in one place (via LeaFAQ), i decided to contribute something from my own experience.
This time it should be an article (i dont know now, how long this post will be) about menu state machine. Additionaly, there would be some information about interfases and inheritance.
I didnt wrote atricles for a long time. Last time it was in about 2006, about 3DGameStudio, in Russian language (some states dated 2006-2007 are mine ...Nostalgy... ).
Healthy critic is wellcome (even about grammar mistakes), as this is my first article in English.
The main idea was taken from Irrlicht forum, when i made a game prototype for Gameloft content. So, lets rock!
1. We have inerface - the abstract class, that can't be instanced. Our game states we will be inherited from this interface.
// IGameStates.h
interface IGameStates {
public:
string name;
virtual void Activate()=0;
virtual void Deactivate()=0;
virtual int Update(float deltaTime)=0;
virtual void Render(float deltaTime)=0;
virtual ~IGameStates() {};
private:
};
gameStateName; is optional, for debug reasons.
Note that function is written like "Activate()=0", that means that functions must be defined in child class or you'll get an error.
1.1 Also, we should have enumeration of all our game states. Commented code will show some possible usage of this system.
// IGameStates.h
extern enum GameStatesEnum
{
//LogoState = 0,
//SplashState,
//LoadingState,
MenuState = 0,
GameState,
ChatState
};
[!!!]We will call states(integer) as MenuState, and states(classes) as StateMenu, so please, be carefull.
2. Next, we will make new class, which will describe game state. First one will be main menu, here we should define those interface functions
// StateMenu.h
class StateMenu : public IGameStates
{
public:
StateMenu(void);
virtual ~StateMenu(void);
// IGameStates
virtual void Activate();
virtual void Deactivate();
virtual int Update(float deltaTime);
virtual void Render(float deltaTime);
***
}
2.1. Activate() describes activation of certain state (of cource, you can add Initialize() and call this only first time to load all resourses). In my case it will be
// StateMenu.cpp
void StateMenu::Activate()
{
_isLoginChatPressed = false;
_isLoginGamePressed = false;
_isQuitPressed = false;
****
}
DeactivateState(), obviously, describes deactivation of that state.
Theese both functions will be called each time when game state would be changed to MenuState.
2.2. UpdateState() is one of the most interesting parts. As you may notice, it returns int.
[!!!] The main feature: in the main loop we will call this update function and we will check result. If state is the same (user didnt do anything), it should returns its own state. If not - it whould returns next state. I'll explain this little later, while describing main loop.
// StateMenu.cpp
int StateMenu::Update(float deltaTime)
{
SingletonOisManager.Update();
if (_isLoginChatPressed)
{
return ChatState;
}
if (_isLoginGamePressed)
{
return GameState;
}
if (_isQuitPressed)
{
SingletonGameManager.SetQuit(true);
}
return MenuState;
}
So, if player set one of this flags to "true", Update function will return ID of new state.
RenderState() is just a simple render of menu of 3d world.
//StateMenu.cpp
void StateMenu::Render(float deltaTime)
{
SingletonGuiManager.Render(deltaTime);
Flip(1); // 1 = 60, 0 = unlimited
}
3. StateGame is the same, but it renders whole world, instead of Gui. StateChat is UI for testing network.
4. In the header of our GameManager class (class, which contains main loop) we should make a container for all states.
// GameManager.h
IGameStates* _activeState;
vector <IGameStates*> _listOfStates;
int _stateReturns;
int _previousStateReturns;
Note, that vector holds IGameStates*, the parent of StateGame, StateChat, StateMenu and so on.
5.1 In the body of our GameManager class, we should create and store all our states to vector
// GameManager.cpp
_listOfStates.push_back(new StateMenu());
_listOfStates.push_back(new StateGame());
_listOfStates.push_back(new StateChat());
Now, we should select, which state will be the first.
[!!!] Note, this is perfect for development: if you wish to work on your menu, you just set first state to load from. If you want to work on your game, you set game state. If you want to show it to your girlfriend (including splashscreen), you set splashscreen first, the next will be menu, and so on .
After that, we should activate selected state and run it for the first time.
// GameManager.cpp
_activeState = _listOfStates.at(0);
_activeState->Activate();
_stateReturns = _activeState->Update(0); // if you'll set here 0, you'll get StateMenu, if 1 - ypu'll get StateGame and so on
_previousStateReturns = _stateReturns;
5.2 Main loop
Its simple. Really
// GameManager.cpp
// Main loop
float CurrentTime = AppTime();
_deltaTime = (CurrentTime - _elapsedTime) * 0.001f; //1
_elapsedTime = CurrentTime;
_stateReturns = _activeState->Update(_deltaTime); //2
_activeState->Render(_deltaTime); //3
if (_stateReturns != _previousStateReturns) { // 4
_previousStateReturns = _stateReturns;
_activeState->Deactivate(); // 5
_activeState = _listOfStates.at(_stateReturns); // 6
_activeState->Activate(); // 7
}
At first (1), we're counting deltatime.
Next (2), we run UpdateState() and save results. Result can be the same state or new one.
Then (3) we Render current state.
Last step: we check (4) the result of UpdateState() with current state. If they are equal - its okay. If they are not equal, we should Deactivate current state(5), set new state (6) and activate it(7)!
The loop is over, and it waits for new state changes.
PS: if you'll ask me, how i set _isLoginChatPressed in StateMenu::UpdateState(), i'll answer that i'm also using event system, where each entity can be subscripted for a sertain event and be rised in time. This is large article, probably i'll make another one.
Also, i'm using imgui, which is perfect for debugging, as its code-driven UI.
void StateMenu::RenderGuiEvent( float gameLoopTime )
{
imguiBeginScrollArea("Main Menu",
400, 250, 200, 200, &_debugScroll);
_isLoginChatPressed = imguiButton("Login to chat",true);
_isLoginGamePressed = imguiButton("Login to play",true);
_isQuitPressed = imguiButton("Quit",true);
imguiEndScrollArea();
}
PPS: GameManager contains "Manager" on its end because its singleton.
PPPS: "We will call states(integer) as MenuState, and states(classes) as StateMenu, so be carefull."
This can disappoint for the first time, but believe me, its really simple.