Skip to content

Allegro Vivace: Graphics

Doug Thompson edited this page Jan 20, 2019 · 66 revisions

◀ Basic game structure


It's time to make our program output something that doesn't make you want to scream "Cripes! My computer's being assimilated by the ghost of the Amiga!"

In other words, we're going to put something more interesting on the screen. Meet Mysha:

If you've had a look through Allegro's example programs, you may already be familiar.

Our program is soon to feature this rodent, so download the above image to the same directory as your hello.c file. Make sure it's saved as mysha.png.

Loading and displaying images

Load the image as part of your program's initialization by adding the following code before the main while loop starts:

ALLEGRO_BITMAP* mysha = al_load_bitmap("mysha.png");
if(!mysha)
{
    printf("couldn't load mysha\n");
    return 1;
}

At the end of the program, add a corresponding call to al_destroy_bitmap():

al_destroy_bitmap(mysha);

al_destroy_font(font);
al_destroy_display(disp);
al_destroy_timer(timer);
al_destroy_event_queue(queue);

Then compile and run your program. However, you should prepare yourself for disappointment; the program's window will immediately disappear and you'll note the printf() doing its job in your terminal window:

couldn't load mysha

The reason being: we need to use the image addon. You'll need to get used to applying addons as needed, and what better reason to learn than for a fluffy albino scavenger?

Applying an addon

Add allegro5/allegro_image.h as an include; you should now have four:

#include <stdio.h>
#include <allegro5/allegro5.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_image.h>

Secondly, before the call to al_load_bitmap(), you'll need to use al_init_image_addon():

if(!al_init_image_addon())
{
    printf("couldn't initialize image addon\n");
    return 1;
}

ALLEGRO_BITMAP* mysha = al_load_bitmap("mysha.png");
// ...

Additionally, you'll need to keep a slightly-altered "compile and run" command to hand, as you'll need to instruct it to link the image addon.

If you're using Visual Studio, there's a GUI option for this. If not, this will most likely do the trick:

gcc hello.c -o hello $(pkg-config allegro-5 allegro_font-5 allegro_image-5 --libs --cflags)
./hello

Displaying the image

Now compile and run the program. There's Mysha!

Psyche - you've only loaded her. You must now render her! Update your render code by adding a call to al_draw_bitmap() above al_flip_display():

al_clear_to_color(al_map_rgb(0, 0, 0));
al_draw_text(font, al_map_rgb(255, 255, 255), 0, 0, 0, "Hello world!");

al_draw_bitmap(mysha, 100, 100, 0);

al_flip_display();

Then compile and run again. Mysha should grace you with her presence.

Try:

  • ...changing Mysha's location in the window.
  • ...moving Mysha to the same area as the "hello world" text.
  • ...making the text appear on top of Mysha (it'll currently be underneath).
  • ...changing Mysha's color.

Grab the code below once you're finished messing about.

View source
#include <stdio.h>
#include <stdlib.h>
#include <allegro5/allegro5.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_image.h>

void must_init(bool test, const char *description)
{
    if(test) return;

    printf("couldn't initialize %s\n", description);
    exit(1);
}

int main()
{
    must_init(al_init(), "allegro");
    must_init(al_install_keyboard(), "keyboard");

    ALLEGRO_TIMER* timer = al_create_timer(1.0 / 30.0);
    must_init(timer, "timer");

    ALLEGRO_EVENT_QUEUE* queue = al_create_event_queue();
    must_init(queue, "queue");

    ALLEGRO_DISPLAY* disp = al_create_display(640, 480);
    must_init(disp, "display");

    ALLEGRO_FONT* font = al_create_builtin_font();
    must_init(font, "font");

    must_init(al_init_image_addon(), "image addon");
    ALLEGRO_BITMAP* mysha = al_load_bitmap("mysha.png");
    must_init(mysha, "mysha");

    al_register_event_source(queue, al_get_keyboard_event_source());
    al_register_event_source(queue, al_get_display_event_source(disp));
    al_register_event_source(queue, al_get_timer_event_source(timer));

    bool done = false;
    bool redraw = true;
    ALLEGRO_EVENT event;

    al_start_timer(timer);
    while(1)
    {
        al_wait_for_event(queue, &event);

        switch(event.type)
        {
            case ALLEGRO_EVENT_TIMER:
                // game logic goes here.
                redraw = true;
                break;

            case ALLEGRO_EVENT_KEY_DOWN:
            case ALLEGRO_EVENT_DISPLAY_CLOSE:
                done = true;
                break;
        }

        if(done)
            break;

        if(redraw && al_is_event_queue_empty(queue))
        {
            al_clear_to_color(al_map_rgb(0, 0, 0));
            al_draw_text(font, al_map_rgb(255, 255, 255), 0, 0, 0, "Hello world!");

            al_draw_bitmap(mysha, 100, 100, 0);

            al_flip_display();

            redraw = false;
        }
    }

    al_destroy_bitmap(mysha);
    al_destroy_font(font);
    al_destroy_display(disp);
    al_destroy_timer(timer);
    al_destroy_event_queue(queue);

    return 0;
}

This is so we can make sure we're all on the same page, but additionally to slim it down a bit. The code we've been working on will now have exceeded 100 lines - not that there's anything wrong with that number, but we're in the business of keeping things as concise as possible.

must_init(al_init(), "allegro");

So, we've added a helper function, must_init(), to make our initialization phase a bit less unruly with a slick check that each step executes successfully. If it doesn't, the failure is printed to the terminal and the program exits immediately - like what we had before with the multiple printf() and return statements.

Drawing primitives

In world of graphics programming, simple lines, shapes and points are referred to as primitives.

Allegro has an addon for these - so again, you'll need to include it. Append this to the list at the top of your code:

#include <allegro5/allegro_primitives.h>

Then you'll need to link it. Visual Studio users should do as they did for the image addon, but for primitives instead. Most other users can use the below commands to compile and run their program from this point on:

gcc hello.c -o hello $(pkg-config allegro-5 allegro_font-5 allegro_image-5 allegro_primitives-5 --libs --cflags)
./hello

Lurid colors

We're going to add various primitives to the screen such that it begins to look like early 90s cover art.

As with the image addon, the primitives addon needs initialization. Before the main loop starts, call al_init_primitives_addon():

must_init(al_init_primitives_addon(), "primitives");

Then add these lines just above al_flip_display():

al_draw_filled_triangle(35, 350, 85, 375, 35, 400, al_map_rgb_f(0, 1, 0));
al_draw_filled_rectangle(240, 260, 340, 340, al_map_rgba_f(0, 0, 0.5, 0.5));
al_draw_circle(450, 370, 30, al_map_rgb_f(1, 0, 1), 2);
al_draw_line(440, 110, 460, 210, al_map_rgb_f(1, 0, 0), 1);
al_draw_line(500, 220, 570, 200, al_map_rgb_f(1, 1, 0), 1);

Then compile and run:

Some things to note about the above:

  • We're using a variant of al_map_rgb() called al_map_rgb_f(). This takes floating point values instead when specifying a color, on a scale of 0 to 1 rather than 255. Not necessarily as precise, but more readable.
  • The rectangle over Mysha is translucent! We've done this with al_map_rgba_f(), which takes an extra argument for alpha (ie. opacity).

Try moving things around and adding more primitives. Have a look at the documentation for the primitives; there are far more functions to use.

Aside: a common error to make with the primitives functions is forgetting to use the filled variants to draw solid shapes. You'll notice that the circle is just an outline; you'll also have to specify a line thickness in these cases. Here, we've used 2.

Antialiasing

What's that we hear you say? "It's not smooth enough"?

al_set_new_display_option(ALLEGRO_SAMPLE_BUFFERS, 1, ALLEGRO_SUGGEST);
al_set_new_display_option(ALLEGRO_SAMPLES, 8, ALLEGRO_SUGGEST);
al_set_new_bitmap_flags(ALLEGRO_MIN_LINEAR | ALLEGRO_MAG_LINEAR);

Try copying the above lines just prior to your call to al_create_display(). This will smooth the edges of primitives, but also any images you've drawn smaller or larger than their native size. (you'll see an example of the latter later on.)

Most graphics hardware these days will be fine with this - though there are some exceptions - for example, it's normally possible to force antialiasing on or off in one's graphics driver configuration.

All being well, things should get smoother:

See the documentation for al_set_new_display_option() and al_set_new_bitmap_flags() if you're curious.

More control

If you want to do much beyond drawing single-color primitives - such as rendering gradients or textured shapes - you'll need to use the low-level drawing routines. They aren't always as trivial to use, because they're only capable of drawing points, lines and triangles - but (as you may know) you can make any shape from multiple triangles - even circles in the world of CGI.

Let's use al_draw_prim() to put another rectangle on the screen. You'll need to pass it an array of ALLEGRO_VERTEX structs:

ALLEGRO_VERTEX v[] = {
    { .x = 210, .y = 320, .z = 0, .color = al_map_rgb_f(1, 0, 0) },
    { .x = 330, .y = 320, .z = 0, .color = al_map_rgb_f(0, 1, 0) },
    { .x = 210, .y = 420, .z = 0, .color = al_map_rgb_f(0, 0, 1) },
    { .x = 330, .y = 420, .z = 0, .color = al_map_rgb_f(1, 1, 0) },
};

al_draw_prim(v, NULL, NULL, 0, 4, ALLEGRO_PRIM_TRIANGLE_STRIP);

As you can see, this is a little more complex. It'll probably look familiar to those who've worked with 3D before - but if not, we're drawing a triangle strip using the four vertices that make up the rectangle.

Here's what you should expect to see as a result:

Beautiful. Phone the gallery, they'll buy it. Here's a recap:

View source
#include <stdio.h>
#include <stdlib.h>
#include <allegro5/allegro5.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_primitives.h>

void must_init(bool test, const char *description)
{
    if(test) return;

    printf("couldn't initialize %s\n", description);
    exit(1);
}

int main()
{
    must_init(al_init(), "allegro");
    must_init(al_install_keyboard(), "keyboard");

    ALLEGRO_TIMER* timer = al_create_timer(1.0 / 30.0);
    must_init(timer, "timer");

    ALLEGRO_EVENT_QUEUE* queue = al_create_event_queue();
    must_init(queue, "queue");

    al_set_new_display_option(ALLEGRO_SAMPLE_BUFFERS, 1, ALLEGRO_SUGGEST);
    al_set_new_display_option(ALLEGRO_SAMPLES, 8, ALLEGRO_SUGGEST);
    al_set_new_bitmap_flags(ALLEGRO_MIN_LINEAR | ALLEGRO_MAG_LINEAR);

    ALLEGRO_DISPLAY* disp = al_create_display(640, 480);
    must_init(disp, "display");

    ALLEGRO_FONT* font = al_create_builtin_font();
    must_init(font, "font");

    must_init(al_init_image_addon(), "image addon");
    ALLEGRO_BITMAP* mysha = al_load_bitmap("mysha.png");
    must_init(mysha, "mysha");

    must_init(al_init_primitives_addon(), "primitives");

    al_register_event_source(queue, al_get_keyboard_event_source());
    al_register_event_source(queue, al_get_display_event_source(disp));
    al_register_event_source(queue, al_get_timer_event_source(timer));

    bool done = false;
    bool redraw = true;
    ALLEGRO_EVENT event;

    al_start_timer(timer);
    while(1)
    {
        al_wait_for_event(queue, &event);

        switch(event.type)
        {
            case ALLEGRO_EVENT_TIMER:
                // game logic goes here.
                redraw = true;
                break;

            case ALLEGRO_EVENT_KEY_DOWN:
            case ALLEGRO_EVENT_DISPLAY_CLOSE:
                done = true;
                break;
        }

        if(done)
            break;

        if(redraw && al_is_event_queue_empty(queue))
        {
            al_clear_to_color(al_map_rgb(0, 0, 0));
            al_draw_text(font, al_map_rgb(255, 255, 255), 0, 0, 0, "Hello world!");

            al_draw_bitmap(mysha, 100, 100, 0);

            al_draw_filled_triangle(35, 350, 85, 375, 35, 400, al_map_rgb_f(0, 1, 0));
            al_draw_filled_rectangle(240, 260, 340, 340, al_map_rgba_f(0, 0, 0.5, 0.5));
            al_draw_circle(450, 370, 30, al_map_rgb_f(1, 0, 1), 2);
            al_draw_line(440, 110, 460, 210, al_map_rgb_f(1, 0, 0), 1);
            al_draw_line(500, 220, 570, 200, al_map_rgb_f(1, 1, 0), 1);

            ALLEGRO_VERTEX v[] = {
                { .x = 210, .y = 320, .z = 0, .color = al_map_rgb_f(1, 0, 0) },
                { .x = 330, .y = 320, .z = 0, .color = al_map_rgb_f(0, 1, 0) },
                { .x = 210, .y = 420, .z = 0, .color = al_map_rgb_f(0, 0, 1) },
                { .x = 330, .y = 420, .z = 0, .color = al_map_rgb_f(1, 1, 0) },
            };

            al_draw_prim(v, NULL, NULL, 0, 4, ALLEGRO_PRIM_TRIANGLE_STRIP);

            al_flip_display();

            redraw = false;
        }
    }

    al_destroy_bitmap(mysha);
    al_destroy_font(font);
    al_destroy_display(disp);
    al_destroy_timer(timer);
    al_destroy_event_queue(queue);

    return 0;
}

Animation

Everything on the screen thus far has been static; let's change that.

A common first-time project for games programmers is Pong. In anticipation of this, we're going to make all of the above objects bounce around the screen.

To accomplish this, we'll need to store everything's position and velocity and update everything continually. Rather than going through the code patch-by-patch, we're just going to give you the whole thing and explain it later; there are a lot of changes to be made!

So, compile and run the below. For bonus points, spot the differences first.

View source
#include <stdio.h>
#include <stdlib.h>
#include <allegro5/allegro5.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_primitives.h>

void must_init(bool test, const char *description)
{
    if(test) return;

    printf("couldn't initialize %s\n", description);
    exit(1);
}

enum BOUNCER_TYPE {
    BT_HELLO = 0,
    BT_MYSHA,
    BT_TRIANGLE,
    BT_RECTANGLE_1,
    BT_RECTANGLE_2,
    BT_CIRCLE,
    BT_LINE1,
    BT_LINE2,
    BT_N
};

typedef struct BOUNCER
{
    float x, y;
    float dx, dy;
    int type;
} BOUNCER;

int main()
{
    must_init(al_init(), "allegro");
    must_init(al_install_keyboard(), "keyboard");

    ALLEGRO_TIMER* timer = al_create_timer(1.0 / 30.0);
    must_init(timer, "timer");

    ALLEGRO_EVENT_QUEUE* queue = al_create_event_queue();
    must_init(queue, "queue");

    al_set_new_display_option(ALLEGRO_SAMPLE_BUFFERS, 1, ALLEGRO_SUGGEST);
    al_set_new_display_option(ALLEGRO_SAMPLES, 8, ALLEGRO_SUGGEST);
    al_set_new_bitmap_flags(ALLEGRO_MIN_LINEAR | ALLEGRO_MAG_LINEAR);

    ALLEGRO_DISPLAY* disp = al_create_display(640, 480);
    must_init(disp, "display");

    ALLEGRO_FONT* font = al_create_builtin_font();
    must_init(font, "font");

    must_init(al_init_image_addon(), "image addon");
    ALLEGRO_BITMAP* mysha = al_load_bitmap("mysha.png");
    must_init(mysha, "mysha");

    must_init(al_init_primitives_addon(), "primitives");

    al_register_event_source(queue, al_get_keyboard_event_source());
    al_register_event_source(queue, al_get_display_event_source(disp));
    al_register_event_source(queue, al_get_timer_event_source(timer));

    bool done = false;
    bool redraw = true;
    ALLEGRO_EVENT event;

    BOUNCER obj[BT_N];
    for(int i = 0; i < BT_N; i++)
    {
        BOUNCER* b = &obj[i];
        b->x = rand() % 640;
        b->y = rand() % 480;
        b->dx = ((((float)rand()) / RAND_MAX) - 0.5) * 2 * 4;
        b->dy = ((((float)rand()) / RAND_MAX) - 0.5) * 2 * 4;
        b->type = i;
    }

    al_start_timer(timer);
    while(1)
    {
        al_wait_for_event(queue, &event);

        switch(event.type)
        {
            case ALLEGRO_EVENT_TIMER:
                for(int i = 0; i < BT_N; i++)
                {
                    BOUNCER* b = &obj[i];
                    b->x += b->dx;
                    b->y += b->dy;

                    if(b->x < 0)
                    {
                        b->x  *= -1;
                        b->dx *= -1;
                    }
                    if(b->x > 640)
                    {
                        b->x -= (b->x - 640);
                        b->dx *= -1;
                    }
                    if(b->y < 0)
                    {
                        b->y  *= -1;
                        b->dy *= -1;
                    }
                    if(b->y > 480)
                    {
                        b->x -= (b->y - 480);
                        b->dy *= -1;
                    }
                }

                redraw = true;
                break;

            case ALLEGRO_EVENT_KEY_DOWN:
            case ALLEGRO_EVENT_DISPLAY_CLOSE:
                done = true;
                break;
        }

        if(done)
            break;

        if(redraw && al_is_event_queue_empty(queue))
        {
            ALLEGRO_VERTEX v[4];
            al_clear_to_color(al_map_rgb(0, 0, 0));

            for(int i = 0; i < BT_N; i++)
            {
                BOUNCER* b = &obj[i];
                switch(b->type)
                {
                    case BT_HELLO:
                        al_draw_text(font, al_map_rgb(255, 255, 255), b->x, b->y, 0, "Hello world!");
                        break;

                    case BT_MYSHA:
                        al_draw_bitmap(mysha, b->x, b->y, 0);
                        break;

                    case BT_TRIANGLE:
                        al_draw_filled_triangle(b->x, b->y, b->x + 50, b->y + 25, b->x, b->y + 50, al_map_rgb_f(0, 1, 0));
                        break;

                    case BT_RECTANGLE_1:
                        al_draw_filled_rectangle(b->x, b->y, b->x + 100, b->y + 80, al_map_rgba_f(0, 0, 0.5, 0.5));
                        break;

                    case BT_RECTANGLE_2:
                        v[0].x = b->x;       v[0].y = b->y;       v[0].z = 0; v[0].color = al_map_rgb_f(1, 0, 0);
                        v[1].x = b->x + 120; v[1].y = b->y;       v[1].z = 0; v[1].color = al_map_rgb_f(0, 1, 0);
                        v[2].x = b->x;       v[2].y = b->y + 100; v[2].z = 0; v[2].color = al_map_rgb_f(0, 0, 1);
                        v[3].x = b->x + 120; v[3].y = b->y + 100; v[3].z = 0; v[3].color = al_map_rgb_f(1, 1, 0);

                        al_draw_prim(v, NULL, NULL, 0, 4, ALLEGRO_PRIM_TRIANGLE_STRIP);
                        break;

                    case BT_CIRCLE:
                        al_draw_circle(b->x, b->y, 30, al_map_rgb_f(1, 0, 1), 2);
                        break;

                    case BT_LINE1:
                        al_draw_line(b->x, b->y, b->x + 20, b->y + 100, al_map_rgb_f(1, 0, 0), 1);
                        break;

                    case BT_LINE2:
                        al_draw_line(b->x, b->y, b->x + 70, b->y - 20, al_map_rgb_f(1, 1, 0), 1);
                        break;
                }
            }

            al_flip_display();
            redraw = false;
        }
    }

    al_destroy_bitmap(mysha);
    al_destroy_font(font);
    al_destroy_display(disp);
    al_destroy_timer(timer);
    al_destroy_event_queue(queue);

    return 0;
}

Initialization

enum BOUNCER_TYPE {
    BT_HELLO = 0,
    BT_MYSHA,
    BT_TRIANGLE,
    BT_RECTANGLE_1,
    BT_RECTANGLE_2,
    BT_CIRCLE,
    BT_LINE1,
    BT_LINE2,
    BT_N
};

typedef struct BOUNCER
{
    float x, y;
    float dx, dy;
    int type;
} BOUNCER;

First order of the day is to define all of the things we're animating; we do this with the BOUNCER_TYPE enumeration. Then, as everything's bouncing around in exactly the same way, we define each thing as a BOUNCER.

BOUNCER obj[BT_N];
for(int i = 0; i < BT_N; i++)
{
    BOUNCER* b = &obj[i];
    b->x = rand() % 640;
    b->y = rand() % 480;
    b->dx = ((((float)rand()) / RAND_MAX) - 0.5) * 2 * 4;
    b->dy = ((((float)rand()) / RAND_MAX) - 0.5) * 2 * 4;
    b->type = i;
}

Later on, before the main loop starts, every BOUNCER is given a random position on the screen and a random velocity (dx, dy) between -4 and +4.

type is set incrementally, so we get one of every BOUNCER_TYPE.

Some actual game logic

Up until now, we haven't had to make anything change between frames. You'll notice that lots of things are moving around now though! So, we've finally removed:

