• Recent Tutorials

  • Artillery Game Tutorial - Part 3: Placing The Tanks

    Introduction

    As promised I will now show you how we place our tanks on our battlefield. However, to do this nicely there are a few things we should consider.


    Naturally we'll want to place a tank on a level area of ground for starters. So we need to make a spot for it. After that we don't want them to be too close to each other. Whats the fun in that? So we'll need to look at ways to pick out spots, so that our tanks will be separate from each other, at least enough so that aiming your shots will pose something of a small challenge.

    But first...


    Part 2 Recap

    Last tutorial I showed you how to create a random battlefield, or dirt. I got into some of the theory, but I didn't really cover some things. So lets go over just a few things here that I may have missed.


    Maximum Land Height

    The way we process the Highest value of our land in our GenerateLand() function, it is unlikely that we will ever truly reach the exact maximum that we set. Especially considering that we will run SmoothenLand() afterwards to create a more realistic peek.

    Truth is, there will always be a degree of space between the generated land and where you set your maximum. Just know this so you don't think that there is something terribly wrong with the code.


    Use of Constants & Other Values

    Though we went through the entire code, I didn't really give you a break down of the values that I pumped into the initialization of the TBattlefield object and the constants used in the generation of the land.

    Code:
         Level := TBattlefield.Init(GameScreen.w, GameScreen.h, $229900, $0000ee, True, 'DesertEclipse.bmp');
    Not much to talk about here, but if you notice, I've used the width and height of the GameScreen as the confines of the battlefield. You don't have to stick to the size of your screen. In fact you can go wider and taller and use scrolling much like how Worms had. Smaller even, if you want a fixed interface along one of the sides and prefer not to scroll.

    However you would have to modify the drawing routine to go along with whatever else you do.

    The other thing is the bitmap background. An alternative to using a bitmap would be to use a solid color instead.

    Code:
         Level := TBattlefield.Init(GameScreen.w, GameScreen.h, $229900, $0000ee, False, '');
    This would give you an off-blue color for your background instead.

    Code:
         Level.GenerateLand(LandHighest, LandLowest, LandVariation);
         Level.SmoothenLand(LandSmoothing);
    Okay, remember those numbers in GameConstantsUnit.pas? Well these are those. You don't have to stick with these values however as the code was designed to be easily customizable during design or even gameplay.

    Play around with them and see what happens. Just remember that the larger the value of LandSmoothing, the more it will resemble rolling hills rather than rocky mountains.

    LandVariation is also something you'll have to experiment with, to see what you can come up with, but you may not want to set it too low unless you want a near flat surface, which doesn't offer any cover for the tanks.


    New Image Files

    Be sure to get these new files and add them to your images folder!

    Download the images package!
    a63_ImageFiles.zip


    TTank Object

    Here is a simple version of the TTank Object that we'll look at now.

    Code:
      TTank = class(TObject)
        X, Y: Real;
        TrackSize: Cardinal;
        TurretX, TurretY,
        TurretLength: Integer;
        AimAngle, AimPower: Real;
        Color: Cardinal;
        Sprite: PSDL_Surface;
        constructor Init(oTrackSize: Cardinal; oTurretX, oTurretY, oTurretLength: Integer; oColor: Cardinal; ImageFile: String);
        procedure Draw(GameScreen: PSDL_Surface);
      end;
    X and Y are the screen coordinates of the tank.
    TrackSize is the length from the center of the tank to the end of either front or back of the tank.
    TurretX and TurretY is the offset from the tank's position to the base of it's turret.
    TurretLength is the length of the turret.
    AimAngle and AimPower are the values the tank will use to fire. We will not be covering these yet, instead we will get to them in Part 4.
    Color is the color. We use this value to color the tank's turret when drawing.
    Sprite is the sprite used for the body of the tank.

    Add this to the type definitions at the top of GameObjectUnit.pas, but put it ABOVE TBattlefield object.


    Initializing The Tank

    Before we get into placement, lets see how we create and draw one...

    Code:
    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;
    
        // Load Sprite File
        Sprite := LoadImage('images/' + ImageFile, True);
    end;
    It's as simple as that folks! Most values are assigned with the TTank.Init() call and the Aim values are set to a default. X and Y will remain at (0, 0) until we place our tank. We'll do this in another function that I'll explain soon.

    Go ahead and add this to your GameObjects.pas unit for now.


    Drawing The Tank

    These lines will draw the body of the tank.

    Code:
    procedure TTank.Draw(GameScreen: PSDL_Surface);
    var
      SrcRect: TSDL_Rect;
      DestRect: TSDL_Rect;
    begin
         // Draw Turret
         SDL_DrawLine(GameScreen, Round(X + TurretX), Round(GameScreen.h - 1 - Y + TurretY),
                      Round(X + TurretX + TurretLength), Round(GameScreen.h - 1 - Y + TurretY), Color);
         // Draw Body
         SrcRect := SDLRect(0, 0, Sprite.w, Sprite.h);
         DestRect := SDLRect(Round(X - Sprite.w / 2), Round(GameScreen.h - 1 - Y - Sprite.h), Sprite.w, Sprite.h);
         SDL_BlitSurface(Sprite, @SrcRect, GameScreen, @DestRect);
    end;
    You may notice that our Tank.X and Tank.Y values reference the center-bottom of our tank sprites.

    Also, even though our default Aim direction of our turret is at +45 degrees, we are drawing it as if it were at +0 degrees. This is on purpose as I don't want to go into any math just yet. We will however come back to revise this in Part 4 after we have control of our turret.

    As before you may add this to your GameObjectUnit.pas!


    Placement

    So now we can create and see the tanks, but how do we place them on the ground?


    Where we want to place our tank!

    This is where PlaceTank() comes in...

    Code:
    procedure TBattlefield.PlaceTank(x, gap: Integer; var Tank: TTank);
    var
      i: Integer;
      Lowest: Cardinal;
    begin
    end;
    Yup, that's right. It IS in the TBattlefield object! Why? It's there because it is easier to manipulate the ground to support the tank from inside the TBattlefield object than it is to do so from the TTank or any other 'foreign' class object.

    x is the place along the ground which we will be putting out tank.
    gap is the amount of space we will give the tank on either side of where it will sit.
    Tank is simply the tank object which we will be placing. Notice the 'var' preceding it's name...


    First in this function, we'll make sure that the tank's track is not off either side of the battlefield. Such a situation would cause us some issues with the way we manipulate of the ground under the tank. Besides what good is the tank if it's not in the battlefield?

    Code:
         if (x - Tank.TrackSize < 0) then
            x := Tank.TrackSize;
         if (x + Tank.TrackSize > Width - 1) then
            x := Width - 1 - Tank.TrackSize;
    I think you see the importance of the TrackSize value now. It helps greatly to determine where the tank sits. If either side of it's track is off-screen, this code will nudge it over so it's just inside again.

    Now that we're sure that the tank sits within the screen, we can check for the lower level of the surface that we are going to set the tank on. We do this so that we can alter the level of this area and not worry about the tank looking awkward while sitting upright.

    Code:
         Lowest := LandHeight[x];
         for i := x - Tank.TrackSize to x + Tank.TrackSize do
             if (LandHeight[i] < Lowest) then
                Lowest := LandHeight[i];
    Now that we know this value, we can go ahead and flatten the ground...

    Code:
         for i := x - Tank.TrackSize - gap to x + Tank.TrackSize + gap do
             if (i >= 0) or (i <= Width - 1) then
                LandHeight[i] := Lowest;
    There... all flat! Including the extra space specified by gap.

    Another thing to see is that we've included a check to see that the land values that we we're trying to flatten were in fact still on screen. It's important because we did not include the extra gap space when we checked that the tank's track was on screen.

    Now we just have to move the tank's location...

    Code:
         Tank.X := x;
         Tank.Y := Lowest;
    ...and we're done!


    Our tank, placed!

    Go ahead and put it together and add it to GameObjectUnit.pas.

    Oh, and don't forget to add it to the existing TBattlefield object above!

    Code:
      TBattlefield = class(TObject)
    ...
        procedure PlaceTank(x, gap: Integer; var Tank: TTank);
    ...
      end;
    Random Deployment

    Well now we can place our tanks all over the battle field. However, we still have to find a way to randomly scatter all the tanks on the surface to help create different battle situations each time we play.

    First we'll go to the top of the main source where we declare our global variables and add this under our 'Level Data' stuff.

    Code:
      // Tank Data
      NumberOfTanks: Integer;
      Tanks: Array[0 .. 3] of TTank;
    Now lets go into our game's main code block and add some other things. Between our Level.SmoothenLand() and RunClock := 0; lines, we're going to put in our tank placement code.

    Code:
         Level.SmoothenLand(LandSmoothing);
    ...
         // Place Tanks
    ...
         RunClock := 0;
         repeat
    Start with these 5 lines...

    Code:
         NumberOfTanks := 4;
         Tanks[0] := TTank.Init(13, -1, -8, 12, $008442, 'TK-1.bmp');
         Tanks[1] := TTank.Init(13, 0, -8, 12, $e74218, 'TK-2.bmp');
         Tanks[2] := TTank.Init(13, -4, -8, 13, $8400ff, 'TK-3.bmp');
         Tanks[3] := TTank.Init(13, -5, -8, 14, $ffff84, 'TK-4.bmp');
    This will create all 4 tanks that we will use. And because I like diagrams, here are the layouts of all 4 tanks.

    TK-1 (Green)TK-2 (Orange)
    TK-3 (Violet)TK-4 (Tan)



    NOTE: I've decided to go into detail here so that you will know how a tank takes up space. This will give you a better understanding for this next function.


    Okay, now we want to actually place the tanks randomly, which will take a little bit of code. So lets not junk up the main code block and we'll note where we left off here and flip back over to the GameObjectUnit.pas unit.

    Here we'll make another new function called ScatterTanks() inside our TBattlefield class. And here it is...

    Code:
    procedure TBattlefield.ScatterTanks(var Tanks: Array of TTank; NumberOfTanks, MinTankDist, DirtGap: Integer);
    var
      i, j: Integer;
      x: Integer;
      SpaceClear: Boolean;
    begin
         for i := 0 to NumberOfTanks - 1 do
         begin
              repeat
                SpaceClear := True;
    
                // Generate random location
                x := Random(Width);
                
                // Check if location is off screen
                if (x - Tanks[i].TrackSize < 0) or
                   (x + Tanks[i].TrackSize > Width - 1) then
                   SpaceClear := False;
    
                // Check with other tanks
                for j := 0 to NumberOfTanks - 1 do
                    if (i <> j) then
                    begin
                         // Location taken by other tank
                         if ((x + Tanks[i].TrackSize >= Tanks[j].X - Tanks[j].TrackSize - MinTankDist) and
                             (x + Tanks[i].TrackSize <= Tanks[j].X + Tanks[j].TrackSize + MinTankDist)) or
                            ((x - Tanks[i].TrackSize >= Tanks[j].X - Tanks[j].TrackSize - MinTankDist) and
                             (x - Tanks[i].TrackSize <= Tanks[j].X + Tanks[j].TrackSize + MinTankDist)) then
                            SpaceClear := False;
                    end;
              until (SpaceClear);
              // Place Tank
              PlaceTank(x, DirtGap, Tanks[i]);
         end;
    end;
    I didn't want to break this one up as it might have been a bit hard to follow with the repeat .. until loop in it, but basically what we have in steps is this;

    1. Reset the SpaceClear flag and Generate the new location.

    2. Check if on-screen.


    3. Check if spot is taken by another tank.


    4. If the
    SpaceClear flag didn't go down, PlaceTank() and move on to the next one. Otherwise, reset the SpaceClear flag and repeat steps 1 thru 3 until the flag remains up.

    Go ahead and add this to the unit...

    Don't forget to also add it to the TBattlefield class definition above!


    Random Deployment (Completion)

    Now before we head back to our main code block, lets just add a few more lines to our GameConstantsUnit.pas...

    Code:
      TankPlaceGap    = 3;
      TankMinDistance = 50;
    Okay, that'll do it. Now back to our main code block!

    Put in this one line after our created tanks...

    Code:
         Level.ScatterTanks(Tanks, 4, TankMinDistance, TankPlaceGap);
    Almost done, we just have to draw them now. So lets jump over to the DrawScreen; procedure and change it to look like this.

    Code:
    procedure DrawScreen;
    var
      i: Integer;
    begin
         Level.DrawSky(GameScreen);
         Level.DrawLand(GameScreen);
    
         for i := 0 to NumberOfTanks - 1 do
             Tanks[i].Draw(GameScreen);
    
         SDL_Flip(GameScreen);
    end;
    If every thing went according to plan, you should be able to press F9 to compile/run and see the functions in action now. Try closing and re-running it to see that it will in fact maintain the parameters that were put in.


    Last Little Touch-up

    We're basically done here. There really isn't anything left to add at this point and we can easily move on to the next part. But there is one little tweak that I want to make to the land surrounding our newly placed tanks before I call it quits.

    Take a look at this close-up of how the dirt sits around the tank.


    See how it's stacked so perfectly high and vertical? Doesn't look very natural does it? So how can we improve this?

    Lets try using SmoothenLand() just after placing all our tanks in the map.

    Code:
    ...
         Level.ScatterTanks(Tanks, 4, TankMinDistance, TankPlaceGap);
    
         Level.SmoothenLand(1); // <-- Add this line in!
    
         RunClock := 0;
         repeat
    ...
    Ah, that looks much better now, don't you think?



    End of Part 3

    Well that's it for this one. Next time should be quite a bit bigger as we have a fair bit to cover, including some basic trigonometry, in 'Ready, Aim, Fire!'.

    Until then, play around with this and try different values and such on your own to see what you might come up with.

    If you have anything to add or notice something that I might have missed then by all means, feedback is welcome and encouraged!





    Source Code

    Download the source package here!
    a63_Source.zip


    - Jason McMillen
    Pascal Game Development
    Comments 2 Comments
    1. Ñuño Martínez's Avatar
      Ñuño Martínez -
      I see the TTank class doesn't has destructor. Then, how do it "unload" the graphics? Does SDL unoads they automaticly? Allegro doesn't.
    1. WILL's Avatar
      WILL -
      I believe SDL will dump the textures it's self upon SDL_Quit; It's been a long time since I've written these older articles. Today I would have thought to show a proper way up cleaning up your assets on program closing. SDL it's self does do the work for you though, but if you write more complicated games where multiple sets of assets (graphics, sprites, etc) need to be loaded and unloaded, then leaving it for SDL to do the work is not the way to go!