Writing a basic game loop
To some degree, we already have a simple game loop, although we did not create a function called game_loop explicitly. We are going to modify our code to have a more explicit game loop that will separate the input, move, and render functions. At this point, our main function becomes an initialization function that finishes by using Emscripten to set the game loop. The code for this new app is larger than earlier apps. Let's first walk through the code at a high level, introducing each section. Then we will walk through each of the individual sections of code in detail.
We begin the code with our #include and #define preprocessor macros:
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <emscripten.h>
#include <stdio.h>
#include <stdbool.h>
#include <math.h>
#define SPRITE_FILE "sprites/Franchise.png"
#define PI 3.14159
#define TWO_PI 6.28318
#define MAX_VELOCITY 2.0
After the preprocessor macros, we have a few global time variables:
Uint32 last_time;
Uint32 last_frame_time;
Uint32 current_time;
We will then define several SDL-related global variables:
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Rect dest = {.x = 160, .y = 100, .w = 16, .h = 16 };
SDL_Texture *sprite_texture;
SDL_Event event;
After our SDL global variables, we have a block of keyboard flags:
bool left_key_down = false;
bool right_key_down = false;
bool up_key_down = false;
bool down_key_down = false;
The last global variables track player data:
float player_x = 160.0;
float player_y = 100.0;
float player_rotation = PI;
float player_dx = 0.0;
float player_dy = 1.0;
float player_vx = 0.0;
float player_vy = 0.0;
float delta_time = 0.0;
Now that we have all of our global variables defined, we need two functions that rotate the player's spaceship left and right:
void rotate_left() {
player_rotation -= delta_time;
if( player_rotation < 0.0 ) {
player_rotation += TWO_PI;
}
player_dx = sin(player_rotation);
player_dy = -cos(player_rotation);
}
void rotate_right() {
player_rotation += delta_time;
if( player_rotation >= TWO_PI ) {
player_rotation -= TWO_PI;
}
player_dx = sin(player_rotation);
player_dy = -cos(player_rotation);
}
We then have three movement-related functions for our player's ship. We use them to accelerate and decelerate our spaceship, and to capp the velocity of our spaceship:
void accelerate() {
player_vx += player_dx * delta_time;
player_vy += player_dy * delta_time;
}
void decelerate() {
player_vx -= (player_dx * delta_time) / 2.0;
player_vy -= (player_dy * delta_time) / 2.0;
}
void cap_velocity() {
float vel = sqrt( player_vx * player_vx + player_vy * player_vy );
if( vel > MAX_VELOCITY ) {
player_vx /= vel;
player_vy /= vel;
player_vx *= MAX_VELOCITY;
player_vy *= MAX_VELOCITY;
}
}
The move function performs the high-level movement of the game objects:
void move() {
current_time = SDL_GetTicks();
delta_time = (float)(current_time - last_time) / 1000.0;
last_time = current_time;
if( left_key_down ) {
rotate_left();
}
if( right_key_down ) {
rotate_right();
}
if( up_key_down ) {
accelerate();
}
if( down_key_down ) {
decelerate();
}
cap_velocity();
player_x += player_vx;
if( player_x > 320 ) {
player_x = -16;
}
else if( player_x < -16 ) {
player_x = 320;
}
player_y += player_vy;
if( player_y > 200 ) {
player_y = -16;
}
else if( player_y < -16 ) {
player_y = 200;
}
}
The input function determines the keyboard input states and sets our global keyboard flags:
void input() {
if( SDL_PollEvent( &event ) ){
switch( event.type ){
case SDL_KEYDOWN:
switch( event.key.keysym.sym ){
case SDLK_LEFT:
left_key_down = true;
break;
case SDLK_RIGHT:
right_key_down = true;
break;
case SDLK_UP:
up_key_down = true;
break;
case SDLK_DOWN:
down_key_down = true;
break;
default:
break;
}
break;
case SDL_KEYUP:
switch( event.key.keysym.sym ){
case SDLK_LEFT:
left_key_down = false;
break;
case SDLK_RIGHT:
right_key_down = false;
break;
case SDLK_UP:
up_key_down = false;
break;
case SDLK_DOWN:
down_key_down = false;
break;
default:
break;
}
break;
default:
break;
}
}
}
The render function draws the player's sprite to the canvas:
void render() {
SDL_RenderClear( renderer );
dest.x = player_x;
dest.y = player_y;
float degrees = (player_rotation / PI) * 180.0;
SDL_RenderCopyEx( renderer, sprite_texture,
NULL, &dest,
degrees, NULL, SDL_FLIP_NONE );
SDL_RenderPresent( renderer );
}
The game_loop function runs all of our high-level game objects in each frame:
void game_loop() {
input();
move();
render();
}
As always, the main function does all of our initialization:
int main() {
char explosion_file_string[40];
SDL_Init( SDL_INIT_VIDEO );
SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );
SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );
SDL_RenderClear( renderer );
SDL_Surface *temp_surface = IMG_Load( SPRITE_FILE );
if( !temp_surface ) {
printf("failed to load image: %s\n", IMG_GetError() );
return 0;
}
sprite_texture = SDL_CreateTextureFromSurface( renderer,
temp_surface );
SDL_FreeSurface( temp_surface );
last_frame_time = last_time = SDL_GetTicks();
emscripten_set_main_loop(game_loop, 0, 0);
return 1;
}
You may have noticed that in the preceding code we have added a significant number of global variables to define player-specific values:
float player_x = 160.0;
float player_y = 100.0;
float player_rotation = PI;
float player_dx = 0.0;
float player_dy = 1.0;
float player_vx = 0.0;
float player_vy = 0.0;
In the Game objects section, we will begin to create game objects and move these values from global definitions into objects, but, for the time being, having them as global variables will work. We are adding the ability to move the player's ship around in a way that is similar to the classic arcade game Asteroids. In the final version of our game, we will have two spaceships fighting in a duel. To do this, we will need to keep track of the x and y coordinates of our ship and the ship's rotation; player_dx and player_dy make up a normalized direction vector for our spaceship.
The player_vx and player_vy variables are the player's current x and y velocities respectively.
Instead of having the left and right keys move the spaceship left or right while they are being held down, we are going to have those keys turn the spaceship to the left or the right. To do this, we will have our input function call the rotate_left and rotate_right functions:
void rotate_left() {
player_rotation -= delta_time;
if( player_rotation < 0.0 ) {
player_rotation += TWO_PI;
}
player_dx = sin(player_rotation);
player_dy = -cos(player_rotation);
}
void rotate_right() {
player_rotation += delta_time;
if( player_rotation >= TWO_PI ) {
player_rotation -= TWO_PI;
}
player_dx = sin(player_rotation);
player_dy = -cos(player_rotation);
}
If the player is turning left, we subtract the delta_time variable from the player rotation, which is the amount of time in seconds since the last frame rendered. The player_rotation variable is the player's rotation in radians, where 180 degrees = π (3.14159…). That means that the player can turn 180 degrees by pressing and holding the left or right arrows for about three seconds. We also have to correct our rotation if the player's rotation goes below 0 or if the player's rotation goes above 2π (360 degrees). If you are not familiar with radians, it is an alternative to the system of measuring angles in which there are 360 degrees in a circle. Using radians, you think of how far you would have to walk around the circumference of a unit circle to get to that angle. A circle with a radius of 1 is called a unit circle.
The unit circle is on the left:
The formula for the diameter of a circle is 2πr (in our code 2 * PI * radius). So, 2π in radians is the same as saying 360 degrees. Most game engines and math libraries use radians instead of degrees, but for some reason SDL uses degrees when it rotates sprites, so we will need to change our rotation in radians back to degrees when we render our game objects (yuck!).
We also need to accelerate or decelerate the spaceship if the player hits the up or down keys on the keyboard. To do this, we will create accelerate and decelerate functions that are called when the player holds down the up or down keys:
void accelerate() {
player_vx += player_dx * delta_time;
player_vy += player_dy * delta_time;
}
void decelerate() {
player_vx -= (player_dx * delta_time) / 2.0;
player_vy -= (player_dy * delta_time) / 2.0;
}
Both these functions take the player_dx and player_dy variables that were calculated using sin and -cos in our rotation functions and use those values to add to the player's x and y velocity stored in the player_vx and player_vy variables. We multiply the value by delta_time, which will set our acceleration to 1 pixel per second squared. Our decelerate function divides that value by 2, which sets our deceleration rate to 0.5 pixels per second squared.
After we define the accelerate and decelerate functions, we will need to create a function that will cap the x and y velocity of our spaceship to 2.0 pixels per second:
void cap_velocity() {
float vel = sqrt( player_vx * player_vx + player_vy * player_vy );
if( vel > MAX_VELOCITY ) {
player_vx /= vel;
player_vy /= vel;
player_vx *= MAX_VELOCITY;
player_vy *= MAX_VELOCITY;
}
}
That is not the most efficient way to define this function, but it is the easiest to understand. The first line determines the magnitude of our velocity vector. If you do not know what that means, let me explain it a little better. We have a speed along the x axis. We also have a speed along the y axis. We want to cap the overall speed. If we capped the x and y velocities individually, we would be able to go faster by traveling diagonally. To calculate our total velocity, we need to use the Pythagorean theorem (do you remember high-school trigonometry?). If you don't remember, when you have a right triangle, to calculate its hypotenuse you take the square root of the sum of the square of the other two sides (remember A2 + B2 = C2?):
So, to calculate our velocity overall we need to square the x velocity, square the y velocity, add them together, and then take the square root. At this point, we check our velocity against the MAX_VELOCITY value, which we have defined as 2.0. If the current velocity is greater than this maximum velocity, we need to adjust our x and y velocities so that we are at a value of 2. We do this by dividing both the x and y velocities by the overall velocity, then multiplying by MAX_VELOCITY.
We will eventually need to write a move function that will move all of our game objects, but for the moment we will only be moving our player's spaceship:
void move() {
current_time = SDL_GetTicks();
delta_time = (float)(current_time - last_time) / 1000.0;
last_time = current_time;
if( left_key_down ) {
rotate_left();
}
if( right_key_down ) {
rotate_right();
}
if( up_key_down ) {
accelerate();
}
if( down_key_down ) {
decelerate();
}
cap_velocity();
player_x += player_vx;
if( player_x > 320 ) {
player_x = -16;
}
else if( player_x < -16 ) {
player_x = 320;
}
player_y += player_vy;
if( player_y > 200 ) {
player_y = -16;
}
else if( player_y < -16 ) {
player_y = 200;
}
}
The first thing we need to do is get the current time for this frame, and then use that in combination with our previous frame time to calculate the delta_time. The delta_time variable is the amount of time in seconds since the last frame time. We will need to tie much of the movement and animation to this value to get a consistent game speed that's independent of the frame rate on any given computer. After that, we need to rotate and accelerate or decelerate our spaceship based on the flags we set in our input function. We then cap our velocity and use the x and y values to modify the x and y coordinates of the player's spaceship.
There were a series of flags we used in the move function that told us whether we were currently holding down specific keys on the keyboard. To set those flags, we need an input function that uses SDL_PollEvent to find keyboard events and set the flags accordingly:
void input() {
if( SDL_PollEvent( &event ) ){
switch( event.type ){
case SDL_KEYDOWN:
switch( event.key.keysym.sym ){
case SDLK_LEFT:
left_key_down = true;
break;
case SDLK_RIGHT:
right_key_down = true;
break;
case SDLK_UP:
up_key_down = true;
break;
case SDLK_DOWN:
down_key_down = true;
break;
default:
break;
}
break;
case SDL_KEYUP:
switch( event.key.keysym.sym ){
case SDLK_LEFT:
left_key_down = false;
break;
case SDLK_RIGHT:
right_key_down = false;
break;
case SDLK_UP:
up_key_down = false;
break;
case SDLK_DOWN:
down_key_down = false;
break;
default:
break;
}
break;
default:
break;
}
}
}
This function includes a few switch statements that look for the arrow key presses and releases. If one of the arrow keys is pressed, we set the appropriate flag to true; if one is released, we set that flag to false.
Next, we define the render function. This function currently renders our spaceship sprite and will eventually render all of our sprites to the HTML canvas:
void render() {
SDL_RenderClear( renderer );
dest.x = player_x;
dest.y = player_y;
float degrees = (player_rotation / PI) * 180.0;
SDL_RenderCopyEx( renderer, sprite_texture,
NULL, &dest,
degrees, NULL, SDL_FLIP_NONE );
SDL_RenderPresent( renderer );
}
This function clears the HTML canvas, sets the destination x and y values to player_x and player_y, calculates the player's rotation in degrees, and then renders that sprite to the canvas. We swapped out our previous call to SDL_RenderCopy with a call to SDL_RenderCopyEx. This new function allows us to pass in a value that rotates the sprite of our spaceship.
After we defined our render function, we have our new game_loop function:
void game_loop() {
input();
move();
render();
}
This function will be called by emscripten_set_main_loop from within our main function. This function runs every frame that is rendered and is responsible for managing all the activities that go on within our game. It currently calls the input, move, and render functions that we defined earlier in our game code, and in the future it will call our AI code, sound effects, physics code, and more.