Artillery Part 1

Welcome to the first in a series of tutorials that I hope will help new developers and fans of the genre. When I first started to learn about computers and even before I got serious about programming, one of the first PC games I ever owned was a shareware game called 'Scorched Earth'. It was a cleverly designed game where you were in a tank against up to 9 other tanks on a landscaped 2D screen, and the main idea was to try to destroy all of them and be the last one standing to win the round. Do this for a chosen number of rounds and whoever has the highest score at the end, wins the game.

In addition, you had many nice features where you could buy all kinds of different weapons and items to aid you in fighting. This money was earned as you played and destroyed other tanks for points. Different tank types, fuel to get around, levels of computer AI, randomly generated yet configurable land and many other things that made this an exciting, addictive and extremely fun game. The 'Artillery Game' genre was born, and I was hooked!

You may have also played a more recent game that is based on the same concept of game play called 'Worms'. It also falls under this genre. It has had several sequels that have been leading the way of this type of game adding new things in each edition of the series.

Throughout this tutorials series I will be teaching how to design an Artillery Game yourself. It will feature the basics of the game play and then move on to the many neat features and systems you see within these great games.

Scorched Earth 1.5

Scorched Earth 1.5

What do I need before we start?

I will be using Lazarus and JEDI-SDL to create the game. I will not, however, cover how to use Lazarus or JEDI-SDL. This does not mean that if you have not used it before you will not be able to follow along. The concepts and game play design will be independent of this. You may substitute with any Object Pascal compiler/IDE or graphics library you like instead.

I will also be making use of objects using Object Pascal. I will assume that the reader knows at least the basics of 'Object Oriented Programming' or OOP for short. (See the PPS OOP Tutorial for the basics). This will help to keep the code clean and manageable as we go though its development.

Though you do not need to know a lot of mathematics for this game, it would most certainly help in some cases. More specifically, it will help with the game physics portion where we get into a small bit of trigonometry. But it is not by any means an obstacle, so please read on!

Direction and Scope

Like all tutorials, I will start by covering the basics first. We will create the game as we go along, separating each step, so that it'll make for easy consumption and act as a reference for later.

We'll start off in the 2nd instalment with how to generate the battlefield, draw it and display it. Then in the 3rd, we'll place the tanks into the battlefield. Afterwards we'll move along to the tank controls, the game's physics for fired shots and collision detection. Then we'll later learn tank damage, wind and weapons explosions. These are the basic features of the game before you can go ahead and make it all fancy afterwards.

Worms World Party

Worms World Party

And now, our humble beginnings; Enter The Code!

We will start out with the main source that we will build upon and GraphicsUnit.pas to help speed along some of the graphics handling.

When run it should show the included background scene. You can then press Esc to close the program. If you are new to JEDI-SDL or even Pascal then you will be able to play with this further to get a better grasp of how to work with the two before you jump onto Part 2.

Download the source files here!

Artillery1_Source.zip

program scorch2d;

{$IFDEF Win32}
  {$IFDEF FPC}
    {$APPTYPE GUI}     // FreePascal/Lazarus
  {$ELSE}
    {$APPTYPE CONSOLE} // Delphi
  {$ENDIF}
{$ENDIF}
{$APPTYPE GUI}
uses
  SysUtils,
  // JEDI-SDL
  sdl,
  sdlutils,
  GraphicsUnit;

type
  // Game Modes
  TGameMode = (gmMainMenu, gmAiming, gmShooting, gmMenu, gmQuit);

  // Game Controls
  TGameControls = Record
    Up, Down, Left, Right: Boolean;
    Fire, Select, Menu: Boolean;
  end;
  
var
  GAME_FPS: Cardinal = 30; {30}

  // Video Screens
  GameScreen: PSDL_Surface; // Main GameScreen!

  Background: PSDL_Surface; // Background Graphic

  // Game Data
  GameMode: TGameMode = gmAiming;
  RunClock: Integer;

  GameInput: TGameControls;

function Get_FPS(fps: Cardinal): UInt32;
begin
     if (fps > 0) then
        Result := 1000 div fps
     else
         Result := 0;
