Skip to content

Allegro Vivace – Input

Daniel T edited this page Sep 11, 2023 · 3 revisions

◀ Graphics


The only input our program has accepted so far is to immediately quit upon pressing any key. This is quite prickly behaviour for a game - perhaps unless it's become self-aware - so we're going to learn how to properly handle input.

Firstly, we'll scale back our game's code such that it's free of distractions, then we'll just add some simple controls to move things around.

If you went off on a tangent towards the end of the graphics section, you'll probably want to save your work and make the below an entirely new file.

View source
#include <stdio.h>
#include <stdlib.h>
#include <allegro5/allegro5.h>
#include <allegro5/allegro_font.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_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;

    float x, y;
    x = 100;
    y = 100;

    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_textf(font, al_map_rgb(255, 255, 255), 0, 0, 0, "X: %.1f Y: %.1f", x, y);
            al_draw_filled_rectangle(x, y, x + 10, y + 10, al_map_rgb(255, 0, 0));

            al_flip_display();

            redraw = false;
        }
    }

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

    return 0;
}

You'll only need the fonts and primitives addons now. Visual Studio users should update their settings accordingly; for those on the command line, here's a recap:

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

Compile and run the program. You'll see we've gotten rid of all the bouncing objects and are left with a box and some text. We've given the box X and Y position variables - but at the moment, they don't change.

Keyboard input

First order of the day is to get the box moving when the user presses the arrow keys. This might seem simple, but it's hard to master - mainly because there are several different ways of doing it, and it probably won't be immediately clear which to use.

The mostly-wrong way

We're going to start with what not to do - at least not in production. This method is quick (if dirty), so it's useful if you want to quickly check what keys are pressed if you're just messing around with a sandbox program.

To begin the mostly-wrong way, meet ALLEGRO_KEYBOARD_STATE. Try replacing // game logic goes here with the following:

al_get_keyboard_state(&ks);

if(al_key_down(&ks, ALLEGRO_KEY_UP))
    y--;
if(al_key_down(&ks, ALLEGRO_KEY_DOWN))
    y++;
if(al_key_down(&ks, ALLEGRO_KEY_LEFT))
    x--;
if(al_key_down(&ks, ALLEGRO_KEY_RIGHT))
    x++;

if(al_key_down(&ks, ALLEGRO_KEY_ESCAPE))
    done = true;

It should be relatively obvious from the last couple of lines that we've now set the 'Esc' key to be the one that quits the program. So, we'll now need to stop any key from also setting done = true - you can do this by removing the ALLEGRO_KEY_DOWN case:

case ALLEGRO_EVENT_KEY_DOWN: // delete this line!
case ALLEGRO_EVENT_DISPLAY_CLOSE:
    done = true;
    break;

Finally, add the declaration for ks before the loop starts:

ALLEGRO_KEYBOARD_STATE ks;

