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.
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.
Level := TBattlefield.Init(GameScreen.w, GameScreen.h, $229900, $0000ee, True, 'DesertEclipse.bmp');
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.
Level := TBattlefield.Init(GameScreen.w, GameScreen.h, $229900, $0000ee, False, '');
Level.GenerateLand(LandHighest, LandLowest, LandVariation); Level.SmoothenLand(LandSmoothing);
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!
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;
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...
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;
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 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;
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!
So now we can create and see the tanks, but how do we place them on the ground?
This is where PlaceTank() comes in...
procedure TBattlefield.PlaceTank(x, gap: Integer; var Tank: TTank); var i: Integer; Lowest: Cardinal; begin end;
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?
if (x - Tank.TrackSize < 0) then x := Tank.TrackSize; if (x + Tank.TrackSize > Width - 1) then x := Width - 1 - Tank.TrackSize;
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];
for i := x - Tank.TrackSize - gap to x + Tank.TrackSize + gap do if (i >= 0) or (i <= Width - 1) then LandHeight[i] := Lowest;
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...
Tank.X := x; Tank.Y := Lowest;
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!
TBattlefield = class(TObject) ... procedure PlaceTank(x, gap: Integer; var Tank: TTank); ... end;
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.
// Tank Data NumberOfTanks: Integer; Tanks: Array[0 .. 3] of TTank;
Level.SmoothenLand(LandSmoothing); ... // Place Tanks ... RunClock := 0; repeat
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');
|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...
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;
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...
TankPlaceGap = 3; TankMinDistance = 50;
Put in this one line after our created tanks...
Level.ScatterTanks(Tanks, 4, TankMinDistance, TankPlaceGap);
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;
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.
... Level.ScatterTanks(Tanks, 4, TankMinDistance, TankPlaceGap); Level.SmoothenLand(1); // <-- Add this line in! RunClock := 0; repeat ...
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!
Download the source package here!
- Jason McMillen
Pascal Game Development
Pascal Game Development