Artillery Part 4: Ready, Aim, Fire!

Hello again! I'm back with another instalment of my tutorial. This time I will be getting into the most important part of the whole game; the aiming and firing of the tank's gun.happy

Now this is going to involve a little bit of trigonometry and a little bit of simplified physics.

If you have not studied trigonometry mathematics yet then I recommend checking out one of these fine articles as a brief primer. However this is optional as you don't really have to understand the equations so much as know what they do.

Dave's Short Trig Course
Trigonometry (Wikipedia)
Introduction to Trigonometric Functions

Also, you may notice that I've changed the way I'm going over the code in this tutorial. Before, I walked you through the additions. Here, what I'll do instead is simply explain each part and let you see for yourself how it all fits together in the source files I give you. You'll get just as much detailed information, just without a long set of step-by-step instructions.

Aiming

Aiming

Part 3 Recap

As I've done in Part 3, I'm going to do a bit of a recap of what we did last time. Unlike last time, I don't really have too much to add.

In our previous lesson we learned how to place our tanks in random locations. We also learned how to keep them separate so that a set distance can be imposed that ensures a decent set-up for a start of a round.

Setting The Distance

When you start playing around with the distance at which you can set your tanks apart, take care not to enter an impossible value.

You could create a fail-safe that would reduce the distance set to a reasonable value considering the amount of tanks, their sizes and, of course, the battlefield width itself.

You may not want to set it too low either. Something like 0 to 3 would negate the purpose of the function. Of course, this is entirely up to how you wish to proceed. You may later on want to create a 'teams mode' of play, in which it may be alright to have 2 tanks of the same team initially side-by-side. This is entirely up to you and your creativity.

A Few More New Files

Before we get started with our task here, there are a couple of new files that we are going to need.

AdvancedMathUnit.pas

This new unit will help us by creating a look-up table and includes a few functions that will take care of some basic angle calculations that we will need. We'll touch on the inner-workings of this unit later.

FontUnit.pas

This unit will allow us to draw text characters to the screen so that we can display important data such as the exact power of shot and the angle of our tank's barrel.

SystemFontWhite.bmp

This is our font's character set. You'll notice that each character is 8 x 12. We'll be sticking this file into the 'images' folder along with the rest.

Here are the new files:
New Units - a73_UnitFiles.zip (2.27 KB)
Font Bitmap - a73_ImageFile.zip (1.13 KB)

NOTE: Make sure you have put both unit files in the Project's main folder and the image file into the images folder!

Update TTank

We are going to revisit the TTank object to add a few more things. These additions will allow us to aim the gun and control the direction that the tank is facing based on the gun's angle.

Here is our new TTank class.

TTank = class(TObject)
    X, Y: Real;
    TrackSize: Cardinal;
    TurretX, TurretY,
    TurretLength: Integer;
    TurretEndX, TurretEndY: Real; // New!
    AimAngle, AimPower: Integer; // Modified
    Facing: Integer; // New!
    Color: Cardinal;
    Sprite: PSDL_Surface;
    constructor Init(oTrackSize: Cardinal; oTurretX, oTurretY, oTurretLength: Integer; oColor: Cardinal; ImageFile: String);
    procedure ChangeTurret(NewAngle: Integer); // New!
    procedure Draw(GameScreen: PSDL_Surface);
  end;  
    

TurretEndX and TurretEndY are the offset from TurretX and TurretY which will be the end of the gun's barrel.
Facing is just the direction of the tank represented as either -1 for left or 1 for right.
ChangeTurret() is the procedure that will update the angle of the tank's aim.

AimAngle and AimPower were changed from Real to Integer in this version as I saw no need to keep them as floating point values.

Not too much was added to the class definition here, but there are more modifications to the Init() and Draw() procedures we will go over as well.

Before we head on to the functions, jump to the top of the unit and add the 'AdvancedMathUnit' unit under your uses clause. It should now look like this:

uses
  SysUtils,
  // JEDI-SDL
  sdl,
  sdlutils,

  AdvancedMathUnit,
  GraphicsUnit,
  GameConstantsUnit;  
    

You'll understand why we needed this in a bit.happy

Update TTank.Init()

Here is the new Init() for our TTank class...

