Chapter 4. Programming Sprites in Allegro 5
4.1. The Fundamentals
4.1.1. Using Static Sprites in Allegro 5
In Allegro 5, a sprite is simply another bitmap object that can be displayed on the screen. You may notice that this sounds similar to the description of bitmap given in the previous chapter which raises an important question: What distinguishes sprites from ordinary bitmaps? A sprite bitmap (which we will just refer to as a sprite) is typically a moving object and not considered part of the background of a game. So we have now two layers, if you will, of images being utilized: a bitmap for a game background and then the moving of stationary objects on top of that—sprites. Sprites tend then to be small graphic images, with sizes like 32 × 32 and 64 × 64, or 128 × 128 and 256 × 256, although they could be of any size. However, when it comes to the animation aspect of the sprite, these smaller sizes are of benefit (since an animate sprite is a matrix of single bitmaps amalgamated in a larger object—more on that later).
Sprites are commonly utilized for the player character, nonplayer characters (NPCs), in-game objects, and weapons fire (projectiles, beams, etc.) The sprite is then a self-contained object that will have characteristics allowing it to be unitized through various means: movement around the screen (or the denial of movement), detection of collisions with objects, and so on.
As we look at an example of a sprite program, here we see some of the logic in the simplistic style of Allegro, providing the ability to use the same routines, building upon them to do more complex actions, thus easing the programmer’s work by using familiar processes. As such, you will see commonalities in the routines and setup used for bitmaps and what we classify as sprites, allowing us to use similar library routines but for differing purposes.
In the example program, we load two bitmaps: one to use as the background and the other as the movable sprite. We’ll use our CatCosmos.png as the background and a simple flying saucer sprite, SmallSaucer.png (see figure 4.1)
Now one of the issues we have to deal with for sprites is, What do we display and what do we hide? If we use the saucer image as is, we will see the full image: the red square and the saucer. Oh, but it’s way too big for the size of our screen, as you can see in figure 4.2.
Figure 4.1: Saucer sprite image. Illustrated by Walter Ridgewell.
Figure 4.2: Saucer image displayed on a background image. Image by Walter Ridgewell.
So we need to resize the saucer (we can do that in a graphics package like GIMP) and hide the red areas.
4.1.2. Alpha Blending and Its Purpose
In computer graphics, we use a function called alpha blending to make areas transparent or opaque. In addition to RGB (red, green, blue), some graphics come with a fourth value associated with their colours, giving them RGBA, where the A value is an alpha, a bit value used to indicate transparency. If the bitmap you decide to use doesn’t have an alpha channel, Allegro has a routine to specify what colour the developer wishes to define as that alpha value, which will display as transparent or opaque. The process is to define the specified colour to establish a “mask” (the area that will be transparent, letting a background show through), defining it as an “alpha channel.”
The routine to do this is:
void al_convert_mask_to_alpha(ALLEGRO_BITMAP *bitmap, ALLEGRO_COLOR mask_color)
Colours in Allegro 5 are typically represented using the ALLEGRO_COLOR structure, which includes red, green, blue, and alpha (transparency) components. For the routine, one can define a transparent colour in advance:
ALLEGRO_COLOR transparentColor = al_map_rgba(255, 0, 0, 128); // Red color with 50% transparency
Here, you can use the ALLEGRO_COLOR al_map_rgb(unsigned char r, unsigned char g, unsigned char b) routine, which converts r, g, b (ranging from 0 to 255) into an ALLEGRO_COLOR, using 255 for alpha, and then call it like so:
al_convert_mask_to_alpha(sprite, transparentColor); // Red color with 50% transparency
Alternately, one can embed the al_map_rgb routine right in the other routine:
al_convert_mask_to_alpha(sprite, al_map_rgb(255, 0, 0)); // Red (255, 0, 0) becomes fully transparent
We then need to call the blending routine in our code to enable the use of the alpha channel and have transparency:
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
Then Allegro will handle the rest when we draw the sprite.
Transparency is a powerful tool for creating effects like ghostly figures, smoke, shadows, or UI elements that need to overlay the game without completely obscuring the background. Translucent sprites are also useful for visual cues, such as indicating that a character is in stealth mode or partially invisible, or for effects like energy shields and force fields.
Our sprite’s red background, though, isn’t pure red. This becomes clear when one tries to run a test program using the bitmap and setting the transparency value of 255 for al_map_rgb. If one does that, you will still see the red square background. Computers are precise when it comes to numerical values, and so we need to find out what “shade” of red this is. As such it is necessary here to use a graphics program (like the free GIMP graphics package) and then a tool like the colour sampler (usually a small dropper type of icon), which will tell you the RGB values for that point, for example.
From the information provided, we see the RGB values are 237, 28, 36 for the red square area. We then would change those values in the code like so:
al_convert_mask_to_alpha(sprite, al_map_rgb(237, 28, 36))
Figure 4.3: Saucer image open in a graphics editing program. Image by Walter Ridgewell.
Figure 4.4: Saucer image with alpha transparency applied. Image by Walter Ridgewell.
This allows the red areas to become transparent, as seen in figure 4.4.
Let’s look at the program code to do this. Here is a simple Allegro 5 program that displays a spaceship sprite and lets one move it across a background image using the keyboard keys.
4.1.3. Program Example—SpriteMove.c
#include <allegro5/allegro.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_primitives.h>
#include <stdio.h>
int main(int argc, char **argv) {
ALLEGRO_DISPLAY *display = NULL;
ALLEGRO_BITMAP *background = NULL;
ALLEGRO_BITMAP *sprite = NULL;
ALLEGRO_EVENT_QUEUE *event_queue = NULL;
ALLEGRO_TIMER *timer = NULL;
float sprite_x = 496; // Center of the screen (1024/2—32/2)
float sprite_y = 368; // Center of the screen (768/2—32/2)
bool key[4] = { false, false, false, false }; // Array to keep track of key states
bool redraw = true;
if (!al_init()) {
fprintf(stderr, "Failed to initialize Allegro!\n");
return—1;
}
if (!al_install_keyboard()) {
fprintf(stderr, "Failed to initialize the keyboard!\n");
return—1;
}
timer = al_create_timer(1.0 / 60.0);
if (!timer) {
fprintf(stderr, "Failed to create timer!\n");
return—1;
}
display = al_create_display(1024, 768);
if (!display) {
fprintf(stderr, "Failed to create display!\n");
al_destroy_timer(timer);
return—1;
}
if (!al_init_image_addon()) {
fprintf(stderr, "Failed to initialize image addon!\n");
al_destroy_display(display);
al_destroy_timer(timer);
return—1;
}
background = al_load_bitmap("CatCosmos.png");
if (!background) {
fprintf(stderr, "Failed to load background image!\n");
al_destroy_display(display);
al_destroy_timer(timer);
return—1;
}
sprite = al_load_bitmap("SmallSaucer.png");
// Convert a specific color to alpha (e.g., pure red)
al_convert_mask_to_alpha(sprite, al_map_rgb(237, 28, 36)); // Not quite Red (255, 0, 0) becomes fully transparent
if (!sprite) {
fprintf(stderr, "Failed to load sprite image!\n");
al_destroy_bitmap(background);
al_destroy_display(display);
al_destroy_timer(timer);
return—1;
}
event_queue = al_create_event_queue();
if (!event_queue) {
fprintf(stderr, "Failed to create event_queue!\n");
al_destroy_bitmap(sprite);
al_destroy_bitmap(background);
al_destroy_display(display);
al_destroy_timer(timer);
return—1;
}
al_register_event_source(event_queue, al_get_display_event_source(display));
al_register_event_source(event_queue, al_get_timer_event_source(timer));
al_register_event_source(event_queue, al_get_keyboard_event_source());
al_start_timer(timer);
while (1) {
ALLEGRO_EVENT ev;
al_wait_for_event(event_queue, &ev);
if (ev.type == ALLEGRO_EVENT_TIMER) {
if (key[0] && sprite_y >= 4.0) {
sprite_y—= 4.0;
}
if (key[1] && sprite_y <= 768—36.0) {
sprite_y += 4.0;
}
if (key[2] && sprite_x >= 4.0) {
sprite_x—= 4.0;
}
if (key[3] && sprite_x <= 1024—36.0) {
sprite_x += 4.0;
}
redraw = true;
} else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
break;
} else if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {
switch (ev.keyboard.keycode) {
case ALLEGRO_KEY_UP:
key[0] = true;
break;
case ALLEGRO_KEY_DOWN:
key[1] = true;
break;
case ALLEGRO_KEY_LEFT:
key[2] = true;
break;
case ALLEGRO_KEY_RIGHT:
key[3] = true;
break;
}
} else if (ev.type == ALLEGRO_EVENT_KEY_UP) {
switch (ev.keyboard.keycode) {
case ALLEGRO_KEY_UP:
key[0] = false;
break;
case ALLEGRO_KEY_DOWN:
key[1] = false;
break;
case ALLEGRO_KEY_LEFT:
key[2] = false;
break;
case ALLEGRO_KEY_RIGHT:
key[3] = false;
break;
}
}
if (redraw && al_is_event_queue_empty(event_queue)) {
redraw = false;
// Clear screen to white
al_clear_to_color(al_map_rgb(255, 255, 255));
al_draw_bitmap(background, 0, 0, 0);
// Enable blending
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
al_draw_bitmap(sprite, sprite_x, sprite_y, 0);
al_flip_display();
}
}
al_destroy_bitmap(sprite);
al_destroy_bitmap(background);
al_destroy_timer(timer);
al_destroy_display(display);
al_destroy_event_queue(event_queue);
return 0;
}
Now let’s walk through the structure so we understand how it all fits together.
4.1.4. Program Overview
Step 1. Initialization
First, the program starts by including the necessary Allegro libraries for core functionality, images, and drawing primitives. These are the core components for our “game setup”:
#include <allegro5/allegro.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_primitives.h>
#include <stdio.h>
Here, we also define variables like the display, sprite positions, and event handling objects (e.g., event_queue, timer).
Step 2. Preparing the Environment
Before proceeding, we need to ensure everything is set up correctly:
- Initialize Allegro: We check if Allegro and its components (like the keyboard and image add-ons) are loaded successfully. If not, we quit early.
- Create a Display: A window is created with dimensions 1024 × 768, which acts as the canvas for our game:
display = al_create_display(1024, 768);
- Load Assets: The program loads the background and sprite images. If the assets don’t load properly, we clean up and exit to avoid crashes:
background = al_load_bitmap("CatCosmos.png");
sprite = al_load_bitmap("SmallSaucer.png");
- Transparency Trick: By calling al_convert_mask_to_alpha(sprite, al_map_rgb(237, 28, 36)), it makes a specific colour in the sprite (near-red) transparent. This ensures the sprite blends well with the background.
Step 3. The Core Timer and Event Queue
The heart of our game logic is driven by the event queue and a timer. The timer runs at 60 frames per second (1.0 / 60.0), ensuring smooth updates:
timer = al_create_timer(1.0 / 60.0);
Events like key presses, timer ticks, and display closing are registered here. Think of the event queue as a to-do list the program processes continuously.
Step 4. The Game Loop
This is where the action happens! The program runs an infinite while loop to handle events. Here’s the breakdown:
- Check for Events: The al_wait_for_event() routine listens for events like timer updates, key presses, or the display closing.
- Move the Sprite: If the timer fires (ALLEGRO_EVENT_TIMER), the sprite’s position is adjusted based on which keys are pressed (key[0] to key[3] for up, down, left, right). The program ensures the sprite stays within screen boundaries using conditions like:
if (key[0] && sprite_y >= 4.0) {
sprite_y—= 4.0;
}
- Update Key States: When keys are pressed (ALLEGRO_EVENT_KEY_DOWN) or released (ALLEGRO_EVENT_KEY_UP), the corresponding flags in the key[] array are updated. This is like flipping switches for movement.
- Redraw the Screen: If the redraw flag is set and no events are in the queue, the display is updated:
- • The background is drawn first.
- • The sprite is then drawn on top, with transparency applied.
al_draw_bitmap(sprite, sprite_x, sprite_y, 0);
Step 5. Cleanup
After the user closes the window or exits, the program cleans up by destroying all created resources (e.g., the sprite, background, display, timer, and event queue).
Key Highlights
The game logic ties sprite movement directly to key states, ensuring smooth, responsive controls.
The al_set_blender() routine enables blending, making the sprite’s transparency work correctly.
Safety checks are sprinkled throughout to handle failures gracefully (e.g., missing images).
In short, this program combines Allegro’s features to create a basic game where a spaceship moves over a background. It’s modular, with clear steps for initializing, handling events, updating positions, and rendering graphics. It’s like building a foundation for a game you can expand on later—perhaps by adding collisions, sound, or even enemies!
4.2. Additional Sprite Handling Routines
We will now expand on the previous code to demonstrate the different sprite manipulation techniques and discuss why you might want to use these routines in a game. Additionally, we’ll explain the importance of using al_get_bitmap_width and al_get_bitmap_height routines in some of the routines. We’ve seen how to draw a sprite; here in this section we’ll explore additional methods we can use to manipulate those sprites—scaling, flipping, rotating, pivoting, and applying translucency.
We’ll use the following base code snippet from our previous example program as our starting point:
if (redraw && al_is_event_queue_empty(event_queue)) {
redraw = false;
// Clear screen to white
al_clear_to_color(al_map_rgb(255, 255, 255));
al_draw_bitmap(background, 0, 0, 0);
// Enable blending
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
al_draw_bitmap(sprite, sprite_x, sprite_y, 0);
al_flip_display();
}
}
To begin, we’ll be looking at what changes to this code segment line can be implemented from our previous example, which draws a sprite at a specified position (sprite_x and sprite_y) on the screen via the following:
al_draw_bitmap(sprite, sprite_x, sprite_y, 0);
4.2.1. Drawing Scaled Sprites
Scaling a sprite involves resizing it by specific factors along the x and y axes. Scaling is useful when you want to create different-sized enemies or objects without needing multiple versions of the same sprite. For instance, you might have a boss enemy that’s a larger version of a regular enemy sprite. Scaling can also be used for visual effects, like zooming in on a specific game element or making an object appear closer or farther away. If your game was a target shooting game, you could start with a small-scale sprite making the target appear farther away and then gradually increase the scale, creating the appearance of the object getting closer.
To scale a sprite in Allegro 5, you use the al_draw_scaled_bitmap routine. This routine allows you to stretch or shrink the sprite’s width and height to fit your desired dimensions.
al_draw_scaled_bitmap
void al_draw_scaled_bitmap(ALLEGRO_BITMAP *bitmap,float sx, float sy, float sw, float sh, float dx, float dy, float dw, float dh, int flags)
This draws a scaled version of the given bitmap to the target bitmap.
- sx: source x
- sy: source y
- sw: source width
- sh: source height
- dx: destination x
- dy: destination y
- dw: destination width
- dh: destination height
- flags: same as for al_draw_bitmap
In the following code line, the sprite is scaled to twice its original size in both dimensions, as indicated by these parameters in the routine call referencing the destination width and height (dw and dh, respectively):
(al_get_bitmap_width(sprite) * 2, al_get_bitmap_height(sprite) * 2).
So in our previous example, we replace:
al_draw_bitmap(sprite, sprite_x, sprite_y, 0);
with:
al_draw_scaled_bitmap(sprite, 0, 0, al_get_bitmap_width(sprite), al_get_bitmap_height(sprite), sprite_x, sprite_y, al_get_bitmap_width(sprite) * 2, al_get_bitmap_height(sprite) * 2, 0);
You may also notice that we are utilizing two other Allegro routines here, al_get_bitmap_width() and al_get_bitmap_height(), in place of simply source width and source height—our previous sh and sw values, respectively.
Why do we want to use al_get_bitmap_width and al_get_bitmap_height? In some of these bitmap manipulation routines, we need to specify information related to the dimensions of the sprite in use. The routines al_get_bitmap_width(sprite) and al_get_bitmap_height(sprite) retrieve the original width and height of the sprite, respectively. These values are crucial because they ensure that you are scaling the sprite based on its actual dimensions. Without these routines, you would need to manually track the sprite’s size, which can lead to errors and inconsistencies, especially if the sprite’s dimensions change during development or if you are working with multiple sprites of varying sizes. As such, we simplify the processes here by getting that information from the sprite being used at that moment.
So going back to our code snippet, it would look like this for a scaled bitmap:
if (redraw && al_is_event_queue_empty(event_queue)) {
redraw = false;
// Clear screen to white
al_clear_to_color(al_map_rgb(255, 255, 255));
al_draw_bitmap(background, 0, 0, 0);
// Enable blending
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
al_draw_scaled_bitmap(sprite, 0, 0, al_get_bitmap_width(sprite), al_get_bitmap_height(sprite), sprite_x, sprite_y, al_get_bitmap_width(sprite) * 2, al_get_bitmap_height(sprite) * 2, 0);
al_flip_display();
}
}
4.2.2. Drawing a Flipped Sprite
Flipping a sprite involves mirroring it across the x- or y-axis. Allegro 5 allows you to flip a sprite horizontally, vertically, or both by using the al_draw_bitmap routine with specific flags. Flipping is particularly useful for character sprites that need to face different directions. For example, a character sprite facing right can be flipped to face left without needing a separate sprite. This is efficient for handling movement in platformers or side-scrolling games. Similarly, flipping can be used to mirror environmental elements or objects (think a reflection in a mirror or water here), adding enhancements to the game world environment without extra resources.
Here, we are utilizing the flags available for the al_draw_bitmap routine, namely:
- ALLEGRO_FLIP_HORIZONTAL: Flip the bitmap about the y-axis
- ALLEGRO_FLIP_VERTICAL: Flip the bitmap about the x-axis
al_draw_bitmap(sprite, sprite_x, sprite_y, ALLEGRO_FLIP_HORIZONTAL);
That example flips the sprite horizontally. You can also flip it vertically using ALLEGRO_FLIP_VERTICAL. You can also flip both horizontally and vertically in one function call by combining the flags (ALLEGRO_FLIP_HORIZONTAL | ALLEGRO_FLIP_VERTICAL).
4.2.3. Drawing Rotated Sprites
To rotate a sprite, Allegro 5 provides the al_draw_rotated_bitmap routine. This routine allows you to specify an angle of rotation in radians and the center point around which the sprite will rotate. Rotation is essential for many gameplay mechanics, such as aiming weapons, rotating objects, or handling vehicles like cars or spaceships. For example, in a top-down shooter, the player’s ship might rotate to aim in the direction of the mouse or joystick. Rotation can also be used for environmental elements like rotating platforms or spinning objects:
al_draw_rotated_bitmap
void al_draw_rotated_bitmap(ALLEGRO_BITMAP *bitmap,
float cx, float cy, float dx, float dy, float angle, int flags)
This draws a rotated version of the given bitmap to the target bitmap. The bitmap is rotated clockwise by the specified angle, measured in radians. The point at cx/cy relative to the upper left corner of the bitmap will be drawn at dx/dy, and the bitmap is rotated around this point. If cx,cy is 0,0, the bitmap will rotate around its upper left corner:
- cx: center x (relative to the bitmap)
- cy: center y (relative to the bitmap)
- dx: destination x
- dy: destination y
- angle: angle by which to rotate (radians)
- flags: same as for al_draw_bitmap
Once again, we look to substitute our code line:
al_draw_bitmap(sprite, sprite_x, sprite_y, 0);
with the new function, and so we might use something like the following:
al_draw_rotated_bitmap(sprite, al_get_bitmap_width(sprite) / 2, al_get_bitmap_height(sprite) / 2,sprite_x, sprite_y, ALLEGRO_PI / 4, 0);
In this example, the sprite drawn on-screen is now rotated 45 degrees (π / 4 radians) around its center.
Here once more we call some other Allegro routines to get required information, making our coding a bit easier. We are using the al_get_bitmap_width(sprite) / 2 and al_get_bitmap_height(sprite) / 2 routines to make sure we find the center of rotation. These are the center x and center y (cx and cy values). This ensures that the sprite rotates around its center point, which is typically the desired behavior for most game objects. Without these routines, you would need to manually calculate the center based on known dimensions, which is prone to errors and less flexible if the sprite size changes.
4.2.4. Drawing Pivoted Sprites
Pivoting a sprite involves rotating it around a specified pivot point other than its center. Here we still use the al_draw_rotated_bitmap routine. We noted in an earlier chapter the differences between “rotate” and “pivot” as a function of how we define the rotational axis point in our sprite. Pivoted rotation is useful when you want to rotate objects around a specific point, such as a door rotating around its hinge or a windmill’s blades rotating around their base. It adds realism to animations and interactions, making game mechanics more intuitive and visually appealing:
al_draw_rotated_bitmap(sprite, 0, 0, // Pivot point at the top-left corner of the sprite
sprite_x, sprite_y, ALLEGRO_PI / 4, 0);
Here, the sprite rotates 45 degrees around its top-left corner (0, 0).
In other cases, you might use the previously studied get width and height routines to help you identify different pivot points. Determining the dimensions of the sprite provides information that allows you to calculate relative positions within the sprite.
For example, you might want to pivot around the sprite’s bottom-right corner by implementing (al_get_bitmap_width(sprite), al_get_bitmap_height(sprite)) in the previous code:
al_draw_rotated_bitmap(sprite, (al_get_bitmap_width(sprite), al_get_bitmap_height(sprite)),
sprite_x, sprite_y, ALLEGRO_PI / 4, 0);
Any of the three previously discussed routines can be substituted in the code example in place of the original al_draw_bitmap(sprite, sprite_x, sprite_y, 0) routine:
if (redraw && al_is_event_queue_empty(event_queue)) {
redraw = false;
// Clear screen to white
al_clear_to_color(al_map_rgb(255, 255, 255));
al_draw_bitmap(background, 0, 0, 0);
// Enable blending
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
al_draw_rotated_bitmap(sprite,(al_get_bitmap_width(sprite),al_get_bitmap_height(sprite)), sprite_x, sprite_y, ALLEGRO_PI / 4, 0);
al_flip_display();
}
}
4.2.5. Translucent Sprites
We previously covered the concept of transparency in the discussion of the alpha channel and hiding sprite backgrounds with transparency; now let’s look at if we want to modify the sprite itself.
Transparency enables effects such as ghosts, smoke, shadows, and UI overlays and is commonly used for visual cues like stealth, partial invisibility, energy shields, and force fields. To draw a translucent sprite, you can modify its alpha (transparency) value before drawing it. This can be done through the routine al_draw_tinted_bitmap:
void al_draw_tinted_bitmap(ALLEGRO_BITMAP *bitmap, ALLEGRO_COLOR tint,
float dx, float dy, int flags)
Here, the value for ALLEGRO_COLOR tint will be a call to the al_map_rgba_f routine, which will allow us to vary the intensity of the red, green, and blue values, as illustrated in the Allegro documents:
al_draw_tinted_bitmap(bitmap, al_map_rgba_f(0.5, 0.5, 0.5, 0.5), x, y, 0);
This will draw the bitmap at 50% transparency (the RGB values are premultiplied with the alpha component).
This alpha represents a value where 1 is solid and 0 is transparent. Values between these two numbers then represent the opaqueness or transparency level, so .5 is the 50%.
The concept of a premultiplied alpha is the RGB colour values with the alpha value applied (multiplied by) so A * R, A * G, and A * B, where A would be the same value, so for a half transparent value of 50%, we would have .5 * R, .5 * G and .5 * B or just the 0.5 values seen in the code previously.
Once more using our code snippet as the example, we will replace the basic draw bitmap line with this new routine, resulting in a semitransparent bitmap image:
if (redraw && al_is_event_queue_empty(event_queue)) {
redraw = false;
// Clear screen to white
al_clear_to_color(al_map_rgb(255, 255, 255));
al_draw_bitmap(background, 0, 0, 0);
// Enable blending
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
al_draw_tinted_bitmap(bitmap, al_map_rgba_f(0.5, 0.5, 0.5, 0.5), sprite_x, sprite_y, 0);
al_flip_display();
}
}
4.3. Allegro Blending Function and Sprites
Another routine line we haven’t talked about much but that is very important for our earlier example code is why we set the blending mode before utilizing other routines implementing an alpha level:
// Enable blending
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
al_draw_tinted_bitmap(sprite, al_map_rgba_f(0.5, 0.5, 0.5, 0.5), sprite_x, sprite_y, 0);
Here, the context of a blending function is the process of combining the new colours we are utilizing on the screen with preexisting colours. In some circumstances, we need to blend the colour of the bitmap object we are adding to an existing bitmap (so a sprite onto a background image) to have it look good or to affect some other graphic process.
Previously we took the red background on our saucer image, defined it as a mask area, and made that portion of the graphic transparent by assigning it to the alpha channel. We then called the blending routine and assigned it parameters. When the two images were combined, the saucer was visible on top of the background image and the transparency of the alpha channel portion let the background show through.
Perhaps the best explanation of the processes involved here is in the Allegro 5 documentation on al_set_blender:
void al_set_blender(int op, int src, int dst)
which sets the function to use for blending for the current thread.
Blending means the source and destination colours are combined in drawing operations.
Assume the source colour (e.g., colour of a rectangle to draw, or pixel of a bitmap to draw) is given as its red/green/blue/alpha components (if the bitmap has no alpha, it always is assumed to be fully opaque, so 255 for 8-bit or 1.0 for floating point): s = s.r, s.g, s.b, s.a. And this colour is drawn to a destination, which already has a colour: d = d.r, d.g, d.b, d.a.
The conceptual formula used by Allegro to draw any pixel then depends on the parameters:
ALLEGRO_ADD
r = d.r * df.r + s.r * sf.r
g = d.g * df.g + s.g * sf.g
b = d.b * df.b + s.b * sf.b
a = d.a * df.a + s.a * sf.a
ALLEGRO_DEST_MINUS_SRC
r = d.r * df.r - s.r * sf.r
g = d.g * df.g - s.g * sf.g
b = d.b * df.b - s.b * sf.b
a = d.a * df.a - s.a * sf.a
ALLEGRO_SRC_MINUS_DEST
r = s.r * sf.r - d.r * df.r
g = s.g * sf.g - d.g * df.g
b = s.b * sf.b - d.b * df.b
a = s.a * sf.a—d.a * df.a
Valid values for the factors sf and df passed to this function are as follows, where s is the source colour, d the destination colour, and cc the colour set with al_set_blend_color (white by default):
ALLEGRO_ZERO
f = 0, 0, 0, 0
ALLEGRO_ONE
f = 1, 1, 1, 1
ALLEGRO_ALPHA
f = s.a, s.a, s.a, s.a
ALLEGRO_INVERSE_ALPHA
f = 1 - s.a, 1 - s.a, 1 - s.a, 1 - s.a
ALLEGRO_SRC_COLOR (since: 5.0.10, 5.1.0)
f = s.r, s.g, s.b, s.a
ALLEGRO_DEST_COLOR (since: 5.0.10, 5.1.8)
f = d.r, d.g, d.b, d.a
ALLEGRO_INVERSE_SRC_COLOR (since: 5.0.10, 5.1.0)
f = 1 - s.r, 1 - s.g, 1 - s.b, 1 - s.a
ALLEGRO_INVERSE_DEST_COLOR (since: 5.0.10, 5.1.8)
f = 1 - d.r, 1 - d.g, 1 - d.b, 1 - d.a
ALLEGRO_CONST_COLOR (since: 5.1.12, not supported on OpenGLES 1.0)
f = cc.r, cc.g, cc.b, cc.a
ALLEGRO_INVERSE_CONST_COLOR (since: 5.1.12, not supported on OpenGLES 1.0)
f = 1 - cc.r, 1 - cc.g, 1 - cc.b, 1 - ccc.a
Blending Examples
So for example, to restore the default of using premultiplied alpha blending, you would use:
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
As formula, the process is
r = d.r * (1 − s.a) + s.r * 1
g = d.g * (1 − s.a) + s.g * 1
b = d.b * (1 − s.a) + s.b * 1
a = d.a * (1 − s.a) + s.a * 1
I’m not going to continue illustrating the formulaic processes for the following routines for the sake of brevity.
If you are using non-pre-multiplied alpha, you could use this:
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
Additive blending would be achieved with the following:
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ONE);
Copying the source to the destination (including alpha) unmodified:
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ZERO);
Multiplying source and destination components:
al_set_blender(ALLEGRO_ADD, ALLEGRO_DEST_COLOR, ALLEGRO_ZERO)
Tinting the source (like al_draw_tinted_bitmap):
al_set_blender(ALLEGRO_ADD, ALLEGRO_CONST_COLOR, ALLEGRO_ONE);
al_set_blend_color(al_map_rgb(0, 96, 255)); /* nice Chrysler blue */
Averaging source and destination pixels:
al_set_blender(ALLEGRO_ADD, ALLEGRO_CONST_COLOR, ALLEGRO_CONST_COLOR);
al_set_blend_color(al_map_rgba_f(0.5, 0.5, 0.5, 0.5));
4.3.1. Program Example—SpriteMoveEffects.c
In the following example, we’ve taken our previous demonstration program and added the following effects:
- 1. Scaled the saucer size down by 50%
- 2. Flipped the saucer image horizontally
- 3. Rotated it 45 degrees clockwise
- 4. Made the transparency a variable function that oscillates
This numbered list of effects will be referenced in the comments of the code that follows:
#include <allegro5/allegro.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_primitives.h>
#include <stdio.h>
int main(int argc, char** argv) {
ALLEGRO_DISPLAY* display = NULL;
ALLEGRO_BITMAP* background = NULL;
ALLEGRO_BITMAP* sprite = NULL;
ALLEGRO_EVENT_QUEUE* event_queue = NULL;
ALLEGRO_TIMER* timer = NULL;
// Same sprite coordinates for top-left of the sprite
float sprite_x = 496;
float sprite_y = 368;
bool key[4] = { false, false, false, false };
bool redraw = true;
// (4) Variables for controlling translucent "fade in/out" of the sprite
float alpha = 1.0f; // current alpha
bool alpha_increasing = false; // direction of alpha change
if (!al_init()) {
fprintf(stderr, "Failed to initialize Allegro!\n");
return—1;
}
if (!al_install_keyboard()) {
fprintf(stderr, "Failed to initialize the keyboard!\n");
return—1;
}
timer = al_create_timer(1.0 / 60.0);
if (!timer) {
fprintf(stderr, "Failed to create timer!\n");
return—1;
}
display = al_create_display(1024, 768);
if (!display) {
fprintf(stderr, "Failed to create display!\n");
al_destroy_timer(timer);
return—1;
}
if (!al_init_image_addon()) {
fprintf(stderr, "Failed to initialize image addon!\n");
al_destroy_display(display);
al_destroy_timer(timer);
return—1;
}
background = al_load_bitmap("CatCosmos.png");
if (!background) {
fprintf(stderr, "Failed to load background image!\n");
al_destroy_display(display);
al_destroy_timer(timer);
return—1;
}
sprite = al_load_bitmap("SmallSaucer.png");
al_convert_mask_to_alpha(sprite, al_map_rgb(237, 28, 36)); // Make a specific color transparent
if (!sprite) {
fprintf(stderr, "Failed to load sprite image!\n");
al_destroy_bitmap(background);
al_destroy_display(display);
al_destroy_timer(timer);
return—1;
}
event_queue = al_create_event_queue();
if (!event_queue) {
fprintf(stderr, "Failed to create event_queue!\n");
al_destroy_bitmap(sprite);
al_destroy_bitmap(background);
al_destroy_display(display);
al_destroy_timer(timer);
return—1;
}
al_register_event_source(event_queue, al_get_display_event_source(display));
al_register_event_source(event_queue, al_get_timer_event_source(timer));
al_register_event_source(event_queue, al_get_keyboard_event_source());
al_start_timer(timer);
// Get sprite dimensions for proper pivoting in rotation
float sprite_w = (float)al_get_bitmap_width(sprite);
float sprite_h = (float)al_get_bitmap_height(sprite);
while (1) {
ALLEGRO_EVENT ev;
al_wait_for_event(event_queue, &ev);
if (ev.type == ALLEGRO_EVENT_TIMER) {
// Movement logic remains the same
if (key[0] && sprite_y >= 4.0) {
sprite_y—= 4.0;
}
if (key[1] && sprite_y <= 768—36.0) {
sprite_y += 4.0;
}
if (key[2] && sprite_x >= 4.0) {
sprite_x—= 4.0;
}
if (key[3] && sprite_x <= 1024—36.0) {
sprite_x += 4.0;
}
// (4) Update alpha for a continuous fade in/out effect
if (alpha_increasing) {
alpha += 0.01f;
if (alpha >= 1.0f) {
alpha = 1.0f;
alpha_increasing = false;
}
}
else {
alpha—= 0.01f;
if (alpha <= 0.0f) {
alpha = 0.0f;
alpha_increasing = true;
}
}
redraw = true;
}
else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
break;
}
else if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {
switch (ev.keyboard.keycode) {
case ALLEGRO_KEY_UP:
key[0] = true;
break;
case ALLEGRO_KEY_DOWN:
key[1] = true;
break;
case ALLEGRO_KEY_LEFT:
key[2] = true;
break;
case ALLEGRO_KEY_RIGHT:
key[3] = true;
break;
}
}
else if (ev.type == ALLEGRO_EVENT_KEY_UP) {
switch (ev.keyboard.keycode) {
case ALLEGRO_KEY_UP:
key[0] = false;
break;
case ALLEGRO_KEY_DOWN:
key[1] = false;
break;
case ALLEGRO_KEY_LEFT:
key[2] = false;
break;
case ALLEGRO_KEY_RIGHT:
key[3] = false;
break;
}
}
if (redraw && al_is_event_queue_empty(event_queue)) {
redraw = false;
// Clear screen to white
al_clear_to_color(al_map_rgb(255, 255, 255));
al_draw_bitmap(background, 0, 0, 0);
// Enable blending
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
// (1) Scale by 50%
// (2) Flip horizontally (ALLEGRO_FLIP_HORIZONTAL)
// (3) Rotate 45 degrees clockwise
// (4) Tint with variable alpha for translucency
al_draw_tinted_scaled_rotated_bitmap(
sprite,
al_map_rgba_f(1.0f, 1.0f, 1.0f, alpha), // Tint color with current alpha
sprite_w / 2, // pivot x (center of sprite)
sprite_h / 2, // pivot y (center of sprite)
sprite_x + sprite_w / 2, // draw x so sprite center ends up at (sprite_x, sprite_y)
sprite_y + sprite_h / 2, // draw y
0.5f, 0.5f, // (1) scale by 50% 0.5f horizontally & vertically
ALLEGRO_PI / 4, // (3) 45 degrees in radians (clockwise)
ALLEGRO_FLIP_HORIZONTAL // (2) flip horizontally
);
al_flip_display();
}
}
al_destroy_bitmap(sprite);
al_destroy_bitmap(background);
al_destroy_timer(timer);
al_destroy_display(display);
al_destroy_event_queue(event_queue);
return 0;
}
A screencap of our semitransparent 45 degree rotated and flipped saucer is shown in figure 4.5.
As you can see, Allegro 5 offers a good selection of functions to modify the colour of screen graphics using various blending functions. These routines can be utilized in a variety of ways to create a more dynamic screen presentation of objects (changing fire intensity, glowing particles, or sparkles), new stylized looks in a scene (changing the palette to pastel shades or using watercolour effects), or immediate visual feedback (a player character glowing after drinking a potion or an indication its been hit by a nonplayer character [NPC] attack).
4.4. Advanced Sprite Use in Allegro 5
4.4.1. Using Dynamic Sprites
In the last chapter, we focused on static sprites, which means we worked with a single, unchanging image for each sprite. While we adjusted things like size, orientation, and position, the actual image—what the sprite looked like—stayed the same. We learned how to move the sprite around the screen or change how it appeared, but the sprite’s visual state itself didn’t evolve.
Figure 4.5: Saucer image as semitransparent, rotated, and flipped. Image by Walter Ridgewell.
Now, when we talk about more advanced graphics, we introduce the idea of making the sprite’s image change as well. This is where dynamic sprites come into play. Instead of using a single image, we can swap between multiple images to give the impression that the sprite is moving or performing an action. This is what we call animation.
Animation is all about cycling through different images, or frames, to create the illusion of movement. For example, if you have a character walking, instead of just sliding the same image across the screen, we show different frames where the character’s legs are in different positions. By displaying these frames in quick succession, it looks like the character is actually walking.
We can combine this idea of changing the sprite image with the other transformations we already learned—like adjusting its size or position—to create a fully animated and interactive graphic. In the next steps, we’ll explore how to manage these image sequences and control the speed at which they change. This will allow us to create smooth animations, like making a character run, jump, or even perform more complex actions.
So what does animating a sprite mean? It means swapping between a series of images (or frames) over time to simulate movement. And how do we achieve that? We’ll break it down step-by-step, starting with loading multiple frames for a sprite and figuring out how to time their display to create a seamless animation.
4.4.2. Animated Sprites—Drawing an Animated Sprite
The process of how animation occurs in computer systems is the same as it is in video or film or in retrospect static sprite movement. We have a single frame of image information; we display it, then we make some change to that information and display it, repeating the process as needed. For our static sprites, we displayed the same image repeatedly, for animated sprites we want then to display a changed image or better yet multiple changed images.
We will then have multiple sprite images (variations of a base/initial image) that we will play in sequence, giving the static sprite a dynamic, changing appearance. There are many familiar animation scenes in video games that utilize this effect: the player’s character walking, jumping, or running as opposed to standing still; character combat or weapons firing; and vehicles moving or items being destroyed (e.g., in explosions). All these functions can be accomplished with animated sprites.
The basic routine goes like this: You start with an initial sprite image or a frame, borrowing from video/film terminology. Our character begins standing, then in the next image, he lifts a leg somewhat. In the following image, he extends it forward, then he lowers the leg, and finally the character moves up to that location. So in four images, you have a character take a step forward. Since computers like doing things in power of 2, the typical sequence of frames with some intermediate images will be some multiple, such as 4, 8, or 16. We can refer to this collection of images as a “sprite sheet.”
In the context of a classical 8-bit sprite being displayed, we could look at it as 4 different sprite positions displayed while moving the character around the screen, as in figure 4.6, for example.
Figure 4.6: Sprite sheet for character animation. Illustrated by Jana Ochse for 2DPIXX, published under a CC BY-SA 3.0 license and modified for size by Kyle Flemmer.
Now, we have a few choices in terms of the coding to do this. We start with 8 images that need to be displayed in sequence, so an easy process here would be loading and storing our bitmaps into an array, then calling up the indexed image as required. This could get complicated, though, depending on how many bitmaps we have related to the animation complexity and the number of game elements. It would be better to be able to somehow minimize the number of individual images required while keeping them associated with a character or object but still allowing us the animation ability that the multiple modified bitmaps provide. So what if we can combine those 8 images into a single larger image, a sprite sheet. Then the task coding-wise is to select a portion of that single bitmap representing one of the images in the sequence of 8. If one does this, then we only load the sprite sheet image once (as opposed to loading 8 images), and we use the array function to designate a location within the sprite sheet, like the top left and bottom right coordinates of the individual image we want. By making our sizes convenient, like 32 × 32, the math for selection can be very easy to implement.
In the previous example, we illustrated a forward walking motion; however, typically video game characters move in multiple directions. In this respect, your sprite sheet will need to contain multiple rows of sprites, with each row being utilized for a specific direction. Your images, then, might be arranged as four rows of image sequences—up, left, right, and down. If we use 4 images as the minimal number of changes (although there is nothing stopping you from using only 3 or 2 if you really want a minimalist graphics set or for learning), then that gives us a matrix of 16 images arranged in a 4 × 4 layout. Again, if you keep the individual images’ sizes convenient multiples, the math is logical in its implementation. Recall the array from table 4 in the previous chapter, shown again here as table 9, and assume that each sprite image or cell is 32 × 32.
The x- and y-coordinates for the first row of images in terms of top-left corner and bottom right would be [(0,0), (32,32)]; the second cell, then, is [(32,0), (64,32)]; the third cell [(64,0), (96,32)]; and finally [(96,0), (128,32)]. For the second row, then, the first cell is [(0,32), (32,64)], the second cell [(32,32), (64,64)], and so on—easy multiples of 32 to work with (see table 10).
X→ | ||||
|---|---|---|---|---|
Y ↓ | 0,0 | 0,1 | 0,2 | 0,3 |
1,0 | 1,1 | 1,2 | 1,3 | |
2,0 | 2,1 | 2,2 | 2,3 | |
3,0 | 3,1 | 3,2 | 3,3 | |
4.4.3. Grabbing Sprite Frames from an Image—the Sprite Sheet
Sprite sheets are a combination of animation frames in a single larger bitmap; we then can load that single bitmap image and ideally select animation frame/areas from that. The process then becomes one of loading a main bitmap, the sprite sheet selecting fixed areas of that (the animation frames), and copying those into the arrays or matrixes for use. Allegro provides us with a function to copy areas of a larger bitmap image al_draw_bitmap_region, and through some clever foreplanning in terms of sizes of the images we create, the math to specify which images to select can become quite easy. If, for example, we have sprite images that are 32 × 32 and we have 4 frames, we combine them to create a main character bitmap that is 128 bits high and 128 bits wide (4 × 32 = 128).
The Allegro 5 function al_draw_bitmap_region is used to draw a specific rectangular region of a bitmap (image) onto the screen. This function is particularly useful when working with sprite sheets, where a single image contains multiple subimages (frames) that represent different parts of an animation or different states of a character:
void al_draw_bitmap_region(ALLEGRO_BITMAP *bitmap, float sx, float sy,float sw, float sh, float dx, float dy,int flags);
Parameters
- bitmap: The source image (sprite sheet) from which the region will be drawn
- sx (source x): The x-coordinate (top-left corner) of the region in the source bitmap
- sy (source y): The y-coordinate (top-left corner) of the region in the source bitmap
(0,0), (32,32) | (32,0), (64,32) | (64,0), (96,32) | (96,0), (128,32) |
(0,32), (32,64) | (32,32), (64,64) | (64,32), (96,64) | (96,32), (128,64) |
(0,64), (32,96) | (32,64), (64,96) | (64,64), (96,96) | (96,64), (128,96) |
(0,96), (32,128) | (32,96), (64,128) | (64,96), (96,128) | (96,96), (128,128) |
- sw (source width): The width of the region to draw
- sh (source height): The height of the region to draw
- dx (destination x): The x-coordinate on the screen where the top-left corner of the region will be drawn
- dy (destination y): The y-coordinate on the screen where the top-left corner of the region will be drawn
- flags: Determines how the bitmap is drawn. Common flags include:
- 0: Normal drawing (no transformations)
- ALLEGRO_FLIP_HORIZONTAL: Flips the image horizontally
- ALLEGRO_FLIP_VERTICAL: Flips the image vertically
Let’s go back to our example, the sprite sheet that contains 16 frames in a 4 × 4 grid, where each frame is 32 × 32 pixels. Let’s say you want to draw the third frame in the first row, the one labelled 0,2 in the diagram (frame index 2 in zero-based counting):
// Parameters for al_draw_bitmap_region
ALLEGRO_BITMAP *sprite_sheet = al_load_bitmap("sprite_sheet.png");
int frame = 2; // Third frame (zero-based)
int row = 0; // First row (zero-based)
int cell_size = 32; // Size of each frame (width and height)
float sx = frame * cell_size; // Source X
float sy = row * cell_size; // Source Y
float sw = cell_size; // Source Width
float sh = cell_size; // Source Height
float dx = 100; // Destination X
float dy = 100; // Destination Y
// Draw the selected frame from the sprite sheet
al_draw_bitmap_region(sprite_sheet, sx, sy, sw, sh, dx, dy, 0);
Here, you see we’re calculating the sx and sy values based on the frame index and cell size, allowing us to specify that image. In the case of an animation sequence, we can then increment or decrement values to achieve that effect. If we want to go through the animation sequence in the first row, we increment the frame value from 0 to 3; if we want to use a different sequence, we change the row value.
In the simple demonstration program to follow, we’ll use 4 movement animations to go with 4 directions from the keyboard. Pressing the Up, Down, Left, and Right keys changes the character’s direction and updates its position, so we are selecting different sprite images and moving the sprite as well. This means we need 4 animation sets, and it will then fit nicely with our 4 × 4 sprite sheet.
The 4 × 4 sprite sheet layout assumes four rows for movement directions:
- Row 0: Down
- Row 1: Up
- Row 2: Left
- Row 3: Right
For the movement aspect, we then cycle through four frames (columns) for each direction. For our sprite sheet, we’ll use an asset from OpenGameArt.org. One of the things a game designer will need if they aren’t going to create their own graphic and sound assets are open-source graphics and sounds. Many sites can be found that provide these under the auspices of Creative Commons licensing agreements. In many cases, all the artists request is proper acknowledgement of their work as per the applicable Creative Commons licenses, and as a video game designer, you should always seek to attribute and acknowledge the work of others that you have used.
For this example, I’m using the sprite sheet RPG Hero (Walking Cycle) illustrated by Jana Ochse for 2DPIXX and published under a CC BY-SA 3.0 license (see figure 4.6). Many thanks to Jana for providing artwork for those of us lacking the graphical skill to create such necessary and integral assets in our quest to create new games for others to enjoy.
4.4.4. Complete Coding Example
// OneSpriteMove.c
#include <allegro5/allegro.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_primitives.h>
#include <iostream>
// Constants
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SPRITE_WIDTH = 32; // Width of each sprite cell
const int SPRITE_HEIGHT = 64; // Height of each sprite cell
const int FPS = 30;
const int NUM_FRAMES = 4;
// Directions
enum Direction { UP = 1, LEFT = 3, RIGHT = 2, DOWN = 0 }; // Values match sprite sheet layout
int main() {
// Initialize Allegro
if (!al_init()) {
std::cerr << "Failed to initialize Allegro." << std::endl;
return—1;
}
if (!al_init_image_addon()) {
std::cerr << "Failed to initialize Allegro Image Addon." << std::endl;
return—1;
}
if (!al_install_keyboard()) {
std::cerr << "Failed to initialize keyboard input." << std::endl;
return—1;
}
// Create display
ALLEGRO_DISPLAY* display = al_create_display(SCREEN_WIDTH, SCREEN_HEIGHT);
if (!display) {
std::cerr << "Failed to create display." << std::endl;
return—1;
}
// Set up timer and event queue
ALLEGRO_TIMER* timer = al_create_timer(1.0 / FPS);
ALLEGRO_EVENT_QUEUE* event_queue = al_create_event_queue();
al_register_event_source(event_queue, al_get_display_event_source(display));
al_register_event_source(event_queue, al_get_timer_event_source(timer));
al_register_event_source(event_queue, al_get_keyboard_event_source());
// Load sprite sheet
ALLEGRO_BITMAP* sprite_sheet = al_load_bitmap("rpg-character.png");
// Convert a specific color to alpha (As noted one can use graphics program like GIMP and the color selector to find values)
al_convert_mask_to_alpha(sprite_sheet, al_map_rgb(192, 0, 128)); // Here the pinkish background has values of (R:192 G:0, B:128) classic magic pink values
if (!sprite_sheet) {
std::cerr << "Failed to load sprite sheet." << std::endl;
al_destroy_display(display);
return—1;
}
// Variables for animation
int sprite_x = SCREEN_WIDTH / 2—SPRITE_WIDTH / 2;
int sprite_y = SCREEN_HEIGHT / 2—SPRITE_HEIGHT / 2;
int frame = 0;
Direction current_direction = DOWN;
bool redraw = true;
bool running = true;
al_start_timer(timer);
while (running) {
ALLEGRO_EVENT ev;
al_wait_for_event(event_queue, &ev);
if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
running = false;
}
if (ev.type == ALLEGRO_EVENT_TIMER) {
redraw = true;
// Update animation frame
//frame = (frame + 1) % NUM_FRAMES;
}
if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {
frame = (frame + 1) % NUM_FRAMES;
switch (ev.keyboard.keycode) {
case ALLEGRO_KEY_UP:
current_direction = UP;
sprite_y—= SPRITE_HEIGHT;
break;
case ALLEGRO_KEY_DOWN:
current_direction = DOWN;
sprite_y += SPRITE_HEIGHT;
break;
case ALLEGRO_KEY_LEFT:
current_direction = LEFT;
sprite_x—= SPRITE_WIDTH;
break;
case ALLEGRO_KEY_RIGHT:
current_direction = RIGHT;
sprite_x += SPRITE_WIDTH;
break;
case ALLEGRO_KEY_ESCAPE:
running = false;
break;
}
}
if (redraw && al_is_event_queue_empty(event_queue)) {
redraw = false;
// Clear screen
al_clear_to_color(al_map_rgb(0, 0, 0));
// Calculate source rectangle for current frame and direction
int sx = frame * SPRITE_WIDTH;
int sy = current_direction * SPRITE_HEIGHT;
// Draw sprite
al_draw_bitmap_region(sprite_sheet, sx, sy, SPRITE_WIDTH, SPRITE_HEIGHT, sprite_x, sprite_y, 0);
// Flip display
al_flip_display();
}
}
// Cleanup
al_destroy_bitmap(sprite_sheet);
al_destroy_timer(timer);
al_destroy_event_queue(event_queue);
al_destroy_display(display);
return 0;
}
Our result here is the character sprite on-screen changing orientation with respect to the direction it’s moving in and a small animation sequence showing its steps (see figure 4.7). All the graphics required to do this are in the single loaded sprite sheet.
4.4.5. Animated Sprites in Allegro 5
Animated sprites in games have both pros and cons.
Pros
- 1. Greater Control
Using sprites gives you more control over every aspect of the animation, including frame rate, sequence, and transitions. You can dynamically change animations in response to user input or game events.
Figure 4.7: Changing character orientation on-screen. Image by Walter Ridgewell.
- 2. Better Quality
Since you’re not limited to the GIF format’s colour palette, you can use full-colour images with higher quality.
- 3. Performance Optimization
Sprites can be more efficient, especially when combined with sprite sheets. This reduces the memory footprint and can improve rendering performance.
Cons
- 1. Complexity
Implementing sprite-based animations requires more coding effort. You need to handle the logic for frame switching, timing, and possibly interpolating between frames.
- 2. Asset Management
You’ll have to manage multiple image files or sprite sheets, which can increase the complexity of asset management.
- 3. Development Time
More time is needed to develop and fine-tune sprite animations, especially if you require sophisticated or interactive animations.
The choice between using animated GIFs and sprites in Allegro 5 largely depends on your project’s specific needs. If simplicity and ease of use are your primary concerns, you are experienced with utilizing/implementing external libraries, and the limitations of GIFs are acceptable for your project, then using animated GIFs might be the way to go. However, if you need higher-quality animations, more control, and better performance in terms of using routines that already exist in Allegro 5, then implementing animations with sprites is usually the better choice.
Exercises, Homework Questions, and Projects
Basic Sprite Handling
- 1. Theory: Explain the purpose of al_convert_mask_to_alpha and provide an example of its usage.
- 2. Code Modification: Modify SpriteMove.c to allow diagonal movement using the WASD keys.
- 3. Project: Create a program where a sprite’s transparency (alpha value) oscillates between 0% and 100% over time.
Sprite Transformations
- 4. Theory: Compare al_draw_scaled_bitmap and al_draw_rotated_bitmap. When would you use each?
- 5. Code Modification: Update SpriteMoveEffects.c to make the sprite rotate counterclockwise instead of clockwise.
- 6. Project: Implement a sprite that scales up when the mouse hovers over it and returns to normal when the mouse exits.
- 7. Theory: Explain how al_set_blender works and provide two blending mode examples not covered in the chapter.
Animation and Sprite Sheets
- 8. Project: Design a sprite sheet with four frames for a “jumping” animation. Write code to cycle through these frames when the space bar is pressed.
- 9. Code Modification: Modify OneSpriteMove.c to animate the character’s walking cycle automatically while moving, rather than per key press.
- 10. Theory: What are the advantages of using sprite sheets over individual frame files?
Advanced Sprite Effects
- 11. Project: Create a particle system where translucent sprites (e.g., sparks) emit from a central point and fade over time.
- 12. Code Modification: Add collision detection to SpriteMove.c to prevent the saucer from moving over a specific region of the background.
- 13. Theory: How would you simulate a “shadow” effect under a sprite using alpha blending?
Game Integration
- 14. Project: Develop a simple “collectathon” game where the player controls a sprite to collect items (static sprites) scattered on the screen.
- 15. Code Modification: Integrate sound effects into SpriteMoveEffects.c (e.g., play a sound when the saucer changes direction).
Blending and Visual Effects
- 16. Project: Use al_draw_tinted_bitmap to create a day-night cycle by gradually tinting the background and sprites.
- 17. Theory: Describe how to use ALLEGRO_FLIP_HORIZONTAL and ALLEGRO_FLIP_VERTICAL to mirror a sprite dynamically.
Performance and Optimization
- 18. Project: Benchmark the performance difference between using individual sprites and a sprite sheet for a 16-frame animation.
- 19. Theory: What steps would you take to optimize memory usage when loading multiple high-resolution sprites?
Practical Challenges
- 20. Project: Create a “bullet hell” demo where the player sprite dodges projectiles (rotating/scaling sprites) fired from the edges of the screen.
- 21. Code Modification: Implement smooth mouse-following movement for the saucer in SpriteMove.c.
Creative Applications
- 22. Project: Design an interactive UI with button sprites that change colour when hovered over and trigger actions (e.g., pause/resume).
- 23. Code Modification: Add a “pivot point” to the saucer in SpriteMoveEffects.c so it rotates around its center when moving diagonally.
Exploration and Analysis
- 24. Theory: Research and explain how to implement sprite z-ordering to manage layers (e.g., background, player, foreground).
- 25. Project: Use trigonometric functions to animate a sprite moving in a circular or sine-wave pattern.
Final Project
- 26. Capstone: Build a mini 2D platformer with the following:
- • Animated player sprite (idle, running, jumping)
- • Parallax-scrolling background
- • Collectible items and enemies (dynamic sprites)
- • Collision detection and health bar (using scaling sprites)
Experimental Tasks
- 27. Project: Simulate a “water reflection” effect by drawing a flipped, translucent copy of the sprite below its original position.
- 28. Code Modification: Replace the static background in SpriteMove.c with a dynamically generated starfield using al_draw_pixel.
Documentation and Debugging
- 29. Theory: Explain how to use Allegro’s debug tools to identify memory leaks in a sprite-heavy program.
- 30. Project: Write a technical report comparing the use of animated GIFs vs. sprite sheets in Allegro 5, including pros/cons and performance metrics.