Graphics
◀ Basic game structure | Input ▶
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 helloworld.pas file. Make sure it's saved as mysha.png.
Loading and displaying images
First, we need a variable to store the image, so in the VAR
section add this:
Mysha: ALLEGRO_BITMAPptr;
Load the image as part of your program's initialization by adding the following code before the main REPEAT
loop starts:
Mysha := al_load_bitmap ('mysha.png');
IF Assigned (Mysha) THEN
BEGIN
WriteLn ('Couldn''t load mysha.');
WriteLn ('Press [Enter] to close.');
ReadLn;
Halt (1)
END;
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 WriteLn
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 al5image
as an used unit; you should now have three:
USES
allegro5, al5font, al5image;
Secondly, before the call to al_load_bitmap
, you'll need to use al_init_image_addon:
IF NOT al_init_image_addon THEN
BEGIN
WriteLn ('couldn''t initialize image addon.');
WriteLn ('Press [Enter] to close.');
ReadLn;
Halt (1)
END;
Mysha := al_load_bitmap ('mysha.png');
{ ... }
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 from here once you're finished messing about.
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.
MustInit (al_init, 'Allegro');
So, we've added a helper function, MustInit
, 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 WriteLn
and Halt
statements. Also there are a few comments here and there.
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 of used units at the top of your code:
USES
allegro5, al5font, al5image, al5primitives;
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:
MustInit (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. Note: Delphi has some issues with a few primitives.
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 OR 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:
...
(* Helper to assign vertex. *)
FUNCTION CreateVertex (CONST x, y: REAL; CONST Color: ALLEGRO_COLOR)
: ALLEGRO_VERTEX; INLINE;
BEGIN
CreateVertex.x := x; CreateVertex.y := y; CreateVertex.z := 0;
CreateVertex.u := 0; CreateVertex.v := 0;
CreateVertex.color := Color
END;
VAR
...
v: ARRAY [0..3] OF ALLEGRO_VERTEX;
BEGIN
...
al_register_event_source (Queue, al_get_timer_event_source (Timer));
v[0] := CreateVertex (210, 320, al_map_rgb_f(1, 0, 0));
v[1] := CreateVertex (330, 320, al_map_rgb_f(0, 1, 0));
v[2] := CreateVertex (210, 420, al_map_rgb_f(0, 0, 1));
v[3] := CreateVertex (330, 420, al_map_rgb_f(1, 1, 0));
Redraw := TRUE;
...
al_draw_prim (v, Nil, 0, 4, ALLEGRO_PRIM_TRIANGLE_STRIP);
al_flip_display;
...
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.
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 this file. For bonus points, spot the differences first.
Initialization
TYPE
BOUNCER_TYPE = (
BT_HELLO = 0,
BT_MYSHA,
BT_TRIANGLE,
BT_RECTANGLE_1,
BT_RECTANGLE_2,
BT_CIRCLE,
BT_LINE1,
BT_LINE2
);
BOUNCER = RECORD
x, y: REAL;
dx, dy: REAL;
bType: BOUNCER_TYPE;
END;
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
.
VAR
...
Obj: ARRAY [0..ORD (BT_LINE2)] OF BOUNCER;
...
{ Initialize bouncers. }
Randomize;
FOR Ndx := LOW (Obj) TO HIGH (Obj) DO
BEGIN
Obj[Ndx].x := Random (640); Obj[Ndx].y := Random (480);
Obj[Ndx].dx := ((Random (65635) / 65635) - 0.5) * 2 * 4;
Obj[Ndx].dy := ((Random (65635) / 65635) - 0.5) * 2 * 4;
Obj[Ndx].bType := BOUNCER_TYPE (Ndx);
END;
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.
bType 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:
{ Update bouncers. }
FOR Ndx := LOW (Obj) TO HIGH (Obj) DO
BEGIN
Obj[Ndx].x := Obj[Ndx].x + Obj[Ndx].dx;
Obj[Ndx].y := Obj[Ndx].y + Obj[Ndx].dy;
IF Obj[Ndx].x < 0 THEN
Obj[Ndx].dx := Obj[Ndx].dx * (-1);
IF Obj[Ndx].x > 640 THEN
Obj[Ndx].dx := Obj[Ndx].dx * (-1);
IF Obj[Ndx].y < 0 THEN
Obj[Ndx].dy := Obj[Ndx].dy * (-1);
IF Obj[Ndx].y > 480 THEN
Obj[Ndx].dy := Obj[Ndx].dy * (-1);
END;
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:
{ Draw bouncers. }
FOR Ndx := LOW (Obj) TO HIGH (Obj) DO
BEGIN
CASE Obj[Ndx].bType OF
BT_HELLO:
al_draw_text (
Font, al_map_rgb (255, 255, 255),
Obj[Ndx].x, Obj[Ndx].y, 0,
'Hello world!'
);
...
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 BOUNCER
s and deciding how to draw each as it comes up - depending on its bType.
The call to al_draw_text
itself is pretty similar to what it was before - but you'll notice the inclusion of Obj[Ndx].x and Obj[Ndx].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 have different size!)
- 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.