constructor TTank.Init(oTrackSize: Cardinal; oTurretX, oTurretY, oTurretLength: Integer; oColor: Cardinal; ImageFile: String);
begin
    X := 0;
    Y := 0;
    TrackSize := oTrackSize;

    TurretX := oTurretX;
    TurretY := oTurretY;
    TurretLength := oTurretLength;

    AimAngle := 45;  // Default Value
    AimPower := 500; // Default Value

    Color := oColor;

    Facing := 1;

    // Load Sprite File
    Sprite := LoadImage('images/' + ImageFile, True);
end;  
     

The only thing we added here was the direction of Facing.

Update TTank.Draw()

We've added a bit more to Draw() as you'll notice here...

procedure TTank.Draw(GameScreen: PSDL_Surface);
var
  SrcRect: TSDL_Rect;
  DestRect: TSDL_Rect;
  TurretEndX, TurretEndY: Real;

  TempFrame: PSDL_Surface;
begin
     // Create Temp Frame for Animation
     TempFrame := SDL_AllocSurface(SDL_SWSURFACE, Sprite.w, Sprite.h, GameScreen.format.BitsPerPixel, 0, 0, 0, 0);
     SrcRect := SDLRect(0, 0, Sprite.w, Sprite.h);
     DestRect := SDLRect(0, 0, Sprite.w, Sprite.h);
     SDL_BlitSurface(Sprite, @SrcRect, TempFrame, @DestRect);

     // Flip Tank Body around to match tank's facing!
     if (Facing < 0) then
        SDL_FlipRectH(TempFrame, @DestRect);
     SDL_SetColorKey(TempFrame, (SDL_SRCCOLORKEY or SDL_RLEACCEL), PUInt32(TempFrame.pixels)^);

     // Draw Turret
     TurretEndX := RotateXDeg(TurretLength, 0, Round(AimAngle));
     TurretEndY := RotateYDeg(TurretLength, 0, Round(AimAngle));
     SDL_DrawLine(GameScreen, Round(X + (TurretX * Facing)), Round(GameScreen.h - 1 - Y + TurretY),
                  Round(X + (TurretX * Facing) + TurretEndX), Round(GameScreen.h - 1 - Y + TurretY - TurretEndY), Color);
     // Draw Body
     DestRect := SDLRect(Round(X - Sprite.w / 2), Round(GameScreen.h - 1 - Y - Sprite.h), Sprite.w, Sprite.h);
     SDL_BlitSurface(TempFrame, @SrcRect, GameScreen, @DestRect);

     // Free Temp Frame
     SDL_FreeSurface(TempFrame);
end;  
    

The first thing you've probably noticed is that we're creating a new surface for the tank's main body. Since SDL itself doesn't have a function to draw an image as horizontally or vertically flipped, we have to do this ourselves. The simplest way to do this is just have a temporary surface to flip when needed.

Next big thing is the drawing of the turret. Remember when I told you to add 'AdvancedMathUnit' to the uses clause? Well it saved us having to get into the Trigonometry for this part.

RotateXDeg() and RotateYDeg() will give you the proper new X and Y of your barrel's end point. This will allow you to visually see where you are aiming.

The last part is simply drawing the body of the tank as we did before, but from the temp surface that we created at the beginning of the function.

Now when you see your tank it will be facing the right direction depending on the angle you've set. Also you'll see the correct angle of the barrel to give you a better depiction of where you are aiming.

Adding Aiming Controls

Now that we've gotten our TTank object updated to hold our turret information and display it, we'll move on to changing the barrel angle and adjusting the power of our shot. We've already put in some input controls, but for the purpose of controlling our tanks we have a few modifications.

Let's start at the top of scorch2d.lpr with the uses clause.

uses
  SysUtils,
  // JEDI-SDL
  sdl,
  sdlutils,
  // Scorch 2D
  AdvancedMathUnit,  // Added
  GraphicsUnit,
  FontUnit,          // Added
  GameObjectUnit,
  GameConstantsUnit;  
     

We've simply added the 2 new units so we can make use of them. Now on to the global types and variables.

type
  // Game Modes
  TGameMode = (gmMainMenu, gmAiming, gmShooting, gmMenu, gmQuit);

  // Game Controls
  TGameControls = Record
    Up, Down, Left, Right: Boolean;
    FastUp, FastDown: Boolean;       // Added
    Fire, Select, Menu: Boolean;
  end;
  