// game logic goes here

...and in its place:

for(int i = 0; i < BT_N; i++)
{
    BOUNCER* b = &obj[i];
    b->x += b->dx;
    b->y += b->dy;

    if(b->x < 0)
    {
        b->x  *= -1;
        b->dx *= -1;
    }
    if(b->x > 640)
    {
        b->x -= (b->x - 640);
        b->dx *= -1;
    }
    if(b->y < 0)
    {
        b->y  *= -1;
        b->dy *= -1;
    }
    if(b->y > 480)
    {
        b->x -= (b->y - 480);
        b->dy *= -1;
    }
}

If you've spent your life wondering how to make things bounce off the edges of the screen, today's your lucky day.

For each BOUNCER, we change their x and y coordinates by adding each dimension's respective velocity (dx, dy) to them. This means the velocities we set up earlier - between -4 and +4 - are the number of pixels they'll move every frame.

However, we also need to consider what happens if each object is about to go off screen. If it's bumped into the left or right, we invert dx so that it immediately starts travelling in the other direction; vice versa for dy for the top or bottom. We then make sure that the object isn't out-of-bounds any more by putting it back within-bounds by the amount it came out - so if x == -2, it becomes x == 2.

Updating the render code

The render code has gotten considerably bigger, so we won't put the whole thing here. Instead, we'll just consider the example of the "hello world" text:

for(int i = 0; i < BT_N; i++)
{
    BOUNCER* b = &obj[i];
    switch(b->type)
    {
        case BT_HELLO:
            al_draw_text(font, al_map_rgb(255, 255, 255), b->x, b->y, 0, "Hello world!");
            break;

...

We've had to take into account that everything we render is now a moving object. So, once again, we're looping through all of the BOUNCERs and deciding how to draw each as it comes up - depending on its type.

The call to al_draw_text() itself is pretty similar to what it was before - but you'll notice the inclusion of b->x and b->y. As these values get updated on every frame (as we saw earlier), the text appears to move. Exactly the same thing applies to all the other objects on the screen. Animation: sorted.

However, you want to try changing it, don't you?

  • Make it so there are two of every bouncing object we've defined.

  • You'll notice that every object drifts out of the screen slightly before it bounces off the bottom and right edges.

    • Figure out why this is.
    • Correct it, if you can be bothered. (Hint: each object will need its own care and attention!)
  • Try making the objects translucent, then playing around with blending. In particular, try additive blending - objects will light each other up:

    al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ONE);

That's graphics done for now. Well done for enduring this; we've covered a lot of stuff. Luckily for us all, the next section won't be nearly as trying - but regardless, it's pretty damn important.

Input ▶

You can’t perform that action at this time.