Skip to main content

Practical Game Programming: 7. Advanced Topics in Practical Game Programming

Practical Game Programming
7. Advanced Topics in Practical Game Programming
  • Show the following:

    Annotations
    Resources
  • Adjust appearance:

    Font
    Font style
    Color Scheme
    Light
    Dark
    Annotation contrast
    Low
    High
    Margins
  • Search within:
    • My Notes + Comments
    • Notifications
    • Privacy
  • Project HomePractical Game Programming
  • Learn more about Manifold

Notes

table of contents
  1. Cover
  2. Acknowledgements
  3. 1. Introduction
    1. 1.1. Introduction to Video Games and Game Programming
    2. 1.2. History of Video Games
    3. 1.3. Types of Video Games
    4. 1.4. Road Map of Game Development
    5. 1.5. Set Up Game Development IDE
    6. 1.6. The Allegro 5 Game Library
      1. Exercises, Homework Questions, and Projects
  4. 2. Essentials of Game Programming with Allegro
    1. 2.1. Common Structure of a C/C++ Program with Allegro 5
    2. 2.2. Handling User Input
    3. 2.3. Collision Detection
    4. 2.4. Adding Sound Effects to Games
    5. 2.5. Timers and Game Timing
    6. 2.6. Using Files for Games
      1. Exercises, Homework Questions, and Projects
  5. 3. Using Graphics in Games
    1. 3.1. Bitmap Fundamentals
    2. 3.2. Bitmaps Creation and Use in Allegro 5
    3. 3.3. Utilizing Bitmaps in a Program
    4. 3.4. Scaling and Rotating Bitmaps
    5. 3.5. Displaying a Section of a Bitmap and the Concept of a Viewport
    6. 3.6. Advanced Graphics Techniques in Allegro 5
    7. 3.7. How Transformations Help with Resolution Independence
    8. 3.8. Antialiasing and Texture Smoothing
    9. 3.9. Shaders
    10. 3.10. Using Premultiplied Alpha in Allegro 5
      1. Exercises, Homework Questions, and Projects
  6. 4. Programming Sprites in Allegro 5
    1. 4.1. The Fundamentals
    2. 4.2. Additional Sprite Handling Routines
    3. 4.3. Allegro Blending Function and Sprites
    4. 4.4. Advanced Sprite Use in Allegro 5
      1. Exercises, Homework Questions, and Projects
  7. 5. Programming Backgrounds for Video Games
    1. 5.1. Types of Backgrounds
    2. 5.2. Creating and Using Static Backgrounds
    3. 5.3. Creating and Using Scrolling Backgrounds
    4. 5.4. Creating and Using Dynamic Backgrounds
    5. 5.5. Creating and Using 3D Backgrounds
      1. Exercises, Homework Questions, and Projects
  8. 6. Game Design and Development Fundamentals
    1. 6.1. Conceive an Amazing Game
    2. 6.2. Transforming a Billion-Dollar Idea into a Detailed Game Design
    3. 6.3. Implement the Game
      1. Exercises, Homework Questions, and Projects
  9. 7. Advanced Topics in Practical Game Programming
    1. 7.1. Multithreading
    2. 7.2. Custom Shaders
    3. 7.3. Networking for Multiplayer Games
    4. 7.4. AI Programming
    5. 7.5. Optimization Techniques
    6. 7.6. Custom GUI Systems
    7. 7.7. Advanced Audio
      1. Exercises, Homework Questions, and Projects
  10. 8. Game Development Projects
    1. 8.1. Action Game: Space Shooter
      1. Exercises, Homework Questions, and Projects
    2. 8.2. Platform Game: Tank War
      1. Exercises, Homework Questions, and Projects
    3. 8.3. Adventure Game
      1. Exercises, Homework Questions, and Projects
    4. 8.4. Arcade Game
      1. Exercises, Homework Questions, and Projects
    5. 8.5. Board Game
      1. Exercises, Homework Questions, and Projects
    6. 8.6. Emulator Game
      1. Exercises, Homework Questions, and Projects
    7. 8.7. Puzzle Game
      1. Exercises, Homework Questions, and Projects
    8. 8.8. Role-Playing Game
      1. Exercises, Homework Questions, and Projects
    9. 8.9. Sports Game
      1. Exercises, Homework Questions, and Projects
    10. 8.10. Strategy Game
      1. Exercises, Homework Questions, and Projects
    11. 8.11. Utilities Game
      1. Exercises, Homework Questions, and Projects
  11. References and Resources

Chapter 7. 7 Advanced Topics in Practical Game Programming

Allegro 5 is a powerful library for game development in C++. So far, in the previous chapters, we have learned some fundamental knowledge and skills of game programming with Allegro 5 in C/C++. However, there are still some more advanced topics to explore and learn that can have added benefits for enhancing the graphics, sound, and performance aspects of your game.

7.1. Multithreading

Multithreading is essential in video games for several reasons, primarily related to performance and responsiveness.

7.1.1. Key Benefits of Multithreading

The following are some key benefits multithreading can offer to video games:

Improved Performance

This is achieved through:

  • Parallel Processing: Multithreading allows different parts of the game to run simultaneously on multiple CPU cores. This parallel processing can significantly improve performance, especially in complex games with many simultaneous tasks.
  • Efficient Resource Utilization: By distributing tasks across multiple threads, the game can make better use of the available CPU resources, reduce idle time, and increase overall efficiency.

Responsiveness

This is reflected as:

  • Smooth Gameplay: Multithreading helps maintain a smooth and responsive gameplay experience by ensuring that critical tasks, like rendering and input handling, are not delayed by other processes.
  • Reduced Lag: By handling tasks like physics calculations, AI processing, and asset loading on separate threads, the game can reduce lag and provide a more seamless experience.

Scalability

  • Future-Proofing: As hardware evolves and more CPU cores become available, multithreaded games can scale to take advantage of these advancements, ensuring better performance on newer systems.
  • Complex Simulations: Multithreading enables more complex simulations and interactions within the game world, allowing for richer and more detailed environments.

Task Separation

  • Dedicated Threads for Specific Tasks: Different aspects of the game—such as rendering, physics, AI, and audio—can be handled on separate threads. This separation ensures that intensive tasks do not interfere with one another, leading to a more stable and efficient game.

7.1.2. Uses of Multithreading in Games

The main purpose of using multithreading is to speed things up, and it is often used in the following three areas:

  • Rendering: One thread can handle rendering graphics while another manages game logic.
  • Physics: Physics calculations can be offloaded to a separate thread to ensure they do not slow down the main game loop.
  • AI: AI processing can run on its own thread, allowing for more complex and responsive behaviors without impacting frame rates.

7.1.3. Tools and Techniques for Multithreading

  • Threading Libraries: Just as in many game engines and game libraries, Allegro 5 provides built-in support for multithreading.
  • Synchronization: Proper synchronization techniques, like mutexes and semaphores, are crucial to avoid issues like race conditions and deadlocks.
  • Threads: These are independent paths of execution within a program. Use ALLEGRO_THREAD to create and manage threads.
  • Mutexes: These are used to prevent multiple threads from accessing shared resources simultaneously. Use ALLEGRO_MUTEX to create and manage mutexes.
  • Conditions: These synchronize threads by allowing them to wait for certain conditions to be met. Use ALLEGRO_COND for condition variables.

7.1.4. Essential Steps to Implement Multithreading

  1. 1. Include Headers and Initialize Allegro

    As always, ensure your required language library and Allegro add-ons are installed.

  2. 2. Define Shared Data and Mutexes

    Use ALLEGRO_MUTEX to protect shared resources (e.g., game state, player position).

  3. 3. Separate Thread-Safe and Non-Thread-Safe Tasks
    • Main Thread: Handle rendering, input events, and display updates.
    • Worker Threads: Offload tasks like physics, AI, or network communication.
  4. 4. Create Threads with Allegro’s Core API

    Use al_create_thread() to spawn threads and define their entry functions.

  5. 5. Synchronize Threads

    Use al_lock_mutex() and al_unlock_mutex() to safely access shared data.

  6. 6. Cleanup Resources

    Join threads with al_join_thread() and destroy mutexes to avoid leaks.

Multithreading is a powerful tool in game development that, when used correctly, can greatly enhance the performance and experience of a game.

The following example demonstrates how to create and manage threads to perform tasks concurrently:

#include <allegro5/allegro.h>

#include <allegro5/allegro_font.h>

#include <allegro5/allegro_ttf.h>

#include <stdio.h>

#include <stdbool.h>

// Shared mutex for drawing/state updates

ALLEGRO_MUTEX *mutex;

ALLEGRO_FONT *font;

typedef struct {

    int id;              // 1 or 2 (for display)

    int counter;         // per-thread counter

    bool running;        // true while thread loop is active

} ThreadData;

// Worker thread function

void* worker_thread(ALLEGRO_THREAD *thread, void *arg) {

    ThreadData *td = (ThreadData *)arg;

    // mark running

    al_lock_mutex(mutex);

    td->running = true;

    al_unlock_mutex(mutex);

    while (!al_get_thread_should_stop(thread)) {

        // update this thread's own counter

        al_lock_mutex(mutex);

        td->counter++;

        al_unlock_mutex(mutex);

        // simulate some work at different paces so you can see both change

        // Thread 1 = 1.0s, Thread 2 = 0.5s

        al_rest(td->id == 1 ? 1.0 : 0.5);

    }

    // mark stopping

    al_lock_mutex(mutex);

    td->running = false;

    al_unlock_mutex(mutex);

    return NULL;

}

