Your guide to learning game development
In this chapter we’re going to introduce the basic game loop, event handling, and simple input response. Let’s begin by opening treasure_grabber.d (or whatever name you chose) and removing the call to SDL_Delay. In its place, add the following code:
bool isRunning = true;
while (isRunning)
{
}
This is called the “game loop” and it’s the driving force behind every video game. At each iteration of the loop, called a “frame”, player input is processed, the game world is simulated, and the game is drawn to the window. These steps will infinitely repeat until the loop is terminated by setting isRunning to false. If you’ll recall we’re already doing one of those three steps; rendering the game. Cut and paste the rendering code from where it is now to the inside of the game loop where it belongs.
Right now our game loop contains no logic so compiling and running as-is will cause the game to stall until the player forcibly terminates the process. This is obviously no good. We should at least allow the player to quit the game by clicking the close button on the game’s window. We can do this by processing system events.
Let’s talk a little about what these events are and where they come from. The operating system is responsible for managing applications launched by the user. One way it does this is by sending the application information such as when the mouse has moved, a key has been pressed, the close button has been clicked, and many other things. This information is packaged into a data type known as an "event" and the application can access events it's received from an “event queue.” The way to access this queue and the types of events sent depends on the operating system on which the application is run. Fortunately for us, SDL2 provides an OS-independent means of receiving and processing events by way of the SDL_PollEvent function. Add the following to the top of our game loop, before the rendering code:
SDL_Event evt;
while (SDL_PollEvent(&evt))
{
switch (evt.type)
{
case SDL_QUIT:
isRunning = false;
break;
default: break;
}
}
Here we instantiate an SDL_Event and we pass it by pointer to the SDL_PollEvent function. This function pops an event off the event queue and copies it into the SDL_Event pointer we provided. Calling SDL_PollEvent only pops one event at a time, but we need to process all pending events each iteration of the game loop or we’ll quickly fall behind. Since SDL_PollEvent helpfully returns 0 when no events are left in the queue we can easily do this by wrapping our call in a while loop.
We use a switch statement to test the type of each event we pop off the event queue and see if any of them are an SDL_QUIT event. This type of event is generated when the user tries to close the game window. If there is an SDL_QUIT event in the queue we set isRunning to false which in turn ends our game loop and exits the game. A professional-quality game would ask the player for confirmation or save any progress before exiting like this. We won’t do anything that complicated for a game as simple as this one, however. It's worth noting that any events not explicitly handled in our event polling loop are effectively discarded.
Now that we know how to process events we can move on to handling basic user input. Add the follow just below all those import statements:
enum
{
INPUT_KEY_LEFT,
INPUT_KEY_RIGHT,
INPUT_KEY_UP,
INPUT_KEY_DOWN,
INPUT_KEY_RETRY,
INPUT_KEY_TOTAL,
}
struct GameInput
{
bool[INPUT_KEY_TOTAL] isKeyDown;
int[INPUT_KEY_TOTAL] keycode;
}
In this code we define the GameInput struct which consists of two parellel arrays. As its name suggests, the isKeyDown array will tell us if the player is holding down a key during the current frame. The keycode array will hold a list of IDs that represent a key on the keyboard. This second array will be used to associate a given key on the keyboard with an element in the isKeyDown array. This allows us to store only information regarding the keys we care about when we process keyboard events. We also define some enum values to clarify which key we're looking for when indexing into the isKeyDown and keycode arrays.
Let’s go ahead and add an instance of GameInput just before the game loop:
GameInput input;
input.keycode[INPUT_KEY_LEFT] = SDLK_a;
input.keycode[INPUT_KEY_RIGHT] = SDLK_d;
input.keycode[INPUT_KEY_UP] = SDLK_w;
input.keycode[INPUT_KEY_DOWN] = SDLK_s;
input.keycode[INPUT_KEY_RETRY] = SDLK_RETURN;
We instantiate a new GameInput variable and assign an SDL_Keycode constant to each element of the keycode array. The names of each constant (prefixed with SDLK) are all self-explanatory, but you can consult the list of each SDL_Keycode and the keys they represent if you want to learn more.
Let's go ahead and process some keyboard events since we now have a place to store user input. The following should go inside the switch statement from earlier:
case SDL_KEYDOWN:
case SDL_KEYUP:
foreach (i; 0 .. INPUT_KEY_TOTAL)
{
if (input.keycode[i] == evt.key.keysym.sym)
{
input.isKeyDown[i] = evt.key.state == SDL_PRESSED;
}
}
break;
In this new code we check to see if the event we just received is either an SDL_KEYDOWN or SDL_KEYUP event. These events are triggered when a key on the keyboard has just been pushed down or released by the player, respectively. If we receive one of those events we can safely access further information through the "key" member of the SDL_Event. The keycode of the key that generated this even can be obtained by checking the key.keysym.sym member of the SDL_Event. We check each element of our keycode array against the keycode from the event to see if this concerns one of the keys in which we're interested. If it is, we record the status of the key by using the expression "evt.key.state == SDL_PRESSED", which will evaluate to "true" if the key has just been pressed or "false" if it's just been released.
Since we’re now processing user input let's end this chapter by adding some very primitive interactivity to our game. Add the following just before the call to SDL_RenderClear:
if (input.isKeyDown[INPUT_KEY_RETRY])
{
SDL_SetRenderDrawColor(renderer, 0, 168, 178, 255);
}
Now when you run the game you can hold the the enter key to change the background to a light-teal color. It isn’t much, but this is another step towards a fully playable video game.