MegaTinyEngine - Chapter 1
Abstract what and why?
The first thing we want to do when writing a game engine, is creating an abstraction for all the underlying platform stuff, like creating a window, loading a texture, getting a keypress, etc.The reason is both that these things vary from platform to platform, but might also change quite rapidly on some platforms. This means that you have to do large amounts of refactoring when anything in those libraries change, and possibly do things in multiple different ways depending on platform, all around your codebase.
To save ourself from the worst low-level platform dependt code, we will use the SDL library. SDL (Simple Direct Media Layer) for those who don't know, is a library that creates a simple abstraction layer on top of a lot of the multi media related functionality like creating a window, drawing a line, loading a texture, etc.
Because of its simplicity, it is ported to almost every platform know to man. Everything from Windows to Linux, AmigaOS, Nintendo Switch, Playstation and more. This also makes it relatively easy to port our games to those platforms.
SDL is a C library though, so this just gives us yet another reason for creating a nice C++ abstraction. Here is a very simplified and usual way to initialize SDL in C:
int main()
{
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
return; // Error initializing library
}
SDL_Window *window = SDL_CreateWindow("My Game", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 320, 200, SDL_WINDOW_SHOWN);
if (window == nullptr) {
return; // Error creating window
}
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if(renderer == nullptr) {
return; // error creating renderer
}
// Do game stuff....
while( run_game_loop() );
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
As you can see we have to manage all the raw pointers manually. There are also a lot of error cases not covered here,
which could result in memory not being deallocated correctly. Scale this up to SDL being spread all across our engine,
with pointers to textures, the renderer etc. flying all around the place. This can quickly become dangerous, not to mention
what we would do if the SDL API suddenly changed, or we discovered that we did things wrong in 100 different places.
A better way: Object life times
A better way would be to encapsulate all this and utilize the object life times in C++.Take a look a the following code:
class Core {
private:
// Private constructor, to force usage of the create() method below.
Core();
public:
static std::unique_ptr<Core> create(int pixelWidth, int pixelHeight, const std::string &title)
{
std::unique_ptr<Core> core = std::unique_ptr<Core>(new Core());
// Init SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0)
{
std::cerr << "SDL_Init Error: " << SDL_GetError() << std::endl;
return nullptr;
}
m_window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, pixelWidth,
pixelHeight, SDL_WINDOW_SHOWN);
if (m_window == nullptr)
{
return nullptr; // Error creating window
}
m_renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (m_renderer == nullptr)
{
return nullptr; // error creating renderer
}
return core;
}
Core::~Core()
{
if (m_renderer != nullptr)
{
SDL_DestroyRenderer(m_renderer);
}
if (m_window != nullptr)
{
SDL_DestroyWindow(m_window);
}
SDL_Quit();
}
}
There are two important decisions made here:
1) C++ dont have failable constructors, except by throwing an exception, which requires the program to be compiled with exception support, and that can be a technical limitation on some platforms. So in order to be able to return an error when constructing the Core class, we use a "factory method" instead. We then mark the constructor as private, so the only way of creating a Core object is through the create() method.
2) The destructor on the Core object automatically does all the clean-up, when the Core class goes out of scope. That also means that no matter where we bail out early in create() due to an SDL error, the unique_ptr with an instance of Core makes sure to call the destructor before returning, as it then goes out of scope.
The complete Core class will look like this:
class Core
{
public:
~Core();
// Delete copy constructor and copy assignment
Core(const Core &) = delete;
Core &operator=(const Core &) = delete;
static std::unique_ptr create(int pixelWidth, int pixelHeight, int scaling, bool resizable,
const std::string &title);
int runGame(IGame &game);
void drawRect(int x, int y, int w, int h, Color color);
void clearScreen(const Color &color);
protected:
Core() = default;
};
We can therefore have a pretty safe and clean main() like this.
int main()
{
auto core = Core::create(320, 200, 3, false, "Mega Tiny Game v1.0");
MyGame game;
if (core == nullptr)
{
std::cerr << "Could not initialize core and create window.";
}
else
{
core->runGame(game);
}
return 0;
}
Now it's a lot harder for the user of our engine to do anything wrong, or forget to deallocate something. There is also the possibility of making the Core a singleton, and probably everyone reading this will have a different tweak to it. My focus here though, is making it easy to read, understand, and use. We can always complicate things later :)
Next up is creating a nice way for the Core to interface with a game.
class IGame
{
public:
/**
* Called once when the game is initialized.
* Use this to load resources, allocate memory, and set up starting conditions.
*/
virtual void gameInitialize(Core &core) = 0;
/**
* Called once per frame.
* Use this to update the state of all things in the game: Move player, check if anythings collides, change sprite
* frames, etc.
* @param deltaTimeInSeconds The amount of seconds elapsed since last frame. Typically a few milliseconds, e.g.
* 0.001
*/
virtual void gameUpdate(Core &core, float deltaTimeInSeconds) = 0;
/**
* Called once per frame.
* Use this to draw everything on screen.
* @param renderer The SDL renderer used to draw stuff. Pass this to methods like SDL_RenderDrawLine,
* SDL_RenderDrawRect, etc.
*/
virtual void gameDraw(Core &core) = 0;
/**
* Called every time we receive an event from SDL. It can be mouse move, a button press or window related events.
* Notice that you can easily receive quite a lot of events in between each game draw/update.
* If i.e. the mouse or game controller is moved quite of lot of events are generated.
* So don't move the player each time a button press is received. Instead, set a flag of some sort, and do the
* actual movement in your gameUpdate method. example
* @param event
*/
virtual void gameHandleInput(Key key, bool pressed) = 0;
/**
* Called once when the game is to be destroyed.
* Deallocate and clean up stuff here.
*/
virtual void gameDestroy() = 0;
};
When the runGame(IGame game) is called on the Core, it continuously runs a loop like this:
// Pseudo code
gameInit()
while (!quit)
{
game.gameDraw();
game.gameUpdate( elasedTimeSinceLastFrame );
// Get all the SDL events that have occurred since last we checked (windows, keyboard, mouse, etc.)
while( any_input_event() ){
game.gameHandleInput( pressed_key )
}
}
gameDestroy()
All of SDL is now invisible to the game, and the only way for the game to draw stuff, is by calling core->drawRect() (for now).
But for now, our main goal of creating a skeleton and an abstraction for SDL is accomplished.
Download the full source at github.com/megakode/cpp-game-engine-tutorial