Part 3: Game Class and Game Loop
Creating our Game Class and our Game Loop
So far we have learnt how to setup a project for SDL2 development and how to initialise SDL2. Now we're going to move onto the first game specific element; The game class and the game loop.
The first step in the process is to create a new class and call it "Game". Now, to create a new class you're going to want to right click on your project name on the left, (again not the solution), go to "Add" and choose "Class...". In the window that pops up, choose Visual C++ on the left, then choose "Class" in the center pane, then click "Add" at the bottom:
In the window that pops up, you'll get something like the image above but with empty boxes. Type "Game" into the "Class Name:" field, and it will fill in the other two fields for you. When your's looks like the one above, hit finish.
*[When I ask you to create a class in future, with a certain name follow this process but with the name I give/suggest at the time]*
Now once we've clicked finish, we'll have 2 empty files pop up in the file structure on the left, and they'll also open for us ready to edit. Firstly go into the Game.h file so we can setup our game class; you'll see something like this:
#pragma once
class Game { public: Game(); ~Game(); }; |
Now firstly I would advise spacing things out so they're easier to read, like so:
#pragma once
class Game { public: Game(); ~Game(); }; |
Once we've done that what we want to do is setup some functions that our game class will use. So add some functions as shown here:
#pragma once
class Game { public: Game(); ~Game(); bool init(const char* title, int x, int y, int width, int height, int flags); void update(); void handleEvents(); void render(); void clean(); void quit(); }; |
Now at first you may be confused by the functions ending in ";" (semi-colon) instead of "{ }" like with int main() in main.cpp; this means that we will setup these functions in the corresponding Game.cpp file that was created, but we'll worry about that in a short while.
Next our Game class is going to need some private members; remember the window and renderer variable from the last part (SDL Init), we're going to store them here from now on. Don't go removing the ones in main.cpp yet, we'll cross that bridge when we come to it. We're also going to need a new variable, bool running (bool is short for boolean which is a variable that stores a true or false value), which we will use to establish whether our game is running for our game loop (so we know when to exit and close the window, again, more on that later). So let's add those in; to do so we need to add a private case for our class:
Next our Game class is going to need some private members; remember the window and renderer variable from the last part (SDL Init), we're going to store them here from now on. Don't go removing the ones in main.cpp yet, we'll cross that bridge when we come to it. We're also going to need a new variable, bool running (bool is short for boolean which is a variable that stores a true or false value), which we will use to establish whether our game is running for our game loop (so we know when to exit and close the window, again, more on that later). So let's add those in; to do so we need to add a private case for our class:
#pragma once
class Game { public: bool init(const char* title, int x, int y, int width, int height, int flags); void update(); void handleEvents(); void render(); void clean(); void quit(); private: Game(){} ~Game(){} SDL_Window* window; SDL_Renderer* renderer; bool running; }; |
As you can see, we also moved the Game(); and ~Game(); functions. These are known as the constructor (Game()) and destructor (~Game()) and these are called whenever an object of this class is created (constructor) and whenever it is destroyed (destructor). We have made them private and also swapped out the ";" in favour of "{}". We've done this because neither the constructor nor the destructor will have any code inside them so we have the ability to use "{}" in the header file, meaning we don't have to initialise it in our .cpp file, saving space and looking cleaner. We're making them private in the first place as part of our transformation of the Game class into a singleton (more on this later). You'll probably notice an error with the SDL_Window and SDL_Renderer variables, and that's because we haven't included SDL.h to use SDL in this class like we did in main.cpp so go ahead and add those includes after #pragma once and before class Game.
#include <SDL.h>
#include <iostream> #include <string> |
You'll notice that they're the same includes in the main.cpp file, thats because when we're done, the initialisation of SDL will be done from the Game class and so we need the same functionality.
Next we can add some getters to gain read-only access to our private variables in public (any other file we use Game in).
Next we can add some getters to gain read-only access to our private variables in public (any other file we use Game in).
#pragma once
class Game { public: bool init(const char* title, int x, int y, int width, int height, int flags); void update(); void handleEvents(); void render(); void clean(); void quit(); bool isRunning() { return running; } SDL_Renderer* getRenderer() const { return renderer; } SDL_Window* getWindow() const { return window; } private: Game(){} ~Game(){} SDL_Window* window; SDL_Renderer* renderer; bool running; }; |
Getters can be created in header files and completely formed in header files, stopping them from cluttering our .cpp files as you can see above. We have getters for the boolean variable running so we can see if the game is running from outside our game class (like our game loop for example which will be in main.cpp). We also have getters for the window and the renderer so that we can read their properties (width and height of the window are particularly useful ones) and use the values themselves elsewhere (we will certainly be using the renderer variable elsewhere so that we can render anything from anywhere onto our game screen). Now, let's get down to initialising our public functions in our .cpp file. Start by deleting any pre-existing contents in Game.cpp apart from the include line at the top.
So now with our Game.cpp file and the line #include "Game.h" we can begin to initialise all our functions and polish off our Game class.
First things first the init function, first things first lay it out like so below the include line, preferably with 1 lines space between (trust me, neat code is easier to read):
So now with our Game.cpp file and the line #include "Game.h" we can begin to initialise all our functions and polish off our Game class.
First things first the init function, first things first lay it out like so below the include line, preferably with 1 lines space between (trust me, neat code is easier to read):
bool Game::init(const char* title, int x, int y, int width, int height, int flags)
{ } |
You see the return type is bool like established in the .h file but something we haven't seen before is the "Game::" preceeding init. What this means is that we are initiated the init function that belongs to Game as a class.
Once we've done this we're going to want to grab some code from our main.cpp file and put it in this function. Go ahead and cut the following lines from main.cpp:
Once we've done this we're going to want to grab some code from our main.cpp file and put it in this function. Go ahead and cut the following lines from main.cpp:
if (SDL_Init(SDL_INIT_EVERYTHING)==0)
{ std::cout << "SDL initialising successful" << std::endl; window = SDL_CreateWindow("SDL Practice", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 960, 640, SDL_WINDOW_SHOWN); if (window != 0) { std::cout << "Window creation succeeded" << std::endl; renderer = SDL_CreateRenderer(window, -1, 0); if (renderer != 0) { std::cout << "Renderer creation succeeded" << std::endl; SDL_SetRenderDrawColor(renderer, 0, 0, 100, 255); } else { std::cout << "Renderer creation failed" << std::endl; return false; } } else { std::cout << "Window creation failed" << std::endl; return false; } } else { std::cout << "SDL initalisation failed" << std::endl return false; } SDL_RenderClear(renderer); SDL_RenderPresent(renderer); SDL_Delay(5000); |
You should now also have in main.cpp your int main function with just the line return 0; inside. Go ahead and cut out the lines SDL_RenderClear(renderer); and SDL_RenderPresent(renderer); from our new function as they're going to be put elsewhere in our game class, which we'll get to soon enough. Also remove SDL_Delay(5000); as that will be handled in the main.cpp when we get round to creating our game loop. Add the lines "running = true;" and "return true" at the end of the init function so we know that if it the function completes without reaching any of the false return statements, then initialisation was a success and the game is now running.
Now let us concentrate on the creation of the window in our new init function. At the moment the values are hard coded directly into the method; but we're going to be calling init, with all its parameters, inside the main.cpp so we want to use the parameters we have set up for this function. Go ahead and change out the values for the parameters declared in the function:
Now let us concentrate on the creation of the window in our new init function. At the moment the values are hard coded directly into the method; but we're going to be calling init, with all its parameters, inside the main.cpp so we want to use the parameters we have set up for this function. Go ahead and change out the values for the parameters declared in the function:
window = SDL_CreateWindow(title, x, y, width, height, flags);
|
Now we can move onto our next function; we'll initialise render() next. So below out init function at the bottom of Game.cpp we'll leave another 1 line space and add our render function, bare in mind the return type and any parameters:
void Game::render()
{ } |
Of course, there weren't any parameters for render and the return type is void meaning we don't need a return statement (you'll recall if you read the functions lesson linked in part 2).
In the render function is where we'll be drawing the elements of our game, so it's in here where we want to place the two SDL render functions we removed earlier:
In the render function is where we'll be drawing the elements of our game, so it's in here where we want to place the two SDL render functions we removed earlier:
void Game::render()
{ SDL_RenderClear(renderer); SDL_RenderPresent(renderer); } |
Between these two functions, SDL_RenderClear and SDL_RenderPresent is where we will draw all out elements. The way it works is that the renderer is cleared to the colour of the renderer set using SDL_SetRenderDrawColor (which can be called anywhere should you want to change the colour of the renderer background at anytime). Then any and all elements are drawn to the renderer and once done we call SDL_RenderPresent which takes all the elements drawn to the renderer and presents them to the window the renderer is attached to. The process is repeated; clearing the screen, drawing the elements, and presenting the renderer. A process called double buffering, makes drawing of elements smoother and doesn't leave a trail of pre-drawn graphics (thanks to SDL_RenderClear).
Our next function to initialise is the update function, for now there won't be anything in here but in future this will be where we update any properties of elements in our game such as player position or health. Like with render() go to the bottom of Game.cpp, leave a line space and intialise update():
Our next function to initialise is the update function, for now there won't be anything in here but in future this will be where we update any properties of elements in our game such as player position or health. Like with render() go to the bottom of Game.cpp, leave a line space and intialise update():
void Game::update()
{ } |
For now that's perfectly fine, it's initialised and can be left empty for now. Let's move onto our next function, handleEvents() but this time we're going to add a few things like with renderer():
void Game::handleEvents()
{ SDL_Event event; while(SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { quit(); } } } |
As you can see, there's a fair bit inside this function. First off we declare a variable to hold an event when it happens (like key press, or even pressing the exit button on the title bar). We then use a while loop to cycle through all the events that are happening at the time of the function being called and store them in event each time. We then use the event variable to check what event is occuring (in our case we're checking to see if the user has hit the exit button on the title bar) and depending on the event, execute some code (in our case, calling the function quit(), which we'll initialise soon). In the while loop, we can add more if statements to check for other events, but we'll eventually use a seperate class for event handling and also a slightly different modus operandi (method of operation), so for now this will do for demonstration purposes.
Next do the same for clean():
Next do the same for clean():
void Game::clean()
{ SDL_DestroyWindow(window); SDL_DestroyRenderer(renderer); SDL_Quit(); } |
With C++ you are in control of all memory used, and so when we're done using variables, we must clean them up in order to best manage memory available to us and optimise the program the best we can. For now we'll keep it as simple as destroying elements when we've finished with them.
Our final function is exit() which has one line:
Our final function is exit() which has one line:
void Game::exit()
{ running = false; } |
Quite simply, the quit function sets running to false so that our loop (coming soon) will exit and then the game will close.
Next we'll move onto making our Game class a singleton. A singleton is a class which only ever has one instance in a program that can be used everywhere always with the same values, there will never be two differing instances of a singleton, and nor will there be for our Game class.
So to make our Game class a singleton the first thing we need to do is wander back on over to our Game.h file and add a variable to the private section just above the Constructor and Deconstructor (Game(){} and ~Game(){}). The variable will be called "instance" and it's type will be Game, the same as the class it is in (don't let it confuse you, just let it happen).
Next we'll move onto making our Game class a singleton. A singleton is a class which only ever has one instance in a program that can be used everywhere always with the same values, there will never be two differing instances of a singleton, and nor will there be for our Game class.
So to make our Game class a singleton the first thing we need to do is wander back on over to our Game.h file and add a variable to the private section just above the Constructor and Deconstructor (Game(){} and ~Game(){}). The variable will be called "instance" and it's type will be Game, the same as the class it is in (don't let it confuse you, just let it happen).
private:
static Game* instance; |
We declare the variable to be static also, meaning that it effectively "stays where it is, as it is" (the official definition is that it is a member of the class itself, never a object of that class; so that like the class itself once a singleton, will only ever have 1 instance).
Once we've declared this variable, we can shimmy on up to the public section of the Game.h file and add a function which will return the one and only instance of our Game class no matter what; again stick it above the other public functions:
Once we've declared this variable, we can shimmy on up to the public section of the Game.h file and add a function which will return the one and only instance of our Game class no matter what; again stick it above the other public functions:
public:
static Game* Instance() { if (instance==0) { instance=new Game(); } return instance; } |
As you can see it is effectively a getter, but it includes the if statement which says that if the instance is equal to nothing then make it equal to a new Game object (this will only ever happen once and then our game will forever have 1 and only 1 instance).
Next we need to "type define" our Game class' instance so that when we use it we don't confuse it with the class itself, but with the singleton instance of our class.
Add the following line after everything in the Game.h file (even after the final "};"):
Next we need to "type define" our Game class' instance so that when we use it we don't confuse it with the class itself, but with the singleton instance of our class.
Add the following line after everything in the Game.h file (even after the final "};"):
typedef Game _Game;
|
From now on, whenever we want to access the game class outside of its own files we #include "Game.h" and call "_Game", more on that when we set up the game loop. Now for out final act in making Game a singleton, we must go over into the Game.cpp file again and initialise the instance as 0 so that it doesn't throw up uninitialised variable errors. To do so add the following line between the include line/s at the top and the first function intialisation:
Game* Game::instance=0;
|
Now we can go ahead and use our Game as a singleton in other classes and go ahead and create our game loop in main.cpp, so let's revisit our fairly barren looking main.cpp:
#include <SDL.h>
#include <iostream> #include <string> SDL_Window* window; SDL_Renderer* renderer' int main(int argc, char* args[]) { return 0; } |
First thing's first, we need to change up everything outside the main function. Get rid of all the includes and the two variables for window and renderer. Then add one and only one include:
#include "Game.h"
|
Once we've done that we have access to all the public members of Game, so the first thing we're gonna do is call init to intialise SDL. We can do so using our singleton, which you can see how we do here:
int main(int argc, char* args[])
{ if (_Game::Instance()->init("SDL Practice", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 960, 640, SDL_WINDOW_SHOWN)) { std::cout << "Game Init Succeeded" << std::endl; } else { std::cout << "game init failed" << SDL_GetError() << std::endl; return -1; } return 0; } |
As you can see we call init, but see how we go about it... To use Game as a singleton we call it using the typedef we setup "_Game", then we get the instance using the public function Instance() which returns a pointer to our one and only game instance. Because it's a pointer, we access it's members (public only) using the syntax "->" (which if you ask me is cool, dunno why, just is). You should see the IntelliSense pop up after the "->" so you know it's working. If not working, it could be down to computer performance, so write in the function init anyway and see if it springs an error after you've written all of the above code.
As you can see we call init, and init has parameters, the same parameters as SDL_CreateWindow. So remember the parameters you put in before, put them in here now as you can see above.
We then use two simple lines outputting to the console to tell us whether it was successful or not (feel free to add SDL_Delay(5000); just before return 0; to give you time to read the console, but remember to remove it after).
Once we've successfully initialised the game we can create our game loop. First off we're going to need a couple of global variables. We declare and initialise these outside our main function after the includes, and here they are:
As you can see we call init, and init has parameters, the same parameters as SDL_CreateWindow. So remember the parameters you put in before, put them in here now as you can see above.
We then use two simple lines outputting to the console to tell us whether it was successful or not (feel free to add SDL_Delay(5000); just before return 0; to give you time to read the console, but remember to remove it after).
Once we've successfully initialised the game we can create our game loop. First off we're going to need a couple of global variables. We declare and initialise these outside our main function after the includes, and here they are:
const int FPS=60;
const float DELAY_TIME=1000.0f / FPS; |
You can alter the value of FPS to any value you want, I recommend 60 fps because it's smooth and your eyes can't detect much more than that. DELAY_TIME is the number of milliseconds we will be putting into our SDL_Delay function to create ample delay to make our game run smoothly, and not just in the blink of an eye.
Next thing we need to do is to create a couple of local variables to keep an eye on how much time has gone by so we can calculate our delay more effectively should our system be under strain. Put this line at the beginning of the main function, before our init if statement:
Next thing we need to do is to create a couple of local variables to keep an eye on how much time has gone by so we can calculate our delay more effectively should our system be under strain. Put this line at the beginning of the main function, before our init if statement:
Uint32 frameStart, frameTime;
|
The variable frameStart stores the value for the start of each frame, the frameTime stores the value for the amount of time it took to process the frame. Now to put these variables to use we can create our game loop.
int main(int argc, char* args[])
{ if (_Game::Instance()->init("SDL Practice", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 960, 640, SDL_WINDOW_SHOWN)) { std::cout << "Game Init Succeeded" << std::endl; while (_Game::Instance()->isRunning()) { frameStart = SDL_GetTicks(); _Game::Instance()->update(); _Game::Instance()->handleEvents(); _Game::Instance()->render(); frameTime=SDL_GetTicks() - frameStart; if (frameTime < DELAY_TIME) { SDL_Delay((int)(DELAY_TIME - frameTime)); } else { SDL_Delay((int)DELAY_TIME); } } } else { std::cout << "game init failed" << SDL_GetError() << std::endl; return -1; } _Game::Instance()->clean(); return 0; } |
Now let me take a moment to explain all the features of this game loop we have here.
1) We create a while loop in which all of the actions in our game will be ran repeatedly until the game is over (running==false) at which point it exits the while loop.
2) We start our game loop by getting the start time of this particular cycle using SDL_GetTicks() and storing it in our local variable frameStart. SDL_GetTicks() returns the number of milliseconds since SDL initialisation.
3) We then call the three fundamental functions of our loop using our game singleton; update(), handleEvents(), and render().
4) We then store the amount of time it took to complete those functions in frameTime by taking frameStart (the amount of time passed since SDL intialisation before the functions were called) and taking it away from SDL_GetTicks(), which gets the amount of time since SDL intialisation after the functions have been called.
5) Having ran through the necessary functions and armed with frameTime we can calculate the delay we need to get as close to 60 FPS as possible. We first check to see if it took less time to perform the functions (frameTime) than the value of DELAY_TIME. If so, we take frameTime from the DELAY_TIME and only delay the loop by that amount of time. However, if it takes longer than the DELAY_TIME then we don't want to wait more time than we already have to loop round again, so in that case there is no delay.
6) After everything is done, before we exit (return 0;) we call the game function clean() to clean up before we exit, and then let the program exit.
You have now successfully created your game loop and Game class singleton! Onwards! to more game development with C++ and SDL2!
1) We create a while loop in which all of the actions in our game will be ran repeatedly until the game is over (running==false) at which point it exits the while loop.
2) We start our game loop by getting the start time of this particular cycle using SDL_GetTicks() and storing it in our local variable frameStart. SDL_GetTicks() returns the number of milliseconds since SDL initialisation.
3) We then call the three fundamental functions of our loop using our game singleton; update(), handleEvents(), and render().
4) We then store the amount of time it took to complete those functions in frameTime by taking frameStart (the amount of time passed since SDL intialisation before the functions were called) and taking it away from SDL_GetTicks(), which gets the amount of time since SDL intialisation after the functions have been called.
5) Having ran through the necessary functions and armed with frameTime we can calculate the delay we need to get as close to 60 FPS as possible. We first check to see if it took less time to perform the functions (frameTime) than the value of DELAY_TIME. If so, we take frameTime from the DELAY_TIME and only delay the loop by that amount of time. However, if it takes longer than the DELAY_TIME then we don't want to wait more time than we already have to loop round again, so in that case there is no delay.
6) After everything is done, before we exit (return 0;) we call the game function clean() to clean up before we exit, and then let the program exit.
You have now successfully created your game loop and Game class singleton! Onwards! to more game development with C++ and SDL2!