al_start_timer(timer);
while(1)
{
    // ...

Compile and run, and you should see the box changing position when you hold down any of arrow keys.

What's the problem with this? Well, try lightly tapping one of the arrow keys; you'll probably intermittently see that the box doesn't move at all.

But why!?

Firstly, we'll note that our game runs at 30 FPS - ie. 30 frames-per-second (those who figured this out earlier can cash in on smugness now) - meaning that, providing there's no frameskip, the game's logic code will run precisely 30 times a second. On each of those 30 executions, ks is updated with the keys that are held down at precisely the point of calling al_get_keyboard_state.

This would be fine if we could guarantee that every keypress will be at least as long as a 1/30th of a second - but, alas, we can't. Your fingers are too quick! Sometimes, the key is pressed and released in the gap between game logic executions; to your program, it's as if the key were never pressed at all.

That's why this method generally isn't a good idea. Never fear though - we can do better...

You'll probably recall we were using this event to previously quit the program on any keypress - but we're going to try using it to move the square.

Remove the declaration of ks (lest your compiler warn you it's unused), and replace your entire switch(event.type) block with the following code:

switch(event.type)
{
    case ALLEGRO_EVENT_TIMER:
        // once again, no game logic. fishy? maybe.
        redraw = true;
        break;

    case ALLEGRO_EVENT_KEY_DOWN:
        if(event.keyboard.keycode == ALLEGRO_KEY_UP)
            y--;
        if(event.keyboard.keycode == ALLEGRO_KEY_DOWN)
            y++;
        if(event.keyboard.keycode == ALLEGRO_KEY_LEFT)
            x--;
        if(event.keyboard.keycode == ALLEGRO_KEY_RIGHT)
            x++;

        if(event.keyboard.keycode != ALLEGRO_KEY_ESCAPE)
            break;

    case ALLEGRO_EVENT_DISPLAY_CLOSE:
        done = true;
        break;
}

Compile and run, and you'll notice you have to press your keys a lot to make the damn box move any distance - one press per pixel, actually.

On the plus side, it fixes the keypress-inbetween-frames problem we described above - but unfortunately, this is only really useful for keys that trigger an action only once per press.

So, you won't want to use this for most in-game movement - but perhaps for eg. triggering the pause menu.

Allegro provides another keyboard event type that we haven't looked at yet. In your switch block, replace:

case ALLEGRO_EVENT_KEY_DOWN:

...with:

case ALLEGRO_EVENT_KEY_CHAR:

Then compile and run and try moving the box again. It'll probably be faster this time - though this depends on your OS.

What's happening here is that the box is moving at the rate of your OS's native key repeat rate. Speed-wise, it's the equivalent of clicking on a text box, holding a character down, and seeing it repeat itself.

So again, unfortunately, not that useful for character movement. You don't want to have to demand your users set their key repeat rates to the speed your character moves "to make it fair". ;)

However, this makes ALLEGRO_EVENT_KEY_CHAR very useful for any kind of character input, or perhaps navigating your in-game menus - as it will more than likely trigger actions at a 'comfortable' rate for the user. So, like ALLEGRO_EVENT_KEY_DOWN, keep it in mind.

A better method

With all of the above in mind, we want to combine the smooth movement of the "mostly-wrong way" with the precision afforded by Allegro's events system - so that the box keeps moving whilst the key is held down, but that keypresses shorter than a frame are also acknowledged. What a pain.

Luckily, there's a relatively easy way of dealing with this. True to form, we're going to stick with our careless method of "code now, explain later". Sorry about that.

So, add the following before your loop starts:

#define KEY_SEEN     1
#define KEY_RELEASED 2

unsigned char key[ALLEGRO_KEY_MAX];
memset(key, 0, sizeof(key));

...and once again, replace your switch block with this:

switch(event.type)
{
    case ALLEGRO_EVENT_TIMER:
        if(key[ALLEGRO_KEY_UP])
            y--;
        if(key[ALLEGRO_KEY_DOWN])
            y++;
        if(key[ALLEGRO_KEY_LEFT])
            x--;
        if(key[ALLEGRO_KEY_RIGHT])
            x++;

        if(key[ALLEGRO_KEY_ESCAPE])
            done = true;

        for(int i = 0; i < ALLEGRO_KEY_MAX; i++)
            key[i] &= KEY_SEEN;

        redraw = true;
        break;

    case ALLEGRO_EVENT_KEY_DOWN:
        key[event.keyboard.keycode] = KEY_SEEN | KEY_RELEASED;
        break;
    case ALLEGRO_EVENT_KEY_UP:
        key[event.keyboard.keycode] &= KEY_RELEASED;
        break;

    case ALLEGRO_EVENT_DISPLAY_CLOSE:
        done = true;
        break;
}

Compile and run; problem solved. Smooth movement, no missed keypresses, have a beer (and a recap):

View source
#include <stdio.h>
#include <stdlib.h>
#include <allegro5/allegro5.h>
#include <allegro5/allegro_font.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_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;

    float x, y;
    x = 100;
    y = 100;


    #define KEY_SEEN     1
    #define KEY_RELEASED 2

    unsigned char key[ALLEGRO_KEY_MAX];
    memset(key, 0, sizeof(key));


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

        switch(event.type)
        {
            case ALLEGRO_EVENT_TIMER:
                if(key[ALLEGRO_KEY_UP])
                    y--;
                if(key[ALLEGRO_KEY_DOWN])
                    y++;
                if(key[ALLEGRO_KEY_LEFT])
                    x--;
                if(key[ALLEGRO_KEY_RIGHT])
                    x++;

                if(key[ALLEGRO_KEY_ESCAPE])
                    done = true;

                for(int i = 0; i < ALLEGRO_KEY_MAX; i++)
                    key[i] &= KEY_SEEN;

                redraw = true;
                break;

            case ALLEGRO_EVENT_KEY_DOWN:
                key[event.keyboard.keycode] = KEY_SEEN | KEY_RELEASED;
                break;
            case ALLEGRO_EVENT_KEY_UP:
                key[event.keyboard.keycode] &= KEY_RELEASED;
                break;

            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_textf(font, al_map_rgb(255, 255, 255), 0, 0, 0, "X: %.1f Y: %.1f", x, y);
            al_draw_filled_rectangle(x, y, x + 10, y + 10, al_map_rgb(255, 0, 0));

            al_flip_display();

            redraw = false;
        }
    }

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

    return 0;
}