end;


procedure doGameInput;
var
  event: TSDL_Event;
begin
     while (SDL_PollEvent(@event) > 0) do
     begin
          case (event.type_) of
            SDL_KEYDOWN : begin
                               if (event.key.keysym.sym = SDLK_UP) then
                                  GameInput.Up := True;
                               if (event.key.keysym.sym = SDLK_DOWN) then
                                  GameInput.Down := True;
                               if (event.key.keysym.sym = SDLK_LEFT) then
                                  GameInput.Left := True;
                               if (event.key.keysym.sym = SDLK_RIGHT) then
                                  GameInput.Right := True;
                               if (event.key.keysym.sym = SDLK_SPACE) or (event.key.keysym.sym = SDLK_RETURN) then
                                  GameInput.Fire := True;
                               if (event.key.keysym.sym = SDLK_TAB) then
                                  GameInput.Select := True;
                               if (event.key.keysym.sym = SDLK_ESCAPE) then
                                  GameInput.Menu := True;
                          end;
            SDL_KEYUP   : begin
                               if (event.key.keysym.sym = SDLK_UP) then
                                  GameInput.Up := False;
                               if (event.key.keysym.sym = SDLK_DOWN) then
                                  GameInput.Down := False;
                               if (event.key.keysym.sym = SDLK_LEFT) then
                                  GameInput.Left := False;
                               if (event.key.keysym.sym = SDLK_RIGHT) then
                                  GameInput.Right := False;
                               if (event.key.keysym.sym = SDLK_SPACE) or (event.key.keysym.sym = SDLK_RETURN) then
                                  GameInput.Fire := False;
                               if (event.key.keysym.sym = SDLK_TAB) then
                                  GameInput.Select := False;
                               if (event.key.keysym.sym = SDLK_ESCAPE) then
                                  GameInput.Menu := False;
                          end;
          end;
     end;
     
     // -- Game Key Actions -- //
     if (GameInput.Menu) then
        GameMode := gmQuit;

     // -- Reset Game Keys -- //
     GameInput.Fire := False;
     GameInput.Select := False;
     GameInput.Menu := False;
end;

procedure GameCycle;
begin
end;

procedure DrawScreen;
begin
     DrawBackgound(GameScreen, Background);
     
     SDL_Flip(GameScreen);
end;

// -- INIT & DEINIT -- //
procedure ProgramCreate;
var
  Buffer, s: Word;
begin
     // Update Random Seed value (Helps make random values more random!)
     Randomize;
     DecodeTime(Time, Buffer, Buffer, Buffer, s);
     RandSeed := s;

     // Init Video Mode //
     if (SDL_Init(SDL_INIT_VIDEO) <> 0) then
        Halt;

     // Set the title bar in environments that support it
     SDL_WM_SetCaption('Scorch 2D', nil);

     // Set Video Mode
     GameScreen := SDL_SetVideoMode(800, 600, 32, SDL_SWSURFACE);
     if (GameScreen = nil) then
     begin
          SDL_Quit;
          Halt;
     end;

     // Load Background
     Background := LoadImage('images\\DesertEclipse.bmp', False);

     // Hide Cursor
     SDL_ShowCursor(SDL_DISABLE);

     // Reset RunClock
     RunClock := 0;
end;
// Deinit Program
procedure ProgramClose;
begin
     // Show Cursor
     SDL_ShowCursor(SDL_ENABLE);

     // Shutdown SDL
     SDL_Quit;
end;
// -- End of INIT & DEINIT -- //


 // ~~~ | ------------ | ~~~ //
 // === | MAIN PROGRAM | === //
 // ~~~ | ------------ | ~~~ //
begin
     ProgramCreate;

     RunClock := 0;
     repeat
       SDL_Delay(Get_FPS(GAME_FPS));

       doGameInput;

       GameCycle;

       DrawScreen;

       inc(RunClock);
     until (GameMode = gmQuit); // Exit when any key is pressed!

     ProgramClose;
end.

unit GraphicsUnit;

interface

uses
  sdl,
  sdlutils;

