Allegro.pas

Gameplay

◀ Sound | Epilogue ▶

The hour is late.  The skies grow dark; the people grow restless.  Brother pitted against brother.  A nation at war.  The rise of a hero.  Also, you'll be programming a game now.

It's pew pew time

To be precise, you'll be making a space shooter:  this is good for a first game, because the movement of the objects - spaceships, aliens, bullets et al - can be pretty simple to program.

We'll be making a game that's relatively easy to build on.  So - if you want to make it ludicrously complex once we've finished - you can go right ahead, provided you've not flipped your desk by then.

Pong and Tetris are also good newbie-programmer games for the same reasons (provided you're not planning on a battle royale).

Achtung!

TLDR: this part of the tutorial boils down to repeatedly adding units to a program, then reading our discussion of that unit code.  Skip ahead if you don't care why and you know what and how UNITs are used.

Just so you're aware, things are about to kick up a gear.  We're going to be using everything we've learnt so far, in addition to a fair amount of new stuff.

Previously, we've talked through the process of building up our programs, examining the obstacles as we go.  However, this time, we'll be ending up with just shy of 1,000 lines of code.

With that much code, asking you to add a line here and there would be a long, difficult-to-debug process.  So, to keep things simple, I've divided the program in several UNITs (as in any complex program), explaining each one as we go.

Whait, what's a UNIT?

Until now all example sources were in a single file, the PROGRAM, but Pascal allows you to divide your program in smaller pieces called UNIT so you can organize your code more easy.  Also this allows you to reuse the same UNIT in different programs.

Let see one simple example:

UNIT MyUnit;
(* A simple example UNIT. *)

INTERFACE

  PROCEDURE SayHello;

IMPLEMENTATION

  FUNCTION AskName: STRING;
  VAR
    Name: STRING;
  BEGIN
    Write ('What''s your name?');
    ReadLn (Name);
    EXIT (Name)
  END;



  PROCEDURE SayHello;
  VAR
    UserName: STRING;
  BEGIN
    UserName := AskName;
    WriteLn ('Hello, ', UserName, '!')
  END;

END.

It is very simple.

UNIT MyUnit;

That tells the compiler the unit name.  To make things easy, save the unit with the name myunit.pas.

INTERFACE

  PROCEDURE SayHello;

The interface tells the compiler what parts of the unit can be used (called) from other parts of the program; this is the public part of the unit.

IMPLEMENTATION

  FUNCTION AskName: STRING;
  VAR
    Name: STRING;
  BEGIN
    Write ('What''s your name?');
    ReadLn (Name);
    EXIT (Name)
  END;



  PROCEDURE SayHello;
  VAR
    UserName: STRING;
  BEGIN
    UserName := AskName;
    WriteLn ('Hello, ', UserName, '!')
  END;

As name indicates, this is what the unit actually does.  Here we have a FUNCTION and a PROCEDURE.

And how we can use this?  Simple:

PROGRAM UsingUnit;
(* How to use a unit. *)

  USES
    MyUnit;

BEGIN
  SayHello
END.

First, the USES section says that we want to use MyUnit; see that it doesn't need the .pas extension.  Then we call SayHello.

If you save the previous program in the same directory than the unit as usingunit.pas you can compile with Free Pascal just executing:

fpc usingunit.pas

The compiler will be smart enough to find the units in the same directory!

Also note that we can't call AskName from the PROGRAM; try and see what happens.  That's because it wasn't declared in the INTERFACE section.  This allows to keep parts of the units private.

How we'll do this

So, to make things easy download this ZIP that contains the whole project.  As we progress, open appropriate file and read our walkthrough of that code.

Let's get started.  First: the fun stuff.

Assets

Here's some assorted space-related imagery.  Curiously, it's all packed into one image; more on that shortly.  It should be in the same directory than the executable, making sure it's named spritesheet.png.

Next up, some sound effects that absolutely, definitely have never been used in any game before.  Again, they should be in the same directory, making sure they're named as shown:

You'll notice that these are FLAC files.  Generally, FLAC is a good format for short samples of the pew-pew or kaboom persuasion, because they're smaller (but of the same quality) as WAVs.  There's info aplenty on their website if you're interested in why.

If you want to listen to those sound effects now, VLC Player will do the trick.

General stuff

Time for some global variables and helper functions.  Open the general.pas unit.  Other than the usual MustInit, this is where things start getting interesting.

Ubiquitous variables

INTERFACE

  VAR
  (* Frame count. *)
    Frames: LONGINT;

We're interested in the number of frames we've rendered so far.  More on this later.

Randomness

(* Random generator. *)
  FUNCTION IntegerBetween (CONST Lower, Higher: LONGINT): LONGINT;
  BEGIN
    IntegerBetween := Lower + Random (Higher - Lower)
  END;

  FUNCTION FloatBetween (CONST Lower, Higher: REAL): REAL;
  BEGIN
    FloatBetween :=
      IntegerBetween (TRUNC (Lower * 10000), TRUNC (Higher * 10000)) / 10000
  END;

We've added a cheeky couple of functions here to generate random numbers within a range (Lower and Higher).

As you may be aware, most games rely on a degree of randomness.  Ours will be no different.

Display stuff

Let's add some code to set up and use an ALLEGRO_DISPLAY.  Open display.pas.

Back near the beginning of this tutorial, we mentioned that our program's window is pretty small - especially if you're using a retina display - and that we'd sort this out later.

We've finally dealt with this problem in the above code.  It's now possible to scale up the display - giving us a nice pixelated look.

How it works

We start by defining some kooky-lookin' constants:

CONST
  (* Graphics buffer size. *)
    BUFFER_W = 320; BUFFER_H = 240;
  (* Display size. *)
    DISPLAY_SCALE = 3;
    DISPLAY_W = BUFFER_W * DISPLAY_SCALE; DISPLAY_H = BUFFER_H * DISPLAY_SCALE;

The BUFFER we're referring to here has a relatively small size:  320x240.  Regardless of the size of the player's screen, this is the size of the playfield we want to present to them:

Next, we define our DISPLAY's size as a multiple of BUFFER_W and BUFFER_H.  We then initialize both the display and the buffer in our Initialize function:

VAR
  fDisplay: ALLEGRO_DISPLAYptr;
  fBuffer: ALLEGRO_BITMAPptr;

(* Initializes display. *)
  PROCEDURE Initialize;
  BEGIN
  { Create window. }
    al_set_new_display_option(ALLEGRO_SAMPLE_BUFFERS, 1, ALLEGRO_SUGGEST);
    al_set_new_display_option(ALLEGRO_SAMPLES, 8, ALLEGRO_SUGGEST);
    fDisplay := al_create_display (DISPLAY_W, DISPLAY_H);
    MustInit (Assigned (fDisplay), 'Display');

    fBuffer := al_create_bitmap (BUFFER_W, BUFFER_H);
    MustInit (Assigned (fBuffer), 'bitmap buffer')
  END;

  ...

(* Release resources. *)
  PROCEDURE Deinitialize;
  BEGIN
    al_destroy_bitmap (fBuffer);
    al_destroy_display (fDisplay)
  END;

See that both variables are insid the IMPLEMENTATION section, so they can't be used outside the unit.  This way we can protect the internals of a unit.  That's also why we added GetEventSource function as we'll need it later.

The last time we created an ALLEGRO_BITMAP, we used the image addon.  This time, not so!  We've just gone ahead and created one with nothing in particular on it.  Yet.

And, like the lawful neutrals we are, we've also added a Deinitialize function to destroy these before the program ends.

Now comes the kicker:

(* Prepares the output. *)
  PROCEDURE PreDraw;
  BEGIN
    al_set_target_bitmap (fBuffer);
    al_clear_to_color (al_map_rgb (0,0,0))
  END;



(* Finalizes the output and swap buffers. *)
  PROCEDURE PostDraw;
  BEGIN
  { Scales backbuffer. }
    al_set_target_backbuffer (fDisplay);
    al_draw_scaled_bitmap (
      fBuffer,
      0, 0, BUFFER_W, BUFFER_H,
      0, 0, DISPLAY_W, DISPLAY_H,
      0
    );
  { Show frame. }
    al_flip_display
  END;

We're going to call these functions before and after we draw all of our game's graphics.  Here's what's going on:

Show me!

But of course. We've set our DISPLAY_SCALE to 3, meaning our 320x240 buffer will be scaled up - in all of its chunky, pixelated glory - to fill a 960x720 window:

Why do we need buffer?

The simple answer is:  you can't draw a bitmap to itself.

In we could do that, we could create an ALLEGRO_DISPLAY of any size we liked, draw our playfield in the top-left corner, then scale-up the display's backbuffer onto itself.

Unfortunately, this isn't possible, and as a result, you've had to sit through this explanation.  Sad!

Keyboard stuff

Take a look to the keyboard.pas unit:

CONST
  (* Key statuses. *)
    KEY_SEEN     = 1;
    KEY_RELEASED = 2;

  VAR
    Key: ARRAY [0..ALLEGRO_KEY_MAX] OF BYTE;

  ...

(* Initialization. *)
  PROCEDURE Initialize;
  VAR
    Ndx: INTEGER;
  BEGIN
    MustInit (al_install_keyboard, 'keyboard');
    FOR Ndx := LOW (Key) TO HIGH (Key) DO Key[Ndx] := 0
  END;



(* Handles keyboard events. *)
  PROCEDURE HandleEvent (CONST aEvent: ALLEGRO_EVENT);
  VAR
    Ndx: INTEGER;
  BEGIN
    CASE aEvent.ftype OF
    ALLEGRO_EVENT_TIMER:
      FOR Ndx := LOW (Key) TO HIGH (Key) DO Key[Ndx] := Key[Ndx] AND KEY_SEEN;

    ALLEGRO_EVENT_KEY_DOWN:
      Key[aEvent.keyboard.keycode] := KEY_SEEN OR KEY_RELEASED;
    ALLEGRO_EVENT_KEY_UP:
      Key[aEvent.keyboard.keycode] :=
        Key[aEvent.keyboard.keycode] AND KEY_RELEASED;
    END
  END;

A relief for us both, as there's basically nothing new here:

Come out, ye sprites!

Remember that spritesheet.png above?  Time to put it to good use. Be warned, this is a big'un:

INTERFACE

  USES
    allegro5;

  CONST
  (* Sprite sizes. *)
    SHIP_W = 12; SHIP_H = 13;
    SHIP_SHOT_W = 2; SHIP_SHOT_H = 9;

    LIFE_W = 6; LIFE_H = 6;

    ALIEN_W: ARRAY [0..2] OF INTEGER = (14, 13, 45);
    ALIEN_H: ARRAY [0..2] OF INTEGER = (9, 10, 27);

    ALIEN_BUG_W      = 14; ALIEN_BUG_H    = 9;    { ALIEN[0] }
    ALIEN_ARROW_W    = 13; ALIEN_ARROW_H = 10;    { ALIEN[1] }
    ALIEN_THICCBOI_W = 45; ALIEN_THICCBOI_H = 27; { ALIEN[2] }

    ALIEN_SHOT_W = 4; ALIEN_SHOT_H = 4;

  (* Animation lengths. *)
    EXPLOSION_FRAMES = 4;
    SPARK_FRAMES = 3;

  VAR
  (* Reference to sprite graphics.  Use them as if they were read-only. *)
    Ship: ALLEGRO_BITMAPptr;
    ShipShot: ARRAY [0..1] OF ALLEGRO_BITMAPptr;
    Life: ALLEGRO_BITMAPptr;

    Alien: ARRAY [0..2] OF ALLEGRO_BITMAPptr;
    AlienShot: ALLEGRO_BITMAPptr;

    Explosion: ARRAY [0..EXPLOSION_FRAMES - 1] OF ALLEGRO_BITMAPptr;
    Sparks: ARRAY [0..SPARK_FRAMES - 1] OF ALLEGRO_BITMAPptr;

    PowerUp: ARRAY [0..3] OF ALLEGRO_BITMAPptr;

   ...

IMPLEMENTATION

  VAR
    fSheet: ALLEGRO_BITMAPptr;

(* Loads and initializes sprite graphics. *)
  PROCEDURE Initialize;

    FUNCTION SpriteGrab (CONST x, y, w, h: INTEGER): ALLEGRO_BITMAPptr;
    BEGIN
      SpriteGrab := al_create_sub_bitmap (fSheet, x, y, w, h);
      MustInit (Assigned (SpriteGrab), 'sprite grab')
    END;

  BEGIN
    MustInit (al_init_image_addon, 'image');

    fSheet := al_load_bitmap ('spritesheet.png');
    MustInit (Assigned (fSheet), 'spritesheet');

    Ship := SpriteGrab (0, 0, SHIP_W, SHIP_H);

    Life := SpriteGrab (0, 14, LIFE_W, LIFE_H);

    ShipShot[0] := SpriteGrab (13, 0, SHIP_SHOT_W, SHIP_SHOT_H);
    ShipShot[1] := SpriteGrab (16, 0, SHIP_SHOT_W, SHIP_SHOT_H);

    Alien[0] := SpriteGrab (19,  0, ALIEN_BUG_W,      ALIEN_BUG_H);
    Alien[1] := SpriteGrab (19, 10, ALIEN_ARROW_W,    ALIEN_ARROW_H);
    Alien[2] := SpriteGrab ( 0, 21, ALIEN_THICCBOI_W, ALIEN_THICCBOI_H);

    AlienShot := SpriteGrab (13, 10, ALIEN_SHOT_W, ALIEN_SHOT_H);

    Explosion[0] := SpriteGrab (33, 10, 9, 9);
    Explosion[1] := SpriteGrab (43, 9, 11, 11);
    Explosion[2] := SpriteGrab (46, 21, 17, 18);
    Explosion[3] := SpriteGrab (46, 40, 17, 17);

    Sparks[0] := SpriteGrab (34, 0, 10, 8);
    Sparks[1] := SpriteGrab (45, 0, 7, 8);
    Sparks[2] := SpriteGrab (54, 0, 9, 8);

    Powerup[0] := SpriteGrab (0, 49, 9, 12);
    Powerup[1] := SpriteGrab (10, 49, 9, 12);
    Powerup[2] := SpriteGrab (20, 49, 9, 12);
    powerup[3] := SpriteGrab (30, 49, 9, 12)
  END;



(* Releases resources. *)
  PROCEDURE Deinitialize;

    PROCEDURE ReleaseArray (SprArray: ARRAY OF ALLEGRO_BITMAPptr);
    VAR
      Ndx: INTEGER;
    BEGIN
        FOR Ndx := LOW (SprArray) TO HIGH (SprArray) DO
          al_destroy_bitmap (SprArray[Ndx])
    END;

  BEGIN
    al_destroy_bitmap (Ship);
    ReleaseArray (ShipShot);
    ReleaseArray (Alien);
    al_destroy_bitmap (AlienShot);
    ReleaseArray (Explosion);
    ReleaseArray (Sparks);
    ReleaseArray (PowerUp);

    al_destroy_bitmap (fSheet)
  END;

For those unaware, sprites are small bitmaps that are often loaded from a spritesheet - a larger bitmap that has lots of smaller bitmaps - sub-bitmaps, if you will - packed into it.  Hence, our spritesheet.png.

The vast majority of graphics drawn by older consoles - not least the NES and Sega Genesis - are sprites.  And, believe it or not, using spritesheets is *still* far more efficient than loading in graphics from separate image files - even in the Fortnite age.

Again, we could go into a huge amount of discussion about this - but we're going to leave it there.

Anyway, that explanation aside, the above isn't as complicated as you might think:

Collision detection

Apologies in advance for what you're about to read.

(* Tests if two rectangles collide. *)
  FUNCTION Collide (CONST ax1, ay1, ax2, ay2, bx1, by1, bx2, by2: INTEGER)
    : BOOLEAN;
  BEGIN
    IF (ax1 > bx2) OR (ax2 < bx1) OR (ay1 > by2) OR (ay2 < by1) THEN
      EXIT (FALSE)
    ELSE
      EXIT (TRUE)
  END;

If you've gone anywhere near games development before, you'll likely know well of the nightmare that is collision detection.

In simple terms, this is how we know whether the things in our game - the player, the enemies, the bullets - have hit each other.  If we didn't do this, nobody would be able to kill anybody else. How awful!

There are plenty of ways to do collision detection, but in the interests of not driving you completely mad, we're going with the simplest method:  bounding boxes.

How does this work, you ask?  Well, imagine we want to check whether two objects - A and B - are hitting each other.  We give each object a box, and then - in Collide - we do a nifty bit of math to check whether the boxes overlap:

In the example on the left, Collide would return FALSE; on the right, it'd return TRUE.

For the moment, we'll leave it at that.  Bottom line: Collide returns TRUE when things overlap.

Audio stuff

We don't have much to do here other than load (and later destroy) the sound effects (audio.pas):

VAR
  (* Audio samples.  Use them as if they were read-only. *)
    SampleShot: ALLEGRO_SAMPLEptr;
    SampleExplode: ARRAY [0..1] OF ALLEGRO_SAMPLEptr;
   ...

(* Initializes. *)
  PROCEDURE Initialize;
  BEGIN
    MustInit (al_install_audio, 'audio');
    MustInit (al_init_acodec_addon, 'audio codecs');
    MustInit (al_reserve_samples (128), 'reserve samples');

    SampleShot := al_load_sample ('shot.flac');
    MustInit (Assigned (SampleShot), 'shot sample');

    SampleExplode[0] := al_load_sample ('explode1.flac');
    MustInit (Assigned (SampleExplode[0]), 'explode[0] sample');
    SampleExplode[1] := al_load_sample ('explode2.flac');
    MustInit (Assigned (SampleExplode[1]), 'explode[1] sample')
  END;



  PROCEDURE Deinitialize;
  BEGIN
    al_destroy_sample (SampleShot);
    al_destroy_sample (SampleExplode[0]);
    al_destroy_sample (SampleExplode[1])
  END;

You might be seeing a pattern emerge now:  we define a bunch of variables, maybe add some constants, write an Initialization function (and sometimes a Deinitialize too), and then anything else we might need to call while we're playing the game.

That said, none of the code we've added so far has actually drawn anything to the screen!  This is frankly ridiculous and must be remedied.

Effects, FX, and explosion-related stuff

Let's see the particles.pas unit now.

As Adam Sandler once quipped in a poor Israeli accent, it is time to make the bang boom. We begin by defining a RECORD that stores everything we could need to know about our bangs/booms:

CONST
(* How many particles. *)
  MAX_PARTICLES = 128;
  ...

TYPE
(* Stores particle. *)
  TParticle = RECORD
  (* Position. *)
    X, Y: INTEGER;
  (* Image. *)
    Frame: INTEGER;
  (* State. *)
    Spark, Used: BOOLEAN;
  END;

VAR
{ Particles. }
  fParticle: ARRAY [1..MAX_PARTICLES] OF TParticle;

We've got 128 of these bad bois ready to go in our fParticle array.  In Initialize, we then mark all of them as not Used:

(* Initializes. *)
  PROCEDURE Initialize;
  VAR
    Cnt: INTEGER;
  BEGIN
    FOR Cnt := 1 TO MAX_PARTICLES DO fParticle[Cnt].Used := FALSE
  END;

That's all well and good, but what if you do want explosions on screen?

Spawning the effects

(* Adds a particle. *)
  PROCEDURE Add (CONST aSpark: BOOLEAN; CONST aX, aY: INTEGER);
  VAR
    Cnt: INTEGER;
  BEGIN
  { Sound effect, if needed. }
    IF NOT aSpark THEN
      al_play_sample (
        SampleExplode[IntegerBetween (0, 2)],
        0.75, 0, 1, ALLEGRO_PLAYMODE_ONCE,
        NIL
      );
  { Add particle. }
    FOR Cnt := 1 TO MAX_PARTICLES DO
      IF NOT fParticle[Cnt].Used THEN
      BEGIN
        fParticle[Cnt].x := aX;
        fParticle[Cnt].y := aY;
        fParticle[Cnt].Frame := 0;
        fParticle[Cnt].Spark := aSpark;
        fParticle[Cnt].Used := TRUE;
        EXIT
      END
  END;

Firstly, we should talk about aSpark.  We want to show two types of effects:  sparks and explosions.  Sparks are smaller and appear when shots hit their target; explosions are bigger and indicate that something has actually blown up.

We've already got sparks and explosions loaded into our Sprites unit, ready to be animated:

Fig. 12a: the terrible graphics you'll shortly be subjected to.<br />Frames for the sparks are at the top, frames for explosions are below.
Fig. 12a: the terrible graphics you'll shortly be subjected to.
Frames for the sparks are at the top, frames for explosions are below.

So, in Add, we're looping through the fParticle array until we find one that isn't Used.  Once we do, we set it up with the given coordinates, and whether it's to be a Spark or not.

In the case of explosions, we also take the opportunity to play either explode1.flac or explode2.flac.

Cycling through our frames

(* Updates particles. *)
  PROCEDURE Update;
  VAR
    Ndx: INTEGER;
  BEGIN
    FOR Ndx := 1 TO MAX_PARTICLES DO
      IF fParticle[Ndx].Used THEN
      BEGIN
        INC (fParticle[Ndx].Frame);
        IF (NOT fParticle[Ndx].Spark
            AND (fParticle[Ndx].Frame = EXPLOSION_FRAMES * 2))
        OR (    fParticle[Ndx].Spark
            AND (fParticle[Ndx].Frame = SPARK_FRAMES * 2))
        THEN
          fParticle[Ndx].Used := FALSE
      END
  END;

fx_update is to run every frame, and makes sure that the Frame variables of Used effects are incremented.  Once the effect has run its course, it is returned to being un-used.

Note that we defined EXPLOSION_FRAMES and SPARKS_FRAMES back when we dealt with our sprites - and on that subject...

Drawing them

(* Draw particles. *)
  PROCEDURE Draw;
  VAR
    Ndx, lFrame, lX, lY: INTEGER;
    Bmp: ALLEGRO_BITMAPptr;
  BEGIN
    FOR Ndx := 1 TO MAX_PARTICLES DO
      IF fParticle[Ndx].Used THEN
      BEGIN

Draw loops through the fParticle array yet again - but this time, with the intent of drawing Used effects to the screen.

Firstly - if you happened to wonder why, in Update, we doubled the number of frames of explosions and sparks - here's your answer:

lFrame := fParticle[Ndx].Frame DIV 2;

We're actually cutting Frame in half.  Why?  Well, this is purely out of laziness; otherwise, we'd have to have twice as many frames in our spritesheet for our effects.  I'm afraid that's all there is to it:  your wiki authors are lazy.

We could draw the 3 or 4 frames we have without the DIV 2, but the effect would disappear pretty quickly.  So, there you have it:  this is the programming equivalent of polyfiller.

Next, we pick the ALLEGRO_BITMAP to draw (depending on Spark and Frame), and finally get to the business of calling al_draw_bitmap:

  IF fParticle[Ndx].Spark THEN
    Bmp := Sprites.Sparks[lFrame]
  ELSE
    Bmp := Sprites.Explosion[lFrame];
{ Calculate position. }
  lX := fParticle[Ndx].x - (al_get_bitmap_width (Bmp) DIV 2);
  lY := fParticle[Ndx].y - (al_get_bitmap_height (Bmp) DIV 2);
{ Draw. }
  al_draw_bitmap (Bmp, lX, lY, 0)

You'll notice that we're centering the bitmap on x and y here; this is because the size of the effect's bitmap varies depending on the frame, so it's easier to just say x and y are the center of the effect.

We mentioned earlier that a pattern was emerging with our code.  From this point on, that pattern is going to get even more rigid.

For the rest of the objects in the game, we're going to be doing very similar things to what we've just done:

Confused?  Pretend you're not!  Let's continue.

Pew-pew actualization

Time to add some code for shots, because space shooters involve shooting.  Load unit shots.pas

We'll start with a quick look at our TShot = RECORD. It's pretty similar to TParticle:

TYPE
(* Defines a shot. *)
  TShot = RECORD
  (* Position. *)
    X, Y: INTEGER;
  (* Motion. *)
    dX, dY: INTEGER;
  (* Current frame. *)
    Frame: INTEGER;
  (* Who shooted. *)
    Ship: BOOLEAN;
  (* Is used. *)
    Used: BOOLEAN;
  END;

VAR
(* Shots. *)
  fShot: ARRAY [1..MAX_SHOTS] OF TShot;

...although there are a few additions, and no spark:

Next up, some choice cuts from Add, which is predictably called when the player or an alien wants to shoot:

FUNCTION Add (CONST aShip, aStraight: BOOLEAN; CONST aX, aY: INTEGER)
    : BOOLEAN;
...

BEGIN
{ Sound effect. }
  IF aShip THEN SndEffect := 1 ELSE SndEffect := FloatBetween (1.5, 1.6);
  al_play_sample (
    SampleShot,
    0.3, 0, SndEffect, ALLEGRO_PLAYMODE_ONCE,
    NIL
  );

shot.flac is played in all its bloopy glory here, but is tweaked depending on the context:  alien shots crank up the speed of the sample, making them sound higher-pitched.

What's that aStraight function argument, I hear you cry?  Well, as you'll see later on, we want some of our aliens to shoot straight ahead, while others are capable of sending shots anywhere.  Its value makes no difference to player shots, which always fly straight ahead.

While positioning the shot, we therefore use this to set its dX and dY:

IF aStraight THEN
BEGIN
  fShot[Cnt].dX := 0;
  fShot[Cnt].dY := 2
END
ELSE BEGIN
  fShot[Cnt].dX := IntegerBetween (-2, 2);
  fShot[Cnt].dY := IntegerBetween (-2, 2)
END;

There's a small chance that - with these calls to IntegerBetween - we'll create a shot with dx := 0 and dy := 0. In this case, we just give up:

{ If the shot has no speed, don't bother. }
  IF (fShot[Cnt].dX OR fShot[Cnt].dY) = 0 THEN EXIT (TRUE);

...and that's it for notable stuff in Add.

Onto Update- what happens to a live shot on every frame?  Well, that depends whether it's the player's or not:

IF fShot[Ndx].Ship THEN
BEGIN
  DEC (fShot[Ndx].y, 5);

  IF fShot[Ndx].y < -SHIP_SHOT_H THEN
    fShot[Ndx].Used := FALSE
END

Those of an eagle-eyed persuasion will have noticed that, back in Add, we didn't bother setting dX and dY for shots from the player's ship; this is because they always move at the same speed (upwards at 5 pixels per frame).

Once shots are completely out-of-view, they're removed from play.  As player shots only travel upwards, this is a bit easier to detect than with alien shots, which we'll look at next:

ELSE BEGIN { Alien. }
  INC (fShot[Ndx].x, fShot[Ndx].dX);
  INC (fShot[Ndx].y, fShot[Ndx].dY);

  IF (-ALIEN_SHOT_W > fShot[Ndx].x) OR (fShot[Ndx].x > BUFFER_W)
  OR (-ALIEN_SHOT_H > fShot[Ndx].y) OR (fShot[Ndx].y > BUFFER_H)
  THEN
    fShot[Ndx].Used := FALSE
END;

Nothing surprising in how these are moved, then:  we add the shot's velocity to its position.  Likewise, because they can move in any direction, there's >ing to be done, so to speak.

Clash of the pew-pew titans

Next, we need a way of checking whether any shot has hit something. You'll recall the fun we had with `collide()` earlier on - time to give it a go:

(* Check collisions with shots. *)
  FUNCTION Collide (CONST aShip: BOOLEAN; CONST x, y, w, h: INTEGER): BOOLEAN;
  VAR
    Ndx, sw, sh: INTEGER;
  BEGIN
  { Shoot size. }
    IF aShip THEN
    BEGIN
      sw := ALIEN_SHOT_W; sh := ALIEN_SHOT_H
    END
    ELSE BEGIN
      sw := SHIP_SHOT_W; sh := SHIP_SHOT_H
    END;
  { Check collisions. }
    FOR Ndx := 1 TO MAX_SHOTS DO
      IF fShot[Ndx].Used
    { Don't collide with one's own shots. }
      AND (fShot[Ndx].Ship <> aShip)
      THEN BEGIN
        IF Sprites.Collide (
           x, y, x + w, y + h,
           fShot[Ndx].x, fShot[Ndx].y, fShot[Ndx].x + sw, fShot[Ndx].y + sh
        ) THEN
        BEGIN
          Particles.Add (
            TRUE,
            fShot[Ndx].x + (sw DIV 2), fShot[Ndx].y + (sh DIV 2)
          );
          fShot[Ndx].Used := FALSE;
          EXIT (TRUE)
        END
      END;
    EXIT (FALSE)
  END;

Despite our earlier protestations, this function isn't the most terrible of the bunch:

You'll see Shots.Collide called twice later on.  10 points for each correct guess as to where.

A quick draw

Lastly we have Draw, which is mostly trivial at this point:  it just draws the bitmap for every active shot.  There is one thing to note when drawing alien shots, though:

IF lFrame <> 0 THEN
  Tint := al_map_rgb_f (1, 1, 1)
ELSE
  Tint := al_map_rgb_f (0.5, 0.5, 0.5);
al_draw_tinted_bitmap (
  Sprites.AlienShot,
  Tint, fShot[Ndx].x, fShot[Ndx].y,
  0
)

For the player's shots, we use the lFrame variable to choose a bitmap (either ShipShot[0] or [1]).  In the case of aliens, though, there's only one:  AlienShot.

To give this some pizzazz, then, we flash it in and out with al_draw_tinted_bitmap.

As we cover more and more of the same patterns here, we hope you'll forgive the increased pace; we anticipate that readers' desks are beginning to be flipped at this point.  Remember to grab a coffee / beer at some point.  Deodorant is good too.

Otherwise, let's push on.  Next up, we give the protagonist so well-needed attention.

Ready Player 1

It's time for player.pas.  It is different from our Particles and Shots, because there's only one ship - so no looping needed.  Let's have a look at how things start out:

  TYPE
  (* Defines the player ship. *)
    TShip = RECORD
    (* Position. *)
      x, y: INTEGER;
    (* Lives. *)
      Lives: INTEGER;
    (* Statuses. *)
      ShotTimer, RespawnTimer, InvincibleTimer: INTEGER;
    END;

  VAR
    fShip: TShip;

(* Initializes. *)
  PROCEDURE Initialize;
  BEGIN
    fShip.x := (BUFFER_W DIV 2) - (SHIP_W DIV 2);
    fShip.y := (BUFFER_H DIV 2) - (SHIP_H DIV 2);
    fShip.ShotTimer := 0;
    fShip.Lives := 3;
    fShip.RespawnTimer := 0;
    fShip.InvincibleTimer := 120
  END;

So, they get 3 lives, are centred on the screen, and are invincible - at least for a couple of seconds.  These variables (and the Score in the INTERFACE section) might be relatively self-explanatory at this point, but we'll see how they work shortly.

Now let's head onto Update:

(* Updates ship. *)
  PROCEDURE Update;
  VAR
    lX, lY: INTEGER;
  BEGIN
  { Check ship is ready. }
    IF fShip.Lives < 0 THEN EXIT;

A frosty reception.  Necessary, though, unless we're planning on the player living forever.

Life ain't fair, though - so if the player has totally run out of lives, there's no Update to do. a Likewise, if they've just been blown up, they might be waiting to respawn, constituting another early `return`:

IF fShip.RespawnTimer > 0 THEN
BEGIN
  DEC (fShip.RespawnTimer);
  EXIT
END;

Next, we move the ship around according to the keys pressed:

{ Move ship. }
  IF Key[ALLEGRO_KEY_LEFT]  <> 0 THEN DEC (fShip.x, SPEED);
  IF Key[ALLEGRO_KEY_RIGHT] <> 0 THEN INC (fShip.x, SPEED);
  IF Key[ALLEGRO_KEY_UP]    <> 0 THEN DEC (fShip.y, SPEED);
  IF Key[ALLEGRO_KEY_DOWN]  <> 0 THEN INC (fShip.y, SPEED);

  IF fShip.x < 0 THEN fShip.x := 0;
  IF fShip.y < 0 THEN fShip.y := 0;

  IF fShip.x > MAX_X THEN fShip.x := MAX_X;
  IF fShip.y > MAX_Y THEN fShip.y := MAX_Y;

There's some hemming-in of the player's movements going on here, because we don't want them flying out of the playfield.  That's because it'd make it too easy for them to avoid what comes next:

{ Collisions. }
  IF fShip.InvincibleTimer > 0 THEN
    DEC (fShip.InvincibleTimer)
  ELSE IF Shots.Collide (TRUE, fShip.x, fShip.y, SHIP_W, SHIP_H) THEN
  BEGIN
    lX := fShip.x + (SHIP_W DIV 2);
    lY := fShip.y + (SHIP_H DIV 2);
    Particles.Add (FALSE, lx    , ly    );
    Particles.Add (FALSE, lx + 4, ly + 2);
    Particles.Add (FALSE, lx - 2, ly - 4);
    Particles.Add (FALSE, lx + 1, ly - 5);

    DEC (fShip.Lives);
    fShip.RespawnTimer := 90;
    fShip.InvincibleTimer := 180
  END;

At this point - if they've not got some invincibility to spare - we're checking whether the player is dodging the aliens' barrage successfully by making our first call to Shots.Collide.  If that comes back positive, it's bad news:

However, if they were lucky enough to dodge every alien shot currently on the field, they'll be able to fire some shots of their own:

{ Shooting. }
  IF fShip.ShotTimer > 0 THEN
    DEC (fShip.ShotTimer)
  ELSE IF Key[ALLEGRO_KEY_X] <> 0 THEN
  BEGIN
    lX := fShip.x + (SHIP_W DIV 2);
    IF Shots.Add (TRUE, FALSE, lX, fShip.y) THEN fShip.ShotTimer := 5
  END

We use ShotTimer to space out the shots a bit; having one appear on every single frame would be a tad excessive.  On the other hand, if we're ready to shoot, the X key will trigger it, with Shots.Add doing most of the heavy lifting.

Believe it or not, that's almost all there is to say here.  All that's left is to draw the ship itself:

(* Draws ship. *)
  PROCEDURE Draw;
  BEGIN
  { Check if draw or not. }
    IF (fShip.Lives < 0) OR (fShip.RespawnTimer > 0)
    OR (((fShip.InvincibleTimer DIV 2) MOD 3) = 1) { Blinking. }
    THEN
      EXIT;
  { Draw. }
    al_draw_bitmap (Sprites.Ship, fShip.x, fShip.y, 0)
  END;

No real feat of engineering, other than to say that the ship obviously isn't drawn when there's a game over (or it's just been blown up), and that it characteristically blinks when invincible:

OR (((fShip.InvincibleTimer DIV 2) MOD 3) = 1) { Blinking. }

This unwieldy equation evaluates to "don't show the ship on the 2nd and 3rd frame in every 6 frames", which happens to be a good balance for a blinking effect.

The villains appear

We're going to implement those aliens now.  If you looked at spritesheet.png under a magnifying glass, you'll have seen the 3 distinct types above - and while they all behave pretty similarly, it's the differences between them that bulk out the code here.  So, be warned, this is the largest of the sections.

The unit file is aliens.pas.

This time, we've had something else to define before our RECORD: the three types we mentioned.

TYPE
(* Alien types. *)
  TAlienTypes = (
    atBug = 0,
    atArrow,
    atThiccboi,
    atN
  );

(* Defines an alien. *)
  TAlien = RECORD
  (* Position. *)
    x, y: INTEGER;
  (* Type. *)
    AlienType: TAlienTypes;
  (* Statuses. *)
    ShotTimer, Blink, Life: INTEGER;
    Used: BOOLEAN;
  END;

VAR
(* The aliens. *)
  fAlien: ARRAY [1..NUM_ALIENS] OF TAlien;

Lots of familiar variables here, but with some fun additions: AlienType, blink and life.

Life is the number of hits the alien can take before it explodes, and Blink's purpose will become clear shortly - though you can probably make an educated guess.

Filling the quota

In Update, there's some odd stuff going on right off the bat:

VAR
  Ndx, NewQuota, NewX, cX, cY: INTEGER;
BEGIN
{ Values for new enemies. }
  IF Frames MOD 120 <> 0 THEN
    NewQuota := 0
  ELSE
    NewQuota := IntegerBetween (2, 4);
  NewX := IntegerBetween (10, BUFFER_W - 50);

This is part of the mechanism for adding aliens to the playfield.  Remember that global Frames variable we declared years ago?  Well, this is one of its uses:  every 120 frames, we're saying we want a fresh batch of aliens to appear.

So, if it's time, NewQuota will be set, along with that mysterious NewX attribute.  Either way, we now set off looping through the fAlien array.

{ For each enemy... }
  FOR Ndx := 1 TO NUM_ALIENS DO
  BEGIN
    IF NOT fAlien[Ndx].Used THEN
    BEGIN
    { If this alien is unused, should it spawn? }
      IF NewQuota > 0 THEN
      BEGIN
        INC (NewX, IntegerBetween (40, 80));
        IF NewX > BUFFER_W - 60 THEN DEC (NewX, BUFFER_W - 60);

        fAlien[Ndx].x := NewX;

        fAlien[Ndx].y := IntegerBetween (-40, -30);
        fAlien[Ndx].AlienType := TAlienTypes (IntegerBetween (0, ORD (atN)));
        fAlien[Ndx].ShotTimer := IntegerBetween (1, 99);
        fAlien[Ndx].Blink := 0;
        fAlien[Ndx].Used := TRUE;

        CASE fAlien[Ndx].AlienType OF
        atBug:
          fAlien[Ndx].Life := 4;
        atArrow:
          fAlien[Ndx].Life := 2;
        atThiccboi:
          fAlien[Ndx].Life := 12;
        END;
        DEC (NewQuota)
      END
    END

Here, if we come across an unused alien - one that normally wouldn't be updated - we're taking the opportunity to spawn it onto the playfield.

The spawned alien's x is then set, and NewX is nudged by a random amount towards the edge of the screen. Once it goes past a certain point, it's pushed back towards the left, like some kind of ephemeral typewriter.

All this does is ensure an even spread of aliens across the top of the screen.  We'd prefer that two aliens didn't arrive on top of each other, because this could look a bit silly (or confusing) to the player.

All other fields of TAlien are then set - other than Life, whose initial value depends on the alien's type; we want it such that bigger, slower-moving aliens can take more of a walloping from the player.

With that, our NewQuota drops by 1, and we continue looping.

They live!

As for the aliens that are already alive at this point, it's time for an update. Firstly, how fast should they move?

{ Updates enemy. }
  CASE fAlien[Ndx].AlienType OF
  atBug:
    IF Frames MOD 2 <> 0 THEN INC (fAlien[Ndx].y);
  atArrow:
    INC (fAlien[Ndx].y);
  atThiccboi:
    IF Frames MOD 4 = 0 THEN INC (fAlien[Ndx].y);
  END;

We're using Frames here to dictate how fast each type of alien moves.  atArrow`s are fastest and move down a pixel every frame; atBugs and atThiccbois move every second and fourth frame respectively.

{ Collisions with shots. }
  IF Shots.Collide (
    FALSE,
    fAlien[Ndx].x, fAlien[Ndx].y,
    ALIEN_W[ORD (fAlien[Ndx].AlienType)], ALIEN_H[ORD (fAlien[Ndx].AlienType)]
  ) THEN
  BEGIN
    DEC (fAlien[Ndx].Life);
    fAlien[Ndx].Blink := 4
  END;

Later, we've got our obligatory call to Shots.Collide.  If the alien is hit, it (perhaps sadly) doesn't explode immediately, but its Life does decrease.  Blink is also set (which, again, we'll explain later).

We then need to consider whether said alien might be out of luck:

{ Test enemy life. }
  IF fAlien[Ndx].Life <= 0 THEN
  BEGIN
    Particles.Add (FALSE, cX, cY);

    CASE fAlien[Ndx].AlienType OF
    atBug:
      INC (Player.Score, 200);
    atArrow:
      INC (Player.Score, 150);
    atThiccboi:
      BEGIN
        INC (Player.Score, 800);

        Particles.Add (FALSE, cX - 10, cY -  4);
        Particles.Add (FALSE, cX +  4, cY + 10);
        Particles.Add (FALSE, cX +  8, cY +  8)
      END;
    END;
    fAlien[Ndx].Used := FALSE;
    CONTINUE
  END;

If this is indeed the case, AlienType is called upon once again; it's finally time to add to the Player.Score!  The number of points added depends on the alien's type, as does the number of explosions produced: most aliens produce a meager single explosion, while atThiccbois add an extra 3.

On the other hand - if this alien is to live to fight another day - it may well want to fight now.  Time to add some shots:

{ Check if shot. }
  DEC (fAlien[Ndx].ShotTimer);
  IF fAlien[Ndx].ShotTimer <= 0 THEN
    CASE fAlien[Ndx].AlienType OF
    atBug:
      BEGIN
        Shots.Add (FALSE, FALSE, cX, cY);
        fAlien[Ndx].ShotTimer := 150
      END;
    atArrow:
      BEGIN
        Shots.Add (FALSE, FALSE, cX, fAlien[Ndx].y);
        fAlien[Ndx].ShotTimer := 80
      END;
    atThiccboi:
      BEGIN
        Shots.Add (FALSE, TRUE, cX - 5, cY);
        Shots.Add (FALSE, TRUE, cX + 5, cY);
        Shots.Add (FALSE, TRUE, cX - 5, cY + 8);
        Shots.Add (FALSE, TRUE, cX + 5, cY + 8);
        fAlien[Ndx].ShotTimer := 200
      END;
    END

More AlienType-dependent stuff here, both for the number of shots fired, and the amount of time before the next batch.

With that, the aliens are up-to-speed.  Compared to that, Draw is pretty minimal:

(* Draw enemies. *)
  PROCEDURE Draw;
  VAR
    Ndx: INTEGER;
  BEGIN
    FOR Ndx := 1 TO NUM_ALIENS DO
      IF (fAlien[Ndx].Used) AND (fAlien[Ndx].Blink <= 2) THEN
        al_draw_bitmap (
          Sprites.Alien[ORD (fAlien[Ndx].AlienType)],
          fAlien[Ndx].x, fAlien[Ndx].y,
          0
        )
  END;

However, we finally see Blink come into its own here.  Because it's set to 4 when an alien is hit, it flickers the alien out for a couple of frames, which should do the trick as a further indicator to the player that they were on target.

Most of the difficult stuff is out of the way now.  If you're still reading, you deserve a medal.  Or how about some stars?

Shiny stuff

Load unit starfield.pas.  Here's our first bit of polish.  Compared with the behemothic RECORDs we've seen so far, stars are simple:

CONST
  NUM_STARS = (BUFFER_W DIV 2) - 1;

TYPE
(* Star definition. *)
  TStar = RECORD
    y, Speed: REAL;
  END;

VAR
  fStar: ARRAY [1..NUM_STARS] OF TStar;

There's no used this time, because all of the stars are permanently displayed.  What a treat.

Note the odd definition of NUM_STARS here:  this means there's one star for almost every other horizontal pixel on the playfield.

So, in Initialize, we scatter the stars across the Y-axis randomly:

(* Initializes the starfield. *)
  PROCEDURE Initialize;
  VAR
    Cnt: INTEGER;
  BEGIN
    FOR Cnt := 1 TO NUM_STARS DO
    BEGIN
      fStar[Cnt].y := FloatBetween (0, BUFFER_H);
      fStar[Cnt].Speed := FloatBetween (0.1, 1)
    END
  END;

Update then (perhaps predictably) adds Speed to y on every frame.  If a star goes out of view, it's nudged back to the top and given a different (but still random) speed:

(* Updates starfield. *)
  PROCEDURE Update;
  VAR
    Ndx: INTEGER;
  BEGIN
    FOR Ndx := 1 TO NUM_STARS DO
    BEGIN
      fStar[Ndx].y := fStar[Ndx].y + fStar[Ndx].Speed;
      IF fStar[Ndx].y >= BUFFER_H THEN
      BEGIN
        fStar[Ndx].y := 0;
        fStar[Ndx].Speed := FloatBetween (0.1, 1)
      END
    END
  END;

And, to finish:

(* Draws starfield. *)
  PROCEDURE Draw;
  VAR
    StarX, Clr: REAL;
    Ndx: INTEGER;
  BEGIN
    StarX := 1.5;
    FOR Ndx := 1 TO NUM_STARS DO
    BEGIN
      Clr := fStar[Ndx].Speed * 0.8;
      al_draw_pixel (StarX, fStar[Ndx].y, al_map_rgb_f (Clr, Clr, Clr));
      StarX := StarX + 2;
    END
  END;

This is why we defined NUM_STARS as we did: in order to get a nice, evenly-distributed set of stars on the screen, we draw one on every other pixel on the X-axis.  Cool?  No?  Okay.

Heads up

Y'all heard of a HUD?  This is where we display info to the player on their progress - like their score and remaining lives.  The one useb by our game is in the appropriately named hud.pas file.

Before we display anything of this variety, we've got some setup to do.  Firstly, we haven't created a font yet; secondly, how about animating the score a bit?

  VAR
    fFont: ALLEGRO_FONTptr;
    fScore: LONGINT;

(* Initializes HUD. *)
  PROCEDURE Initialize;
  BEGIN
    fFont := al_create_builtin_font;
    MustInit (Assigned (fFont), 'font');

    fScore := 0
  END;
  ...

(* Releases resources. *)
  PROCEDURE Deinitialize;
  BEGIN
    al_destroy_font (fFont)
  END;

No surprises here.  Next, let's see how that fScore works:

(* Updates HUD. *)
  PROCEDURE Update;
  VAR
    i, Diff: LONGINT;
  BEGIN
    IF Frames MOD 2 = 0 THEN
      FOR i := 5 DOWNTO 1 DO
      BEGIN
        Diff := 1 SHL i;
        IF fScore <= Player.Score - Diff THEN
          INC (fScore, Diff)
      END
  END;

What's happening here is that we're gradually increasing fScore until it matches Player.Score.  However, as with many other things in our game, we're only going to do this every other frame - lest it look frantically fast.

As for how it's increased: there's some bit-shifting trickery at work here.  In short, if fScore is a long way behind Player.Score, it increases faster.  As for how, read the following if you're interested - but, if we're being honest, this is more code-flagellation than important knowledge.

You may know that 1 SHL i is a computationally cheap way of calculating 2i.  In the FOR loop above, we are checking whether fScore is at least 2i behind Player.Score (where i starts at 5 and decreases to 1), and - if this is true - we add 2i to fScore.  This happens to give us a nicely adaptive 'rolling' score animation for the numbers we're dealing with.

At long last, we've got everything in place to draw the HUD.  Let's examine each piece of Draw in turn:

CONST
  Spacing = LIFE_W + 1;
VAR
  Cnt: INTEGER;
BEGIN
  al_draw_textf (
    fFont, al_map_rgb_f (1,1,1),
    1, 1, 0,
    '%.06d', [fScore]
  );

First, we draw the score in the upper-left corner. The '%.06d' format ensures we have lots of leading zeroes - 6 seems to be a good number).

FOR Cnt := Player.NumLives - 1 DOWNTO 0 DO
  al_draw_bitmap (Sprites.Life, 1 + (Cnt * Spacing), 10, 0);

Next, the player's remaining lives - one tiny ship sprite for each.

IF Player.NumLives < 0 THEN
  al_draw_text (
    fFont, al_map_rgb_f (1, 1, 1),
    BUFFER_W DIV 2, BUFFER_H DIV 2, ALLEGRO_ALIGN_CENTER,
    'G A M E  O V E R'
  )

Finishing as we started (on a grave note), we let the player know if they're out of lives.

We've now got everything in place.  All that remains is to put it all together...

Finale

The crazy thing about our PROGRAM Game (file game.pas) is that there's barely anything to say about it.  It's just a standard Allegro event loop - of which you've seen plenty in this tutorial - but with all of our hard work from above slotted into it.

Compile & run

The Zip file (back here) contains two scripts to compile it with Free Pascal.

On Delphi rename game.pas to game.dpr and open it with Delphi; this will create a new project.  Set up the output directories and that's all.

If you use Lazarus it is a bit harder:  Create a simple program and save it in the src/ directory with name game.  Then remove the game.lpr and rename game.pas to game.lpr.  Open Lazarus again, load the project and set up the directories.

fp IDE users should know what to do...  Ok, I almost never used fp IDE so I'm not sure how to do it.

At long last - providing everything's in place (or should we say 'in paste') - you should be greeted with a functional game.

For those who've forgotten the controls amidst the sea of code:

You want more?

Seriously!?  Okay, well while we imagine you'll have already staggered off to the bar by this point, there's a ton of stuff that can be improved with what we've already done.  To name a few:

And with yet another barrage of information, we draw the a close.  We're honoured that you've stuck with us until now. We're not quite done yet though...

◀ Sound | Epilogue ▶