var
  GAME_FPS: Cardinal = 30; {30}

  // Video Screens
  GameScreen: PSDL_Surface; // Main GameScreen!

  // Font Data
  GameFont: TFont;  // Added
  
  // Game Data
  GameMode: TGameMode = gmAiming;
  RunClock: Integer;
  WhosTurn: Integer;               // Added

  GameInput: TGameControls;
  
  // Level Data
  Level: TBattlefield;

  // Tank Data
  NumberOfTanks: Integer;
  Tanks: Array[0 .. 3] of TTank;
  
  // Shot Data
  Shot: TShot;   // Added  

The new stuff added, though fairly straightforward, is as follows...

TGameControls.FastUp and TGameControls.FastDown are flags for a bit of a faster rate of increase and decrease in shot power. Power will instead increase at a rate 10 times faster than using the Up and Down keys.

GameFont is a font object that will allow you to load a bitmap font and draw text with it.

WhosTurn stores who's turn (out of the 4 tanks) it is.

TShot is the new class we'll be adding soon to make our shots with. I'll get more into it after we've finished covering aiming and controls.

With the preliminary stuff out of the way, lets move onto the main code block where we can have a look at how our new version of the game will work. Then I'll break down the changes one at a time for a closer look.

begin
     ProgramCreate;

     // Create Battlefield
     ...

     // Place Tanks
     ...

     Level.SmoothenLand(1);

     GameMode := gmAiming;    // <-- Added!
     
     RunClock := 0;
     repeat
       SDL_Delay(Get_FPS(GAME_FPS));

       if (GameMode = gmAiming) then    // <-- Added!
          doGameInput;

       if (GameMode = gmShooting) then  // <-- Added!
          GameCycle;

       DrawScreen;
     until (GameMode = gmQuit); // Exit when any key is pressed!

     ...

     ProgramClose;
end.  
     

Not much has changed there except for 3 main things as follows.

GameMode will now be used to keep track of which mode we are in. We will either be in an aiming or a shooting mode.

doGameInput will only be executed when in aiming mode.

GameCycle will only be executed when in shooting mode.

This will allow us to take input during the aiming mode and restrict this in shooting mode. Also we will only have a shot flying through the air in shooting mode and not while another tank is aiming.

So let's take a look at the new guts of the doGameInput procedure. Warning: The new version is a bit of a beast...

procedure doGameInput;
var
  event: TSDL_Event;
  TurretEndX, TurretEndY: Real;
begin
     while (SDL_PollEvent(@event) > 0) do
     begin
          case (event.type_) of
            SDL_KEYDOWN : case (event.key.keysym.sym) of
                            SDLK_UP       : GameInput.Up := True;
                            SDLK_PAGEUP   : GameInput.FastUp := True;
                            SDLK_DOWN     : GameInput.Down := True;
                            SDLK_PAGEDOWN : GameInput.FastDown := True;
                            SDLK_LEFT     : GameInput.Left := True;
                            SDLK_RIGHT    : GameInput.Right := True;
                            SDLK_SPACE,
                            SDLK_RETURN   : GameInput.Fire := True;
                            SDLK_TAB      : GameInput.Select := True;
                            SDLK_ESCAPE   : GameInput.Menu := True;
                          end;
            SDL_KEYUP   : case (event.key.keysym.sym) of
                            SDLK_UP       : GameInput.Up := False;
                            SDLK_PAGEUP   : GameInput.FastUp := False;
                            SDLK_DOWN     : GameInput.Down := False;
                            SDLK_PAGEDOWN : GameInput.FastDown := False;
                            SDLK_LEFT     : GameInput.Left := False;
                            SDLK_RIGHT    : GameInput.Right := False;
                            SDLK_SPACE,
                            SDLK_RETURN   : GameInput.Fire := False;
                            SDLK_TAB      : GameInput.Select := False;
                            SDLK_ESCAPE   : GameInput.Menu := False;
                          end;
          end;
     end;
     
     // -- Game Key Actions -- //
     if (GameInput.Menu) then
        GameMode := gmQuit;

     if (GameInput.Up) then
     begin
          Tanks[WhosTurn].AimPower := Tanks[WhosTurn].AimPower + 1;
          if (Tanks[WhosTurn].AimPower > ShotMaxPower) then
             Tanks[WhosTurn].AimPower := ShotMaxPower;
     end;
     if (GameInput.FastUp) then
     begin
          Tanks[WhosTurn].AimPower := Tanks[WhosTurn].AimPower + 10;
          if (Tanks[WhosTurn].AimPower > ShotMaxPower) then
             Tanks[WhosTurn].AimPower := ShotMaxPower;
     end;
     if (GameInput.Down) then
     begin
          Tanks[WhosTurn].AimPower := Tanks[WhosTurn].AimPower - 1;
          if (Tanks[WhosTurn].AimPower < ShotMinPower) then
             Tanks[WhosTurn].AimPower := ShotMinPower;
     end;
     if (GameInput.FastDown) then
     begin
          Tanks[WhosTurn].AimPower := Tanks[WhosTurn].AimPower - 10;
          if (Tanks[WhosTurn].AimPower < ShotMinPower) then
             Tanks[WhosTurn].AimPower := ShotMinPower;
     end;

     if (GameInput.Left) then
     begin
          Tanks[WhosTurn].ChangeTurret(Tanks[WhosTurn].AimAngle + 1);
     end;
     if (GameInput.Right) then
     begin
          Tanks[WhosTurn].ChangeTurret(Tanks[WhosTurn].AimAngle - 1);
     end;

     if (GameInput.Fire) then
     begin
          Shot := TShot.Init(Tanks[WhosTurn], 10);
          RunClock := 0;
          GameMode := gmShooting;
     end;

     // -- Reset Game Keys -- //
     GameInput.Fire := False;
     GameInput.Select := False;
     GameInput.Menu := False;
