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 an entirely new file with this. 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:
BEGIN
al_get_keyboard_state (KeyState);
IF al_key_down (KeyState, ALLEGRO_KEY_UP) THEN y := y - 1;
IF al_key_down (KeyState, ALLEGRO_KEY_DOWN) THEN y := y + 1;
IF al_key_down (KeyState, ALLEGRO_KEY_LEFT) THEN x := x - 1;
IF al_key_down (KeyState, ALLEGRO_KEY_RIGHT) THEN x := x + 1;
IF al_key_down (KeyState, ALLEGRO_KEY_ESCAPE) THEN EndProgram := TRUE;
Redraw := TRUE
END;
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 EndProgram := TRUE
- you can do this by removing the ALLEGRO_KEY_DOWN
case:
ALLEGRO_EVENT_KEY_DOWN, { <- delete this! }, ALLEGRO_EVENT_DISPLAY_CLOSE:
EndProgram := TRUE;
Finally, add the declaration for KeyState in the variable list:
VAR
...
x, y: REVAL;
KeyState: ALLEGRO_KEYBOARD_STATE;
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, KeyState 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 KeyState (lest your compiler warn you it's unused), and replace your entire CASE Event.ftype OF
block with the following code:
CASE Event.ftype OF
ALLEGRO_EVENT_TIMER:
{ Once again, no game logic. Fishy? maybe. }
Redraw := TRUE;
ALLEGRO_EVENT_KEY_DOWN:
CASE Event.keyboard.keycode OF
ALLEGRO_KEY_UP: y := y - 1;
ALLEGRO_KEY_DOWN: y := y + 1;
ALLEGRO_KEY_LEFT: x := x - 1;
ALLEGRO_KEY_RIGHT: x := x + 1;
ALLEGRO_KEY_ESCAPE: EndProgram := TRUE;
END;
ALLEGRO_EVENT_DISPLAY_CLOSE:
{ Close the program. }
EndProgram := TRUE;
END;
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 CASE
block, replace:
ALLEGRO_EVENT_KEY_DOWN:
...with:
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 after your USES
section:
CONST
{ Key statuses. }
KEY_SEEN = 1;
KEY_RELEASED = 2;
...also add these new variables:
Key: ARRAY [0..ALLEGRO_KEY_MAX] OF BYTE;
Ndx: INTEGER;
...then insert next code after initialization but before the main loop:
{ Key statuses. }
FOR Ndx := 0 TO ALLEGRO_KEY_MAX DO Key[Ndx] := 0;
...and once again, replace your `switch` block with this:
CASE Event.ftype OF
ALLEGRO_EVENT_TIMER:
BEGIN
IF Key[ALLEGRO_KEY_UP] <> 0 THEN y := y - 1;
IF Key[ALLEGRO_KEY_DOWN] <> 0 THEN y := y + 1;
IF Key[ALLEGRO_KEY_LEFT] <> 0 THEN x := x - 1;
IF Key[ALLEGRO_KEY_RIGHT] <> 0 THEN x := x + 1;
IF Key[ALLEGRO_KEY_ESCAPE] <> 0 THEN EndProgram := TRUE;
FOR Ndx := 0 TO ALLEGRO_KEY_MAX DO
Key[Ndx] := Key[Ndx] AND KEY_SEEN;
Redraw := TRUE
END;
ALLEGRO_EVENT_KEY_DOWN:
Key[Event.keyboard.keycode] := KEY_SEEN OR KEY_RELEASED;
ALLEGRO_EVENT_KEY_UP:
Key[Event.keyboard.keycode] := Key[Event.keyboard.keycode] AND KEY_RELEASED;
ALLEGRO_EVENT_DISPLAY_CLOSE:
{ Close the program. }
EndProgram := TRUE;
END;
Compile and run; problem solved. Smooth movement, no missed keypresses, have a beer (and a recap).
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:
Key: ARRAY [0..ALLEGRO_KEY_MAX] OF BYTE;
We'll need to make sure its contents are zeroed to begin with.:
{ Key statuses. }
FOR Ndx := 0 TO ALLEGRO_KEY_MAX DO Key[Ndx] := 0;
We've also introduced a couple of CONST
s - more on those in a second:
CONST
{ Key statuses. }
KEY_SEEN = 1;
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] <> 0 THEN y := y - 1;
IF Key[ALLEGRO_KEY_DOWN] <> 0 THEN y := y + 1;
IF Key[ALLEGRO_KEY_LEFT] <> 0 THEN x := x - 1;
IF Key[ALLEGRO_KEY_RIGHT] <> 0 THEN x := x + 1;
IF Key[ALLEGRO_KEY_ESCAPE] <> 0 THEN EndProgram := 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 Ndx := 0 TO ALLEGRO_KEY_MAX DO
Key[Ndx] := Key[Ndx] AND KEY_SEEN;
But we'll get to that in a sec. There are a couple of new CASE
s to look at first:
ALLEGRO_EVENT_KEY_DOWN:
Key[Event.keyboard.keycode] := KEY_SEEN OR KEY_RELEASED;
ALLEGRO_EVENT_KEY_UP:
Key[Event.keyboard.keycode] := Key[Event.keyboard.keycode] AND KEY_RELEASED;
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:
00000001 { 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:
00000010 { 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!
(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:
- 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.
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 MustInit
function with this to ensure the mouse was installed successfully, so instead, do this:
MustInit (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 CASE
block to set the x and y variables when the mouse moves (rather than when the arrow keys are pressed):
CASE Event.ftype OF
ALLEGRO_EVENT_TIMER:
BEGIN
IF Key[ALLEGRO_KEY_ESCAPE] <> 0 THEN EndProgram := TRUE;
FOR Ndx := 0 TO ALLEGRO_KEY_MAX DO
Key[Ndx] := Key[Ndx] AND KEY_SEEN;
Redraw := TRUE
END;
ALLEGRO_EVENT_MOUSE_AXES:
BEGIN
x := Event.mouse.x;
y := Event.mouse.y
END;
ALLEGRO_EVENT_KEY_DOWN:
Key[Event.keyboard.keycode] := KEY_SEEN OR KEY_RELEASED;
ALLEGRO_EVENT_KEY_UP:
Key[Event.keyboard.keycode] := Key[Event.keyboard.keycode] AND KEY_RELEASED;
ALLEGRO_EVENT_DISPLAY_CLOSE:
{ Close the program. }
EndProgram := TRUE;
END;
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:
ALLEGRO_EVENT_MOUSE_AXES:
BEGIN
x := Event.mouse.x;
y := Event.mouse.y
END;
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 (Display);
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:
VAR
...
x, y, dx, dy: REAL;
...
{ Configure mouse. }
al_hide_mouse_cursor (Display);
al_grab_mouse (Display);
Then, update your CASE
block as follows. You may recognise some of the additions from the earlier things-flying-around-the-screen example in the graphics section:
CASE Event.ftype OF
ALLEGRO_EVENT_TIMER:
BEGIN
IF Key[ALLEGRO_KEY_ESCAPE] <> 0 THEN EndProgram := TRUE;
FOR Ndx := 0 TO ALLEGRO_KEY_MAX DO
Key[Ndx] := Key[Ndx] AND KEY_SEEN;
{ Move square. }
x := x + dx;
y := y + dy;
{ Bouncing. }
IF x < 0 THEN BEGIN x := x * (-1); dx := dx * (-1); END;
IF x > 640 THEN BEGIN x := x - (x - 640) * 2; dx := dx * (-1); END;
IF y < 0 THEN BEGIN y := y * (-1); dy := dy * (-1); END;
IF y > 480 THEN BEGIN y := y - (y - 480) * 2; dy := dy * (-1); END;
{ Moment. }
dx := dx * 0.9;
dy := dy * 0.9;
Redraw := TRUE
END;
ALLEGRO_EVENT_MOUSE_AXES:
BEGIN
dx := dx + Event.mouse.dx * 0.1;
dy := dy + Event.mouse.dy * 0.1;
al_set_mouse_xy (Display, 320, 240)
END;
ALLEGRO_EVENT_KEY_DOWN:
Key[Event.keyboard.keycode] := KEY_SEEN OR KEY_RELEASED;
ALLEGRO_EVENT_KEY_UP:
Key[Event.keyboard.keycode] := Key[Event.keyboard.keycode] AND KEY_RELEASED;
ALLEGRO_EVENT_DISPLAY_CLOSE:
{ Close the program. }
EndProgram := TRUE;
END;
Compile and run. May be you see a warning telling you that:
Compiling input.pas input.pas(96,18) Warning: Variable "dx" does not seem to be initialized input.pas(97,18) Warning: Variable "dy" does not seem to be initialized
If so, just add an assignation line somewhere in the initialization; for example:
{ Initial position. }
x := 100; y := 100;
dx := 0; dy := 0; { add this }
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 (Display);
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.
{ Move square. }
x := x + dx;
y := y + dy;
{ Bouncindg. }
IF x < 0 THEN BEGIN x := x * (-1); dx := dx * (-1); END;
...
{ Moment. }
dx := dx * 0.9;
dy := 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:
ALLEGRO_EVENT_MOUSE_AXES:
BEGIN
dx := dx + Event.mouse.dx * 0.1;
dy := dy + Event.mouse.dy * 0.1;
al_set_mouse_xy (Display, 320, 240)
END;
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.