int main() {

    // Allegro init

    if (!al_init()) { fprintf(stderr, "Failed to init Allegro\n"); return—1; }

    al_init_font_addon();

    al_init_ttf_addon();

    al_install_keyboard();

    ALLEGRO_DISPLAY *display = al_create_display(800, 600);

    if (!display) { fprintf(stderr, "Failed to create display\n"); return—1; }

    font = al_load_ttf_font("arial.ttf", 24, 0);

    if (!font) { fprintf(stderr, "Failed to load font arial.ttf\n"); return—1; }

    mutex = al_create_mutex();

    // Prepare two thread data blocks

    ThreadData td[2] = {

        {.id = 1, .counter = 0, .running = false},

        {.id = 2, .counter = 0, .running = false}

    };

    // Create & start two threads

    ALLEGRO_THREAD *threads[2];

    threads[0] = al_create_thread(worker_thread, &td[0]);

    threads[1] = al_create_thread(worker_thread, &td[1]);

    al_start_thread(threads[0]);

    al_start_thread(threads[1]);

    // Event loop setup

    ALLEGRO_EVENT_QUEUE *event_queue = al_create_event_queue();

    ALLEGRO_TIMER *timer = al_create_timer(1.0 / 60);  // 60 FPS

    al_register_event_source(event_queue, al_get_keyboard_event_source());

    al_register_event_source(event_queue, al_get_timer_event_source(timer));

    al_register_event_source(event_queue, al_get_display_event_source(display));

    al_start_timer(timer);

    bool running = true;

    while (running) {

        ALLEGRO_EVENT event;

        al_wait_for_event(event_queue, &event);

        if (event.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {

            running = false;

        } else if (event.type == ALLEGRO_EVENT_KEY_DOWN &&

                   event.keyboard.keycode == ALLEGRO_KEY_ESCAPE) {

            running = false;

        }

        if (event.type == ALLEGRO_EVENT_TIMER ||

            event.type == ALLEGRO_EVENT_DISPLAY_EXPOSE) {

            al_clear_to_color(al_map_rgb(0, 0, 0));

            al_lock_mutex(mutex);

            // Labels show which thread the numbers belong to

            al_draw_textf(font, al_map_rgb(255, 255, 255), 400, 220, ALLEGRO_ALIGN_CENTRE,

                          "Thread 1 Counter: %d", td[0].counter);

            al_draw_textf(font, al_map_rgb(200, 200, 0),   400, 260, ALLEGRO_ALIGN_CENTRE,

                          "Thread 1 Status: %s", td[0].running ? "Running..." : "Stopping");

            al_draw_textf(font, al_map_rgb(255, 255, 255), 400, 320, ALLEGRO_ALIGN_CENTRE,

                          "Thread 2 Counter: %d", td[1].counter);

            al_draw_textf(font, al_map_rgb(200, 200, 0),   400, 360, ALLEGRO_ALIGN_CENTRE,

                          "Thread 2 Status: %s", td[1].running ? "Running..." : "Stopping");

            al_unlock_mutex(mutex);

            al_flip_display();

        }

    }

    // Ask both threads to stop and join them

    al_set_thread_should_stop(threads[0]);

    al_set_thread_should_stop(threads[1]);

    al_join_thread(threads[0], NULL);

    al_join_thread(threads[1], NULL);

    // Cleanup

    al_destroy_thread(threads[0]);

    al_destroy_thread(threads[1]);

    al_destroy_mutex(mutex);

    al_destroy_font(font);

    al_destroy_display(display);

    al_destroy_event_queue(event_queue);

    al_destroy_timer(timer);

    return 0;

}

This example initializes Allegro, creates two threads that run concurrently, and prints messages to the console. Each thread runs a loop that prints its identifier and a counter value, simulating work by sleeping for one second between iterations. The main program waits for both threads to finish before cleaning up and exiting.

7.2. Custom Shaders

Shaders are powerful tools for creating visual effects in games. We’ll use an example to guide you through writing, compiling, and using vertex and fragment shaders with Allegro 5 in C/C++. We’ll create a simple colour-shifting effect and a greyscale filter. We’ll use OpenGL’s functions for doing this process in conjunction with Allegro. First, a short explanation of what’s happening in regard to graphics and rendering—the process of putting objects on a computer screen.

When you send a shape such as a triangle, square, or 3D model to the graphics processor, it moves through a series of stages. First, each vertex, or corner point, of the shape is processed. Then the shape is rasterized, which means it gets broken down into fragments, or potential pixels. Each fragment is shaded; given its colour, lighting, and texture; and finally the surviving fragments are output as the pixels you see on your screen. In OpenGL Shading Language (GLSL), we can write shaders that give us control over both the vertex stage and the fragment stage, making it possible to influence how objects are transformed and how their surfaces appear once displayed.

The vertex shader runs once per vertex and handles the geometry side of the pipeline. Its main job is to transform the vertex from model space into screen space and to pass along data like colour, normals, or texture coordinates for later use. After rasterization, the fragment shader takes over, running once for each fragment of the shape. It determines the final pixel colour, applies lighting or textures, and may even discard fragments if they fail certain tests. Shapes are defined by their vertices, but their shading and visual style come from this fragment stage, where interpolated values smooth across surfaces. Put together, the vertex and fragment shaders allow us to move objects into view and shade them so that they look flat, textured, or even fully realistic, depending on the effect we want.

The other concept in GLSL you’ll see in the code that we need to cover is the use of vectors. In this context, a vector is simply a container that holds multiple related values together—rather than keeping red, green, blue, and alpha as four separate variables, we group them into a single four-component vector called a vec4. This makes it easier to pass colour information around, perform math on it, and keep it organized as a single unit. GLSL provides vec2, vec3, and vec4 types to represent pairs, triples, or quadruples of values, which map naturally to things like texture coordinates, 3D positions, or RGBA colours. We use a vec4 here because each fragment’s colour is made up of four channels, and handling them as a single vector lets the shader process them efficiently while still letting us access the individual components when needed.

7.2.1. Setting Up Allegro 5

First, ensure you have Allegro 5 installed with OpenGL support. Initialize Allegro and its add-ons:

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

#include <allegro5/allegro_opengl.h>

int main() {

    al_init();

    al_init_image_addon();

    al_install_keyboard();

    // Create a display with OpenGL context

    al_set_new_display_flags(ALLEGRO_OPENGL);

    ALLEGRO_DISPLAY *display = al_create_display(800, 600);

    ALLEGRO_BITMAP *texture = al_load_bitmap("example.png");

    // Main loop and cleanup code go here

}

7.2.2. Writing Shaders

Shaders are written in GLSL. Two files need to be created:

// Vertex Shader (vertex.glsl)

version 330 core

layout(location = 0) in vec2 al_pos;       // Allegro's position attribute

layout(location = 1) in vec2 al_texcoord;  // Allegro's texture coordinate attribute

out vec2 frag_uv;

uniform mat4 al_projview_matrix;           // Allegro-provided projection, the camera perspective

      //or 'view' of the scene if you will

void main() {

    frag_uv = al_texcoord;

    gl_Position = al_projview_matrix * vec4(al_pos, 0.0, 1.0);

}

This passes texture coordinates to the fragment shader.

// Fragment Shader (fragment.glsl)

#version 330 core

in vec2 frag_uv;

out vec4 color;

uniform sampler2D tex;

uniform float time; // for a pulsating effect

void main() {

    vec4 original = texture(tex, frag_uv);

    color = original * (0.5 + 0.5 * sin(time));

}

The fragment shader then applies a dynamic effect to those elements.

7.2.3. Compiling and Using Shaders

Allegro provides functions to load and compile shaders:

// Load shader sources from files

const char *vertex_src = al_load_shader_source("vertex.glsl");

const char *fragment_src = al_load_shader_source("fragment.glsl");

// Create and compile shader program

ALLEGRO_SHADER *shader = al_create_shader(ALLEGRO_SHADER_GLSL);

al_attach_shader_source(shader, ALLEGRO_VERTEX_SHADER, vertex_src);

al_attach_shader_source(shader, ALLEGRO_FRAGMENT_SHADER, fragment_src);

// Check for errors

if (!al_build_shader(shader)) {

    fprintf(stderr, "Shader Error: %s\n", al_get_shader_log(shader));

    return—1;

}

7.2.4. Applying Shaders in Rendering in C

float time = 0.0;

bool running = true;

ALLEGRO_EVENT_QUEUE *queue = al_create_event_queue();

al_register_event_source(queue, al_get_keyboard_event_source());

while (running) {

    // Update time for animation

    time += 0.01;

    // Handle input (exit on ESC)

    ALLEGRO_EVENT event;

    while (al_get_next_event(queue, &event)) {

        if (event.type == ALLEGRO_EVENT_KEY_DOWN &&

            event.keyboard.keycode == ALLEGRO_KEY_ESCAPE) {

            running = false;

        }

    }

    // Use the shader

    al_use_shader(shader);

    al_set_shader_float("time", time); // Pass 'time' uniform to shader

    // Draw the texture with the shader applied

    al_draw_bitmap(texture, 0, 0, 0);

    al_flip_display();

    al_clear_to_color(al_map_rgb(0, 0, 0));

}

// Cleanup

al_destroy_shader(shader);

al_destroy_bitmap(texture);

al_destroy_display(display);

7.2.5. Examples of Other Shader Effects

Grayscale Filter

To create a greyscale filter, modify the fragment shader—fragment.glsl—to create averages of the red, green, and blue values (original.r, original.g, and original.b) and apply that to the image:

void main() {

    vec4 original = texture(tex, frag_uv);

    float avg = (original.r + original.g + original.b) / 3.0;

    color = vec4(avg, avg, avg, original.a);

}

Wave Distortion

Wave distortions are applied by the fragment shader code here:

void main() {

    vec2 uv = frag_uv + 0.05 * sin(time + frag_uv.y * 10.0);

    color = texture(tex, uv);

}

Next up is a complete example that will load a texture image on the OpenGL fragment (here a rectangle) and slowly fade the image in and out.

Vertex.glsl

#version 330 core

in vec2 al_pos;         // pixel coords from Allegro

in vec2 al_texcoord;    // UVs from Allegro (usually 0..w, 0..h)

out vec2 frag_uv;

uniform vec2 u_view;    // (display_width, display_height)

void main() {

    frag_uv = al_texcoord;

    // Convert pixels—> Normalized Device Coordinates

    // x: 0..W  ->—1..+1

    // y: 0..H  -> +1..-1  (note the flip)

    vec2 ndc;

    ndc.x = (al_pos.x / u_view.x) * 2.0—1.0;

    ndc.y = 1.0—(al_pos.y / u_view.y) * 2.0;

    gl_Position = vec4(ndc, 0.0, 1.0);

}

fragment.glsl

#version 330 core

out vec4 color;

uniform sampler2D tex;

uniform float time;

void main() {

    vec2 wh = vec2(textureSize(tex, 0));

    vec2 uv = gl_FragCoord.xy / wh;      // 0..1 across the window/texture

    vec4 original = texture(tex, uv);

    color = original * (0.5 + 0.5 * sin(time));

}

Shaders.cpp

The following is a complete example of custom shaders.

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

#include <allegro5/allegro_opengl.h>

#include <cstdio>

#include <fstream>

#include <string>

// Fallback define for some MinGW header sets

#ifndef GL_SHADING_LANGUAGE_VERSION

#define GL_SHADING_LANGUAGE_VERSION 0x8B8C

#endif

static bool file_exists(const char* path) {

    std::ifstream f(path, std::ios::binary);

    return f.good();

}

int main() {

    // Init Allegro

    if (!al_init()) return—1;

    if (!al_init_image_addon()) return—1;

    al_install_keyboard();

    // Display (OpenGL)

    al_set_new_display_flags(ALLEGRO_OPENGL);

    ALLEGRO_DISPLAY* display = al_create_display(800, 600);

    if (!display) return—1;

    //——Required files present? (only checks we keep)——

    if (!file_exists("vertex.glsl"))   { std::fprintf(stderr, "Missing vertex.glsl\n");   return—1; }

    if (!file_exists("fragment.glsl")) { std::fprintf(stderr, "Missing fragment.glsl\n"); return—1; }

    ALLEGRO_BITMAP* texture = al_load_bitmap("example.png");

    if (!texture) { std::fprintf(stderr, "Failed to load example.png\n"); return—1; }

    al_convert_bitmap(texture); // ensure GPU/video bitmap

    // Shader: attach & build

    ALLEGRO_SHADER* shader = al_create_shader(ALLEGRO_SHADER_GLSL);

    if (!shader) return—1;

    if (!al_attach_shader_source_file(shader, ALLEGRO_VERTEX_SHADER, "vertex.glsl")) {

        std::fprintf(stderr, "Vertex attach error: %s\n", al_get_shader_log(shader));

        return—1;

    }

    if (!al_attach_shader_source_file(shader, ALLEGRO_PIXEL_SHADER, "fragment.glsl")) {

        std::fprintf(stderr, "Fragment attach error: %s\n", al_get_shader_log(shader));

        return—1;

    }

    if (!al_build_shader(shader)) {

        std::fprintf(stderr, "Shader build error: %s\n", al_get_shader_log(shader));

        return—1;

    }

    // Events

    ALLEGRO_EVENT_QUEUE* queue = al_create_event_queue();

    if (!queue) return—1;

    al_register_event_source(queue, al_get_keyboard_event_source());

    al_register_event_source(queue, al_get_display_event_source(display));

    // Main loop

    bool running = true;

    double start_time = al_get_time();

    while (running) {

        ALLEGRO_EVENT ev;

        while (al_get_next_event(queue, &ev)) {

            if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) running = false;

            if (ev.type == ALLEGRO_EVENT_KEY_DOWN &&

                ev.keyboard.keycode == ALLEGRO_KEY_ESCAPE) running = false;

        }

        float t = static_cast<float>(al_get_time()—start_time);

        // Bind shader + uniforms

        al_use_shader(shader);

        // u_view = backbuffer size (pixels) for pixel->NDC conversion in vertex.glsl

        ALLEGRO_BITMAP* back = al_get_backbuffer(display);

        float view[2] = {

            (float)al_get_bitmap_width(back),

            (float)al_get_bitmap_height(back)

        };

        al_set_shader_float_vector("u_view", 2, view, 1);

        // bind sampler and time

        al_set_shader_sampler("tex", texture, 1);

        al_set_shader_float("time", t);

        // Draw

        al_clear_to_color(al_map_rgb(0, 0, 0));

        al_draw_bitmap(texture, 0, 0, 0);

        // Unbind and present

        al_use_shader(NULL);

        al_flip_display();

    }

    // Cleanup

    al_destroy_shader(shader);

    al_destroy_bitmap(texture);

    al_destroy_event_queue(queue);

    al_destroy_display(display);

    return 0;

}