end;  
     

I warned you that it was big, but it's necessary to control what we need to. As you can see, GameInput.Up, GameInput.Down and their fast versions will modify your shot power up and down accordingly. Also you'll notice that GameInput.Left and GameInput.Right will run the new TTank.ChangeTurret() function. This is what will set the new AimAngle, Facing and TurretEndX/Y values of your tank for you. No need to mess with angle limits, direction and the like. Lets have a look at it now shall we?

TTank.ChangeTurret()

procedure TTank.ChangeTurret(NewAngle: Integer);
begin
     AimAngle := NewAngle;

     // Check for Aim Wrap-around
     if (AimAngle > 180) then
        AimAngle := 0;
     if (AimAngle < 0) then
        AimAngle := 180;

     // Check Tank Direction
     if (AimAngle < 90) then
        Facing := 1;
     if (AimAngle > 90) then
        Facing := -1;

     // Calculate Turret End
     TurretEndX := RotateXDeg(TurretLength, 0, AimAngle);
     TurretEndY := RotateYDeg(TurretLength, 0, AimAngle);
end;  

It's a nice little function to have do all this work for you. When it comes time to fire off your shot you'll not need to worry at all about where it will need to be created or at what angle it should move, etc...

Again you'll see RotateXDeg() and RotateYDeg() getting some more use. Like before, they will give us the new values of the TurretEndX and TurretEndY. They are very handy functions!

TShot Object

Now that we know what is going on inside the aiming mode functions let us have a look at the last piece of the puzzle, the TShot object.

type
  ...
  TShot = class(TObject)
    StartX, StartY: Real;
    OldX, OldY: Real;
    X, Y, VelX, VelY: Real;
    Power, Angle: Real;
    Damage: Integer;
    Remove: Boolean;
    constructor Init(oX, oY, oPower, oAngle: Real; oDamage: Integer); overload;
    constructor Init(Tank: TTank; oDamage: Integer); overload;
    procedure Update(Level: TBattlefield; GameClock: Cardinal);
    procedure Draw(GameScreen: PSDL_Surface);
  end;  

StartX and StartY are the obvious starting position of our shot.
OldX and OldY will keep track of our previous position as we move the shot in our GameCycle function.
X, Y are the current X/Y position. VelX and VelY are the movement speeds of the shot.
Power and Angle are the shot's initial Power and Angle values from the originating TTank object that fires it.
Remove is a flag to indicate that the TShot object is finished and ready to be freed from memory.

Initializing The Shot

You'll notice that we have two different Init() constructors for our TShot object. This was done with a bit of foresight towards future applications of the TShot object. One will create the shot at a specific location, while the other will create the shot based on a chosen tank's aim and power settings. This has its obvious advantages, but let us have a more detailed look to see what is going on.