How it works

What should be immediately apparent is that we're now maintaining an array of all of the keys it's possible to press; the ALLEGRO_KEY_MAX constant tells us how big this array needs to be:

unsigned char key[ALLEGRO_KEY_MAX];

We'll need to make sure its contents are zeroed to begin with. Rather than using a loop, the C standard library's memset function does this efficiently:

memset(key, 0, sizeof(key));

We've also introduced a couple of #defines - more on those in a second:

#define KEY_SEEN     1
#define KEY_RELEASED 2

Thus, onto the game logic. It now looks more like it did when we first tried to handle keys:

if(key[ALLEGRO_KEY_UP])
    y--;
if(key[ALLEGRO_KEY_DOWN])
    y++;
if(key[ALLEGRO_KEY_LEFT])
    x--;
if(key[ALLEGRO_KEY_RIGHT])
    x++;

if(key[ALLEGRO_KEY_ESCAPE])
    done = true;

...except we're now looking at our array, rather than an ALLEGRO_KEYBOARD_STATE, to decide what action to take. What happens next, though, is a bit weirder:

for(int i = 0; i < ALLEGRO_KEY_MAX; i++)
    key[i] &= KEY_SEEN;

But we'll get to that in a sec. There are a couple of new cases to look at first:

case ALLEGRO_EVENT_KEY_DOWN:
    key[event.keyboard.keycode] = KEY_SEEN | KEY_RELEASED;
    break;
case ALLEGRO_EVENT_KEY_UP:
    key[event.keyboard.keycode] &= KEY_RELEASED;
    break;

What's happening here is that when a key is first pressed down (ALLEGRO_EVENT_KEY_DOWN), two of the bits are set to 1 in the key's corresponding entry in our key array:

00000000 // unpressed
00000011 // pressed

When the key is released, an ALLEGRO_EVENT_KEY_UP event is fired. One of the bits is then set to 0:

00000010 // released

Additionally, the for loop I mentioned above ensures that after every run of the game logic, another one of the bits is set to 0:

00000001 // logic has run, key is still pressed
00000000 // logic has run, key is no longer pressed

This means that any given member of the key array has a truthy value (ie. at least a single bit is set to 1) so long as the game logic hasn't 'seen' it yet, or the key is still held down. The combination of those conditions ensures that a keypress can't be missed between game logic runs!

It's worth saying that this will be significantly easier to understand for those who've worked with bit masks in C before. If you haven't, it's definitely worth reading up.

(also: note the binary visualisations above are dependant on endianness - but let's not get into that now...)

Using the mouse

Generally speaking, there are two ways you can choose to interpret mouse movement:

  1. Moving the mouse pointer and clicking around as you normally would.
  2. Using the speed of the mouse to control something in the game. This allows you to use the mouse a bit like a joystick; most PC-based first-person shooters do this to allow the player to speedily aim.

Mouse position

We can easily make our red box just track the position of the mouse. Firstly, you'll need to install the mouse as part of your initialization (like you already do with the keyboard):

al_install_mouse();

...though we'll use our must_init() function with this to ensure the mouse was installed successfully, so instead, do this:

must_init(al_install_mouse(), "mouse");

Again - similarly to the keyboard - we'll need to subscribe to the mouse's events before the main loop starts:

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));
al_register_event_source(queue, al_get_mouse_event_source());       // add this line

Finally, update your switch block to set the x and y variables when the mouse moves (rather than when the arrow keys are pressed):

switch(event.type)
{
    case ALLEGRO_EVENT_TIMER:
        if(key[ALLEGRO_KEY_ESCAPE])
            done = true;

        for(int i = 0; i < ALLEGRO_KEY_MAX; i++)
            key[i] &= KEY_SEEN;

        redraw = true;
        break;

    case ALLEGRO_EVENT_MOUSE_AXES:
        x = event.mouse.x;
        y = event.mouse.y;
        break;

    case ALLEGRO_EVENT_KEY_DOWN:
        key[event.keyboard.keycode] = KEY_SEEN | KEY_RELEASED;
        break;
    case ALLEGRO_EVENT_KEY_UP:
        key[event.keyboard.keycode] &= KEY_RELEASED;
        break;

    case ALLEGRO_EVENT_DISPLAY_CLOSE:
        done = true;
        break;
}