7.3. Networking for Multiplayer Games

Multiplayer functionality adds a dynamic and engaging layer to games, enabling players to interact in real time across local or global networks. Implementing networking in a game built with Allegro 5 and C/C++ requires understanding both technical foundations and practical design patterns.

7.3.1. Understanding Networking Models

Before diving into implementation, choose the appropriate networking model:

  • Peer-to-Peer (P2P): Each client communicates directly with others. Suitable for small-scale games but complex to manage synchronization and security.
  • Client-Server: A central server manages game state and communication. This model is more scalable and secure, ideal for most multiplayer games.

7.3.2. Core Concepts

  • Sockets: This is the mechanism for network communication. C/C++ games typically use reliable TCP (Transmission Control Protocol) or fast but less reliable UDP (User Datagram Protocol) sockets.
  • Serialization: Game data (e.g., player positions, actions) must be serialized for transmission.
  • Latency Compensation: This accounts for delays to maintain smooth gameplay.
  • State Synchronization: This ensures all clients have a consistent view of the game world.

7.3.3. Tools and Libraries

Allegro 5 does not include built-in networking support, so external libraries are required:

  • Berkeley Sockets (POSIX): Native C socket programming
  • ENet: Lightweight, reliable UDP library for games
  • Boost.Asio: Asynchronous networking in C++
  • RakNet: High-level networking engine tailored for games

7.3.4. Basic Architecture

Server Responsibilities

  • • Accept incoming connections.
  • • Maintain authoritative game state.
  • • Broadcast updates to clients.

Client Responsibilities

  • • Send player input to the server.
  • • Receive game state updates.
  • • Render the game locally.

7.3.5. Implementation Steps

1. Initialize Networking

Server Example: TCP Socket Setup

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <netinet/in.h>

int main() {

int server_fd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in address;

int addrlen = sizeof(address);

address.sin_family = AF_INET;

address.sin_addr.s_addr = INADDR_ANY;

address.sin_port = htons(12345);

bind(server_fd, (struct sockaddr *)&address, sizeof(address));

listen(server_fd, 3);

printf("Server listening on port 12345...\n");

int client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);

printf("Client connected!\n");

close(client_fd);

close(server_fd);

return 0;

}

2. Define Protocols

Create message formats for player actions and game events. For example:

struct PlayerUpdate {

int id;

float x, y;

};

3. Handle Communication

Use send() and recv() to transmit serialized data.

Client Example: Sending Player Position

#include <arpa/inet.h>

int sock = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in server_addr;

server_addr.sin_family = AF_INET;

server_addr.sin_port = htons(12345);

inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));

PlayerUpdate update = {1, 100.0f, 200.0f};

send(sock, &update, sizeof(update), 0);

Use threads or nonblocking I/O to manage communication without freezing the game loop.

4. Synchronize Game State

Use timestamps or sequence numbers to manage updates. Apply interpolation or prediction to smooth movement.

5. Security Considerations

  • • Validate all incoming data.
  • • Use server-side authority to prevent cheating.
  • • Consider encryption for sensitive data.

7.3.6. Debugging and Testing

  • Simulate Latency: Introduce artificial delays to test responsiveness.
  • Log Traffic: Print or store message logs for analysis.
  • Stress Testing: Connect multiple clients to evaluate performance.

7.3.7. Integration with Allegro

While Allegro 5 handles graphics, input, and audio, networking must be integrated carefully:

  • • Use Allegro’s event system to queue network events.
  • • Synchronize network updates with the game loop to avoid race conditions.
  • • Ensure thread safety when accessing shared resources.

Example: Integrating Network Events with Allegro Loop

ALLEGRO_EVENT_QUEUE *queue = al_create_event_queue();

ALLEGRO_TIMER *timer = al_create_timer(1.0 / 60);

al_register_event_source(queue, al_get_timer_event_source(timer));

al_start_timer(timer);

while (true) {

ALLEGRO_EVENT ev;

al_wait_for_event(queue, &ev);

if (ev.type == ALLEGRO_EVENT_TIMER) {

// Check for network updates

// Update game state

// Render frame

}

}

When developing multiplayer games, begin with a simple local multiplayer prototype using loopback networking (127.0.0.1) before expanding to online play.

By incorporating networking into your Allegro 5 game, you unlock the potential for competitive, cooperative, and community-driven gameplay. Though it introduces complexity, a well-designed networking layer can significantly enhance player engagement and replayability.

7.4. AI Programming

Artificial intelligence (AI) is crucial for creating engaging NPCs (nonplayer characters) in games. In this section, we will learn how to implement pathfinding, finite state machines (FSM), and basic decision-making for enemies in Allegro 5 using C/C++. We’ll create a simple game where an enemy chases the player using the A* pathfinding algorithm.

7.4.1. Setting Up Allegro 5

First, initialize Allegro and load assets (e.g., sprites, maps).

Example Code

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

#include <allegro5/allegro_primitives.h>

#define GRID_SIZE 32 // Tile size for pathfinding grid

int main() {

    al_init();

    al_init_image_addon();

    al_init_primitives_addon();

    al_install_keyboard();

    ALLEGRO_DISPLAY *display = al_create_display(800, 600);

    ALLEGRO_BITMAP *player = al_load_bitmap("player.png");

    ALLEGRO_BITMAP *enemy = al_load_bitmap("enemy.png");

// Game loop code here...

}

7.4.2. Grid-Based Pathfinding with A*

Define the Grid

Create a 2D grid to represent walkable and blocked tiles:

#define MAP_WIDTH 25

#define MAP_HEIGHT 18

// 0 = walkable, 1 = blocked

int grid[MAP_HEIGHT][MAP_WIDTH] = {

    {0,0,0,1,0,0,...}, // Example map data

//...(fill with your own map)

};

A* Algorithm Implementation

We can use the A* algorithm to find the shortest path between two points. Think of each walkable tile as a node. For every node, we store:

  • • Its grid position (x,y)
  • • The cost from start g
  • • Our heuristic guess to the goal h
  • • The total score f = g + h
  • • A pointer to the parent so we can reconstruct the path

We explore nodes using an open set (a priority queue ordered by the smallest f), and a closed set (visited nodes). The heuristic is our “distance guess.”

On grids where movement is limited to four directions (up, down, left, and right), the Manhattan distance, calculated as ∣x1−x2∣ + ∣y1−y2∣, is a good heuristic choice. It is computationally fast, guarantees optimal paths when used with A*, and helps guide the search efficiently toward the goal.

The processing loop here is:

  • pop the best node from the open set → if it’s the goal, rebuild the path via parents →
  • else relax its neighbors, which means:
    • compute tentative new cost g’ = g + move_cost, update neighbor’s g,h,f,parent and
  • if this is better, and push/update it in the open set.

This process is repeated until we either reach the goal or the open set empties.

Key Coding Steps

  1. 1. Define a Node struct to track grid positions and costs.
  2. 2. Use a priority queue to explore nodes.
  3. 3. Calculate heuristic (e.g., Manhattan distance).

Simplified Code

typedef struct Node {

    int x, y;

    int g, h, f; // g = cost from start, h = heuristic, f = g + h

    struct Node *parent;

} Node;

// Priority queue and heuristic functions omitted for brevity

ALLEGRO_PATH *find_path(int start_x, int start_y, int end_x, int end_y) {

// Implement A* logic here...

    // Return path as a list of grid positions

}

7.4.3. Finite State Machine (FSM) for Enemy Behavior

Define states for the enemy AI (e.g., Idle, Chase, Patrol):

typedef enum {

    STATE_IDLE,

    STATE_CHASE,

    STATE_PATROL

} AIState;

typedef struct {

    int x, y; // Current grid position

    AIState state;

    ALLEGRO_PATH *path;

} Enemy;

State Transitions

Update the enemy’s state based on player proximity:

void update_enemy(Enemy *enemy, int player_x, int player_y) {

    float distance = sqrt(pow(enemy->x—player_x, 2) + pow(enemy->y—player_y, 2));

    switch (enemy->state) {

        case STATE_IDLE:

            if (distance < 5) { // If player is close

                enemy->state = STATE_CHASE;

                enemy->path = find_path(enemy->x, enemy->y, player_x, player_y);

            }

            break;

        case STATE_CHASE:

            if (distance > 10) { // If player is far

                enemy->state = STATE_IDLE;

                al_destroy_path(enemy->path);

            }

            break;

        // Add PATROL logic...

    }

}

7.4.4. Integrating AI with Allegro’s Game Loop

Update and Render

In the game loop, update the enemy’s position and draw sprites:

Enemy enemy = {12, 5, STATE_IDLE, NULL}; // Starting position

int player_x = 8, player_y = 10;

while (running) {

// Handle input (move player with arrow keys)...

// Update enemy AI

    update_enemy(&enemy, player_x, player_y);

// Move enemy along path

    if (enemy.state == STATE_CHASE && enemy.path) {

        ALLEGRO_PATH_POINT point;

        al_get_path_point(enemy.path, 0, &point); // Get next step

        enemy.x = (int)(point.x / GRID_SIZE);

        enemy.y = (int)(point.y / GRID_SIZE);

        al_remove_path_point(enemy.path, 0); // Advance path

    }

// Draw everything

    al_clear_to_color(al_map_rgb(0, 0, 0));

    al_draw_bitmap(player, player_x * GRID_SIZE, player_y * GRID_SIZE, 0);

    al_draw_bitmap(enemy, enemy.x * GRID_SIZE, enemy.y * GRID_SIZE, 0);

    al_flip_display();

}

In the following example, only the Idle and Chase states are implemented. The student can add and experiment with adding code for Patrol and changing the grid matrix to add more blocked (not walkable) tiles. Arrow keys move the player, and if moved in close proximity to the enemy object, it will immediately intercept.

GridAI Example

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

#include <allegro5/allegro_primitives.h>

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

#include <queue>

#include <vector>

#include <string>

#include <cstring>

#include <algorithm> // for std::reverse

#define GRID_SIZE 32

#define MAP_WIDTH 25

#define MAP_HEIGHT 18

#define SCREEN_WIDTH (MAP_WIDTH * GRID_SIZE)

#define SCREEN_HEIGHT (MAP_HEIGHT * GRID_SIZE)

typedef struct {

    int x, y;

    int g, h;

    int f;

    int came_from_x, came_from_y;

} Node;

// Comparator to use Node directly in priority queue

struct NodeCompareMinF {

    bool operator()(const Node& a, const Node& b) const {

        return a.f > b.f; // lower f has higher priority

    }

};

typedef enum {

    STATE_IDLE,

    STATE_CHASE

} AIState;

typedef struct {

    float x, y;

    AIState state;

    std::vector<std::pair<int, int>> path;

} Enemy;

int grid[MAP_HEIGHT][MAP_WIDTH] = {

    {0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}

};