constructor TShot.Init(oX, oY, oPower, oAngle: Real; oDamage: Integer); overload;
begin
     StartX := oX;
     StartY := oY;

     X := oX;
     Y := oY;

     Power := oPower;
     Angle := oAngle;

     VelX := Power * getCOS[Round(Angle)] * ShotPrecision;
     VelY := Power * getSIN[Round(Angle)] * ShotPrecision;

     Damage := oDamage;

     Remove := False;
end;  

It is pretty simple, right? oX and oY are the location, oPower and oAngle are the direction and power of the shot and oDamage is just the amount of damage the shot will produce.

Now let us look at the one that references a TTank object.

constructor TShot.Init(Tank: TTank; oDamage: Integer); overload;
begin
     StartX := Tank.X + (Tank.TurretX * Tank.Facing) + Tank.TurretEndX;
     StartY := Tank.Y - Tank.TurretY + Tank.TurretEndY;

     X := StartX;
     Y := StartY;

     Power := Tank.AimPower / 10;
     Angle := Tank.AimAngle;

     VelX := Power * getCOS[Round(Angle)] * ShotPrecision;
     VelY := Power * getSIN[Round(Angle)] * ShotPrecision;

     Damage := oDamage;

     Remove := False;
end;  
     

It's pretty much the same as the other one except that it now takes the Angle and Power values from the referenced Tank parameter.

Drawing The Shot

Well we're going to need to see our shots so let us do something simple just so that we can see it. You can do other things if you have a hard time seeing it or just don't like the way that I've done it.

procedure TShot.Draw(GameScreen: PSDL_Surface);
     begin
     if (Round(X) >= 0) and (Round(X) < GameScreen.w) and (Round(GameScreen.h - Y) >= 0) and (Round(GameScreen.h - Y) < GameScreen.h) then
     SDL_PutPixel(GameScreen, Round(X), Round(GameScreen.h - Y), $ffffff);
     end;
     // End of TShot
     
     end.

It's just that simple. Make sure that you don't try to use the SDL_PutPixel() command that JEDI-SDL uses in its sdlutils.pas unit, however, as it does not check the edges of the drawable screen and will create an error that will force the game to crash.

Shot Motion Cycle

Well the last thing we need to cover about the TShot object is its motion cycle and how it will move through the air. This is where a little bit of trigonometry comes into play. You may be surprised to know, however, that you've already done all the mathematics and won't actually see it in the TShot.Update function.

Remember these 2 lines that appeared in both of the TShot.Init() functions?

VelX := Power * getCOS[Round(Angle)] * ShotPrecision;
VelY := Power * getSIN[Round(Angle)] * ShotPrecision;  
     

Well this is pretty much all the trig that will be used in the game. Seriously! For those that know trigonometry, it's the mathematical equivalent of the following.

VelX = Power x cos Angle x Shot Precision

VelY = Power x sin Angle x Shot Precision

If you don't understand the mathematics then don't worry. All you have to know is that it will give us the speed along the x-axis and y-axis from the power and angle values.

Knowing this, the following will make a great deal more sense.

procedure TShot.Update(Level: TBattlefield);
begin
     OldX := X;
     OldY := Y;

     X := X + VelX;
     Y := Y + VelY;

     VelY := VelY - Gravity;

     // Level Boundaries
     if (X < 0) or (X > Level.Width - 1) or
        (Y <= Level.LandHeight[Round(X)]) then
        Remove := True;
end;  
     
Level is the battlefield object containing your dirt.

This function is meant to be ran once per game cycle of your game and will allow your shot to move once 'shot' from its origin position. X and Y are incremented by the value of your already generated values of VelX and VelY.

Also, to make our shots behave more realistically, we'll need some sort of gravity to pull them down towards the ground. Otherwise they'll just go flying off in a straight direction. To do this we subtract the value of Gravity from VelY. This will simulate a gravitational pull downwards.

NOTE: You can actually have some fun with this later by increasing or decreasing the value of Gravity to simulate an environment such as the surface of a moon or whatever your imagination can conceive.

Now to make sure that the shot doesn't keep going when it's time to stop we will also check for it going off either side of the battlefield, going lower than the bottom of the screen or hitting the dirt. The Remove flag is then turned on and we let the main game loop know that it needs to be destroyed.

The shot

The shot

Updating The Game Constants

Before we move on, let us quickly take a look at the new set of game constants.

