Artillery Part 3: Placing The Tanks
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. What's 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.
Part 2 Recap
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 peak.
The 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 breakdown of the values that I pumped into the initialization of the TBattlefield object and the constants used in the generation of the land.
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 did. You can go 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 colour instead.
Level := TBattlefield.Init(GameScreen.w, GameScreen.h, $229900, $0000ee, False, '');
This would give you an off-blue colour for your background instead.
There are other changes that you can make:
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. 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!
Here is a simple version of the TTank Object that we'll look at now.
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 centre of the tank to the end of either front or back of the tank.
TurretX and TurretY are the X and Y offsets from the tank's position to the base of its 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 colour. We use this value to colour 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 the TBattlefield object.
Initializing The Tank
Before we get into placement, lets see how we create and draw one...
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.
procedure TTank.Draw(GameScreen: PSDL_Surface); var SrcRect: TSDL_Rect; DestRect: TSDL_Rect; begin // Draw Turret (barrel) 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 centre-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 mathematics 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!
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...
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 where we will be putting our 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 its 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 the ground under the tank. Besides, what good is the tank if it's not in the battlefield?
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 its 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.
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...
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're trying to flatten are 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...
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 the method declaration to the existing TBattlefield class:
TBattlefield = class(TObject) ... procedure PlaceTank(x, gap: Integer; var Tank: TTank); ... end;
Now we can place our tanks all over the battlefield. 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.
// Tank Data NumberOfTanks: Integer; Tanks: Array[0 .. 3] of TTank;
Now let's 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.
Level.SmoothenLand(LandSmoothing); ... // Place Tanks ... RunClock := 0; repeat
Start with these 5 lines...
NumberOfTanks := 4; Tanks := TTank.Init(13, -1, -8, 12, $008442, 'TK-1.bmp'); Tanks := TTank.Init(13, 0, -8, 12, $e74218, 'TK-2.bmp'); Tanks := TTank.Init(13, -4, -8, 13, $8400ff, 'TK-3.bmp'); Tanks := 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.
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 let us 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...
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:
- Reset the SpaceClear flag and generate the new location.
- Check if on-screen.
- Check if spot is taken by another tank.
- 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 through 3 until the flag remains up.
Go ahead and add this to the unit...
Don't forget to add the method declaration to the TBattlefield class definition! See the code of the Game Object Unit if you are unsure.
Now before we head back to our main code block, let's just add a few more lines to our GameConstantsUnit.pas...
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...
Level.ScatterTanks(Tanks, 4, TankMinDistance, TankPlaceGap);
Almost done; we just have to draw them now. So let's jump over to the DrawScreen procedure and change it to look like this.
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.
Dirt around tank before smoothing
See how it's stacked so perfectly high and vertical? It doesn't look very natural, does it? So how can we improve this?
Let's try using SmoothenLand() just after placing all our tanks in the map.
... 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?
Dirt around tank after smoothing
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.
Download the source package here!
Click on the links below to view the source files.
- Jason McMillen Pascal Game Development