In order to stay out of trouble, I'm going back to work.
Previous Blog Entry for this game challenge round.
Today's topic is my drop in game state manager system for moving between game screens such as an introduction sequence, main menu hierarchy, game play and first say on app termination. I don't remember where I initially found this gem because it has been quite a number of years that I've been reusing this. The concept is simple though. Manage screens as a stack. Push a screen pointer to the back to become active and pop to activate the previous. So how do we get a system like this up and running? The following are sloppy source grab copies. Two objects start us out, First a manager and an abstract base class. From the abstract, we derive the game screen objects. Each screen object handles their own behavior not knowing anything about the others.
// file : GameState.cpp
#include "olcPixelGameEngine.h" // <-- yours renderer may be different
#include "GameState.h"
#include "States/intro.h"
#include "States/game.h"
#include "States/menu.h"
using namespace Core;
std::shared_ptr<States::Intro> introState;
std::shared_ptr<States::Menu> menuState;
std::shared_ptr<States::Game> gameState;
void StateManager::initialize(olc::PixelGameEngine* engine, vec2 clientSize)
{
this->engine = engine;
introState = std::make_shared<States::Intro>(this, clientSize);
menuState = std::make_shared<States::Menu>(this, clientSize);
gameState = std::make_shared<States::Game>(this, clientSize);
states.push_back(menuState);
#ifdef _RELEASE
states.push_back(introState);
#endif
}
void StateManager::shutdown()
{
introState->shutdown();
menuState->shutdown();
gameState->shutdown();
}
void StateManager::changeState(States::Base_state* _state) { }
void StateManager::pushState(States::Base_state* _state) { }
void StateManager::popState() { states.pop_back(); }
void StateManager::draw() { states.back()->draw(); }
void StateManager::update(float deltaTime) { states.back()->update(deltaTime); }
void StateManager::handleEvents(eStateAction _action)
{
switch (_action) {
case eStateAction::pause:
if (states.back()->name == "game_state")
states.pop_back();
break;
case eStateAction::play:
if (states.back()->name == "menu_state") {
int w = engine->GetDrawTargetWidth();
int h = engine->GetDrawTargetHeight();
RECT rect = { 0, 0, w, h }; // <-- I know --^
states.push_back(gameState);
}
break;
case eStateAction::win:
if (states.back()->name == "game_state")
{ MessageBox(0, "you win", "", 0);
//winState->winnerName = gameState->getWinnerName();
//winState->winningTime = gameState->getTimeString();
//states.push_back(winState);
}
break;
case eStateAction::loose:
if (states.back()->name == "game_state") {
MessageBox(0, "you loose", "", 0);
//states.push_back(scoreState);
}
break;
case eStateAction::quit:
if (states.back()->name == "menu_state")
PostQuitMessage(0);
if (states.back()->name == "game_state") {
::ShowCursor(true); // umm, you're counting me aren't you
::ClipCursor(nullptr);
states.pop_back();
}
if (states.back()->name == "score_state")
states.pop_back();
if (states.back()->name == "win_state") {
// todo : record score
gameState->reset();
states.pop_back();
}
break;
} // end switch(action)
}
With the manager out of the way, the base class for the stack-able objects.
// file : base_state.h
#ifndef _engine_core_states_base_state_h_
#define _engine_core_states_base_state_h_
#include <olcpixelgameengine.h>
#include <string>
namespace Core {
namespace States {
// abstract
class Base_state
{
public:
Base_state(void* _manager, std::string _name, vec2 _clientSize)
{
pManager = _manager;
name = _name;
clientSize = _clientSize;
}
virtual void initialize() = 0;
virtual void shutdown() = 0;
virtual void pause() = 0;
virtual void resume() = 0;
virtual void handleEvents() = 0;
virtual void update(float _deltaTime) = 0;
virtual void draw() = 0;s
void* pManager; // que sara sara
std::string name; // todo : refactor string to numeric id? :)
vec2 clientSize;
protected:
Base_state() { /* empty constructor */ }
};
}
}
#endif
Correct me if I'm wrong (please) but of all the reasons to use polymorphism, this case is one of the better (subtyping). To be able to keep like items that are different in a common container. The derived class header would be as expected. Base class overloads in place plus specific functions and variables that would be required to make the new object behave as it will. Nothing new. I'll admit, I'm trapped in the common cycle that most online tutorials mold where the loop is divided into update / draw / check state / repeat. Is it correct? <shrug> So all this may be familiar and you hate it, or it's new and reader be cautious. The basic take away here is the stack concept.
On the main loop side, we declare a Core::StateManager instance and initialize. During the loop, update and draw calls to the manager fire. I feel I don't need to list any additional modules but will list an example derived menu class.
#include "menu.h"
#include "../GameState.h"
using namespace Core::States;
void Menu::initialize()
{
rectPlay = { 360, 140, 440, 160 }; // 80 x 20
rectQuit = { 360, 165, 440, 185 }; // need more data or buttons are a known size
}
void Menu::shutdown() { }
void Menu::pause() { }
void Menu::resume() { }
void Menu::handleEvents() { }
void Menu::update(float _deltaTime) { }
void Menu::draw()
{
olc::Pixel colorDefault = olc::Pixel(255, 255, 255);
olc::Pixel colorHover = olc::Pixel(255, 255, 0);
// _
// .--- todo : don't like handling user input inside a draw routine...fix me ----. \_(O,O)_
// V V ~ \
// ------------------- Game Menu -------------------------
Core::StateManager* manager = (StateManager*)pManager;
olc::PixelGameEngine* e = manager->engine;
POINT cursorPos;
cursorPos.x = manager->engine->GetMouseX();
cursorPos.y = manager->engine->GetMouseY();
e->DrawRect(rectPlay.left, rectPlay.top, rectPlay.right - rectPlay.left, rectPlay.bottom - rectPlay.top);
e->DrawRect(rectQuit.left, rectQuit.top, rectQuit.right - rectQuit.left, rectQuit.bottom - rectQuit.top);
std::string cursorInfo = "CursorPos ";
char buf[16] = "";
_itoa_s(int(cursorPos.x), buf, 10);
e->DrawString(20, 20, std::string(buf));
_itoa_s(int(cursorPos.y), buf, 10);
e->DrawString(70, 20, std::string(buf));
vec2 textOffset = { 25.f, 6.f };
int x = int(rectPlay.left + textOffset.x);
int y = int(rectPlay.top + textOffset.y);
// Play -------------------------------------------------
olc::Pixel textColor = colorDefault;
if (::PtInRect(&rectPlay, cursorPos))
{ textColor = colorHover;
if(e->GetMouse(0).bPressed)
manager->handleEvents(Core::eStateAction::play);
}
e->DrawString(x, y, "Play", textColor);
// Quit --------------------------------------------------
x = int(rectQuit.left + textOffset.x);
y = int(rectQuit.top + textOffset.y);
textColor = colorDefault;
if (::PtInRect(&rectQuit, cursorPos))
{ textColor = colorHover;
if(manager->engine->GetMouse(0).bPressed)
{ if (MessageBox(nullptr, "Are you sure you want to quit?", "Serious?", MB_YESNO) == IDYES)
exit(0);
}
}
manager->engine->DrawString(x, y, "Quit", textColor);
}
The game screen state would be similar but behave as what the main game play would be. Every valid screen pointer lives the life of the application but only the one at the back of the stack is active at a given time. This has served me well for adding game state transitions in custom work.
Input and drawing are engine responsibilities so there is a member pointer to contend with in the manager. Perhaps not the best approach, but I don't know of alternatives other than a hard global or worse, singleton. But a subclass gets this functionality through the manager held engine pointer. Crappy note to end on perhaps, but that's where I am. It works, I called it good and have been reusing a number of times. Tips from the leet are always appreciated assuming my words are better than a hodgepodge of nonsense.
edit log
- 2019_05_03 Code cleanup
??