ShotPrecision is the a value we chose to represent as 1 unit of movement for the shot. This is so that we will have a smoother arc as the shot moves through the air. All velocity values will need to be factored by this to ensure proper and accurate movement.
ShotMaxPower is the maximum value that the aiming controls will allow you to set your shot power.
ShotMinPower is the minimum value that the aiming controls will allow you to set your shot power.
Gravity is the value at which objects will be pulled down towards the ground.

The Game Cycle

Well now we are at the last part of getting shots to work, the GameCycle procedure which is located in the main scorch2d.lpr source file.

Before we jump right into its code, remember that this function will only run when we are in shooting mode and not in aiming mode. This is important to note because in the same way as the fire command in the doGameInput procedure sets the game into shooting mode, the GameCycle procedure will be responsible for changing which player's turn it is and putting the game mode back into aiming mode.

We'll take a look back at the firing command in the doGameInput procedure.

if (GameInput.Fire) then
     begin
          Shot := TShot.Init(Tanks[WhosTurn], 10);
          RunClock := 0;
          GameMode := gmShooting;
     end;  
     

Here the Shot object is created from the Tank object's aim, RunClock is reset and the GameMode is put into shooting mode.

Now let us have a look at the GameCycle procedure...

procedure GameCycle;
begin
     Shot.Update(Level);

     if (Shot.Remove) then
     begin
          // Destroy Shot
          Shot.Free;
          
          // Switch back to Aiming game mode
          GameMode := gmAiming;

          // Next Tank's Turn!
          inc(WhosTurn);
          if (WhosTurn > NumberOfTanks - 1) then
             WhosTurn := 0;
     end;
     
     inc(RunClock);
end;  
     

Again this is not too complex really. The TShot.Update() function updates the shot for the game cycle then detects if the TShot.Remove flag is telling it to destroy the object and move on to the next player's turn.

TShot.Free will destroy the shot object. GameMode is put into aiming mode. Then WhosTurn is changed to the next player's value.

Seeing the Results

There is one last part that I have not shown you yet and that is how we display the results on screen. Luckily, it's just a few lines more.

Here is the new DrawScreen procedure.

procedure DrawScreen;
var
  i: Integer;
begin
     Level.DrawSky(GameScreen);
     Level.DrawLand(GameScreen);

     for i := 0 to NumberOfTanks - 1 do
         Tanks[i].Draw(GameScreen);

     if (GameMode = gmShooting) then
        Shot.Draw(GameScreen);

     GameFont.DrawText(GameScreen, 10, 10, 'Tank ' + IntToStr(WhosTurn) + ' is firing!');
     GameFont.DrawText(GameScreen, 10, 22, 'Power: ' + IntToStr(Round(Tanks[WhosTurn].AimPower)));
     GameFont.DrawText(GameScreen, 10, 34, 'Angle: ' + IntToStr(Round(Tanks[WhosTurn].AimAngle)));

     GameFont.DrawText(GameScreen, 10, GameScreen.h - 23, 'RunClock = ' + IntToStr(RunClock));
     GameFont.DrawText(GameScreen, 10, GameScreen.h - 36, 'Facing = ' + IntToStr(Tanks[WhosTurn].Facing));

     SDL_Flip(GameScreen);
end;  
     

The Shot object will only try to draw when it is in shooting mode and the GameFont object will use its GameFont.DrawText() function to output the information we need such as which tank is firing, its power level and its turret angle. Also, for debugging purposes we'll also display the RunClock and the tank's facing direction to make sure everything is working fine.

Showing Text

Showing Text

And with that we are done.happy

The Source

Download the source files here!

Artillery4_Source.zip

Click on the links below to view the source files.

End of Part 4

Another lesson done and one more to go before we can call this an actual playable game. However that doesn't mean we are done after that. Oh no, there are several other things that I can cover beyond simply lobbing single shots at each other. All kinds of fun and interesting things such as factoring in wind, playing with the way a shot will interact with borders, more complex weapons and weapon behaviour and lots more. So look forward to a bunch of those things as I continue to release tutorial after tutorial in the near future.

In 'Death & Destruction' I will polish off the basic gameplay and show how to kill tanks and create explosions that will remove some dirt.

Final Output

Final Output

                                                                      - Jason McMillen
                                                               Pascal Game Development
Programming - a skill for life!

Using SDL, Box2D or the GLScene or Castle Game Engine to write games in Pascal