Using keyboard input to move a sprite
Now that we know how to get keyboard input and use it in our WebAssembly module, let's figure out how we can take that keyboard input and use it to move our spaceship sprite around the HTML canvas. Let's begin by copying sprite_move.c from the Chapter04 directory into the Chapter05 directory. That will give us a good starting point. Now we can start modifying the code. We will need to add a single #include to the beginning of our .c file. Because we need Boolean variables, we must add #include <stdbool.h>. The new start of our .c file will now look as follows:
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <emscripten.h>
#include <stdio.h>
#include <stdbool.h>
After that, all the #define directives will remain unchanged from what they were in the sprite_move.c file, as can be seen in the following code:
#define SPRITE_FILE "sprites/Franchise1.png"
#define ANIM_FILE "sprites/Franchise%d.png"
#define FRAME_COUNT 4
The sprite_move.c file had several global variables that we will continue to use in keyboard_move.c. Do not remove any of these variables; we will only be adding to them:
int current_frame = 0;
Uint32 last_time;
Uint32 current_time;
Uint32 ms_per_frame = 100; // animate at 10 fps
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Rect dest = {.x = 160, .y = 100, .w = 0, .h = 0 };
SDL_Texture *sprite_texture;
SDL_Texture *temp_texture;
SDL_Texture* anim[FRAME_COUNT];
Now we need to bring in some variables from the keyboard.c file that we used in the previous section. We need the SDL_Event global variable so that we have something to pass into our call to SDL_PollEvent, and we need our Boolean key press flags, as follows:
SDL_Event event;
bool left_key_press = false;
bool right_key_press = false;
bool up_key_press = false;
bool down_key_press = false;
We then have the function declarations, which allow us to define the key_press and key_release functions after we have defined our input_loop function, as shown in the following example:
void key_press();
void key_release();
Next, we will bring in the input_loop function from our keyboard.c file. This is the function that we use to call SDL_PollEvent, and, based on the event type returned, either calls key_press or key_release. This function remains unchanged from the version we had in keyboard.c, as can be seen in the following example:
void input_loop() {
if( SDL_PollEvent( &event ) ){
if( event.type == SDL_KEYDOWN ){
key_press();
}
else if( event.type == SDL_KEYUP ) {
key_release();
}
}
}
The key_press and key_release functions follow the input_loop function and remain unchanged from the keyboard.c version. The primary purpose of these functions is to set the keypress flags. The printf statements are now unnecessary, but we will leave them there. This is not a good thing for performance because continuing to add lines to our textarea with every key press and release will eventually slow our game down, but, at this point, I feel it is better to leave these statements in for demonstration purposes:
void key_press() {
switch( event.key.keysym.sym ){
case SDLK_LEFT:
if( !left_key_press ) {
printf("left arrow key press\n");
}
left_key_press = true;
break;
case SDLK_RIGHT:
if( !right_key_press ) {
printf("right arrow key press\n");
}
right_key_press = true;
break;
case SDLK_UP:
if( !up_key_press ) {
printf("up arrow key press\n");
}
up_key_press = true;
break;
case SDLK_DOWN:
if( !down_key_press ) {
printf("down arrow key press\n");
}
down_key_press = true;
break;
default:
printf("unknown key press\n");
break;
}
}
void key_release() {
switch( event.key.keysym.sym ){
case SDLK_LEFT:
if( left_key_press ) {
printf("left arrow key release\n");
}
left_key_press = false;
break;
case SDLK_RIGHT:
if( right_key_press ) {
printf("right arrow key release\n");
}
right_key_press = false;
break;
case SDLK_UP:
if( up_key_press ) {
printf("up arrow key release\n");
}
up_key_press = false;
break;
case SDLK_DOWN:
if( down_key_press ) {
printf("down arrow key release\n");
}
down_key_press = false;
break;
default:
printf("unknown key release\n");
break;
}
}
The next function in the keyboard_move.c file will be show_animation. This function will need to be changed significantly from the version that appears in sprite_move.c, to allow the player to control the spaceship and move it around the canvas. The following example shows you the new function in its entirety before we go through it a piece at a time:
void show_animation() {
input_loop();
current_time = SDL_GetTicks();
int ms = current_time - last_time;
if( ms >= ms_per_frame) {
++current_frame;
last_time = current_time;
}
if( current_frame >= FRAME_COUNT ) {
current_frame = 0;
}
SDL_RenderClear( renderer );
temp_texture = anim[current_frame];
if( up_key_press ) {
dest.y--;
if( dest.y < -16 ) {
dest.y = 200;
}
}
if( down_key_press ) {
dest.y++;
if( dest.y > 200 ) {
dest.y = -16;
}
}
if( left_key_press ) {
dest.x--;
if( dest.x < -16 ) {
dest.x = 320;
}
}
if( right_key_press ) {
dest.x++;
if( dest.x > 320 ) {
dest.x = -16;
}
}
SDL_RenderCopy( renderer, temp_texture, NULL, &dest );
SDL_RenderPresent( renderer );
}
We added the very first line in show_animation to this new version of the function. The call to input_loop is used to set the key press flags every frame. After the call to input_loop, there is a chunk of the code that we have not changed from the sprite_move.c file, as shown in the following example:
current_time = SDL_GetTicks();
int ms = current_time - last_time;
if( ms >= ms_per_frame) {
++current_frame;
last_time = current_time;
}
if( current_frame >= FRAME_COUNT ) {
current_frame = 0;
}
SDL_RenderClear( renderer );
temp_texture = anim[current_frame];
This code calls SDL_GetTicks() to get the current time, and then subtracts the current time from the last time the current frame changed, to get the number of milliseconds it has been since we last had a frame change. If the number of milliseconds since the last frame change is greater than the number of milliseconds that we want to stay on any given frame, we need to advance the current frame. Once we have figured out whether or not we have advanced the current frame, we need to make sure that the current frame is not more than our frame count. If it is, we need to reset it to 0. After that, we need to clear out our renderer and set the texture we are using to the texture in our animation array that corresponds with the current frame.
In sprite_move.c, we moved the y coordinates of our spaceship up one pixel per frame with the following few lines of code:
dest.y--;
if( dest.y < -16 ) {
dest.y = 200;
}
In the new keyboard app, we only want to change our y coordinate when the player presses the up arrow key. To do this, we must enclose the code that changes the y coordinate in an if block that checks the up_key_press flag. Here is the new version of that code:
if( up_key_press ) {
dest.y--;
if( dest.y < -16 ) {
dest.y = 200;
}
}
We also need to add code that moves the spaceship when the player presses the other arrow keys. The following code moves the spaceship down, left or right based on what keys the player is currently pressing:
if( down_key_press ) {
dest.y++;
if( dest.y > 200 ) {
dest.y = -16;
}
}
if( left_key_press ) {
dest.x--;
if( dest.x < -16 ) {
dest.x = 320;
}
}
if( right_key_press ) {
dest.x++;
if( dest.x > 320 ) {
dest.x = -16;
}
}
Finally, we have to render the texture and present it, as follows:
SDL_RenderCopy( renderer, temp_texture, NULL, &dest );
SDL_RenderPresent( renderer );
The main function will not change from the version inside sprite_move.c because none of the initialization has changed. The following code shows the main function as it appears in keyboard_move.c:
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 );
for( int i = 1; i <= FRAME_COUNT; i++ ) {
sprintf( explosion_file_string, ANIM_FILE, i );
SDL_Surface *temp_surface = IMG_Load( explosion_file_string );
if( !temp_surface ) {
printf("failed to load image: %s\n", IMG_GetError() );
return 0;
}
temp_texture = SDL_CreateTextureFromSurface( renderer, temp_surface );
anim[i-1] = temp_texture;
SDL_FreeSurface( temp_surface );
}
SDL_QueryTexture( sprite_texture,
NULL, NULL,
&dest.w, &dest.h ); // query the width and height
dest.x -= dest.w / 2;
dest.y -= dest.h / 2;
SDL_RenderCopy( renderer, sprite_texture, NULL, &dest );
SDL_RenderPresent( renderer );
last_time = SDL_GetTicks();
emscripten_set_main_loop(show_animation, 0, 0);
return 1;
}
As I said earlier, this code is a combination of the last application we wrote in Chapter 4, Sprite Animations in WebAssembly with SDL, and the code we wrote in the section Adding SDL keyboard input to WebAssembly where we were taking input from the keyboard and logging our keys with the printf statement. We kept our input_loop function and added a call to it from the beginning of our show_animation function. Inside show_animation, we no longer move the ship one pixel up every frame, but only move the ship up if we are pressing the up arrow key. Likewise, we move the ship left when the user presses the left arrow key, right when the right arrow key is pressed and down when the user presses the down arrow key.
Now that we have our new keyboard_move.c file, let's compile it and try out our new moving spaceship. Run the following emcc command to compile the code:
emcc keyboard_move.c -o keyboard_move.html --preload-file sprites -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=["png"]
We need to add the --preload-file sprites flag to indicate that we want a virtual file system with the sprites folder included. We also need to add the -s USE_SDL=2 and -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=["png"] flags to allow us to load .png files from the virtual file system. Once you have compiled keyboard_move.html, load it into a browser and use the arrow keys to move the spaceship around the canvas. See the following screenshot: