• Recent Tutorials

  • Artillery Game Tutorial - Part 4: Ready, Aim, Fire!


    Hello again! I'm back with another installment 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.

    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 instead what I'll do is simply explain each part and let you see for yourself in the source files I give you how it all fits together. You'll get just as much detailed information, just without a long set of step by step instructions.

    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. Unfortunately unlike last time I don't really have too much to add.

    However in our previous lesson we learned how to place our tanks in random locations. We also learned how to keep them separate from each other so that a set distance can be imposed that would ensure a decent setup for a start of a round.

    Setting The Distance

    When you start playing around with the distance that you can set you tanks apart by, take care as not to enter in an impossible value.

    If you cared to, a fail-safe could be created that would reduce the distance set to a reasonable value considering the amount of tanks, their sizes and of course the battlefield width it's self.

    You may alternatively 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.


    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.


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


    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 of the tank's 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);
    TurretEndX and TurretEndY are the offset from TurretX and TurretY which will be the end of the gun's turret.
    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 real need to keep them as floating point values. Since one is in degrees and the other These will only ever be whole numbers.

    Not too much 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:

      // JEDI-SDL
    You'll understand why we needed this in a bit.

    Update TTank.Init()

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

        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);
    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);
      SrcRect: TSDL_Rect;
      DestRect: TSDL_Rect;
      TurretEndX, TurretEndY: Real;
      TempFrame: PSDL_Surface;
         // 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
    The first thing you've probably noticed is that we're creating a new surface for the tank's main body. Since SDL it's self 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.

    Both RotateXDeg() and RotateYDeg() will give you the proper new X and Y of your turret'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 turret 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 turret angle and adjusting the power of our shot. We've already put in some input controls in, but for the purpose of controlling our tanks we have a few modifications we'll need still.

    Lets start at the top of scorch2d.lpr with the uses clause.

      // JEDI-SDL
      // Scorch 2D
      AdvancedMathUnit,  // Added
      FontUnit,          // Added
    We've simply added the 2 new units so we can make use of them. Now on to the global types and variables.

      // 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;
      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, are 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 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.

    Adding Aiming Controls (Continued)

    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.

         // Create Battlefield
         // Place Tanks
         GameMode := gmAiming;    // <-- Added!
         RunClock := 0;
           if (GameMode = gmAiming) then    // <-- Added!
           if (GameMode = gmShooting) then  // <-- Added!
         until (GameMode = gmQuit); // Exit when any key is pressed!
    Not too much has really changed there except for 3 main things. 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 lets take a look at the new guts of the doGameInput; procedure. Warning: The new version is a bit of a beast...

    procedure doGameInput;
      event: TSDL_Event;
      TurretEndX, TurretEndY: Real;
         while (SDL_PollEvent(@event) > 0) do
              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_RETURN   : GameInput.Fire := True;
                                SDLK_TAB      : GameInput.Select := True;
                                SDLK_ESCAPE   : GameInput.Menu := True;
                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_RETURN   : GameInput.Fire := False;
                                SDLK_TAB      : GameInput.Select := False;
                                SDLK_ESCAPE   : GameInput.Menu := False;
         // -- Game Key Actions -- //
         if (GameInput.Menu) then
            GameMode := gmQuit;
         if (GameInput.Up) then
              Tanks[WhosTurn].AimPower := Tanks[WhosTurn].AimPower + 1;
              if (Tanks[WhosTurn].AimPower > ShotMaxPower) then
                 Tanks[WhosTurn].AimPower := ShotMaxPower;
         if (GameInput.FastUp) then
              Tanks[WhosTurn].AimPower := Tanks[WhosTurn].AimPower + 10;
              if (Tanks[WhosTurn].AimPower > ShotMaxPower) then
                 Tanks[WhosTurn].AimPower := ShotMaxPower;
         if (GameInput.Down) then
              Tanks[WhosTurn].AimPower := Tanks[WhosTurn].AimPower - 1;
              if (Tanks[WhosTurn].AimPower < ShotMinPower) then
                 Tanks[WhosTurn].AimPower := ShotMinPower;
         if (GameInput.FastDown) then
              Tanks[WhosTurn].AimPower := Tanks[WhosTurn].AimPower - 10;
              if (Tanks[WhosTurn].AimPower < ShotMinPower) then
                 Tanks[WhosTurn].AimPower := ShotMinPower;
         if (GameInput.Left) then
              Tanks[WhosTurn].ChangeTurret(Tanks[WhosTurn].AimAngle + 1);
         if (GameInput.Right) then
              Tanks[WhosTurn].ChangeTurret(Tanks[WhosTurn].AimAngle - 1);
         if (GameInput.Fire) then
              Shot := TShot.Init(Tanks[WhosTurn], 10);
              RunClock := 0;
              GameMode := gmShooting;
         // -- Reset Game Keys -- //
         GameInput.Fire := False;
         GameInput.Select := False;
         GameInput.Menu := False;
    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?


    procedure TTank.ChangeTurret(NewAngle: Integer);
         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);

    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 at, 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. Very handy functions!

    TShot Object

    Now that we know whats going on inside the aiming mode functions lets have a look at the last piece of the puzzle, the TShot object.

      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);
    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 off a chosen tank's aim and power settings. This has it's obvious advantages, but lets have a more detailed look to see whats going on.

    constructor TShot.Init(oX, oY, oPower, oAngle: Real; oDamage: Integer); overload;
         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;
    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 lets look at the one that references a TTank object.

    constructor TShot.Init(Tank: TTank; oDamage: Integer); overload;
         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;
         Damage := oDamage;
         Remove := False;
    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 lets 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);
         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);
    nd(X), Round(GameScreen.h - Y), $ffffff);
    It's just that simple. Make sure that you don't try to use the SDL_PutPixel() command that JEDI-SDL uses in it's 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 it's 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 however to know that you've already done all that math and won't actually see it in the TShot.Update; function.

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

    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 math 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);
         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;
    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 it's 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 when it simply hits the dirt. The Remove flag is then turned on and we let the main game loop know that it needs to be destroyed.

    Updating The Game Constants

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

    unit GameConstantsUnit;
      ShotPrecision  = 0.07;                 // New
      ShotMaxPower   = 1000;                 // New
      ShotMinPower   = 10;                   // New
      Gravity        = 0.6 * ShotPrecision;  // New
      LandSmoothing  = 50;
      LandVariation  = 60;
      LandHighest    = 500;
      LandLowest     = 1;
      TankPlaceGap    = 3;
      TankMinDistance = 50;
    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 it's 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 like how 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.

    A look back at the firing command in the doGameInput; procedure.

         if (GameInput.Fire) then
              RunClock := 0;
              GameMode := gmShooting;
    Here the Shot object is created from the Tank object's aim, RunClock is reset and the GameMode is put into shooting mode.

    Now lets have a look at the GameCycle; procedure...

    procedure GameCycle;
         if (Shot.Remove) then
              // Destroy Shot
              // Switch back to Aiming game mode
              GameMode := gmAiming;
              // Next Tank's Turn!
              if (WhosTurn > NumberOfTanks - 1) then
                 WhosTurn := 0;
    Again 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 did not show you yet and thats how we display the results on screen. Luckily it's just a few lines more.

    Here is the new DrawScreen; procedure.

    procedure DrawScreen;
      i: Integer;
         for i := 0 to NumberOfTanks - 1 do
         if (GameMode = gmShooting) then
         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));
    The Shot object will only try to draw when it is allocated an in shooting mode and the GameFont object will use it's GameFont.DrawText(); function to output the information we need such as; which tank is firing, it's power level and it's 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.

    And with that we are done.

    The Source

    As with all my tutorials I provide a completed copy of the source, images and compiled binaries used in the examples. Here it is... enjoy!

    Download the source files here!

    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 we can cover beyond simply lobbing single shots at each other. All kinds of fun an interesting things such as factoring in wind, playing with the way a shot will interact with borders, more complex weapons and weapon behavior 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.

    - Jason McMillen
    Pascal Game Development
    Comments 2 Comments
    1. mcbipolar's Avatar
      mcbipolar -
      Great tutorial. Please keep them coming. I've learned so much!
    1. OldNESJunkie's Avatar
      OldNESJunkie -
      Great tutorial series, cannot wait for the next one !!!
Comodo SSL