// Loads Images As a Transparent Sprite!!!
function LoadImage(DataFile: String; Transparent: Boolean): PSDL_Surface;
// Unloads Images
procedure UnloadImage(Image: PSDL_Surface);
// Checks If X,Y is On Visible Screen
function onScreen(GameScreen: PSDL_Surface; X, Y, ScreenX, ScreenY: Integer): Boolean;
// Draws Tile onto GameScreen
procedure DrawTile(GameScreen: PSDL_Surface; X, Y, TileIndex, TileSize: Integer; TileSet: PSDL_Surface);
// Draws Background onto GameScreen
procedure DrawBackgound(GameScreen: PSDL_Surface; BGImage: PSDL_Surface);

implementation

// Loads Images As a Transparent Sprite!!!
function LoadImage(DataFile: String; Transparent: Boolean): PSDL_Surface;
var
  Image, Surface: PSDL_Surface;
begin
     Image := SDL_LoadBMP(PChar(DataFile));
     if (Image = nil) then
     begin
          Result := nil;
          Exit;
     end;

     if (Transparent) then
     begin
          // Assuming 32-bit BMP image
          SDL_SetColorKey(Image, (SDL_SRCCOLORKEY or SDL_RLEACCEL),
                          PUint32(Image.pixels)^);
     end;
     Surface := SDL_DisplayFormat(Image);
     SDL_FreeSurface(Image);
     Result := Surface;
end;

// Unloads Images
procedure UnloadImage(Image: PSDL_Surface);
begin
     SDL_FreeSurface(Image);
end;

function onScreen(GameScreen: PSDL_Surface; X, Y, ScreenX, ScreenY: Integer): Boolean;
begin
     Result := False;
     if ((X >= ScreenX) and (X < ScreenX + GameScreen.w)) or
        ((Y >= ScreenY) and (Y < ScreenY + GameScreen.h)) then
        Result := True;
end;

{Draw Tile}
procedure DrawTile(GameScreen: PSDL_Surface; X, Y, TileIndex, TileSize: Integer; TileSet: PSDL_Surface);
var
  SrcRect, DestRect: TSDL_Rect;
begin
     SrcRect  := SDLRect(TileIndex * TileSize, 0, TileSize, TileSize);
     DestRect := SDLRect(X, Y, TileSize, TileSize);
     if ((X < GameScreen.w) or (X > -TileSize)) and ((Y < GameScreen.h) or (Y > -TileSize)) then
        SDL_BlitSurface(TileSet, @SrcRect, GameScreen, @DestRect);
end;

{Draw Background}
procedure DrawBackgound(GameScreen: PSDL_Surface; BGImage: PSDL_Surface);
var
  SrcRect: TSDL_Rect;
begin
     SrcRect := SDLRect(0, 0, BGImage.w, BGImage.h);
     if (BGImage.w > 0) and (BGImage.h > 0) then
        SDL_BlitSurface(BGImage, @SrcRect, GameScreen, @SrcRect);
end;

end.

Set-up

The project file included already has Delphi Compatibility mode enabled, which is required. Ensure that you have it on in case you are copying and pasting my code into a new project of your own making.

You will need to supply the path to the JEDI-SDL folder for the SDL header! In Lazarus go to Projects -> Compiler Options... and click on the Paths tab. There in the Other Unit Files (-Fu) field change C:\Documents and Settings\WILL\My Documents\Pascal Projects\JEDI-SDLv1.0\SDL\Pas\ to match the path of your own copy of JEDI-SDL. Similarly, change the Include Files (-Fi). An example of the changed settings follows.

Other Unit Files

Other Unit files

Lastly, since I have not supplied the sdl.dll file along with the source, you will be required to make a copy of the file to the source directory or a location within your system's path.

Once complete you should be able to compile and run it without any problems!

End of Part 1

That's it for now. In the next tutorial, 'The Dirt' we'll start to get into the good stuff where we'll be creating the play-field on which we'll be causing our mayhem!wink

                                                                      - Jason McMillen
                                                               Pascal Game Development
Programming - a skill for life!

Using SDL, Box2D or the GLScene or Castle Game Engine to write games in Pascal