int heuristic(int x1, int y1, int x2, int y2) {

    return abs(x1—x2) + abs(y1—y2);

}

bool in_bounds(int x, int y) {

    return x >= 0 && y >= 0 && x < MAP_WIDTH && y < MAP_HEIGHT;

}

std::vector<std::pair<int, int>> find_path(int sx, int sy, int ex, int ey) {

    std::vector<std::pair<int, int>> path;

    std::priority_queue<Node, std::vector<Node>, NodeCompareMinF> open_set;

    bool closed[MAP_HEIGHT][MAP_WIDTH] = {false};

    Node came_from[MAP_HEIGHT][MAP_WIDTH];

    Node start = {sx, sy, 0, heuristic(sx, sy, ex, ey), 0,—1,—1};

    start.f = start.g + start.h;

    open_set.push(start);

    while (!open_set.empty()) {

        Node current = open_set.top();

        open_set.pop();

        if (current.x == ex && current.y == ey) {

            while (current.came_from_x !=—1) {

                path.push_back({current.x, current.y});

                current = came_from[current.came_from_y][current.came_from_x];

            }

            std::reverse(path.begin(), path.end());

            return path;

        }

        closed[current.y][current.x] = true;

        const int dx[4] = {-1, 1, 0, 0};

        const int dy[4] = {0, 0,—1, 1};

        for (int i = 0; i < 4; i++) {

            int nx = current.x + dx[i];

            int ny = current.y + dy[i];

            if (!in_bounds(nx, ny) || closed[ny][nx] || grid[ny][nx] != 0) continue;

            Node neighbor = {nx, ny, current.g + 1, heuristic(nx, ny, ex, ey), 0, current.x, current.y};

            neighbor.f = neighbor.g + neighbor.h;

            came_from[ny][nx] = current;

            open_set.push(neighbor);

        }

    }

    return path;

}

void update_enemy(Enemy &enemy, int player_x, int player_y) {

    float distance = sqrt(pow(enemy.x—player_x, 2) + pow(enemy.y—player_y, 2));

    switch (enemy.state) {

        case STATE_IDLE:

            if (distance < 5) {

                enemy.state = STATE_CHASE;

                enemy.path = find_path((int)enemy.x, (int)enemy.y, player_x, player_y);

            }

            break;

        case STATE_CHASE:

            if (distance > 10) {

                enemy.state = STATE_IDLE;

                enemy.path.clear();

            }

            break;

    }

}

int main() {

    al_init();

    al_install_keyboard();

    al_init_image_addon();

    al_init_primitives_addon();

    ALLEGRO_DISPLAY *display = al_create_display(SCREEN_WIDTH, SCREEN_HEIGHT);

    ALLEGRO_EVENT_QUEUE *queue = al_create_event_queue();

    ALLEGRO_TIMER *timer = al_create_timer(1.0 / 60);

    al_register_event_source(queue, al_get_timer_event_source(timer));

    al_register_event_source(queue, al_get_keyboard_event_source());

    int player_x = 2, player_y = 2;

    Enemy enemy = {10, 10, STATE_IDLE};

    bool redraw = true, running = true;

    al_start_timer(timer);

    while (running) {

        ALLEGRO_EVENT ev;

        al_wait_for_event(queue, &ev);

        if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {

            switch (ev.keyboard.keycode) {

                case ALLEGRO_KEY_ESCAPE:

                    running = false;

                    break;

                case ALLEGRO_KEY_UP:

                    if (in_bounds(player_x, player_y—1) && grid[player_y—1][player_x] == 0) player_y—;

                    break;

                case ALLEGRO_KEY_DOWN:

                    if (in_bounds(player_x, player_y + 1) && grid[player_y + 1][player_x] == 0) player_y++;

                    break;

                case ALLEGRO_KEY_LEFT:

                    if (in_bounds(player_x—1, player_y) && grid[player_y][player_x—1] == 0) player_x—;

                    break;

                case ALLEGRO_KEY_RIGHT:

                    if (in_bounds(player_x + 1, player_y) && grid[player_y][player_x + 1] == 0) player_x++;

                    break;

            }

        } else if (ev.type == ALLEGRO_EVENT_TIMER) {

            update_enemy(enemy, player_x, player_y);

            if (enemy.state == STATE_CHASE && !enemy.path.empty()) {

                enemy.x = enemy.path.front().first;

                enemy.y = enemy.path.front().second;

                enemy.path.erase(enemy.path.begin());

            }

            redraw = true;

        }

        if (redraw && al_is_event_queue_empty(queue)) {

            redraw = false;

            al_clear_to_color(al_map_rgb(0, 0, 0));

            for (int y = 0; y < MAP_HEIGHT; y++) {

                for (int x = 0; x < MAP_WIDTH; x++) {

                    ALLEGRO_COLOR color = grid[y][x] == 1 ? al_map_rgb(100, 100, 100) : al_map_rgb(50, 50, 50);

                    al_draw_filled_rectangle(x * GRID_SIZE, y * GRID_SIZE, (x + 1) * GRID_SIZE, (y + 1) * GRID_SIZE, color);

                }

            }

            al_draw_filled_circle(player_x * GRID_SIZE + GRID_SIZE / 2, player_y * GRID_SIZE + GRID_SIZE / 2, GRID_SIZE / 2—2, al_map_rgb(0, 255, 0));

            al_draw_filled_circle(enemy.x * GRID_SIZE + GRID_SIZE / 2, enemy.y * GRID_SIZE + GRID_SIZE / 2, GRID_SIZE / 2—2, al_map_rgb(255, 0, 0));

            al_flip_display();

        }

    }

    al_destroy_display(display);

    al_destroy_event_queue(queue);

    al_destroy_timer(timer);

    return 0;

}

7.4.5. Advanced AI Techniques

Steering Behaviors—Changing What the AI Does Based on Choices

Here is a coding example that demonstrates basic movement behavior using seek as a potential AI decision. The enemy moves toward a target position by normalizing the direction vector and advancing at a fixed speed:

void seek(Enemy *enemy, int target_x, int target_y) {

    float dx = target_x—enemy->x;

    float dy = target_y—enemy->y;

    float distance = sqrt(dx * dx + dy * dy);

    if (distance > 0) {

        enemy->x += (dx / distance) * speed;

        enemy->y += (dy / distance) * speed;

    }

}

Behavior Trees

For more complex AI, we can use behavior trees to manage decision hierarchies, emulating a decision process. Here, we give the AI the choice to chase the user (seek), but if the user moves out of range (flees), the AI returns to a patrol pattern:

// Example: Patrol—> Check for Player—> Chase

bool should_chase(Enemy *enemy) {

    return (distance_to_player < 5);

}

void update_ai(Enemy *enemy) {

    if (should_chase(enemy)) {

        chase_player(enemy);

    } else {

        patrol(enemy);

    }

}

The following implementation introduces a patrol behavior for the enemy and incorporates state transitions that allow the enemy to chase the player and return to patrolling if the player escapes:

// AI_smooth_seek.cpp

// Patrol (square, 5 tiles per edge)—> Check player distance—> Chase

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

#include <allegro5/allegro_primitives.h>

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

#include <queue>

#include <vector>

#include <string>

#include <cstring>

#include <algorithm> // reverse

#define GRID_SIZE 32

#define MAP_WIDTH 25

#define MAP_HEIGHT 18

#define SCREEN_WIDTH (MAP_WIDTH * GRID_SIZE)

#define SCREEN_HEIGHT (MAP_HEIGHT * GRID_SIZE)

// ──────────────────────────────────────────────────────────

// AI state & enemy

// ──────────────────────────────────────────────────────────

typedef enum {

    STATE_PATROL,

    STATE_CHASE

} AIState;

typedef struct {

    float x, y;                       // grid coords (integers stored in float)

    AIState state;

    std::vector<std::pair<int, int> > path; // used during chase

    // Patrol bookkeeping

    int patrol_dir;    // 0=right, 1=down, 2=left, 3=up

    int patrol_steps;  // steps taken along current edge (0..5)

} Enemy;

// ──────────────────────────────────────────────────────────

// Grid/map helpers

// ──────────────────────────────────────────────────────────

int grid[MAP_HEIGHT][MAP_WIDTH] = {

    {0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},

    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}

};

inline bool in_bounds(int x, int y) {

    return x >= 0 && y >= 0 && x < MAP_WIDTH && y < MAP_HEIGHT;

}

inline bool is_free(int x, int y) {

    return in_bounds(x, y) && grid[y][x] == 0;

}

// Manhattan heuristic

inline int heuristic(int x1, int y1, int x2, int y2) {

    return abs(x1—x2) + abs(y1—y2);

}

// ──────────────────────────────────────────────────────────

// Safe A* (fully initialized parents, guarded reconstruction)

// ──────────────────────────────────────────────────────────

std::vector<std::pair<int,int> > find_path(int sx, int sy, int ex, int ey) {

    std::vector<std::pair<int,int> > path;

    if (!in_bounds(sx, sy) || !in_bounds(ex, ey)) return path;

    if (!is_free(sx, sy) || !is_free(ex, ey)) return path;

    struct Parent { short px, py; };

    const int INF = 1e9;

    const int MAX_EXPANSIONS = MAP_WIDTH * MAP_HEIGHT * 8; // safety cap

    struct QN { int x, y, g, f; };

    struct Cmp { bool operator()(const QN& a, const QN& b) const { return a.f > b.f; } };

    int g_cost[MAP_HEIGHT][MAP_WIDTH];

    bool closed[MAP_HEIGHT][MAP_WIDTH];

    Parent parent[MAP_HEIGHT][MAP_WIDTH];

    for (int y = 0; y < MAP_HEIGHT; ++y) {

        for (int x = 0; x < MAP_WIDTH; ++x) {

            g_cost[y][x] = INF;

            closed[y][x] = false;

            parent[y][x] = {—1,—1 };

        }

    }

    std::priority_queue<QN, std::vector<QN>, Cmp> open;

    g_cost[sy][sx] = 0;

    open.push({ sx, sy, 0, heuristic(sx, sy, ex, ey) });

    const int dx[4] = { 1,—1, 0, 0 };

    const int dy[4] = { 0, 0, 1,—1 };

    int expansions = 0;

    while (!open.empty()) {

        QN cur = open.top(); open.pop();

        if (closed[cur.y][cur.x]) continue;

        closed[cur.y][cur.x] = true;

        if (++expansions > MAX_EXPANSIONS) {

            // safety bail-out → no path

            return std::vector<std::pair<int,int> >();

        }

        if (cur.x == ex && cur.y == ey) {

            // Reconstruct

            int cx = ex, cy = ey;

            std::vector<std::pair<int,int> > rev;

            while (!(cx == sx && cy == sy)) {

                rev.push_back(std::make_pair(cx, cy));

                Parent p = parent[cy][cx];

                if (p.px < 0 || p.py < 0) { // broken parent chain → give up safely

                    rev.clear();

                    break;

                }

                cx = p.px; cy = p.py;

            }

            std::reverse(rev.begin(), rev.end());

            return rev; // may be empty if sx==ex && sy==ey or chain broken

        }

        for (int i = 0; i < 4; ++i) {

            int nx = cur.x + dx[i];

            int ny = cur.y + dy[i];

            if (!is_free(nx, ny) || closed[ny][nx]) continue;

            int tentative_g = cur.g + 1;

            if (tentative_g < g_cost[ny][nx]) {

                g_cost[ny][nx] = tentative_g;

                parent[ny][nx] = { (short)cur.x, (short)cur.y };

                int f = tentative_g + heuristic(nx, ny, ex, ey);

                open.push({ nx, ny, tentative_g, f });

            }

        }

    }

    return path; // empty → no path

}

// ──────────────────────────────────────────────────────────

