Allegro.pas

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:

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:

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 BOUNCERs 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?

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.

◀ Basic game structure | Input ▶