Chapter 3.Using Graphics in Games
In the beginning, video games made use of the technologies available to them; the methods of input and output framed the development of games and how they operated. From the earliest text-only games to today’s realistic three-dimensional world renderings, programmers have advanced along with the latest hardware implementations. Changes tend to be incremental in nature; in adventure games, for example, we began with the use of textual descriptions for the scene/area, which then evolved into the use of simple character-based ASCII or extended ASCII graphics to illustrate scenes, or created top-down versions of worlds to explore (see figure 3.1).
Soon after simple primitive screen graphics hardware and colour displays arrived (typically a television in many cases), the era of blocky and simple graphic game styles began; this then was refined further into the familiar 8-bit blocky retro games. The 8-bit character evolved into the development and use of better-organized bitmap objects or “sprite graphics”—the main forte of Allegro’s graphical capabilities.
Figure 3.1: Text-based The Colossal Cave Adventure (left) and Rogue, using ASCII graphics (right). Images by William Crowther and Don Woods (left) and Artoftransformation (right), published under a CC BY-SA 3.0 license and modified for size and fitting by Kyle Flemmer.
3.1. Bitmap Fundamentals
Let’s do a quick overview of bitmap graphics and how they relate to game development. The typical way a computer performs its display of graphics functions is by utilizing a section of memory often referred to as video memory. Graphics-hardware chips access this block memory to display information to the user. This information can be text based, graphical in nature, or a combination of both. A character displayed on a computer screen is essentially a bit matrix composed of two colours (think white and black, or on and off) displaying a letter. That character is then displayed by mapping that pattern onto a location in the screen memory. An example of an early bitmap for characters might look like figure 3.2.
This same process can also be utilized to display more complex information items ranging from game objects to backgrounds.
You will probably be familiar with terms related to screen memory capability: the amount of video RAM in a graphics card (external or on a motherboard) and the screen resolution. Video RAM (VRAM) is memory used to hold information to display on the computer’s screen; it typically will be a combination of the visible screen area, perhaps some “buffer” or holding areas equal to the visible screen area, some working memory, and some additional storage. Video RAM is associated with the graphics-chip hardware and designed for fast-access, high-speed math (computer graphics use lots of that), and so we have graphics processing units (GPUs) to do the heavy lifting. All of this comes together to allow bytes of memory holding 1’s and 0’s to be displayed as a matrix of “bits” to create an image—hence “bitmaps.”
Figure 3.2: A 5 × 8 dot matrix alphabet. Illustrated by Kyle Flemmer.
The screen resolution refers to the dimensions of the screen in pixels (picture elements), which are basically display bits. A standard screen of 1920 × 1024 is 1920 bits wide and 1024 bits tall. Each one of those bits has a memory location and a colour depth (we’ll talk about that in a bit), which can be referenced in most graphical programming languages by an x- and a y-coordinate. The top leftmost location is 0,0, and the bottom rightmost is the screen resolution value minus 1 (as we start at location 0)—so for our example, the screen coordinates 1919, 1023. While it may seem unintuitive to have a coordinate system that uses the top leftmost location as 0,0, this is a legacy association with old video display units (and our tendency to have a left-to-right frame of mind—for example, writing) for creating a screen image. Old computer monitor systems utilizing cathode ray tubes (CRTs) “painted” an image on-screen with an electron beam, starting from the top left, scanning left to right, and moving down line by line. This process then mapped well into “reading” and displaying computer memory, and so early computer graphics had a typical memory layout with locations in the left-to-right format.
Let’s say we have a simple 4 × 4 display screen; our memory might look like table 4.
We could say our screen resolution here is 4 × 4: The top-left pixel coordinate is 0,0 and the bottom right is then 3,3 (as mentioned previously, X-1, Y-1). In our computer system this would be 16 bytes of VRAM—let’s say a memory address location starting at $C000 in HEX and ending at $C00F (see table 5).
Note how the screen sequence of coordinates translates to the linear layout of the memory here so that the 3,3 display location lands in the $C00F memory address location. In its simplest description, game graphics is putting data onto those VRAM memory locations, which correspond then to a change/display in pixels on the screen.
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 | |
$C000 | 0,0 | 0,1 | 0,2 | 0,3 | 1,0 | 1,1 | 1,2 | 1,3 |
|---|---|---|---|---|---|---|---|---|
$C008 | 2,0 | 2,1 | 2,2 | 2,3 | 3,0 | 3,1 | 3,2 | 3,3 |
A single bit by itself would be rather dull, as it can represent only two states—on or off, such as a white area or a black dot. Bytes, however, introduce an additional element that adds variety to visual appearance: colour. In computer graphics, colour depth refers to the number of discrete levels or gradations available for representing colour information on a display. An 8-bit colour depth, for example, allows for 256 possible colour values; a 12-bit depth supports 4,096 values; a 16-bit depth supports 65,536 values; and higher depths allow for even greater colour precision.
In some cases, the value is simply a colour from a palette, so 1 of 256 choices, in other cases it might be the level of one of the primary colours (red, green, or blue—RGB), and the combination of those three values creates a colour. So an RGB value of 0,0,0 would be black (no level/brightness of RGB); to the other end of the spectrum (255,255,255) would be white (maximum RGB brightness).
Let’s look back at our simple display example with a basic range of 256 colours—so a value of 64 is green, and let’s use 128 for red. We will plot a border onto the screen, a green one with a red center—illustrating the VRAM memory of a bitmap screen object derived from table 4.
In VRAM memory, then we would have the values shown in table 6.
This mechanism of writing data (values) to a screen (display) through memory locations (RAM, VRAM) is the basic process for all screen graphics. When we write programs, we are using arrays of data, representing a graphic object—be it a bitmap screen image like a background, a defined area pattern like a square, or a circle like in our example. We refer to mobile bit array objects as “sprites,” a bitmap that we typically move around on a larger object like the screen itself—the background, which could be a large static bitmap.
$C000 | 64 | 64 | 64 | 64 | 64 | 128 | 128 | 64 |
|---|---|---|---|---|---|---|---|---|
$C008 | 64 | 128 | 128 | 64 | 64 | 64 | 64 | 64 |
3.2. Bitmaps Creation and Use in Allegro 5
3.2.1. Bitmap Creation Tools
The game developer has a wide variety of tools and sources to acquire and create graphics utilized in game development. These range from self-developed graphics, to open-source and public domain works one could download, to purchasable artist-created graphical elements. Here, we’ll talk briefly about some of the available options very generally, as they can vary greatly over time and depending on the manufacturer and operating systems used.
Images for use as bitmaps can be drawn, captured, or generated depending on the tools utilized. Most computer systems come with some form of simple graphical drawing program or image display/manipulation program. These programs may let you create stand-alone drawings, such as a Paint-type drawing package, or annotate/manipulate preexisting graphics like photographs.
After creation or modification, those items can then often be saved in differing graphics formats such as .jpg, .bmp, or .png. In the software used for working on images, there typically are functions that can transform the image via, for example, rotating, scaling, changing the number of colours (altering the bitmap colour depth—we’ll talk about this later), or perhaps applying other effects. It is through the use of such tools that the game developer can often create the graphical elements required for their games, such as backgrounds and other in-game objects.
Game developers can also search online for more powerful, free open-source graphical tools to use in the development of their games. These can range from graphical editing programs such as GIMP to artificial intelligence image producers such as DALL-E. With these tools a game developer can create their own graphics for development use.
Intellectual Property and Acknowledging Its Use
An important point to keep in mind when creating and using graphics (or any game resource) is the intellectual property rights involved. If you are creating your own work drawings or photos, those tend to be straightforward, as you know the parties involved and can handle any issues regarding rights and use. When using online sources such as AI-created images or re-editing materials from a source other than your own, you need to consider the context of derived works and the rights and permissions that go along with those. As a game developer you will find a vast amount of available open-source and public domain materials for your work, and someone did you a favor by creating those; all they often ask in return is the proper acknowledgement of that work, a small price to pay for such generosity.
3.2.2. Creating Bitmaps
As noted, one can often use the simple graphics tools that come with a system to create required bitmaps. Let’s say I want to create a game about cats exploring space. I’ll call it the Cat Astronaut Training Simulator, or CATS for short. The game will have a background of outer space, and for that, I’ll use a star field photograph I’ve taken. The size of the images from my camera, resolution-wise, is 3008 × 2000, 24-bit colour, and the file size is 3 MB, so I’ll want to shrink it down to, say, 1024 × 768 and perhaps reduce the colour depth to make it a smaller image.
3.2.3. Supported Bitmap Formats
One of the first questions you might ask at this point is, What does Allegro support for graphics in terms of formats? The choice of format will depend on the specific requirements of the application, such as the desired resolution, colour depth, and level of compression (to save on file space for your resources . . .). Here, the Allegro 5 development library supports a good variety of graphics formats, providing the developer with a wide selection.
Note that to use the bitmap graphics, we need to use another one of Allegro’s many add-ons. Here, for the purpose of graphics, it’s the image handling add-on, so in any program, we would include:
#include <allegro5/allegro_image.h>
And initialize that with:
bool al_init_image_addon(void)
Here is a brief overview of the supported graphics formats and their features:
- BMP (Bitmap): BMP is a raster graphics format that is commonly used for images and animations. It supports a wide range of resolutions and colour depths, making it a versatile choice for game development.
- PNG (Portable Network Graphics): PNG is a lossless image format that is widely used for web and mobile applications. It supports transparency, which allows for the creation of images with transparent backgrounds.
- JPEG (Joint Photographic Experts Group): JPEG is a widely used lossy image format that is commonly used for photographs and other high-quality images. It supports high compression ratios, making it a good choice for applications where file size is a concern.
- GIF (Graphics Interchange Format): GIF is an animated image format that is commonly used for web and mobile applications. It supports animation, which allows for the creation of moving images.
- TGA (True Colour Graphics): TGA is a lossless image format that is commonly used for professional applications. It supports high colour depths and is well suited for applications that require high-quality images.
The supported colour depths and resolutions for bitmaps are:
- • 8-bit greyscale (1 colour)
- • 16-bit greyscale (256 colours)
- • 24-bit colour (16 million colours)
- • 32-bit colour (4,294,967,296 colours)
The 32-bit colour depth is quite impressive but probably seldom used. (From the human perspective, could one even tell 4 billion colour differences from 16 million? And would the file size increase be worth it?)
Allegro 5 supports a wide range of screen resolutions as well. Here is a list of some of the commonly used screen resolutions that Allegro 5 supports:
- • 1920 × 1080 (Full HD)
- • 1600 × 900
- • 1366 × 768
- • 1280 × 720 (HD)
- • 1024 × 768
- • 800 × 600
- • 640 × 480
- • 512 × 384
- • 400 × 300
- • 320 × 240
Note that this is not an exhaustive list; Allegro 5 may support other screen resolutions as well.
3.2.4. Bitmap Information
In every operating system you might use, there will be a way to determine the information for an image, details one will need to know to utilize graphics in their game programs. If you’ve initialized your game to the wrong screen size or wrong colour depth, your graphics will not display properly. As such, it’s prudent to find out that information before implementing a graphical object in your program to avoid errors, since Allegro will load your image fine and not generate an error, but when displayed, you won’t see anything. This situation often leads one to believe that they have an error in their code, but it’s actually a problem with the source image. Improper colour depth can also spoil the look of your game. For example, a 24-bit image rendered in 8-bit will look nothing like the original, losing both resolution and colour variation.
For this example, we will leave the colour depth at 24 but reduce the image size to 1024 × 768. I’m on a Windows 10 system, and so I can use the Photos program that comes with the system. The original image is in a .jpg format, 3008 × 2000, and 3 MB in size. To start with, I want to resize it and then convert it to a bitmap format (.bmp). In the Photos program, under the resize option, when I change the image size to 1024 × 768, it sets the height to 681 to maintain the aspect ratio. I’ll live with that and see how it looks in the demo program, so I select the “save as” function to export the image as CatCosmos.bmp. The file size is now reduced somewhat to 2.75 MB (see figure 3.3).
3.2.5. Choices, Choices . . .
Let’s talk a bit about file types here: the pros and cons of JPEGs and bitmaps. Allegro’s library can handle both formats, but its strength lies in using bitmaps. Bitmaps and JPEGs are both image file formats, but they have some key differences that can affect their performance and usage in certain situations. Here are five potential advantages of using bitmaps over JPEGs; the last three points listed are key for our needs as game developers:
Figure 3.3: CatCosmos.bmp for use as a background image. Photograph by Walter Ridgewell.
- Higher Resolution: Bitmaps are typically used for images that require high resolution, such as logos, icons, and other graphical elements. Bitmaps can have a higher resolution than JPEGs, which means they can display more pixels per inch (PPI) and provide a clearer, sharper image.
- Lossless Compression: Bitmaps use lossless compression, which means that the original image data is preserved during compression. This is important for images that need to be edited or manipulated in the future, as the original data can be restored without any loss of quality. JPEGs, however, use lossy compression, which means that some of the original data is discarded during compression, and the resulting image may not be as sharp or detailed as the original.
- Transparency: Bitmaps can support transparency, which means that certain parts of the image can be made transparent or semitransparent. This is useful for creating images with overlays or other visual effects that require transparency. JPEGs do not support transparency.
- Flexibility: Bitmaps can be used in a variety of contexts, including web design, graphic design, and game development. They can be scaled up or down to different sizes without losing quality, and they can be used in a variety of software applications. JPEGs are primarily used for web images and are not as versatile as bitmaps.
- Smaller File Size: Bitmaps can be smaller in file size than JPEGs, especially in images that do not require high resolution or complex colour schemes. This is because bitmaps use a different compression algorithm than JPEGs, which allows them to use fewer bits per pixel and results in smaller file sizes. However, this comes at the cost of lower resolution and less detail in the image.
3.3. Utilizing Bitmaps in a Program
3.3.1. Loading Bitmaps
To use graphics in programs, we will call various routines in the Allegro 5 library. To load a bitmap in Allegro 5, we will use the al_load_bitmap routine. To load a 24-bit colour image like the one we created previously, CatCosmos.bmp, you would call
ALLEGRO_BITMAP *image = al_load_bitmap("CatCosmos.bmp");
Here, ALLEGRO_BITMAP is a struct, a custom container representing the bitmap image, and *image is our pointer to the routine call, along with our filename. A short program to display the image follows: BitmapDisplay.c.
3.3.2. Complete Coding Example
// BitmapDisplay.c
#include <allegro5/allegro.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_native_dialog.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_ttf.h>
int main(int argc, char** argv)
{
// Initialize Allegro—checks if Allegro is properly initialized.
if (!al_init()) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to initialize Allegro!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
return—1;
}
// Initialize the display—creates a display window of size 1024 × 768
ALLEGRO_DISPLAY* display = al_create_display(1024, 768);
if (!display) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to create display!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
return—1;
}
// Initialize the image addon to load bitmap images.
if (!al_init_image_addon()) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to initialize image addon!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
al_destroy_display(display);
return—1;
}
// Loads the image file CatCosmos.bmp.
ALLEGRO_BITMAP* image = al_load_bitmap("CatCosmos.bmp");
if (!image) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to load image!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
al_destroy_display(display);
return—1;
}
// Initialize the keyboard for input handling
if (!al_install_keyboard()) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to install keyboard!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
al_destroy_bitmap(image);
al_destroy_display(display);
return—1;
}
// Create an event queue to handle our program events.
ALLEGRO_EVENT_QUEUE* event_queue = al_create_event_queue();
if (!event_queue) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to create event queue!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
al_destroy_bitmap(image);
al_destroy_display(display);
return—1;
}
// Register the display and keyboard to the event queue
al_register_event_source(event_queue, al_get_display_event_source(display));
al_register_event_source(event_queue, al_get_keyboard_event_source());
// Clear the display to white
al_clear_to_color(al_map_rgb(255, 255, 255));
// Draw the image on the display
al_draw_bitmap(image, 0, 0, 0);
// Flip the display to show the image
al_flip_display();
// Event loop—keeps the display open and listens for the ESC key press or window close event to exit the program
bool running = true;
while (running) {
ALLEGRO_EVENT ev;
al_wait_for_event(event_queue, &ev);
if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
running = false;
}
else if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {
if (ev.keyboard.keycode == ALLEGRO_KEY_ESCAPE) {
running = false;
}
}
}
// Cleanup
al_destroy_bitmap(image);
al_destroy_display(display);
al_destroy_event_queue(event_queue);
return 0;
}
3.3.3. Manipulating Bitmap Images
While you can manipulate bitmap images in external graphic programs, as we have mentioned previously, you can also do some basic image transformations using the Allegro library on a loaded image in your program.
There are two other transformations—scaling (or changing an image’s size) and rotating (changing the orientation of the displayed image). Allegro provides a few routines representing variations and combinations of those, along with incorporating tinting, so we have:
- • al_draw_scaled_bitmap
- • al_draw_rotated_bitmap
- • al_draw_tinted_bitmap
- • al_draw_scaled_rotated_bitmap
- • al_draw_tinted_scaled_bitmap
- • al_draw_tinted_rotated_bitmap
- • al_draw_tinted_scaled_rotated_bitmap
We’ll just consider the base routine calls (scaled, rotated, and tinted) here, as the combined routines simply add parameters from those base calls.
3.4. Scaling and Rotating Bitmaps
To begin with, we can change the size of the displayed image with 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, where:
- 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
The initial parameters for the routine describe characteristics of the unmodified bitmap, the origin x,y of bitmap source, and the width and height of the object. We then specify how we want to scale those parameters, providing a new destination (x,y) and its associated width and height change. These variables let us scale the image in a variety of ways, expanding or shrinking it as we wish (see figure 3.4).
Next, we can also change the orientation of that image with al_draw_rotated_bitmap:
void al_draw_rotated_bitmap(ALLEGRO_BITMAP *bitmap, float cx, float cy, float dx, float dy, float angle, int flags)
where
- cx: center
- cy: center y
- dx: destination x
- dy: destination y
- angle: angle by which to rotate
- flags: same as for al_draw_bitmap
This draws a rotated version of the specified bitmap. Again, we see the use of some familiar variables with respect to destination x- and y-coordinates. As we are rotating an image about a point, we need the center x- and y-coordinates and an amount to rotate the bitmap by.
In Allegro, we specify an “angle” described using radians rather than degrees, with the rotation going clockwise (turning to the right . . .) around the axis (center-point coordinates).
The point defined by cx/cy is the rotation point inside the bitmap; the image will be drawn at dx/dy on the display, and the bitmap is rotated around this point 90 degrees clockwise. If the cx/cy point is in the center of the object, it’s rotated on that point; if it’s on an edge or corner, the object is rotated at the point.
Figure 3.5 shows two examples rotated 45 degrees.
Figure 3.4: Scaling bitmaps. Illustration by Kyle Flemmer.
Figure 3.5: Bitmaps rotated by 45 degrees. Illustrated by Kyle Flemmer.
In these two examples, we see how the coordinates affect where the rotation occurs. In the first we have different coordinates for the destination x,y of the bitmap (where it’s drawn from dx and dy) and the rotation point in that object (cx and cy, respectively). In the first example, the rotation point is at the center of the object. In the second example, the destination dx,dy and the rotation point cx,cy are the same. Now let’s advance the rotation to 90 degrees.
As you can see in figure 3.6, the location of the center point for rotation makes a difference in the appearance of the image.
Let’s take a diversion here from the example and into the use of radians for the angle, as opposed to degrees, which is what most people will be used to. Degrees and radians are both units for measuring angles. Degrees divide a circle into 360 parts, making them intuitive and easy to use in everyday contexts like navigation and geography. Radians, however, measure angles based on a circle’s radius, with one radian being the angle subtended by an arc equal in length to the radius. So that means there are 2π radians in a full circle (360 degrees) and π radians in a half circle (180 degrees). Radians are preferred in computer graphics because they simplify mathematical formulas, improve computational efficiency, and provide consistency in calculations involving trigonometric functions and rotational transformations. This leads to faster and more accurate rendering of graphics. As a result, the base math library in C (and many other programming languages) requires the use of radians rather than degrees. This is a simple example of a conversion program:
Figure 3.6: Bitmaps rotated by 90 degrees. Illustrated by Kyle Flemmer.
#include <stdio.h>
#define PI 3.14159265358979323846
int main(void) {
// Angle in degrees
double degrees = 45.0;
// Conversion: radians = degrees * (PI / 180)
double radians = degrees * (PI / 180.0);
// Output the result
printf("%.2f degrees is equal to %.5f radians.\n", degrees, radians);
return 0;
}
So rotating an object by 90 degrees in a graphics program translates to
𝜋/2 radians (𝜋 being the equivalent of 180 degrees)
Allegro has a struct ALLEGRO_PI, and so to rotate an object by 90 degrees, you would use a routine call such as:
al_draw_rotated_bitmap(bitmap, cx, cy, dx, dy, ALLEGRO_PI / 2, 0)
We’ll see how this looks in the following demonstration program example BitmapDisplayRotate.c. Here, every press of the space bar rotates the background image 90 degrees clockwise.
3.4.1. Complete Coding Example
// BitmapDisplayRotate.c
#include <allegro5/allegro.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_native_dialog.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_ttf.h>
int main(int argc, char** argv)
{
// Initialize Allegro.
if (!al_init()) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to initialize Allegro!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
return—1;
}
// Create a display (window) of size 1024 × 768.
ALLEGRO_DISPLAY* display = al_create_display(1024, 768);
if (!display) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to create display!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
return—1;
}
// Initialize the image addon to load bitmap images.
if (!al_init_image_addon()) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to initialize image addon!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
al_destroy_display(display);
return—1;
}
// Load the bitmap image "CatCosmos.bmp".
ALLEGRO_BITMAP* image = al_load_bitmap("CatCosmos.bmp");
if (!image) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to load image!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
al_destroy_display(display);
return—1;
}
// Initialize the keyboard for input handling.
if (!al_install_keyboard()) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to install keyboard!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
al_destroy_bitmap(image);
al_destroy_display(display);
return—1;
}
// Create an event queue for handling events.
ALLEGRO_EVENT_QUEUE* event_queue = al_create_event_queue();
if (!event_queue) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to create event queue!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
al_destroy_bitmap(image);
al_destroy_display(display);
return—1;
}
// Register the display and keyboard event sources.
al_register_event_source(event_queue, al_get_display_event_source(display));
al_register_event_source(event_queue, al_get_keyboard_event_source());
// Show initial instructions.
al_show_native_message_box(display,
"Instructions",
"Bitmap Rotation",
"Press SPACE to rotate the image 90° clockwise.\nPress ESCAPE to exit.",
NULL,
0);
// Variables for rotation and positioning.
double angle = 0.0;
int image_width = al_get_bitmap_width(image);
int image_height = al_get_bitmap_height(image);
int display_center_x = 1024 / 2;
int display_center_y = 768 / 2;
// Draw the initial image (no rotation) centered in the display.
al_clear_to_color(al_map_rgb(255, 255, 255));
al_draw_rotated_bitmap(image,
image_width / 2.0, // Rotation center x (center of image)
image_height / 2.0, // Rotation center y (center of image)
display_center_x, // Destination x (center of display)
display_center_y, // Destination y (center of display)
angle, // Rotation angle (in radians)
0);
al_flip_display();
// Main event loop.
bool running = true;
while (running)
{
ALLEGRO_EVENT ev;
al_wait_for_event(event_queue, &ev);
if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
running = false;
}
else if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {
if (ev.keyboard.keycode == ALLEGRO_KEY_ESCAPE) {
running = false;
}
else if (ev.keyboard.keycode == ALLEGRO_KEY_SPACE) {
// Rotate the image 90° clockwise (90° = PI/2 radians).
angle += ALLEGRO_PI / 2;
if (angle >= 2 * ALLEGRO_PI)
angle—= 2 * ALLEGRO_PI;
// Redraw the image with the new rotation.
al_clear_to_color(al_map_rgb(255, 255, 255));
al_draw_rotated_bitmap(image,
image_width / 2.0,
image_height / 2.0,
display_center_x,
display_center_y,
angle,
0);
al_flip_display();
}
}
}
// Cleanup resources.
al_destroy_bitmap(image);
al_destroy_display(display);
al_destroy_event_queue(event_queue);
return 0;
}
3.4.2. Bitmap Colour Manipulation
On to our last base routine: al_draw_tinted_bitmap. Its purpose is to create the ability to “highlight” a specific colour in the bitmap (red, green, blue) by multiplying all colours in the bitmap with the highlight colour. The routine also allows for setting the opacity of the bitmap from solid to transparent. While changing the transparency of a bitmap image in your game may not seem useful immediately, it is a handy function for visual effects when utilized with sprites in games (an example could be of a game character that gets “zapped” by a laser and then disintegrates—fades away, a change in transparency). In relation to the background, perhaps as you change scenes, you might fade out from one and fade into the next. The routine’s structure is:
void al_draw_tinted_bitmap(ALLEGRO_BITMAP *bitmap, tint, float dx, float dy, int flags)
The “tint value” we’re talking about comes from the ALLEGRO_COLOR that you create with the routine al_map_rgba_f(float r, float g, float b, float a). This routine takes four floating-point numbers (each between 0 and 1) representing the red, green, blue, and alpha (transparency) channels, respectively. Here is an example:
al_draw_tinted_bitmap(bitmap, al_map_rgba_f(1, 1, 1, 0.5), x, y, 0);
In this case, you’re drawing the bitmap with full intensity on the red, green, and blue channels (that’s what the 1’s mean), but the alpha value is set to 0.5. This results in the bitmap being rendered at 50% transparency.
Another example is:
al_draw_tinted_bitmap(bitmap, al_map_rgba_f(1, 0, 0, 1), x, y, 0);
Here, the tint is specified to use only the red component (1 for red, 0 for green, 0 for blue) while keeping full opacity (alpha is 1). This means the bitmap gets tinted red.
So essentially, by adjusting the values you pass to al_map_rgba_f, you control both the transparency and the colour tint applied to your bitmap when you draw it.
3.5. Displaying a Section of a Bitmap and the Concept of a Viewport
Allegro 5 will let the user display a selected area of a loaded bitmap, the size of which is determined by the programmer, and have that available for another graphics display. Here, the most recognizable use of this routine would be the on-screen minimap display.
Here, the main loaded bitmap image comprises the whole map or game level, with sections of it outside the main display screen; this is your viewport. We then add to that viewport a minimap, which might show all of the existing map (scaled down), and then a highlighted section indicating which area is currently being seen on the main display.
What follows is an example program, DisplayMap.c, which loads a background image, displays a section of it on the main screen (the viewport) with a minimap showing a scaled-up version of the complete image.
3.5.1. Example Program DisplayMap.c
#include <allegro5/allegro.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_native_dialog.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_font.h>
int main(int argc, char** argv)
{
if (!al_init()) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to initialize Allegro!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
return—1;
}
if (!al_init_image_addon()) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to initialize image addon!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
return—1;
}
if (!al_init_primitives_addon()) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to initialize primitives addon!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
return—1;
}
const int display_w = 1024;
const int display_h = 768;
ALLEGRO_DISPLAY* display = al_create_display(display_w, display_h);
if (!display) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to create display!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
return—1;
}
ALLEGRO_BITMAP* map = al_load_bitmap("CatCosmos.bmp");
if (!map) {
al_show_native_message_box(display, "Error", "Error", "Failed to load map.bmp!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
al_destroy_display(display);
return—1;
}
int map_w = al_get_bitmap_width(map);
int map_h = al_get_bitmap_height(map);
int mini_x = display_w—210;
int mini_y = 10;
int mini_w = 200;
int mini_h = 150;
int mini_view_x = 0;
int mini_view_y = 0;
int mini_view_w = mini_w / 8;
int mini_view_h = mini_h / 8;
const int mini_scroll_speed = 5;
if (!al_install_keyboard()) {
al_show_native_message_box(display, "Error", "Error", "Failed to install keyboard!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
al_destroy_bitmap(map);
al_destroy_display(display);
return—1;
}
ALLEGRO_EVENT_QUEUE* event_queue = al_create_event_queue();
if (!event_queue) {
al_show_native_message_box(display, "Error", "Error", "Failed to create event queue!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
al_destroy_bitmap(map);
al_destroy_display(display);
return—1;
}
al_register_event_source(event_queue, al_get_display_event_source(display));
al_register_event_source(event_queue, al_get_keyboard_event_source());
ALLEGRO_TIMER* timer = al_create_timer(1.0 / 60);
al_register_event_source(event_queue, al_get_timer_event_source(timer));
al_start_timer(timer);
bool running = true;
ALLEGRO_EVENT ev;
while (running)
{
al_wait_for_event(event_queue, &ev);
if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
running = false;
}
else if (ev.type == ALLEGRO_EVENT_KEY_DOWN)
{
if (ev.keyboard.keycode == ALLEGRO_KEY_ESCAPE)
{
running = false;
}
else if (ev.keyboard.keycode == ALLEGRO_KEY_LEFT)
{
mini_view_x—= mini_scroll_speed;
if (mini_view_x < 0) mini_view_x = 0;
}
else if (ev.keyboard.keycode == ALLEGRO_KEY_RIGHT)
{
mini_view_x += mini_scroll_speed;
if (mini_view_x > mini_w—mini_view_w) mini_view_x = mini_w—mini_view_w;
}
else if (ev.keyboard.keycode == ALLEGRO_KEY_UP)
{
mini_view_y—= mini_scroll_speed;
if (mini_view_y < 0) mini_view_y = 0;
}
else if (ev.keyboard.keycode == ALLEGRO_KEY_DOWN)
{
mini_view_y += mini_scroll_speed;
if (mini_view_y > mini_h—mini_view_h) mini_view_y = mini_h—mini_view_h;
}
}
else if (ev.type == ALLEGRO_EVENT_TIMER)
{
al_clear_to_color(al_map_rgb(0, 0, 0));
al_draw_bitmap(map, 0, 0, 0);
al_draw_scaled_bitmap(map,
mini_view_x * (map_w / mini_w), mini_view_y * (map_h / mini_h),
mini_view_w * (map_w / mini_w), mini_view_h * (map_h / mini_h),
mini_x, mini_y, mini_w, mini_h, 0);
al_draw_rectangle(mini_x, mini_y, mini_x + mini_w, mini_y + mini_h, al_map_rgb(255, 255, 255), 2);
al_draw_rectangle((mini_view_x * (map_w / mini_w)), (mini_view_y * (map_h / mini_h)), ((mini_view_x + mini_view_w) * (map_w / mini_w)), ((mini_view_y + mini_view_h) * (map_h / mini_h)), al_map_rgb(255, 255, 255), 2);
al_draw_textf(al_create_builtin_font(), al_map_rgb(255, 255, 255), 10, 10, 0, "mini_x: %d, mini_y: %d, mini_w: %d, mini_h: %d", mini_x, mini_y, mini_w, mini_h);
al_flip_display();
}
}
al_destroy_timer(timer);
al_destroy_event_queue(event_queue);
al_destroy_bitmap(map);
al_destroy_display(display);
return 0;
}
3.5.2. Program Overview
There is a lot going on in this next example: We’re loading a bitmap; magnifying a section of it, which is selected by a moving window; and displaying that in a window on the main bitmap display. As such, let’s go through it in more detail.
Breaking It Down Step-by-Step
1. Initializing Allegro
Before doing anything graphical, Allegro must be initialized:
if (!al_init()) {
al_show_native_message_box(NULL, "Error", "Error", "Failed to initialize Allegro!", NULL, ALLEGRO_MESSAGEBOX_ERROR);
return—1;
}
This ensures that Allegro is ready to be used. If it fails, an error message pops up.
Next, the necessary Allegro add-ons are initialized:
al_init_image_addon();
al_init_primitives_addon();
The image add-on lets us load and draw bitmap images.
The primitives add-on is needed for drawing shapes like rectangles.
2. Creating the Display
const int display_w = 1024;
const int display_h = 768;
ALLEGRO_DISPLAY* display = al_create_display(display_w, display_h);
This creates a 1024 × 768 pixel window for the game. If it fails, the program exits.
3. Loading the Map
ALLEGRO_BITMAP* map = al_load_bitmap("CatCosmos.bmp");
The map image (CatCosmos.bmp) is loaded into memory.
If the file is missing or incorrectly formatted, the program stops.
4. Setting Up the Minimap
int mini_x = display_w—210;
int mini_y = 10;
int mini_w = 200;
int mini_h = 150;
This defines the minimap’s position and size.
The minimap is placed in the top-right corner with a 10-pixel margin.
The viewport within the minimap:
int mini_view_x = 0, mini_view_y = 0;
int mini_view_w = mini_w / 2, mini_view_h = mini_h / 2;
The viewport inside the minimap starts at (0,0).
It’s set to half the minimap’s width and height, which creates a “zoomed-in” effect.
5. Setting Up Keyboard Input
al_install_keyboard();
ALLEGRO_EVENT_QUEUE* event_queue = al_create_event_queue();
al_register_event_source(event_queue, al_get_keyboard_event_source());
The keyboard is installed so we can detect arrow key presses.
The event queue listens for keyboard inputs.
6. Scrolling the Minimap
The program listens for keyboard events:
if (ev.keyboard.keycode == ALLEGRO_KEY_LEFT)
{
mini_view_x—= mini_scroll_speed;
if (mini_view_x < 0) mini_view_x = 0;
}
Pressing the left arrow key moves the minimap viewport left.
The viewport is prevented from scrolling beyond the left boundary (0,0).
Similarly, the Right, Up, and Down keys move the viewport in different directions.
7. Rendering the Scene
Inside the game loop, we clear the screen:
al_clear_to_color(al_map_rgb(0, 0, 0));
This ensures we don’t get unwanted artifacts from previous frames.
The full-sized map is drawn:
al_draw_bitmap(map, 0, 0, 0);
The minimap is drawn by scaling the full map down:
al_draw_scaled_bitmap(map,
mini_view_x * (map_w / mini_w), mini_view_y * (map_h / mini_h),
mini_view_w * (map_w / mini_w), mini_view_h * (map_h / mini_h),
mini_x, mini_y, mini_w, mini_h, 0);
Instead of drawing the entire map, only a selected portion is drawn inside the minimap.
8. Highlighting the Minimap Viewport
al_draw_rectangle((mini_view_x * (map_w / mini_w)),
(mini_view_y * (map_h / mini_h)),
((mini_view_x + mini_view_w) * (map_w / mini_w)),
((mini_view_y + mini_view_h) * (map_h / mini_h)),
al_map_rgb(255, 255, 255), 2);
This rectangle highlights the portion of the minimap that is currently being viewed.
The example program displays the main graphic bitmap with a smaller movable viewport indicator: here, a white rectangle and a static viewport in the top-right corner showing an enlarged image of the bitmap in the rectangle area.
Figure 3.7: Display map with a minimap viewport. Image by Walter Ridgewell.
3.6. Advanced Graphics Techniques in Allegro 5
3.6.1. Resolution Independence
Resolution independence refers to the ability of a software application to run on displays of different resolutions without losing quality or functionality. In Allegro 5, achieving resolution independence involves scaling graphics and user interface elements dynamically based on the screen’s resolution. This ensures that the application looks consistent and sharp across various devices. The typical approach to this is to utilize routines to query the platform the device is running on for display information and then assign those values to appropriate variables in your program—for example, screen_width and screen_height. This looks something like:
int screen_width = al_get_display_width(ALLEGRO_DISPLAY *display)
in screen_height = al_get_display_height(ALLEGRO_DISPLAY *display)
After that, one would optimize routines for screen coordinates to work with the returned information. This can prove to be a complex process if you’re wanting to work with a variety of screen sizes, keep aspect ratios, and so on.
In Allegro 5, one can use transformations to dynamically adapt one’s code to the current system’s screen resolution. By using a transformation that considers screen resolution, you can ensure that graphics scale properly and dynamically. This would be implemented utilizing the following library functions:
al_identity_transform(&transform);
al_scale_transform(&transform, scaleX, scaleY);
al_use_transform(&transform);
where scaleX and scaleY are the scaling factors for width and height (respectively) that take into account the commonly used screen_width and screen_height information:
#include <allegro5/allegro.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_primitives.h>
int main() {
if (!al_init()) {
return—1;
}
al_init_image_addon();
al_init_primitives_addon();
ALLEGRO_DISPLAY *display = al_create_display(1024, 768);
if (!display) {
return—1;
}
// Query display dimensions
int screen_width = al_get_display_width(display);
int screen_height = al_get_display_height(display);
// Setup transformation for resolution independence
float scaleX = (float)screen_width / 1024.0;
float scaleY = (float)screen_height / 768.0;
ALLEGRO_TRANSFORM transform;
al_identity_transform(&transform);
al_scale_transform(&transform, scaleX, scaleY);
al_use_transform(&transform);
al_clear_to_color(al_map_rgb(0, 0, 0));
al_flip_display();
al_rest(2.0);
al_destroy_display(display);
return 0;
}
But we’re a bit ahead of ourselves here, as we haven’t explained transformations. In Allegro 5, these are routines used to modify the position, scale, rotation, and other properties of graphical objects in a 2D space. These effects can be applied to multiple bitmap objects at once, alleviating the need to otherwise perform those actions individually per bitmap object.
Transformations are used extensively in game development and graphics programming. They allow for dynamic object movement, animations, camera movement, and more. For instance, in a platformer game, transformations are used to move characters, enemies, and the camera that follows the player. As such, transformations in Allegro 5 are powerful tools that allow developers to manipulate graphics in 2D space. They are essential for creating dynamic, interactive, and visually appealing applications. By understanding and using translation, scaling, and rotation, and combining these transformations effectively, developers can greatly enhance the visual quality and interactivity of their Allegro 5 projects.
3.6.2. Types of Transformations
Looking at the list of transformation routines here, you’ll note that the first three have equivalent routines to some of the earlier bitmap manipulation operations:
- Translation (Moving Objects): This changes the position of an object in the 2D space. For example, moving an object from one location to another.
- Scaling (Resizing Objects): Scaling either increases or decreases the size of an object. This can be uniform (keeping the aspect ratio the same) or nonuniform (changing the width and height by different amounts).
- Rotation (Spinning Objects): This involves rotating an object around a point, typically its center. Rotation is usually measured in degrees or radians.
- Shearing: Shearing is a less commonly used transformation that skews the shape of an object.
Let’s consider scaling. Using transformations in Allegro 5 (such as al_rotate_transform), vs. relying on simpler drawing commands like al_draw_rotated_bitmap, comes down to flexibility, performance, and ease of applying multiple transformations at once.
Here’s a breakdown of why you might prefer one over the other:
- 1. Flexibility with Multiple Transformations
- Using al_rotate_transform and the transformation matrix (ALLEGRO_TRANSFORM)
If you need to apply multiple transformations together (e.g., rotation, scaling, and translation), a transformation matrix allows you to combine all transformations before drawing.
Example: You can scale, rotate, and translate an object in a single transformation rather than doing them separately with different routines.
- Using al_draw_rotated_bitmap
This routine is simpler but only rotates around a specific point when drawing.
If you need additional transformations, like scaling or skewing, you cannot do that easily with al_draw_rotated_bitmap.
- Using al_rotate_transform and the transformation matrix (ALLEGRO_TRANSFORM)
- 2. Performance Considerations
- Using al_draw_rotated_bitmap
This is optimized for performance when all you need is rotation.
The routine is simpler and faster if you don’t need complex transformations.
It’s great for simple sprite rotation, such as rotating a spaceship or a top-down character.
- Using al_rotate_transform
If you apply multiple transformations, using a transformation matrix is often more efficient than applying multiple individual drawing operations.
It reduces redundant calculations since transformations can be precomputed and reused.
- Using al_draw_rotated_bitmap
- 3. Transforming the Entire Scene vs. Individual Objects
- Using al_use_transform with al_rotate_transform
If you need to apply transformations to the entire coordinate system, using al_use_transform allows all subsequent drawing operations to inherit the transformation.
Example: If you want to rotate everything in a scene (like a rotating camera view), a transformation matrix is the correct approach.
- Using al_draw_rotated_bitmap
If you only need to rotate one object while keeping everything else unchanged, al_draw_rotated_bitmap is easier.
- Using al_use_transform with al_rotate_transform
- 4. Example Comparisons
- Using al_draw_rotated_bitmap (Simpler, Object-Specific)
ALLEGRO_BITMAP *sprite = al_load_bitmap("sprite.png");
float x = 200, y = 200; // Position on-screen
float angle = ALLEGRO_PI / 4; // Rotate 45 degrees
float cx = al_get_bitmap_width(sprite) / 2.0;
float cy = al_get_bitmap_height(sprite) / 2.0;
al_draw_rotated_bitmap(sprite, cx, cy, x, y, angle, 0);
This rotates only one bitmap at a time. It’s good for rotating sprites but not for complex transformations.
- Using al_rotate_transform with al_use_transform (More Advanced, Affects All Drawing)
ALLEGRO_TRANSFORM transform;
al_identity_transform(&transform);
al_translate_transform(&transform,—100,—100); // Move pivot point
al_rotate_transform(&transform, ALLEGRO_PI / 4); // Rotate 45 degrees
al_translate_transform(&transform, 100, 100); // Move back
al_use_transform(&transform);
al_draw_bitmap(sprite, 200, 200, 0);
This approach affects all drawing commands after al_use_transform(). It’s useful for scene-wide effects or handling multiple transformations at once.
As you can see, transforms are useful for more complex operations and can affect a whole screen view, which brings us back to why we might want to use them to achieve display resolution independence in our games.
3.7. How Transformations Help with Resolution Independence
Perhaps one of the most critical application transformations in games is that they allow you to remap coordinates so that your game logic can stay consistent across different resolutions. This is important for a developer if they choose to create a game for multiple platforms while minimizing coding work. Let’s look at some of those possible uses.
Situation | Use al_draw_rotated_bitmap | Use al_rotate_transform |
|---|---|---|
Rotate a single sprite | ✓ | ⨉ |
Rotate multiple objects together | ⨉ | ✓ |
Rotate and scale at the same time | ⨉ | ✓ |
Move a camera or worldview | ⨉ | ✓ |
Simple, fast sprite rotation | ✓ | ⨉ |
3.7.1. Scaling the Entire Scene
By using al_scale_transform(), you can make the coordinate system of your game match the target resolution dynamically. This means that drawing at coordinates (100,100) on a small window will appear in the same relative position on a larger window.
The following code sample scales from a base resolution of 800 × 600 to fit any screen size:
// Base resolution
float base_width = 800;
float base_height = 600;
// Get actual screen size
float screen_width = al_get_display_width(display);
float screen_height = al_get_display_height(display);
// Compute scaling factors
float scale_x = screen_width / base_width;
float scale_y = screen_height / base_height;
ALLEGRO_TRANSFORM transform;
al_identity_transform(&transform);
al_scale_transform(&transform, scale_x, scale_y);
al_use_transform(&transform);
So here in this code segment, if the display size of the platform running the code is 1600 × 1200, the values supplied to the transform would be scale_x = 2 and scale_y = 2, meaning everything is doubled in size.
Conversely, if the screen is 400 × 300 (like on a small mobile device platform), the calculated values of scale_x = 0.5 and scale_y = 0.5 would be applied, meaning everything is shrunk to fit. As a result, all your drawing code remains the same regardless of screen size.
3.7.2. Centering the Game on Any Screen Size
If you scale up or down based on resolution, sometimes you also need to center your game. This can be done by translating the coordinate system.
Example: Centering the Scaled Content
// Compute translation to center the game
float offset_x = (screen_width—(base_width * scale_x)) / 2;
float offset_y = (screen_height—(base_height * scale_y)) / 2;
al_translate_transform(&transform, offset_x, offset_y);
al_use_transform(&transform);
Here, the code ensures that even if the screen is wider or taller than your base resolution, your game remains centered.
3.7.3. Using Logical Coordinates
To abstract your code if you don’t have a specific resolution in mind, sometimes the best approach, instead of working directly with pixels, is to define a virtual resolution (e.g., 800 × 600) and scale everything accordingly. By utilizing this process, you let the game engine/software scale the game screen dynamically to fit the current display screen. The advantages of this method include maintaining the aspect ratio of the screen, better performance optimization and smoother-looking graphics when utilizing antialiasing, and simply keeping game items like the user interface (UI) consistent proportionally to avoid distortions or difficulties reading text.
Example
#define VIRTUAL_WIDTH 800
#define VIRTUAL_HEIGHT 600
float sx = screen_width / VIRTUAL_WIDTH;
float sy = screen_height / VIRTUAL_HEIGHT;
al_identity_transform(&transform);
al_scale_transform(&transform, sx, sy);
al_use_transform(&transform);
So here, all drawing routines use a consistent coordinate system based on VIRTUAL_WIDTH and VIRTUAL_HEIGHT. If you always draw and place objects using your base resolution (e.g., 800 × 600), then no matter what resolution the player uses, everything will be resized properly.
If you look at table 8, you’ll see the benefits of using transformations vs. needing to hand tweak your code for multiple displays (not to mention the fact that a programmer might miss incorporating the multitude of resolutions out there these days).
Approach | Pros | Cons |
|---|---|---|
Manual Scaling (Resize assets, reposition elements manually) | Fine-tuned control over elements | Requires hard-coded adjustments for each resolution |
Transformations (al_scale_transform) | Automatic scaling across all resolutions | Can lead to minor distortions if aspect ratio is not handled |
Transformations + Letterboxing (Black bars to maintain aspect ratio) | Consistent aspect ratio | Some screen space is unused |
Aspect Ratio Considerations
While this might not seem like a concern foremost in a game developer’s mind, when scaling, aspect ratio mismatches can cause stretching. As a result, your nicely circular objects can become oblong, or your tall items suddenly become short and squat.
If you want to maintain the correct aspect ratio, you can apply a transform like so:
- • Scale using the same factor for both axes (min(scale_x, scale_y)).
- • Add black bars (letterboxing) to fill the remaining space:
float scale_factor = fmin(scale_x, scale_y); // Maintain aspect ratio
float new_width = VIRTUAL_WIDTH * scale_factor;
float new_height = VIRTUAL_HEIGHT * scale_factor;
float offset_x = (screen_width—new_width) / 2;
float offset_y = (screen_height—new_height) / 2;
al_identity_transform(&transform);
al_translate_transform(&transform, offset_x, offset_y);
al_scale_transform(&transform, scale_factor, scale_factor);
al_use_transform(&transform);
3.7.4. Summing Up How Transformations Work in Allegro 5
Understanding transformations in Allegro 5 is all about working with an ALLEGRO_TRANSFORM object to change how graphics are drawn. The process begins with creating and initializing this object using al_identity_transform, setting it up with the identity matrix. From there, you can move objects around by translating them with al_translate_transform or resize them while keeping their aspect ratios intact using al_scale_transform. If you need to rotate objects, you can use al_rotate_transform to set a specific angle.
One of the cool things about Allegro 5 is that you can combine multiple transformations in sequence. Once you have your transformations set up just the way you want them, you make it the current transformation for drawing operations with al_use_transform.
A couple of examples might help to clarify things. You might use scaling and centering to maintain the aspect ratio or set up a basic transformation by creating and applying it in a straightforward manner. Overall, transformations are a key part of effectively positioning, resizing, and rotating graphics within Allegro 5.
3.7.5. Creating and Setting a Transformation
Before applying a transformation, you first create an ALLEGRO_TRANSFORM object and then apply various transformation operations to it. After setting up the transformation, you make it the current transformation for drawing operations.
Here’s an example of how to create and set a basic transformation:
ALLEGRO_TRANSFORM transform;
al_identity_transform(&transform); // Initialize with the identity matrix
al_rotate_transform(&transform, angle); // Rotate
al_translate_transform(&transform, x, y); // Move
al_scale_transform(&transform, scaleX, scaleY); // Scale
al_use_transform(&transform); // Set as the current transformation
Transformations can be combined to produce more complex effects. For example, you might want to rotate an object around a point other than its center, scale it, and then move it. The order of transformations is crucial as it affects the final result.
3.7.6. Order of Transformations
The typical order of transformations for achieving a common effect (like rotating an object around its center and then moving it) is:
- 1. Translate the object to the origin (0,0).
- 2. Perform the rotation.
- 3. Scale if necessary.
- 4. Translate the object to its intended position.
When we use a transformation matrix to modify drawing operations, we typically follow these steps.
Reset the Transformation Matrix
When working with transformations in Allegro 5, each transformation matrix builds on the previous one. If you don’t reset the transformation matrix before applying new transformations, your transformations could accumulate in unexpected ways.
By calling al_identity_transform(), you ensure that you start from a clean slate before applying any new transformations.
Imagine a case where you don’t use al_identity_transform() before applying transformations:
ALLEGRO_TRANSFORM transform;
al_rotate_transform(&transform, ALLEGRO_PI / 4);
al_use_transform(&transform);
If transform was already modified elsewhere in the code, this would continue rotating based on the existing transformations, leading to an unexpected effect.
Now, using al_identity_transform() ensures we reset it first:
ALLEGRO_TRANSFORM transform;
al_identity_transform(&transform); // Reset to a clean state
al_rotate_transform(&transform, ALLEGRO_PI / 4);
al_use_transform(&transform);
This ensures that the only transformation applied is the new 45-degree rotation.
The routine al_identity_transform(ALLEGRO_TRANSFORM *trans) is used to reset a transformation matrix to the identity matrix. This means it sets up a transformation that does nothing—it doesn’t rotate, scale, or translate anything.
Apply transformations (translate, rotate, scale, etc.) with:
al_translate_transform(&transform,—100,—100); (Move origin)
al_rotate_transform(&transform, ALLEGRO_PI / 4); (Rotate 45°)
al_translate_transform(&transform, 100, 100); (Move back)
Use the transformation:
al_use_transform(&transform);
This applies the transformations to all subsequent drawing operations.
3.8. Antialiasing and Texture Smoothing
3.8.1. Explanation
Here, we’ll take a brief look at some Allegro routines that can be applied to improve the look of your graphics and to utilize the graphics processing unit (GPU) on your graphics cards for increased performance.
Antialiasing is a technique used to reduce the visual defects (like jagged edges) that occur when high-resolution images are displayed in a lower resolution. Texture smoothing refers to the process of blending the colours of an image’s pixels to create a smoother transition between them.
3.8.2. Allegro 5 Implementation
In Allegro 5, you enable antialiasing and texture smoothing through the use of bitmap flags and settings in the graphics library.
al_set_new_bitmap_flags(ALLEGRO_MIN_LINEAR | ALLEGRO_MAG_LINEAR);
These flags tell Allegro to use linear filtering for minification and magnification, which results in smoother textures.
In Allegro 5, both antialiasing and texture smoothing are important for creating more visually appealing and polished graphics in your applications.
3.8.3. Antialiasing
Antialiasing is used to reduce the jagged edges that can appear on diagonal or curved lines in digital images, a phenomenon often referred to as “aliasing.” This effect is especially noticeable in lower-resolution graphics or when scaling images up or down.
In Allegro 5, antialiasing can be applied in several ways, but one of the most common methods is through the use of multisampling. Multisampling works by taking multiple samples at different points within each pixel and then averaging these samples to determine the pixel’s colour. This process smooths out the edges of lines and curves.
Enabling Multisample Antialiasing in Allegro 5
To enable multisample antialiasing in Allegro 5, you need to set the appropriate display option before creating your display. Here’s an example:
al_set_new_display_option(ALLEGRO_SAMPLE_BUFFERS, 1, ALLEGRO_SUGGEST);
al_set_new_display_option(ALLEGRO_SAMPLES, 4, ALLEGRO_SUGGEST); // Set to 4 samples
ALLEGRO_DISPLAY* display = al_create_display(width, height);
In this example, ALLEGRO_SAMPLE_BUFFERS is set to 1 to enable multisampling, and ALLEGRO_SAMPLES is set to 4 to use four samples per pixel.
3.8.4. Texture Smoothing
Texture smoothing, also known as filtering, is used to improve the appearance of textures when they are scaled or transformed. Without texture smoothing, textures can appear pixelated or blocky, especially when magnified.
In Allegro 5, texture smoothing is typically achieved through linear filtering. Linear filtering works by interpolating the colours of neighboring pixels to calculate the colour of a pixel when a texture is scaled. This results in a smoother transition between pixels.
Enabling Texture Smoothing in Allegro 5
To enable texture smoothing in Allegro 5, you set bitmap flags before loading or creating your bitmaps. Here’s an example:
al_set_new_bitmap_flags(ALLEGRO_MIN_LINEAR | ALLEGRO_MAG_LINEAR);
ALLEGRO_BITMAP* bitmap = al_load_bitmap("image.png");
In this code, ALLEGRO_MIN_LINEAR and ALLEGRO_MAG_LINEAR are set to enable linear filtering for both minification (making the texture smaller) and magnification (making the texture larger), respectively.
3.9. Shaders
Shaders are programs that run on a computer’s GPU to control the rendering of graphics. They are used for a variety of effects in real-time applications, such as video games.
3.9.1. Allegro 5 Implementation
Allegro 5 supports GLSL (OpenGL Shading Language) shaders. You can load and use shaders to affect rendering in your application:
ALLEGRO_SHADER *shader = al_create_shader(ALLEGRO_SHADER_GLSL);
al_attach_shader_source(shader, ALLEGRO_VERTEX_SHADER, vertex_shader_source);
al_attach_shader_source(shader, ALLEGRO_PIXEL_SHADER, pixel_shader_source);
al_build_shader(shader);
al_use_shader(shader);
3.9.2. Shaders and Their Use in Allegro 5
What Are Shaders?
Shaders are small programs that run on a computer’s graphics processing unit (GPU) and are used to control how graphics are rendered. They offer a high degree of flexibility and power, allowing developers to create a variety of visual effects and control the rendering pipeline in fine detail.
In the context of 2D and 3D graphics, there are mainly two types of shaders:
- 1. Vertex Shaders
These process each vertex of a shape. They can manipulate attributes like position, colour, and texture coordinate of each vertex.
- 2. Fragment Shaders (or Pixel Shaders)
These handle how pixels (fragments) are coloured. They’re used for effects like lighting, colour blending, and texture mapping.
Use of Shaders in Allegro 5
Allegro 5 supports the use of shaders through its integration with OpenGL (and DirectX on Windows). This integration allows you to write custom shaders in GLSL (OpenGL Shading Language) or HLSL (High-Level Shading Language for DirectX) and apply them to your graphics in Allegro.
Shaders in Allegro 5 can be used for a variety of tasks such as:
- Creating Special Effects: Like simulating water, fire, shadows, or other complex visual phenomena
- Image Processing: Like colour transformations, blurring, or edge detection
- Performance Optimization: By offloading complex calculations to the GPU
- Custom Rendering Pipelines: Tailoring the rendering process to specific needs of your application
Basic Steps to Use Shaders in Allegro 5
- 1. Write the Shader Code
You write your vertex and fragment shaders. These can be written as separate files or as strings within your C++ code.
- 2. Load and Compile the Shader
In your application, load the shader source code, compile it, and link it to create a shader program.
- 3. Use the Shader
Bind the shader program before drawing operations to apply its effects.
- 4. Passing Data to Shaders
You can pass data (like variables or textures) to the shaders through “uniforms” and “attributes.”
Example
Here’s an outline of the steps of using a shader in Allegro 5:
// Create a shader
ALLEGRO_SHADER *shader = al_create_shader(ALLEGRO_SHADER_GLSL);
// Load shader source code
al_attach_shader_source(shader, ALLEGRO_VERTEX_SHADER, vertex_shader_source);
al_attach_shader_source(shader, ALLEGRO_PIXEL_SHADER, fragment_shader_source);
// Compile and link the shader
if (!al_build_shader(shader)) {
// Handle error
}
// Use the shader
al_use_shader(shader);
// Your rendering code here
// Reset to default shader
al_use_shader(NULL);
// Clean up
al_destroy_shader(shader);
3.10. Using Premultiplied Alpha in Allegro 5
Premultiplied alpha is a technique used in computer graphics to handle transparency more effectively. In traditional alpha blending, an image has RGB (red, green, blue) channels and an alpha channel, which defines the transparency of each pixel. In premultiplied alpha, the RGB values are already multiplied by the alpha value before blending occurs. This approach simplifies the blending equations and helps resolve common issues associated with regular alpha blending, such as handling semitransparent edges and overlapping transparent objects.
3.10.1. Advantages of Premultiplied Alpha
When working with transparency and blending in graphics programming, especially in game development using Allegro 5, the choice between straight alpha and premultiplied alpha can significantly affect both visual quality and performance. Premultiplied alpha offers several practical advantages:
- Simpler Blending Equations: With RGB values already scaled by alpha, blending calculations become more straightforward, often leading to better performance and fewer rendering artifacts.
- Better Handling of Semitransparent Pixels: Premultiplied alpha reduces visual artifacts around the edges of semitransparent textures, such as halos or dark fringes.
- Improved Overlapping Transparency: It ensures more predictable blending behavior when multiple transparent objects overlap, avoiding darkening or incorrect blending effects.
3.10.2. Using Premultiplied Alpha in Allegro 5
To use premultiplied alpha in Allegro 5, follow these steps:
- 1. Prepare Images with Premultiplied Alpha
Ensure that the RGB values in your image are premultiplied by the alpha channel. This can be done in image editing software or programmatically.
- 2. Set the Correct Blender
Use the following Allegro 5 function to set the blending mode:
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
- ALLEGRO_ADD: Adds source and destination colours.
- ALLEGRO_ALPHA: Uses the source’s alpha channel.
- ALLEGRO_INVERSE_ALPHA: Uses the inverse of the source’s alpha.
- 3. Draw Your Images
After setting the blender, draw your images as usual. Allegro will apply premultiplied alpha blending to all subsequent drawing operations.
Example
// Initialize Allegro and create a display...
// Set the blender for premultiplied alpha
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
// Load a bitmap with premultiplied alpha
ALLEGRO_BITMAP* bitmap = al_load_bitmap("premultiplied_alpha_image.png");
// Draw the bitmap
al_draw_bitmap(bitmap, x, y, 0);
// Reset blender if necessary
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
// Continue with other drawing operations...
This example assumes the bitmap has premultiplied alpha. The al_set_blender routine configures the correct blending mode. More details on Allegro 5 blending modes will be covered in the next chapter.
Exercises, Homework Questions, and Projects
- 1. Basic Bitmap Display
Write a program that loads and displays a bitmap centered on a 1024 × 768 screen. Handle errors if the image fails to load.
- 2. Dynamic Resolution Handling
Modify the basic bitmap display to dynamically adjust to different screen resolutions (e.g., 800 × 600, 1920 × 1080).
- 3. Rotating Sprite
Create a sprite that rotates 45 degrees clockwise each time the space bar is pressed.
- 4. Minimap System
Implement a minimap that shows a scaled-down version of a larger game world. Allow arrow keys to pan the viewport.
- 5. Bitmap vs. PNG Comparison
Write a report comparing BMP and PNG formats in terms of file size, transparency support, and compression.
- 6. Resolution Independence with Transformations
Use transformations to ensure a game scene scales proportionally across resolutions (e.g., 640 × 480 to 1920 × 1080).
- 7. Tinting Effects
Create a program that tints a bitmap red when the R key is pressed and resets to normal when released.
- 8. Panning Large Bitmap
Develop a system to pan across a large bitmap (e.g., 4096 × 4096) using arrow keys, displaying only a 1024 × 768 section.
- 9. Colour Depth Experiment
Convert a 24-bit image to 8-bit using an external tool and discuss visual differences in a short report.
- 10. Border Drawing with VRAM
Programmatically draw a coloured border around a bitmap by directly manipulating VRAM values.
- 11. Shader Colour Inversion
Implement a GLSL shader to invert the colours of a loaded bitmap.
- 12. Rotating Background
Use Allegro transformations to create a continuously rotating background image.
- 13. Frame Animation
Load multiple bitmap frames and cycle through them to simulate animation (e.g., a spinning coin).
- 14. Antialiasing Demonstration
Compare screenshots of a diagonal line rendered with and without antialiasing. Explain the differences.
- 15. Texture Smoothing
Load a low-resolution bitmap, scale it up, and apply linear filtering to observe the smoothing effects.
- 16. Combined Transformations
Rotate and scale a bitmap simultaneously using al_draw_scaled_rotated_bitmap.
- 17. Fade-Out Effect
Simulate a fade-out by gradually reducing the alpha value of a tinted bitmap over time.
- 18. Viewport System
Create a viewport that displays only a 512 × 384 section of a larger game world.
- 19. Palette Impact Analysis
Load the same image with different colour palettes (8-bit vs. 24-bit) and document visual changes.
- 20. Aspect Ratio Maintenance
Allow window resizing while maintaining the game’s aspect ratio using letterboxing.
- 21. Particle System
Simulate a particle effect (e.g., fire) using scaled and rotated bitmaps.
- 22. Premultiplied Alpha Compositing
Load two transparent PNGs and composite them correctly using premultiplied alpha blending.
- 23. Tile-Based Level Editor
Build a tool that lets users place bitmap tiles on a grid and export the layout as a file.
- 24. Transformation Performance Test
Compare the performance of rendering 100 sprites with individual rotations vs. batch transformations.
- 25. Intellectual Property Report
Research and summarize legal considerations when using third-party graphics (e.g., AI-generated art).
- 26. Dynamic Health Bar
Create a health bar that changes colour (green to red) based on a player’s health value.
- 27. Day-Night Cycle
Simulate a day-night cycle by dynamically tinting the screen with varying alpha and RGB values.
- 28. Screenshot Utility
Add a feature to capture the screen and save it as a BMP file when the S key is pressed.
- 29. Parallax Scrolling
Implement a parallax background with three layers scrolling at different speeds.
- 30. Greyscale Shader
Write a fragment shader to convert a bitmap to greyscale and apply it in real time.