// Behavior tree–style AI helpers

// ──────────────────────────────────────────────────────────

static inline float dist_to_player(const Enemy* e, int px, int py) {

    float dx = float(e->x)—float(px);

    float dy = float(e->y)—float(py);

    return sqrtf(dx*dx + dy*dy);

}

bool should_chase(Enemy* enemy, int player_x, int player_y) {

    return dist_to_player(enemy, player_x, player_y) < 5.0f;

}

void chase_player(Enemy* enemy, int player_x, int player_y) {

    enemy->path = find_path((int)enemy->x, (int)enemy->y, player_x, player_y);

}

// Patrol: square march, 5 tiles per edge; 90° right turns

void patrol(Enemy* enemy) {

    if (enemy->patrol_steps >= 5) {

        enemy->patrol_steps = 0;

        enemy->patrol_dir = (enemy->patrol_dir + 1) % 4; // right turn

    }

    int nx = (int)enemy->x;

    int ny = (int)enemy->y;

    switch (enemy->patrol_dir) {

        case 0: nx++; break; // right

        case 1: ny++; break; // down

        case 2: nx—; break; // left

        case 3: ny—; break; // up

    }

    if (is_free(nx, ny)) {

        enemy->x = nx;

        enemy->y = ny;

        enemy->patrol_steps++;

    } else {

        // If blocked or out of bounds, force a turn next tick

        enemy->patrol_steps = 5;

    }

}

void update_ai(Enemy* enemy, int player_x, int player_y) {

    if (should_chase(enemy, player_x, player_y)) {

        enemy->state = STATE_CHASE;

        // Only (re)plan when we have no path; keeps things light

        if (enemy->path.empty()) {

            chase_player(enemy, player_x, player_y);

        }

    } else {

        if (enemy->state != STATE_PATROL) {

            enemy->state = STATE_PATROL;

            enemy->patrol_dir = 0;

            enemy->patrol_steps = 0;

            enemy->path.clear();

        }

        patrol(enemy);

    }

}

// ──────────────────────────────────────────────────────────

// Demo loop

// ──────────────────────────────────────────────────────────

int main() {

    al_init();

    al_install_keyboard();

    al_init_image_addon();

    al_init_primitives_addon();

    ALLEGRO_DISPLAY *display = al_create_display(SCREEN_WIDTH, SCREEN_HEIGHT);

    ALLEGRO_EVENT_QUEUE *queue = al_create_event_queue();

    ALLEGRO_TIMER *timer = al_create_timer(1.0 / 8.0); // slow to visualize

    al_register_event_source(queue, al_get_timer_event_source(timer));

    al_register_event_source(queue, al_get_keyboard_event_source());

    al_register_event_source(queue, al_get_display_event_source(display));

    int player_x = 2, player_y = 2;

    Enemy enemy;

    enemy.x = 10; enemy.y = 10;

    enemy.state = STATE_PATROL;

    enemy.patrol_dir = 0;

    enemy.patrol_steps = 0;

    enemy.path.clear();

    bool redraw = true, running = true;

    al_start_timer(timer);

    while (running) {

        ALLEGRO_EVENT ev;

        al_wait_for_event(queue, &ev);

        if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {

            running = false;

        } else if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {

            switch (ev.keyboard.keycode) {

                case ALLEGRO_KEY_ESCAPE: running = false; break;

                case ALLEGRO_KEY_UP:    if (is_free(player_x, player_y-1))—player_y; break;

                case ALLEGRO_KEY_DOWN:  if (is_free(player_x, player_y+1)) ++player_y; break;

                case ALLEGRO_KEY_LEFT:  if (is_free(player_x-1, player_y))—player_x; break;

                case ALLEGRO_KEY_RIGHT: if (is_free(player_x+1, player_y)) ++player_x; break;

            }

        } else if (ev.type == ALLEGRO_EVENT_TIMER) {

            // Update AI (BT root)

            update_ai(&enemy, player_x, player_y);

            // Step along path while chasing

            if (enemy.state == STATE_CHASE) {

                // if already at target, drop back to patrol next update

                if ((int)enemy.x == player_x && (int)enemy.y == player_y) {

                    enemy.state = STATE_PATROL;

                    enemy.patrol_steps = 0;

                    enemy.path.clear();

                } else {

                    if (enemy.path.empty()) {

                        // If no path available (e.g., blocked), try to replan

                        enemy.path = find_path((int)enemy.x, (int)enemy.y, player_x, player_y);

                    }

                    if (!enemy.path.empty()) {

                        enemy.x = enemy.path.front().first;

                        enemy.y = enemy.path.front().second;

                        enemy.path.erase(enemy.path.begin());

                    }

                }

            }

            redraw = true;

        }

        if (redraw && al_is_event_queue_empty(queue)) {

            redraw = false;

            al_clear_to_color(al_map_rgb(0, 0, 0));

            for (int y = 0; y < MAP_HEIGHT; y++) {

                for (int x = 0; x < MAP_WIDTH; x++) {

                    ALLEGRO_COLOR color = grid[y][x] == 1 ? al_map_rgb(100, 100, 100)

                                                          : al_map_rgb(50, 50, 50);

                    al_draw_filled_rectangle(x * GRID_SIZE, y * GRID_SIZE,

                                             (x + 1) * GRID_SIZE, (y + 1) * GRID_SIZE, color);

                }

            }

            // Player (green) and Enemy (red)

            al_draw_filled_circle(player_x * GRID_SIZE + GRID_SIZE / 2,

                                  player_y * GRID_SIZE + GRID_SIZE / 2,

                                  GRID_SIZE / 2—2, al_map_rgb(0, 255, 0));

            al_draw_filled_circle((int)enemy.x * GRID_SIZE + GRID_SIZE / 2,

                                  (int)enemy.y * GRID_SIZE + GRID_SIZE / 2,

                                  GRID_SIZE / 2—2, al_map_rgb(255, 0, 0));

            // Optional: visualize chase path

            /*

            for (size_t i = 0; i < enemy.path.size(); ++i) {

                int px = enemy.path[i].first;

                int py = enemy.path[i].second;

                al_draw_filled_circle(px * GRID_SIZE + GRID_SIZE/2,

                                      py * GRID_SIZE + GRID_SIZE/2,

                                      GRID_SIZE/6, al_map_rgb(255, 128, 0));

            }

            */

            al_flip_display();

        }

    }

    al_destroy_display(display);

    al_destroy_event_queue(queue);

    al_destroy_timer(timer);

    return 0;

}

7.5. Optimization Techniques

Optimization ensures your game runs smoothly and efficiently across various hardware configurations. This section covers some practical techniques to enhance performance (and in some cases, just good resource management options) in Allegro 5, focusing on rendering, memory management, collision detection, and code efficiency.

7.5.1. Efficient Rendering Techniques

Use Texture Atlases

Consider combining multiple sprites into a single texture to reduce draw calls. We’ve seen this concept earlier in the sprite sheets used for animation, but a similar concept can be used for other tasks. You might, for example, put all your sprite images for terrain objects you will use in a game—rock types, ground textures, plants, and so on—in one large image composed of the individual sprite elements. When you need to access an item, you can then utilize a section of the larger image containing the item you want. So here, you load a single resource and utilize parts of it rather than needing multiple loads.

Example

ALLEGRO_BITMAP *atlas = al_load_bitmap("texture_atlas.png");

// Draw a specific region/item from the atlas

al_draw_bitmap_region(

    atlas,

    x, y,        // Source coordinates

    width,       // Region size

    height,

    dest_x,      // Destination coordinates

    dest_y,

    0

);

Batch Drawing

Group similar draw calls to minimize state changes. Here, I’m talking about drawing as many things as possible that share the same GPU state—same texture (atlas), same shader, same blend mode, same transforms—in one go. Every time we switch textures, bind a new shader, tweak blending, or swap target bitmaps, the driver has to reconfigure the pipeline, and that costs time. So instead of drawing one tiny piece at a time and constantly flipping states, we sort our work (e.g., by texture/atlas) and feed the GPU batches of vertices at once.

In Allegro, that process could be, for example, building an array of ALLEGRO_VERTEX that all reference the same atlas, then issuing a single al_draw_prim() to cover the whole set.

Why do this? Because batching keeps the GPU busy doing the fast part (rasterizing triangles) while avoiding the slow part (state churn on the CPU/driver side).

Example

ALLEGRO_VERTEX vertices[4]; // Define vertices for a quad

//...populate vertices...

// Draw all vertices in one call

al_draw_prim(vertices, NULL, atlas, 0, 4, ALLEGRO_PRIM_TRIANGLE_FAN);

So here, we define ALLEGRO_VERTEX vertices[4] for a quad (two triangles) and render in one call with the shared atlas. Here, ALLEGRO_PRIM_TRIANGLE_FAN is just a primitive type constant that tells Allegro how to interpret the list of vertices you’ve passed in. It matches the OpenGL concept of a “triangle fan.”

What a Triangle Fan Is

  • • Imagine you’ve got a central vertex, and every new vertex you add forms a triangle that “fans out” from that center.
  • • With 4 vertices, you can form a quad (two triangles).
  • • With more, you can create convex polygons very efficiently.

For example,

  • • Vertices: [v0, v1, v2, v3]
  • • Triangles drawn:
    • ◦ (v0, v1, v2)
    • ◦ (v0, v2, v3)

So you only need to specify each new vertex once instead of repeating corners like you would in a triangle list.

Now scale that thinking up: Accumulate many quads that all use the same atlas, then call al_draw_prim once for the entire batch. The same idea applies if you’re drawing bitmaps: you can use a texture atlas and al_hold_bitmap_drawing(true) to reduce binds. The net result is fewer state switches, fewer driver round-trips, and measurably better frame times, especially when you’ve got lots of small sprites.

Enable Hardware Rendering

By default, Allegro will give you a hardware-accelerated display, but sometimes it’s worth being explicit with ALLEGRO_OPENGL or ALLEGRO_DIRECT3D. The main reason is control: If you know you’re going to be writing specifically for one of those two—perhaps depending on your development platform of choice or because you’re creating your own shaders, sampling textures per fragment, or using GLSL features—you don’t want Allegro to quietly hand you a Direct3D context on Windows when you want to use OpenGL. By forcing OpenGL (or conversely, Direct3D), you’re guaranteeing that your vertex and fragment shaders will compile and run consistently, and that your fragment operations (like custom lighting, greyscale conversion, or texture effects) behave exactly the way you designed them.

Another case is debugging; it helps to know that for sure you’re working in OpenGL space and can reference vec4, frag_uv, and the rest without worrying about backend differences. In short, the flag isn’t needed for performance, but it’s useful whenever you care about shader compatibility or you want your fragment processing code to behave the same way across platforms.

Specify OpenGL with the flag:

al_set_new_display_flags(ALLEGRO_OPENGL); // Enable OpenGL

ALLEGRO_DISPLAY *display = al_create_display(800, 600);

7.5.2. Memory Management

While systems these days come with plenty of memory, it’s always good coding practice to manage the use and freeing of that resource properly. Memory leaks are a bane of developers to this day, causing games to eventually slow or outright crash, so a little preventive work here can save time and trouble later.

Destroy Unused Resources

Free bitmaps, fonts, and sounds when no longer needed:

ALLEGRO_BITMAP *sprite = al_load_bitmap("sprite.png");

//...use sprite...

al_destroy_bitmap(sprite); // Prevent memory leaks

Reuse Objects

Cache frequently used assets (e.g., fonts, particle effects):

ALLEGRO_FONT *font = al_load_font("arial.ttf", 24, 0);

// Reuse 'font' across multiple menus instead of reloading.

7.5.3. Collision Detection Optimization

Spatial Partitioning with Grids