Compile it, run it, and you should see the box following your mouse cursor. Easy enough; only one thing to point out, though it's almost self-explanatory at this point:

case ALLEGRO_EVENT_MOUSE_AXES:
    x = event.mouse.x;
    y = event.mouse.y;

The ALLEGRO_EVENT_MOUSE_AXES event fires whenever the mouse moves - either in the X, Y or Z axes. The event struct is updated with all of the data you'll need on what just happened (again, as with keyboard events).

(Yes, most mice are three-dimensional these days - bonus points if you can figure out how...)

Getting rid of the cursor

Sick of the your mouse cursor fudging up your red box's style? Easily sorted:

al_hide_mouse_cursor(disp);

Add the above line before your loop starts and it'll disappear. al_hide_mouse_cursor() is one of many functions Allegro has for dealing with the cursor, including the ability to automatically draw a given bitmap at the position of the mouse - like we're doing manually with our box. Read up if you so desire, but we're going to carry on without that.

Mouse speed

If you had a peek at the ALLEGRO_EVENT_MOUSE_AXES documentation, there's a chance that you'll already know what we're about to do. Regardless, we're going to make it so the mouse applies speed to the box rather than controlling its position directly. For this, you'll want to add dx and dy variables before the loop starts, and then hide and grab the mouse cursor:

float dx, dy;
dx = 0;
dy = 0;

al_grab_mouse(disp);

Then, update your switch block as follows. You may recognise some of the additions from the earlier things-flying-around-the-screen example in the graphics section:

switch(event.type)
{
    case ALLEGRO_EVENT_TIMER:
        if(key[ALLEGRO_KEY_ESCAPE])
            done = true;

        x += dx;
        y += dy;

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

        dx *= 0.9;
        dy *= 0.9;

        for(int i = 0; i < ALLEGRO_KEY_MAX; i++)
            key[i] &= KEY_SEEN;

        redraw = true;
        break;

    case ALLEGRO_EVENT_MOUSE_AXES:
        dx += event.mouse.dx * 0.1;
        dy += event.mouse.dy * 0.1;
        al_set_mouse_xy(disp, 320, 240);
        break;

    case ALLEGRO_EVENT_KEY_DOWN:
        key[event.keyboard.keycode] = KEY_SEEN | KEY_RELEASED;
        break;
    case ALLEGRO_EVENT_KEY_UP:
        key[event.keyboard.keycode] &= KEY_RELEASED;
        break;

    case ALLEGRO_EVENT_DISPLAY_CLOSE:
        done = true;
        break;
}

Compile and run. You should see one smooth mover of a box - but also that your cursor vanishes immediately upon running it (regardless of whether it's within your program's window), and that the only way of getting it back is to quit it.

How does it work, Jim?

We'll quickly go over what's happening here (acknowledging that at this point, there's a good chance you already know):

al_grab_mouse(disp);

Earlier, we mentioned 'grabbing' the mouse. This confines it to the window; if we hadn't done this, the mouse would be able to escape if we moved it too far in one direction. Note that in a production game, you may want to provide an obvious way for the user to ungrab the mouse, as this can be perceived as annoying.

x += dx;
y += dy;

if(x < 0)
{
    x *= -1;
    dx *= -1;
}

// ...

dx *= 0.9;
dy *= 0.9;

Our game logic now moves the box around according to the dx and dy values. The various if statements bounce the box off the sides - same as we had by the end of the graphics section. Lastly, dx and dy slowly decrease over time; this makes the box feel 'floaty'. (try lowering the multiplier from 0.9 and observe the box stopping more abruptly.)

And, to finish:

case ALLEGRO_EVENT_MOUSE_AXES:
    dx += event.mouse.dx * 0.1;
    dy += event.mouse.dy * 0.1;
    al_set_mouse_xy(disp, 320, 240);
    break;

Here's where the box obtains its velocity. We have to continuously call al_set_mouse_xy() to re-center the position of the mouse in the window, or the mouse will eventually hit the edge - meaning we'll see no difference in movement. (try changing the event.mouse.dx and .dy multipliers and observe the box accelerating more quickly.)


That's all for input, though we haven't covered joysticks as not everybody has one on hand (and definitely not because the wiki authors can't be arsed) - however, if you've worked with the mouse axes as we've done above, there isn't much more you'll need to learn.

Next up, we'll be giving your game some a m b i e n c e.

Sound ▶

Clone this wiki locally