Summary
I come from a Java background but I am trying to make a game in C++. This is my attempt at a state management system, which should allow me to easily switch between "states" (menu, game, scoreboard, etc.).
The idea is that:
- When the program starts, I create an
Application
. - The Application contains the game loop, which runs until the program exits.
- Each frame, the application updates and renders the current state.
State
is an abstract class, andRival
is a concrete subclass.
Feedback
I would really love any feedback. Specifically, the areas I am most concerned about are:
- Inheritance: I have not really used this before in C++. My understanding is that by using a
unique_ptr
my state gets stored on the heap, and this avoids the problem of object slicing. MyState
methods are all pure virtual, and overridden by the subclass. Am I missing anything? - Ownership: The Application owns the current State; Rival owns the current Scenario. My understanding is that when the Application exits (or changes to a new state), the current state will be destroyed / freed. When Rival gets freed, the current Scenario will subsequently be freed. Have I got that right?
- Heap vs Stack: I understand that the stack is faster to access, but it is quite small and not particularly suitable for long-lived objects (they are freed when they go out of scope), polymorphic objects or variable-size objects. For this reason the State and Scenario live on the heap, but everything else lives on the stack. Does this sound ok?
NOTE: I am not too worried about the technical aspects of the game loop itself at this point (fixed or variable time step, sleep duration, etc.) - I just want to make sure the code is clean, free from bugs / memory leaks, and follows best practices where possible. I would be grateful if you could try to include an explanation along with any suggestions, so that I can learn WHY and not just WHAT.
Code
I have tried to omit any details which are not relevant to this particular mechanism, but the full code can be found here.
Main.cpp
#include "pch.h"
#include <iostream>
#include <stdexcept>
#include "Application.h"
#include "Rival.h"
#include "Scenario.h"
#include "ScenarioBuilder.h"
#include "ScenarioReader.h"
#include "Window.h"
/**
* Entry point for the application.
*/
int main() {
try {
// Create our Window
Rival::Window window(800, 600, "Rival Realms");
window.use();
// Create our Application
Rival::Application app(window);
// Load some scenario
Rival::ScenarioReader reader(Rival::Resources::mapsDir + "example.sco");
Rival::ScenarioBuilder scenarioBuilder(reader.readScenario());
std::unique_ptr<Rival::Scenario> scenario = scenarioBuilder.build();
// Create our initial state
std::unique_ptr<Rival::State> initialState =
std::make_unique<Rival::Rival>(app, std::move(scenario));
// Run the game!
app.start(std::move(initialState));
} catch (const std::runtime_error& e) {
std::cerr << "Unhandled error during initialization or gameplay\n";
std::cerr << e.what() << "\n";
return 1;
}
return 0;
}
Application.h
#ifndef APPLICATION_H
#define APPLICATION_H
#include <memory>
#include "Resources.h"
#include "State.h"
#include "Window.h"
namespace Rival {
class Application {
public:
bool vsyncEnabled;
Application(Window& window);
/**
* Runs the Application until the user exits.
*/
void start(std::unique_ptr<State> state);
/**
* Exits the Application cleanly.
*/
void exit();
Window& getWindow();
Resources& getResources();
private:
Window& window;
Resources res;
std::unique_ptr<State> state;
};
} // namespace Rival
#endif // APPLICATION_H
Application.cpp
#include "pch.h"
#include "Application.h"
#include <SDL.h>
namespace Rival {
bool vsyncEnabled = true;
Application::Application(Window& window)
: window(window) {
// Try to enable vsync
if (SDL_GL_SetSwapInterval(1) < 0) {
printf("Unable to enable vsync! SDL Error: %s\n", SDL_GetError());
vsyncEnabled = false;
}
}
void Application::start(std::unique_ptr<State> initialState) {
// Event handler
SDL_Event e;
state = std::move(initialState);
bool exiting = false;
Uint32 nextUpdateDue = SDL_GetTicks();
// Game loop
while (!exiting) {
Uint32 frameStartTime = SDL_GetTicks();
// Is the next update due?
if (vsyncEnabled || nextUpdateDue <= frameStartTime) {
// Handle events on the queue
while (SDL_PollEvent(&e) != 0) {
if (e.type == SDL_QUIT) {
exiting = true;
} else if (e.type == SDL_KEYDOWN) {
state->keyDown(e.key.keysym.sym);
} else if (e.type == SDL_MOUSEWHEEL) {
state->mouseWheelMoved(e.wheel);
}
}
// Update the game logic, as many times as necessary to keep it
// in-sync with the refresh rate.
//
// For example:
// - For a 30Hz monitor, this will run twice per render.
// - For a 60Hz monitor, this will run once per render.
// - For a 120Hz monitor, this will run every other render.
//
// If vsync is disabled, this should run once per render.
while (nextUpdateDue <= frameStartTime) {
state->update();
nextUpdateDue += TimerUtils::timeStepMs;
}
// Render the game, once per iteration.
// With vsync enabled, this matches the screen's refresh rate.
// Otherwise, this matches our target FPS.
state->render();
// Update the window with our newly-rendered game.
// If vsync is enabled, this will block execution until the
// next swap interval.
window.swapBuffers();
} else {
// Next update is not yet due.
// Sleep for the shortest possible time, so as not to risk
// overshooting!
SDL_Delay(1);
}
}
// Free resources and exit SDL
exit();
}
void Application::exit() {
SDL_Quit();
}
Window& Application::getWindow() {
return window;
}
Resources& Application::getResources() {
return res;
}
} // namespace Rival
State.h
#ifndef STATE_H
#define STATE_H
#include <SDL.h>
namespace Rival {
// Forward declaration to avoid circular reference
class Application;
class State {
public:
/**
* Handles keyDown events.
*/
virtual void keyDown(const SDL_Keycode keyCode) = 0;
/**
* Handles mouse wheel events.
*/
virtual void mouseWheelMoved(const SDL_MouseWheelEvent evt) = 0;
/**
* Updates the logic.
*
* It is assumed that a fixed amount of time has elapsed between calls
* to this method, equal to TimerUtils::timeStepMs.
*/
virtual void update() = 0;
/**
* Renders the current frame.
*/
virtual void render() = 0;
};
} // namespace Rival
#endif // STATE_H
Rival.h
#ifndef RIVAL_H
#define RIVAL_H
#include <SDL.h>
#include <memory>
#include "Application.h"
#include "Scenario.h"
#include "State.h"
#include "Window.h"
namespace Rival {
class Rival : public State {
public:
Rival(Application& app, std::unique_ptr<Scenario> scenario);
// Inherited from State
void keyDown(const SDL_Keycode keyCode) override;
void mouseWheelMoved(const SDL_MouseWheelEvent evt) override;
void render() override;
void update() override;
private:
Application& app;
Window& window;
Resources& res;
std::unique_ptr<Scenario> scenario;
};
} // namespace Rival
#endif // RIVAL_H
Rival.cpp
#include "pch.h"
#include "Rival.h"
namespace Rival {
Rival::Rival(Application& app, std::unique_ptr<Scenario> scenarioToMove)
: app(app),
window(app.getWindow()),
res(app.getResources()),
scenario(std::move(scenarioToMove)) {}
void Rival::update() {
// ...
}
void Rival::render() {
// ...
}
void Rival::keyDown(const SDL_Keycode keyCode) {
// ...
}
void Rival::mouseWheelMoved(const SDL_MouseWheelEvent evt) {
// ...
}
} // namespace Rival
```
vsyncEnabled
is not being set on the Application instance. \$\endgroup\$