Here, we can speed up collision checks by binning objects into a coarse grid. Instead of checking every object against every other one (which can, depending on the number of objects, get cumbersome), each object goes into a cell based on its position, and we only test it against objects in its own cell and its eight neighbors. That keeps comparisons local and less intensive CPU-wise.

Divide the game world into cells to limit collision checks:

#define CELL_SIZE 64

int grid[SCREEN_WIDTH / CELL_SIZE][SCREEN_HEIGHT / CELL_SIZE];

// Update grid with object positions

void update_grid(GameObject *obj) {

    int cell_x = obj->x / CELL_SIZE;

    int cell_y = obj->y / CELL_SIZE;

    grid[cell_x][cell_y].add(obj);

}

// Check collisions only within nearby cells

void check_collisions(GameObject *obj) {

    int cell_x = obj->x / CELL_SIZE;

    int cell_y = obj->y / CELL_SIZE;

    for (int i = cell_x—1; i <= cell_x + 1; i++) {

        for (int j = cell_y—1; j <= cell_y + 1; j++) {

            // Check objects in neighboring cells

        }

    }

}

Use Bounding Boxes

Here, we have a concept we looked at earlier: The idea of checking every pixel location of a sprite for a collision (overlap) vs. having a shaped bounding box that would be applicable for the size and shape of sprites in use.

Replace pixel-perfect checks with simplified bounding boxes:

bool collision_check(GameObject *a, GameObject *b) {

    return (a->x < b->x + b->width &&

            a->x + a->width > b->x &&

            a->y < b->y + b->height &&

            a->y + a->height > b->y);

}

7.5.4. Code-Level Optimizations

Here, we have some common coding efficiencies.

Avoid Expensive Operations in Loops

Precompute values outside loops:

// Bad: sqrt() called every iteration

for (int i = 0; i < 1000; i++) {

    distance = sqrt(dx * dx + dy * dy);

}

// Good: Precompute squared distance

float dist_sq = dx * dx + dy * dy;

if (dist_sq < threshold_sq) {...}

Use Efficient Data Structures

Replace linked lists with arrays or spatial grids for faster iteration:

GameObject *objects[MAX_OBJECTS]; // Faster traversal than linked lists

7.5.5. Multithreading

Offload Nonrendering Tasks

Use Allegro’s threading API for tasks like pathfinding or AI:

void* ai_thread(ALLEGRO_THREAD *thread) {

    while (!al_get_thread_should_stop(thread)) {

        update_enemy_ai(); // Run AI logic in parallel

    }

    return NULL;

}

// Start thread

ALLEGRO_THREAD *thread = al_create_thread(ai_thread, NULL);

al_start_thread(thread);

7.5.6. Profiling and Debugging

Every once in a while a developer might want to check on the performance of their code; a typical metric might be the frames per second (fps) your game runs at or the number of dropped frames when not reaching your desired fps. Here, the developers can use small routines in their game for that purpose.

Measure Frame Time

Track frame duration to identify slowdowns:

double prev_time = al_get_time();

while (running) {

    double current_time = al_get_time();

    double delta_time = current_time—prev_time;

    prev_time = current_time;

    if (delta_time > 0.017) { // >60 FPS?

        printf("Frame drop: %.4f ms\n", delta_time * 1000);

    }

}

In the area of debugging code, this can be as simple as printing out to the console screen variable values, including lines to indicate “I’m in this part of the program code,” “Now executing,” or other status-indicating displays.

Use Profiling Tools

Use tools like Valgrind (Linux) or Visual Studio Profiler (Windows) to detect memory leaks or hotspots.

7.5.7. Asset Optimization

Asset optimization reduces file size and runtime cost by using only the assets necessary for the target platform and experience. Choose image formats, resolutions, and compression levels that match your chosen screen sizes and visual fidelity. Use scaled or tiled textures for large backgrounds, sprite atlases to cut draw calls, and vector art for UI where appropriate.

For audio, pick sample rates and bitrates that preserve perceived quality while minimizing size. Test perceptual differences—players rarely notice modest bitrate reductions, so prefer smaller formats when the difference is negligible (for example, lower bitrates for background music or short SFX). Compress and trim audio assets, and stream long music tracks instead of loading them entirely into memory.

Build automated steps into your pipeline to:

  • • Resize and compress images for each target resolution.
  • • Pack sprites and icons into atlases.
  • • Normalize audio levels, compress files, and stream long music tracks instead of loading them into memory.
  • • Strip unused data from assets and export only required variants.

Measure memory use, load times, and visual/audio fidelity on real devices, then iterate until you hit the best balance of performance, storage, and player experience.

Compress Textures

Use compressed formats like .png with al_load_bitmap_flags:

ALLEGRO_BITMAP *sprite = al_load_bitmap_flags(

    "sprite.png",

    ALLEGRO_NO_PREMULTIPLIED_ALPHA // Reduce blending cost

);

Downsample Audio

Convert audio to lower bitrates if high quality isn’t critical:

ALLEGRO_SAMPLE *sound = al_load_sample("sound.wav");

al_reserve_samples(10); // Preload samples to avoid runtime delays

7.5.8. Testing and Scaling

One of the features of the Allegro library is it can run on multiple platforms, and so the possibility exists for it to use unconventional screen sizes. As such, you might want to explore the way your game looks at various resolutions; your 4K HD masterpiece won’t look the same at 1024 or 720. Or perhaps you’ll want to provide an option to players to lower the quality of your game graphics for better performance, a standard feature in all games these days.

Test on low-end hardware and adjust settings dynamically:

void adjust_graphics_quality() {

    if (system_slow) {

        al_set_new_bitmap_flags(ALLEGRO_MIN_LINEAR); // Lower filtering quality

        disable_shaders();

    }

}

7.6. Custom GUI Systems

In this section, we will learn how to build custom graphical user interfaces (GUIs) for games using Allegro 5. We will create reusable components like buttons, sliders, and menus and handle user interactions such as clicks and hover effects. By the end, you’ll have a functional main menu and options screen.

7.6.1. Setting Up the Project

Initialize Allegro and Create a Window

Start by setting up Allegro and creating a display window:

#include <allegro5/allegro.h>

#include <allegro5/allegro_font.h>

#include <allegro5/allegro_ttf.h>

#include <allegro5/allegro_primitives.h>

#define SCREEN_WIDTH 800

#define SCREEN_HEIGHT 600

int main() {

    al_init();

    al_init_font_addon();

    al_init_ttf_addon();

    al_init_primitives_addon();

    al_install_keyboard();

    al_install_mouse();

    ALLEGRO_DISPLAY *display = al_create_display(SCREEN_WIDTH, SCREEN_HEIGHT);

    ALLEGRO_EVENT_QUEUE *event_queue = al_create_event_queue();

    ALLEGRO_FONT *font = al_load_ttf_font("arial.ttf", 24, 0);

    al_register_event_source(event_queue, al_get_display_event_source(display));

    al_register_event_source(event_queue, al_get_mouse_event_source());

    bool running = true;

    // Game loop here...

    al_destroy_display(display);

    return 0;

}

7.6.2. Building a Button Component

Define a Button Struct

Create a reusable Button struct to track position, size, text, and state:

typedef struct {

    int x, y;          // Position

    int width, height; // Size

    char label[50];    // Text label

    bool hovered;      // Hover state

    bool clicked;      // Click state

} Button;

Draw and Update the Button

Add functions to draw the button and check for interactions:

// Draw the button with hover/click effects

void draw_button(Button *button, ALLEGRO_FONT *font) {

    ALLEGRO_COLOR bg_color = button->hovered ? al_map_rgb(100, 100, 255) : al_map_rgb(50, 50, 200);

    if (button->clicked) bg_color = al_map_rgb(200, 50, 50);

    al_draw_filled_rectangle(

        button->x, button->y,

        button->x + button->width,

        button->y + button->height,

        bg_color

    );

    al_draw_text(

        font, al_map_rgb(255, 255, 255),

        button->x + button->width/2,

        button->y + button->height/2—12,

        ALLEGRO_ALIGN_CENTRE,

        button->label

    );

}

// Check if the mouse is over the button

void update_button(Button *button, int mouse_x, int mouse_y, bool mouse_clicked) {

    button->hovered = (

        mouse_x >= button->x &&

        mouse_x <= button->x + button->width &&

        mouse_y >= button->y &&

        mouse_y <= button->y + button->height

    );

    button->clicked = button->hovered && mouse_clicked;

}

7.6.3. Creating a Main Menu

Initialize Buttons

Create buttons for “Start Game” and “Options”:

Button start_button = {300, 200, 200, 50, "Start Game", false, false};

Button options_button = {300, 300, 200, 50, "Options", false, false};

Handle Mouse Input

In the game loop, track mouse events and update buttons:

ALLEGRO_EVENT event;

bool mouse_clicked = false;

int mouse_x = 0, mouse_y = 0;

while (running) {

    al_wait_for_event(event_queue, &event);

    // Handle exit

    if (event.type == ALLEGRO_EVENT_DISPLAY_CLOSE)

        running = false;

    // Track mouse position and clicks

    if (event.type == ALLEGRO_EVENT_MOUSE_AXES) {

        mouse_x = event.mouse.x;

        mouse_y = event.mouse.y;

    }

    if (event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN)

        mouse_clicked = true;

    else

        mouse_clicked = false;

    // Update buttons

    update_button(&start_button, mouse_x, mouse_y, mouse_clicked);

    update_button(&options_button, mouse_x, mouse_y, mouse_clicked);

    // Draw

    al_clear_to_color(al_map_rgb(0, 0, 0));

    draw_button(&start_button, font);

    draw_button(&options_button, font);

    al_flip_display();

}

7.6.4. Adding a Slider Component

Define a Slider Struct

Create a slider for volume control:

typedef struct {

    int x, y;          // Position

    int width;         // Length

    float value;       // 0.0 to 1.0

    bool dragging;     // Interaction state

} Slider;

Draw and Update the Slider

Implement slider logic:

void draw_slider(Slider *slider) {

    // Draw track

    al_draw_filled_rectangle(

        slider->x, slider->y—5,

        slider->x + slider->width, slider->y + 5,

        al_map_rgb(100, 100, 100)

    );

    // Draw thumb

    int thumb_x = slider->x + (slider->width * slider->value);

    al_draw_filled_circle(thumb_x, slider->y, 10, al_map_rgb(200, 200, 200));

}

void update_slider(Slider *slider, int mouse_x, int mouse_y, bool mouse_down) {

    if (mouse_down) {

        // Check if mouse is near the thumb

        int thumb_x = slider->x + (slider->width * slider->value);

        if (abs(mouse_x—thumb_x) < 15 && abs(mouse_y—slider->y) < 15)

            slider->dragging = true;

    } else {

        slider->dragging = false;

    }

    if (slider->dragging) {

        slider->value = (mouse_x—slider->x) / (float)slider->width;

        slider->value = (slider->value < 0) ? 0 : (slider->value > 1) ? 1 : slider->value;

    }

}

7.6.5. Building an Options Screen

Toggle Between Menu States

Add a state variable to switch between screens:

enum GameState { MENU, OPTIONS };

enum GameState current_state = MENU;

Add Sliders to Options Screen

Initialize a volume slider and handle its logic:

Slider volume_slider = {300, 200, 200, 0.5, false};

// In the game loop:

if (current_state == OPTIONS) {

    update_slider(&volume_slider, mouse_x, mouse_y, mouse_clicked);

    draw_slider(&volume_slider);

}

// Transition to options screen when the "Options" button is clicked

if (options_button.clicked)

    current_state = OPTIONS;

