• Recent Tutorials

  • Artillery Game Tutorial - Part 2: The Dirt

    Introduction

    Hey guys, now that the intro and all the dull stuff is over with, we can get right into the goods! Playing in the dirt.


    In this first lesson I'm going to show you how to generate a nice random 2D battle ground for your tanks to kill each other in. You'll be able to then display it on top of your sky background that will either be a single color or the nice bitmap I've included or one of your own making.

    Okay enough with the explanations, lets get right into the code!


    Some More Files

    I know that I've already given you some files to start with in the intro, but these ones will be the beef of our game code now. Here is what they are for;

    GameConstantsUnit.pas -- All the default settings for our game.
    GameObjectUnit.pas -- Houses all our game objects. As I mentioned before in the intro we'll be using OOP.





    Game Constants

    Lets get this one out of the way shall we? I'll be supplying this file at the end of this tutorial, but if you wish you make simply copy and paste the following code into the file named GameConstantsUnit.pas.

    Code:
    unit GameConstantsUnit;
    
    interface
    
    const
      LandSmoothing  = 50;
      LandVariation  = 60;
      LandHighest    = 500;
      LandLowest     = 1;
    
    implementation
    
    end.
    I will explain these values later on as they are needed.


    TBattlefield Object

    This is our battlefield object which we will make generate our dirt.

    Code:
    type
      TBattlefield = class(TObject)
        Width, Height: Integer;
        LandHeight: Array[0 .. 1024] of Integer;
        LandColor, SkyColor: Cardinal;
    
        isBGImage: Boolean;
        Background: PSDL_Surface; // Background Graphic
    
        constructor Init(ScreenWidth, ScreenHeight: Integer; Land, Sky: Cardinal; useBGImage: Boolean; BGImageFile: String);
        procedure GenerateLand(Highest, Lowest, Variation: Integer);
        procedure SmoothenLand(SmoothSize: Integer); // Tries to smoothen rough generated land!
        procedure DrawSky(GameScreen: PSDL_Surface);
        procedure DrawLand(GameScreen: PSDL_Surface);
      end;
    Init() will create the battlefield object!
    GenerateLand() will do the initial generation of the shape of the land.
    SmoothenLand() will help make the land more usable in the game.
    DrawSky() will draw our sky background.
    DrawLand() will draw the land we generate.


    Initializing The Battlefield

    I don't see a need to go into great depth here so here is the function straight out.

    Code:
    constructor TBattlefield.Init(ScreenWidth, ScreenHeight: Integer; Land, Sky: Cardinal; useBGImage: Boolean; BGImageFile: String);
    begin
         Width := ScreenWidth;
         Height := ScreenHeight;
    
         LandColor  := Land;
         SkyColor   := Sky;
         isBGImage := useBGImage;
    
         // Load Background
         if (useBGImage) then
            Background := LoadImage('images\\' + BGImageFile, False);
    end;
    ScreenWidth & ScreenHeight are the dimensions of the game screen you'll be using.
    Land & Sky are the colors of the land and sky.
    useBGImage is the switch for using a bitmap instead of a solid single color for the sky background.
    BGImageFile is the filename within the images directory of the bitmap to load into the Background surface.


    Initial Generation

    To generate our land we're going to go through it in a couple of passes. Once to get the general shape and then again afterward to make it a little more practical for the game's use. But for now lets look at how we'll get the basic 'shape' of the land we want.

    GenerateLand() uses 2 values to keep the surface of the dirt with a high and a low range. There are Highest and Lowest.

    Have a look at this example to see what effect this has...



    Notice how the peeks will never go above the Highest value and the valleys will never go below the Lowest value?


    Now we have to figure out the rate of which we the land will vary as we go along the surface. We could simply use a random value between Lowest and Highest but that wouldn't produce a very nice shape for our land.

    Instead what we'll do is have a Variation value that will govern the change in the heights of each segment of land from left to right. Each segment will be 1 pixel wide for the most precise effect!

    Here's our function...

    Code:
    procedure TBattlefield.GenerateLand(Highest, Lowest, Variation: Integer);
    var i: Integer;
        rand: Real;
    begin
    end;
    So lets go from left to right as this is most logical way. We should start with a totally random value between the Lowest and Highest as a starting height to work from.

    Code:
         LandHeight[0] := Lowest + Round(Random * (Highest - Lowest));
    Of course we will want the land to randomly go higher and lower so we'll allow the new height of each segment to either go up or down within the range of Variation.

    Code:
         for i := 1 to Width - 1 do
         begin
              repeat
                rand := Random;
                LandHeight[i] := Round(LandHeight[i - 1] + (rand * Variation) - Variation / 2);
                if (LandHeight[i] < Lowest) then
                   LandHeight[i] := Lowest;
              until (LandHeight[i] <= Highest);
         end;
    Here you see we cycle through each segment along the x-axis and calculate a height for the land based on the previous calculated height.

    If the land travels higher than the Highest range, it will recalculate another random height until it fits under the required range. Also when a height value is created lower than Lowest it will, instead of recalculating the height, level the height to the Lowest value.

    Both high and low values are treated this way to simulate peeks and valleys in a more realistic way.

    Our completed function should look like this...

    Code:
    procedure TBattlefield.GenerateLand(Highest, Lowest, Variation: Integer);
    var i: Integer;
        rand: Real;
    begin
         LandHeight[0] := Lowest + Round(Random * (Highest - Lowest));
         for i := 1 to Width - 1 do
         begin
              repeat
                rand := Random;
                LandHeight[i] := Round(LandHeight[i - 1] + (rand * Variation) - Variation / 2);
                if (LandHeight[i] < Lowest) then
                   LandHeight[i] := Lowest;
              until (LandHeight[i] <= Highest);
         end;
    end;
    Smoothen Out The Land

    So now we have our land, but it's very jagged, ugly and unusable. Worry not! This can be done quite easily with a 2nd pass on the land height values.

    The technique that I will be using to create more usable land will involve passing through a set of the initial generated land. It'll take a number of segment values from the left and right sides of the current segment and calculate an average which it will be changed to. The value SmoothSize will determine the amount of samples on each side.

    You will also notice that we must stay within the bounds of the screen so we will be checking that the code that gathers the average from other segments does not drift off the screen.

    This will be our function to do this...

    Code:
    procedure TBattlefield.SmoothenLand(SmoothSize: Integer);
    var i, j: Integer;
        Mass, NumOfMassSamples: Integer;
    begin
         for i := 0 to Width - 1 do
         begin
              // Get average height of selected area...
              Mass := 0;
              NumOfMassSamples := 0;
              for j := i - SmoothSize to i + SmoothSize do
                  if (j > 0) and (j < Width - 1) then // Samples must be in bounds!
                  begin
                       inc(NumOfMassSamples);
                       Mass := Mass + LandHeight[j];
                  end;
    
              // Resize LandHeight element
              LandHeight[i] := Round(Mass / NumOfMassSamples);
         end;
    end;
    Mass is the total added height of all of the segments within the SmoothSize area including it's own height.
    NumOfMassSamples is the count of how many actual samples were taken regardless of the value feed into SmoothSize.

    This should give you a relatively nicer result and land that you can actually use to place tanks on and have them accurately aim and fire at each other.


    Drawing It

    Okay so now that we've got our nice land generating functions, I'm sure that you'll want to actually see it in action. So here is our drawing code...

    Code:
    procedure TBattlefield.DrawLand(GameScreen: PSDL_Surface);
    var
      i: Integer;
    begin
         // Land
         for i := 0 to Width - 1 do
             SDL_DrawLine(GameScreen, i, Height - 1, i, Height - 1 - LandHeight[i], LandColor);
    end;
    NOTE: It makes use of the sdlutils unit so make sure that it's included in the uses path of GameObjectUnit.pas.

    Pretty simple huh?

    The DrawSky function is rather simple too. It will either draw a solid sky color or a stored bitmap surface.

    Code:
    procedure TBattlefield.DrawSky(GameScreen: PSDL_Surface);
    begin
         if (isBGImage) then
             DrawBackgound(GameScreen, Background)
         else
             SDL_FillRect(GameScreen, PSDLRect(0, 0, 800, 600), SkyColor);
    end;
    Putting It All Together

    Now we'll create our battlefield object, run the land generator and draw everything to the screen.

    In scorch2d.lpr add the following under var at the top of the code...

    Code:
      // Level Data
      Level: TBattlefield;
    While there, also remove the following line of code...

    Code:
    Background: PSDL_Surface; // Background Graphic
    It will be replaced by TBattlefield.Background and is no longer needed for testing.

    Find the ProgramCreate; procedure and remove the 2 following lines of code.

    Code:
         // Load Background
         Background := LoadImage('images\\DesertEclipse.bmp', False);
    Go down to the main code block and add the following code after ProgramCreate;...

    Code:
         Level := TBattlefield.Init(GameScreen.w, GameScreen.h, $229900, $0000ee, True, 'DesertEclipse.bmp');
         Level.GenerateLand(LandHighest, LandLowest, LandVariation);
         Level.SmoothenLand(LandSmoothing);
    Now the only thing left to do is add the code to draw the sky background and generated land. Find DrawScreen; and add this as the top 2 lines...

    Code:
         Level.DrawSky(GameScreen);
         Level.DrawLand(GameScreen);
    That should do it. Considering you pieced TBattlefield together properly you should be able to press F9 to compile and run the code. Remember that it's the Esc key to quit!


    The Source

    Here is the complete updated source that shows how it's all done together.

    Download the source files here!
    a62_Source.zip


    End of Part 2

    So now we have our randomly generated dirt to place tanks on and plow shells into. In the next part I'll be showing you how to place our tanks randomly about the land we've generated and do so in a smart manner.

    Until then, play with the code a little bit and see what else you can do with it. There is lots of room for innovation here.

    Suggestions, helpful code snippets and kudos are welcome!

    - Jason McMillen
    Pascal Game Development