Hands-On Game Development with WebAssembly
上QQ阅读APP看书,第一时间看更新

Animating a sprite

In this section, we will learn how to make a quick and dirty little animation in our SDL application. That will not be the way we do animations in our final game, but it will give you an idea of how we could create animations from within SDL by swapping out textures over time. I am going to present the code to animate a sprite broken into two parts. The first part includes our preprocessor macros, global variables, and the show_animation function:

#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>

#include <emscripten.h>
#include <stdio.h>

#define SPRITE_FILE "sprites/Franchise1.png"
#define EXP_FILE "sprites/FranchiseExplosion%d.png"
#define FRAME_COUNT 7

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];

void show_animation() {
current_time = SDL_GetTicks();
int ms = current_time - last_time;

if( ms < ms_per_frame) {
return;
}

if( current_frame >= FRAME_COUNT ) {
SDL_RenderClear( renderer );
return;
}

last_time = current_time;
SDL_RenderClear( renderer );

temp_texture = anim[current_frame++];

SDL_QueryTexture( temp_texture,
NULL, NULL,
&dest.w, &dest.h ); // query the width and
height

dest.x = 160 - dest.w / 2;
dest.y = 100 - dest.h / 2;

SDL_RenderCopy( renderer, temp_texture, NULL, &dest );
SDL_RenderPresent( renderer );
}


After we define our show_animation function, we will need to define our module's main function:

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, EXP_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;
}

There is a lot to unpack here. There are much more efficient ways to do this animation, but what we are doing here takes what we have already done and adds to it. In earlier versions of the code, we rendered a single frame to the canvas, then exited the WebAssembly module. That works well enough if your goal is to render something static to the canvas and never change it. If you are writing a game, however, you need to be able to animate your sprites and move them around the canvas. Here, we run into a problem that we do not have if we are compiling our C++ code for any target other than WebAssembly. Games typically run in a loop and are directly responsible for rendering to the screen. WebAssembly runs inside of the JavaScript engine in your web browser. The WebAssembly module itself cannot update our canvas. Emscripten uses the JavaScript glue code to update the HTML canvas indirectly from the SDL API. However, if the WebAssembly runs in a loop, and uses that loop to animate our sprite through SDL, the WebAssembly module never lets go of the thread it is in, and the JavaScript never has an opportunity to update the canvas. Because of this, we can not put the game loop inside the main function. Instead, we must create a different function, and use Emscripten to set up the JavaScript glue code to call that function every time the browser renders a frame. The function we will use to do that is as follows:

emscripten_set_main_loop(show_animation, 0, 0);

The first parameter we will pass to emscripten_set_main_loop is show_animation. This is the name of a function we defined near the top of the code. I will talk about the specifics of the show_animation function a little later. For now, it is enough to know that this is the function called every time the browser renders a new frame on the canvas.

The second parameter of emscripten_set_main_loop is frames per second (FPS). If you want to set the FPS of your game to a fixed rate, you can do so by passing the target frame rate into the function here. If you pass in 0, this tells emscripten_set_main_loop to run with the highest frame rate it can. As a general rule, you want your game to run with the highest frame rate possible, so passing in 0 is usually the best thing to do. If you pass in a value higher than what the computer is capable of rendering, it will merely render as fast as it is able anyway, so this value only puts a cap on your FPS.

The third parameter we pass in is simulate_infinite_loop. Passing in 0 is equivalent to passing a false value. If the value of this parameter is true, it forces the module to re-enter through the main function for every frame. I am not sure what the use case for this is. I would recommend keeping it at 0 and separating your game loop into another function as we have done here.

Before calling emscripten_set_main_loop, we will set up an array of SDL texture surface pointers:

for( int i = 1; i <= FRAME_COUNT; i++ ) {
sprintf( explosion_file_string, EXP_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 );
}

This loop loads FranchiseExplosion1.png through FranchiseExplosion7.png into an array of SDL textures and stores them into a different array, called anim. That is the array we will loop through later in the show_animation function. There are more efficient ways to do this using sprite sheets, and by modifying the destination rectangle. We will discuss those techniques for rendering animated sprites in later chapters.

Near the top of the code, we defined the show_animation function, called every rendered frame:

void show_animation() {
current_time = SDL_GetTicks();
int ms = current_time - last_time;

if( ms < ms_per_frame) {
return;
}

if( current_frame >= FRAME_COUNT ) {
SDL_RenderClear( renderer );
return;
}

last_time = current_time;
SDL_RenderClear( renderer );

temp_texture = anim[current_frame++];

SDL_QueryTexture( temp_texture,
NULL, NULL,
&dest.w, &dest.h ); // query the width and
height

dest.x = 160 - dest.w / 2;
dest.y = 100 - dest.h / 2;

SDL_RenderCopy( renderer, temp_texture, NULL, &dest );
SDL_RenderPresent( renderer );
}

This function is designed to wait a certain number of milliseconds, then update the texture we are rendering. I have created a seven frame animation that blows up the Starship Franchise in a little pixelated explosion. The reason we need a short wait in this loop is that our refresh rate is probably 60+ FPS, and if we render a new frame of our animation every time show_animation is called, the entire animation would run in about 1/10 of a second. Classic arcade games frequently flipped through their animation sequences at a much slower rate than the games frame rate. Many classic Nintendo Entertainment System (NES) games used two-stage animations where the animation would alternate sprites every few hundred milliseconds, even though the NES ran with a frame rate of 60 FPS.

The core of this function is similar to the single texture render we created earlier. The primary difference is that we wait a fixed number of milliseconds before changing the frame of our animation by incrementing the current_frame variable. That takes us through all seven stages of our animation in a little less than a second.