7.6.6. Key Techniques Used

  1. 1. Modular Components

    Use structs and functions to create reusable UI elements.

  2. 2. Input Handling

    Track mouse position and clicks to update UI states.

  3. 3. State Management

    Use enums to switch between different screens (e.g., menu, options).

  4. 4. Visual Feedback

    Change colours or positions to indicate hover/click states.

The following example demonstrates these concepts but is missing one element, the ability to go “back” to the main screen if you select options. That we’ll leave for you, the student, to implement.

GUI Example

#include <allegro5/allegro.h>

#include <allegro5/allegro_font.h>

#include <allegro5/allegro_ttf.h>

#include <allegro5/allegro_primitives.h>

#include <stdio.h>

#include <string.h>

#include <math.h>

#define SCREEN_WIDTH 800

#define SCREEN_HEIGHT 600

typedef struct {

    int x, y;

    int width, height;

    char label[50];

    bool hovered;

    bool clicked;

} Button;

typedef struct {

    int x, y;

    int width;

    float value;

    bool dragging;

} Slider;

void draw_button(Button *button, ALLEGRO_FONT *font) {

    ALLEGRO_COLOR bg_color = button->hovered ? al_map_rgb(100, 100, 255) : al_map_rgb(50, 50, 200);

    if (button->clicked) bg_color = al_map_rgb(200, 50, 50);

    al_draw_filled_rectangle(button->x, button->y, button->x + button->width, button->y + button->height, bg_color);

    al_draw_text(font, al_map_rgb(255, 255, 255), button->x + button->width/2, button->y + button->height/2—12, ALLEGRO_ALIGN_CENTRE, button->label);

}

void update_button(Button *button, int mouse_x, int mouse_y, bool mouse_clicked) {

    button->hovered = (mouse_x >= button->x && mouse_x <= button->x + button->width && mouse_y >= button->y && mouse_y <= button->y + button->height);

    button->clicked = button->hovered && mouse_clicked;

}

void draw_slider(Slider *slider) {

    al_draw_filled_rectangle(slider->x, slider->y—5, slider->x + slider->width, slider->y + 5, al_map_rgb(100, 100, 100));

    int thumb_x = slider->x + (slider->width * slider->value);

    al_draw_filled_circle(thumb_x, slider->y, 10, al_map_rgb(200, 200, 200));

}

void update_slider(Slider *slider, int mouse_x, int mouse_y, bool mouse_down) {

    int thumb_x = slider->x + (slider->width * slider->value);

    if (mouse_down) {

        if (abs(mouse_x—thumb_x) < 15 && abs(mouse_y—slider->y) < 15)

            slider->dragging = true;

    } else {

        slider->dragging = false;

    }

    if (slider->dragging) {

        slider->value = (mouse_x—slider->x) / (float)slider->width;

        if (slider->value < 0) slider->value = 0;

        if (slider->value > 1) slider->value = 1;

    }

}

int main() {

    al_init();

    al_init_font_addon();

    al_init_ttf_addon();

    al_init_primitives_addon();

    al_install_keyboard();

    al_install_mouse();

    ALLEGRO_DISPLAY *display = al_create_display(SCREEN_WIDTH, SCREEN_HEIGHT);

    ALLEGRO_EVENT_QUEUE *event_queue = al_create_event_queue();

    ALLEGRO_FONT *font = al_load_ttf_font("arial.ttf", 24, 0);

    ALLEGRO_TIMER *timer = al_create_timer(1.0 / 60);

    al_register_event_source(event_queue, al_get_display_event_source(display));

    al_register_event_source(event_queue, al_get_mouse_event_source());

    al_register_event_source(event_queue, al_get_timer_event_source(timer));

    Button start_button = {300, 200, 200, 50, "Start Game", false, false};

    Button options_button = {300, 300, 200, 50, "Options", false, false};

    Slider volume_slider = {300, 200, 200, 0.5, false};

    enum GameState { MENU, OPTIONS };

    enum GameState current_state = MENU;

    int mouse_x = 0, mouse_y = 0;

    bool mouse_clicked = false;

    bool running = true;

    bool redraw = true;

    al_start_timer(timer);

    while (running) {

        ALLEGRO_EVENT event;

        al_wait_for_event(event_queue, &event);

        if (event.type == ALLEGRO_EVENT_DISPLAY_CLOSE)

            running = false;

        if (event.type == ALLEGRO_EVENT_MOUSE_AXES) {

            mouse_x = event.mouse.x;

            mouse_y = event.mouse.y;

        }

        if (event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN) {

            mouse_clicked = true;

        } else if (event.type == ALLEGRO_EVENT_MOUSE_BUTTON_UP) {

            mouse_clicked = false;

        }

        if (event.type == ALLEGRO_EVENT_TIMER) {

            if (current_state == MENU) {

                update_button(&start_button, mouse_x, mouse_y, mouse_clicked);

                update_button(&options_button, mouse_x, mouse_y, mouse_clicked);

                if (options_button.clicked)

                    current_state = OPTIONS;

            } else if (current_state == OPTIONS) {

                update_slider(&volume_slider, mouse_x, mouse_y, mouse_clicked);

            }

            redraw = true;

        }

        if (redraw && al_is_event_queue_empty(event_queue)) {

            redraw = false;

            al_clear_to_color(al_map_rgb(0, 0, 0));

            if (current_state == MENU) {

                draw_button(&start_button, font);

                draw_button(&options_button, font);

            } else if (current_state == OPTIONS) {

                draw_slider(&volume_slider);

                al_draw_textf(font, al_map_rgb(255, 255, 255), 300, 250, 0, "Volume: %.0f%%", volume_slider.value * 100);

            }

            al_flip_display();

        }

    }

    al_destroy_font(font);

    al_destroy_display(display);

    al_destroy_event_queue(event_queue);

    al_destroy_timer(timer);

    return 0;

}

7.7. Advanced Audio

Allegro 5 provides robust audio capabilities for games, including sample mixing, spatial effects, and dynamic audio control. This section covers advanced techniques like 3D sound positioning, audio effects, streaming, and real-time adjustments to elevate your game’s audio experience.

7.7.1. Initialization and Basic Setup

Install Audio Add-Ons

Initialize Allegro’s audio system and codecs to load formats like WAV, OGG, and FLAC:

#include <allegro5/allegro.h>

#include <allegro5/allegro_audio.h>

#include <allegro5/allegro_acodec.h>

int main() {

    al_init();

    al_install_audio(); // Initialize audio subsystem

    al_init_acodec_addon(); // Enable codecs

    al_reserve_samples(16); // Reserve 16 sample instances for mixing

    // Load a sound effect

    ALLEGRO_SAMPLE *laser_sound = al_load_sample("laser.wav");

    if (!laser_sound) {

        fprintf(stderr, "Failed to load sound!\n");

        return—1;

    }

    // Play the sound

    al_play_sample(laser_sound, 1.0, 0.0, 1.0, ALLEGRO_PLAYMODE_ONCE, NULL);

    // Cleanup

    al_destroy_sample(laser_sound);

    al_uninstall_audio();

    return 0;

}

7.7.2. Audio Mixing and Channels

Play Multiple Sounds Simultaneously

Use reserved sample instances to layer sounds (e.g., explosions and background music):

ALLEGRO_SAMPLE *explosion = al_load_sample("explosion.wav");

ALLEGRO_SAMPLE *music = al_load_sample("music.ogg");

// Play music on loop

al_play_sample(music, 0.5, 0.0, 1.0, ALLEGRO_PLAYMODE_LOOP, NULL);

// Play explosion once

al_play_sample(explosion, 1.0, 0.0, 1.0, ALLEGRO_PLAYMODE_ONCE, NULL);

Adjust Volume and Panning Dynamically

Modify volume and pan during playback using al_set_sample_instance_gain and al_set_sample_instance_pan:

ALLEGRO_SAMPLE_INSTANCE *music_instance = al_create_sample_instance(music);

al_attach_sample_instance_to_mixer(music_instance, al_get_default_mixer());

// Set volume to 50% and pan slightly to the left

al_set_sample_instance_gain(music_instance, 0.5);

al_set_sample_instance_pan(music_instance,—0.3);

al_play_sample_instance(music_instance);

7.7.3. Spatial Audio (3D Sound)

Calculate Positional Audio

Adjust pan and volume based on the listener and sound source positions:

typedef struct {

    float x, y; // Sound source position

} SoundSource;

void update_spatial_audio(SoundSource *source, float listener_x, float listener_y) {

    float dx = source->x—listener_x;

    float dy = source->y—listener_y;

    float distance = sqrt(dx * dx + dy * dy);

    // Pan (-1.0 = left, 1.0 = right)

    float pan = (dx / 1000.0); // Adjust divisor for sensitivity

    // Volume attenuation (max distance = 500 pixels)

    float volume = 1.0—(distance / 500.0);

    volume = (volume < 0) ? 0 : volume;

    al_set_sample_instance_pan(sound_instance, pan);

    al_set_sample_instance_gain(sound_instance, volume);

}

Example Usage

SoundSource monster = {400, 300};

float player_x = 200, player_y = 200;

// Update audio every frame

update_spatial_audio(&monster, player_x, player_y);

7.7.4. Audio Effects and Filters

Apply Low-Pass Filter (Simulate Underwater Effect)

Use a custom mixer and adjust frequency to mimic muffled sounds:

ALLEGRO_MIXER *lowpass_mixer = al_create_mixer(44100, ALLEGRO_AUDIO_DEPTH_FLOAT32, ALLEGRO_CHANNEL_CONF_2);

ALLEGRO_MIXER *default_mixer = al_get_default_mixer();

// Attach the low-pass mixer to the default mixer

al_attach_mixer_to_mixer(lowpass_mixer, default_mixer);

// Lower the frequency cutoff (simulate underwater)

al_set_mixer_frequency(lowpass_mixer, 2000); // Normal is 44100 Hz

// Play sounds through the low-pass mixer

al_attach_sample_instance_to_mixer(sound_instance, lowpass_mixer);

7.7.5. Streaming Audio for Music

Load and Play Background Music

Stream large audio files without loading them entirely into memory:

ALLEGRO_AUDIO_STREAM *music_stream = al_load_audio_stream("music.ogg", 4, 2048);

if (!music_stream) {

    fprintf(stderr, "Failed to load music stream!\n");

    return—1;

}

al_set_audio_stream_playmode(music_stream, ALLEGRO_PLAYMODE_LOOP);

al_attach_audio_stream_to_mixer(music_stream, al_get_default_mixer());

Fade-Out Music

Gradually reduce volume over time:

float volume = 1.0;

while (volume > 0) {

    volume—= 0.01;

    al_set_audio_stream_gain(music_stream, volume);

    al_rest(0.1);

}

al_stop_audio_stream(music_stream);

7.7.6. Real-Time Audio Control

Pause/Resume Audio

al_set_audio_stream_playing(music_stream, false); // Pause

al_set_audio_stream_playing(music_stream, true);  // Resume

Dynamic Pitch Shifting

Alter playback speed for effects like slow motion:

al_set_sample_instance_speed(sound_instance, 0.5); // Play at half speed

7.7.7. Best Practices in Using Advanced Audio

Advanced audio has both pros and cons. Please remember to follow the best practices:

  • Limit Active Samples: Use al_reserve_samples() to avoid audio dropouts.
  • Preload Frequently Used Sounds: Reduce latency during gameplay.
  • Use Spatial Audio Sparingly: Use only where needed (calculations can be CPU-intensive).
  • Test on Multiple Devices: Ensure compatibility with different sound cards.

This example program demonstrates the items covered previously:

#include <allegro5/allegro.h>

#include <allegro5/allegro_audio.h>

#include <allegro5/allegro_acodec.h>

#include <allegro5/allegro_font.h>

#include <allegro5/allegro_ttf.h>

#include <math.h>

#include <stdio.h>

#define SCREEN_WIDTH 800

