-
Notifications
You must be signed in to change notification settings - Fork 8
Allegro Vivace – Input
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.
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.
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.
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...
Using ALLEGRO_EVENT_KEY_DOWN
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.
Using ALLEGRO_EVENT_KEY_CHAR
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.
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;
}
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 #define
s - 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 case
s 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...)
Generally speaking, there are two ways you can choose to interpret mouse movement:
- Moving the mouse pointer and clicking around as you normally would.
- 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.
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...)
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.
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.
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.