#define SCREEN_HEIGHT 600

typedef struct { float x, y; } SoundSource;

void update_spatial_audio(ALLEGRO_SAMPLE_INSTANCE *instance,

                          SoundSource *source,

                          float listener_x, float listener_y)

{

    float dx = source->x—listener_x;

    float dy = source->y—listener_y;

    float distance = sqrtf(dx*dx + dy*dy);

    float pan    = dx / 1000.0f;              // crude left/right balance

    float volume = 1.0f—(distance / 500.0f);

    if (volume < 0) volume = 0;

    al_set_sample_instance_pan (instance, pan);

    al_set_sample_instance_gain(instance, volume);

}

int main()

{

    al_init();

    al_install_keyboard();

    al_install_audio();

    al_init_acodec_addon();

    al_init_font_addon();

    al_init_ttf_addon();

    al_reserve_samples(16);

    ALLEGRO_DISPLAY *display = al_create_display(SCREEN_WIDTH, SCREEN_HEIGHT);

    ALLEGRO_FONT    *font    = al_load_ttf_font("arial.ttf", 20, 0);

    ALLEGRO_EVENT_QUEUE *queue = al_create_event_queue();

    ALLEGRO_TIMER  *timer   = al_create_timer(1.0 / 60);

    al_register_event_source(queue, al_get_keyboard_event_source());

    al_register_event_source(queue, al_get_display_event_source(display));

    al_register_event_source(queue, al_get_timer_event_source(timer));

    ALLEGRO_SAMPLE *laser      = al_load_sample("laser.wav");

    ALLEGRO_SAMPLE *explosion  = al_load_sample("explosion.wav");

    ALLEGRO_SAMPLE *music      = al_load_sample("music.ogg");

    ALLEGRO_SAMPLE_INSTANCE *music_instance = al_create_sample_instance(music);

    al_attach_sample_instance_to_mixer(music_instance, al_get_default_mixer());

    ALLEGRO_AUDIO_STREAM *music_stream =

        al_load_audio_stream("music.ogg", 4, 2048);

    if (music_stream) {

        al_set_audio_stream_playmode(music_stream, ALLEGRO_PLAYMODE_LOOP);

        al_attach_audio_stream_to_mixer(music_stream, al_get_default_mixer());

    }

    SoundSource monster = {400, 300};

    float player_x = 200, player_y = 200;

    bool running = true, fade_out = false;

    float volume = 1.0f;

    bool redraw = true;

    al_start_timer(timer);

    while (running) {

        ALLEGRO_EVENT ev;

        al_wait_for_event(queue, &ev);

        if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE)

            running = false;

        if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {

            switch (ev.keyboard.keycode) {

    case ALLEGRO_KEY_0:  // Stop/Reset everything started by 1–9

    fade_out = false;                 // cancel pending fade

    volume   = 1.0f;                  // reset our fade volume

       al_stop_samples();                // stop all one-shot/looped samples

    if (music_stream) {

         al_set_audio_stream_gain(music_stream, 1.0f);

al_set_audio_stream_playing(music_stream, false);  // stop/pause stream

                }

if (music_instance) {

    al_set_sample_instance_speed(music_instance, 1.0f);

al_stop_sample_instance(music_instance);           // stop the instance

                        }

                    break;

                case ALLEGRO_KEY_1:

                    al_play_sample(laser, 1.0, 0.0, 1.0,

                                   ALLEGRO_PLAYMODE_ONCE, NULL);

                    break;

                case ALLEGRO_KEY_2:

                    al_play_sample(music, 0.5, 0.0, 1.0,

                                   ALLEGRO_PLAYMODE_LOOP, NULL);

                    al_play_sample(explosion, 1.0, 0.0, 1.0,

                                   ALLEGRO_PLAYMODE_ONCE, NULL);

                    break;

                case ALLEGRO_KEY_3:

                    al_set_sample_instance_gain(music_instance, 0.5);

                    al_set_sample_instance_pan (music_instance,—0.3);

                    al_play_sample_instance(music_instance);

                    break;

                case ALLEGRO_KEY_4:

                    update_spatial_audio(music_instance, &monster,

                                         player_x, player_y);

                    break;

                case ALLEGRO_KEY_5:

                    if (music_instance) {

                        ALLEGRO_MIXER *lowpass =

                            al_create_mixer(44100,

                                            ALLEGRO_AUDIO_DEPTH_FLOAT32,

                                            ALLEGRO_CHANNEL_CONF_2);

                        al_attach_mixer_to_mixer(lowpass,

                                                 al_get_default_mixer());

                        al_set_mixer_frequency(lowpass, 2000);

                        al_attach_sample_instance_to_mixer(music_instance,

                                                           lowpass);

                        al_play_sample_instance(music_instance);

                    }

                    break;

                case ALLEGRO_KEY_6:

                    if (music_stream)

                        al_set_audio_stream_playing(music_stream, true);

                    break;

                case ALLEGRO_KEY_7:

                    fade_out = true;

                    break;

                case ALLEGRO_KEY_8:

                    if (music_stream)

                        al_set_audio_stream_playing(music_stream, false); // Pause

                    break;

                case ALLEGRO_KEY_9:

                    al_set_sample_instance_speed(music_instance, 0.5); // slow

                    break;

                case ALLEGRO_KEY_ESCAPE:

                    running = false;

                    break;

            }

        }

        /*——Fade-out handler—————————————————————*/

        if (fade_out && music_stream && volume > 0.0f) {

            volume—= 0.01f;

            al_set_audio_stream_gain(music_stream, volume);

            if (volume <= 0.0f) {

                fade_out = false;

                al_set_audio_stream_playing(music_stream, false);  // ← FIX

            }

        }

        if (ev.type == ALLEGRO_EVENT_TIMER)

            redraw = true;

        if (redraw && al_is_event_queue_empty(queue)) {

            redraw = false;

            al_clear_to_color(al_map_rgb(0, 0, 0));

            const char *lines[] = {

                "Press keys 1–9 to play audio examples:",

                "1. Play Sound Effect (Laser)",

                "2. Mix Sound Effects (Explosion + Looping Music)",

                "3. Volume/Pan Example",

                "4. Spatial Audio Based on Position",

                "5. Simulate Underwater with Low-pass",

                "6. Stream Audio (Music)",

                "7. Fade Out Music",

                "8. Pause Music Stream",

                "9. Play Slow Motion Effect",

                "0. Stop / Reset (cancel fade, stop all audio)",

                "ESC to Quit"

            };

           al_clear_to_color(al_map_rgb(0,0,0));

            int nlines = (int)(sizeof(lines) / sizeof(lines[0]));

            for (int i = 0; i < nlines; ++i) {

            al_draw_text(font, al_map_rgb(255,255,255), 20, 20 + i*30, 0, lines[i]);

            }

            al_flip_display();

        }

    }

    /*——Cleanup————————————————————————————*/

    al_destroy_sample(laser);

    al_destroy_sample(explosion);

    al_destroy_sample(music);

    al_destroy_sample_instance(music_instance);

    if (music_stream) al_destroy_audio_stream(music_stream);

    al_destroy_font(font);

    al_destroy_timer(timer);

    al_destroy_event_queue(queue);

    al_destroy_display(display);

    al_uninstall_audio();

    return 0;

}

Exercises, Homework Questions, and Projects

Multithreading

  1. 1. Implement a Dual-Threaded Game Loop: Create a program where one thread handles rendering and input, while a second thread updates game logic (e.g., moving a sprite). Use mutexes to synchronize shared variables like sprite position.
  2. 2. Race Condition Demonstration: Write a program where two threads increment a shared counter without mutexes. Explain why the final value is inconsistent. Fix it using ALLEGRO_MUTEX.
  3. 3. Thread Pool for Physics: Simulate 100 bouncing balls. Use a thread pool to parallelize collision detection and position updates. Compare performance with a single-threaded approach.
  4. 4. Deadlock Scenario: Create a deadlock using two mutexes and threads. Document how to resolve it.

Custom Shaders

  1. 5. Dynamic Colour Shader: Modify the fragment shader to pulse a sprite’s colour based on a sine wave controlled by a time uniform.
  2. 6. Greyscale Transition: Implement a shader that gradually converts a texture to greyscale when the player presses a key.
  3. 7. Screen Distortion Effect: Create a fragment shader that warps the screen using a noise function or sine wave.
  4. 8. Health-Based Shader: Change a character’s appearance (e.g., adding a red tint) based on health value passed from C++ to the shader.

Networking

  1. 9. Multiplayer Pong: Build a TCP-based Pong game where the server manages ball physics and two clients control paddles.
  2. 10. Chat System with UDP: Create a UDP-based chat program where messages are broadcasted to all clients. Compare reliability with TCP.
  3. 11. Lag Compensation: Simulate network latency in a multiplayer game and implement interpolation to smooth player movement.
  4. 12. Lobby System: Extend the server to track player usernames and readiness status before starting a game.

AI Programming

  1. 13. A Pathfinding Visualization: Render a grid-based map with obstacles. Let the player click start/end points, and display the A path.
  2. 14. Finite State Machine for NPCs: Create an enemy that patrols, chases the player if within range, and returns to patrol when out of range.
  3. 15. Flocking Behavior: Implement Boid algorithms (separation, alignment, cohesion) for a group of creatures.
  4. 16. Behavior Tree for Boss AI: Design a boss that cycles through attack patterns (e.g., charge, shoot, retreat) using a decision tree.

Optimization

  1. 17. Texture Atlas Generator: Write a tool to combine multiple sprites into a single texture atlas and update rendering code to use it.
  2. 18. Collision Detection Grid: Implement spatial partitioning to limit collision checks to nearby objects. Benchmark performance gains.
  3. 19. Memory Leak Detector: Use Valgrind or Allegro’s debug tools to identify and fix leaks in a provided buggy code sample.
  4. 20. Dynamic Resolution Scaling: Adjust rendering resolution dynamically based on frame time to maintain 60 fps.

GUI Systems

  1. 21. Pause Menu: Create a pause menu with buttons to resume, adjust volume, or quit. Use custom button components.
  2. 22. Inventory System: Design a drag-and-drop inventory grid with item tool tips.
  3. 23. Settings Screen: Add sliders for volume, resolution, and key bindings. Save settings to a file.
  4. 24. Dialogue Boxes: Implement modal pop-ups for confirmation (e.g., “Are you sure you want to quit?”).

Advanced Audio

  1. 25. 3D Audio Demo: Simulate a sound source moving around the player. Adjust pan and volume based on relative position.
  2. 26. Dynamic Music Mixer: Layer background music with intensity-based tracks (e.g., calm vs. combat music).
  3. 27. Echo Effect: Apply a delay filter to audio samples using Allegro’s mixer API.
  4. 28. Voice Chat System: Stream microphone input between clients over UDP.

Comprehensive Projects

  1. 29. Tower Defense Game
    • • Use multithreading for pathfinding and wave updates.
    • • Implement GUI for tower placement and upgrades.
    • • Add shaders for projectile effects.
    • • Include networked co-op mode.
  2. 30. RPG Engine
    • • Create a state machine for quest-driven NPCs.
    • • Optimize rendering with batch drawing.
    • • Add spatial audio for footsteps and ambient sounds.
    • • Design a skill tree using custom GUI components.

Annotate

Next Chapter
8. Game Development Projects
PreviousNext
This work is licensed under a Creative Commons License (CC BY-NC-SA 4.0), except where otherwise noted. This license allows users to copy and redistribute the material in any medium or format and to remix, transform, and build upon the material as long as the original source is properly credited, the work is not used for commercial purposes, and the new creation is licensed under the same terms.
Powered by Manifold Scholarship. Learn more at
Opens in new tab or windowmanifoldapp.org