DelphiX game tutorial one

By: Michael Dundee

Abstract: Creating an isometric tiling engine.

DelphiX Game Tutorial One:

Creating an isometric tiling engine

By Michael Dundee

When I started programming the only thing I had in mind was game programming. After a while I gave up. I used Delphi but I couldn't find any good game APIs for it. I even started thinking of converting to C/C++, mainly because there was better coverage of such APIs as DirectX and OpenGL. But the hassle it required to use these APIs in C/C++ scared me.

Then I stumbled across DelphiX. This API wrapper is easy to implement, fairly fast, and quite powerful. I like it so much I've used it ever since. And now I'd like to share it with you.

I will not explain the installation of DelphiX here. I will not explain DirectX either, since that would take up too much of this article. For a closer look at DirectX and how to install DelphiX I recomend that you take a look at Dominique Louis's articles and other Borland Developer Community articles on the subject (1, 2, 3).

The goal of this series of tutorials is to create a basic game engine. We'll cover subjects like these: creating an isometric tiling engine, animating units and tiles, pathfinding, basic game AI, and collision detection.

In this chapter we will create a basic tiling engine which can read a map from a disk file. An editor for these files can be downloaded here.

GETTING STARTED

First let's go over some special words which will be used throughout this tutorial:

TILE. A small image of regular shape. Alone it doesn't represent much but if you put a lot of them together it looks like something. Usually used for generating landscapes in games. Tiles can be of different shapes. The most common are diamond, square, and hexagonal. Since this is an isometric tiling engine, we will be using diamond-shaped tiles.

TILING ENGINE. A sequence of code (usually a procedure or part of a procedure) which draws the tiles in their respective positions. It also determines which render frame to show.

TILING. The procedure of placing all the tiles in their proper positions. This is what the tiling engine does.

MAKING AN ISOMETRIC LANDSCAPE

The trick to making an isometric environment is to draw all the tiles so that their edges meet. In this case, the tile is shaped like a diamond. If we simply place all the tiles in even rows side by side it would look like this:

As you can see we have created what some would call a checkerboard. The huge holes in the ground are the transparent parts of the images. The edges don't meet very well, do they? This is what we are trying to achieve:

The way to achieve this is to draw the first row side by side as usual. The next row will not be started not 32 pixels down (the full height of the tile) but 16 pixels. It will also be started 32 pixels to the left. So for every new row the start position will be moved down 16 pixels and left or right (odd rows left, even rows right) 32 pixels.

But that's not all. If you have played Age of empires or AoE2 then you must have noticed that the edge of the map is not zigzag shaped. The entire map is shaped like a diamond. The way to achieve this is to draw just one tile on the first row, two on the next, three, four, five and so on to a certain point. At that point you turn over and start to decrease until you've reached zero. This time you don't need to switch offset direction between right and left. Just offset it left while increasing and right while decreasing. If we make a 3x3 tile map it will look like this:

Do you really want a map with just grass? Just water? If the answer is yes then you could stop here. But to get a bit of variation in the maps (which is needed in a strategy-based game) we have to be able to load the maps from somewhere. A two-dimensional array consisting of integer values would solve this. The routine that draws the current tile checks in this array to determine image to draw. To be able to use multiple maps without having to redefine them at runtime, we have to save them as files. This is a simple task as you will see in the code. A map editor is be supplied with this article. This is a very simple editor that saves landscape arrays to disk. You only get the source to the editor so you will have to compile it yourself. You can get it here.

That's enough theory. Let's do the practical part!

BUILDING THE TILING ENGINE

First, make a folder somewhere on your hard drive. All the graphics and .map files for this project can get lost if they're not organized in one place. 

The next step is to download the graphics from here. If you choose to make your own graphics then remember to keep the size of the images to 64 pixels wide and 32 pixels high.

Now make a subfolder for the editor. Download and unzip those files from here. Make sure DelphiX is installed and ready to use. Compile the editor. Get something to drink and a really comfortable chair. Now you are set to go!

Start a new application in Delphi. Save it in the folder where you put the graphics. I called my files uGame.pas and Game.dpr.

First let's prepare the form. In the type declaration of the form, change TForm to TDXForm. In the object inspector set these properties:

BorderStyle:bsNone
Caption:Whatever you want
Name:MainForm

Now we need a surface to draw on. For that we use a TDXDraw component. Just drop a TDXDraw component on your form and give it these properties:

AlignalClient
Display640x480x8
NameDXDraw
Options
doFullScreenTrue
doAllowRebootTrue
doFullScreenTrue

Next we will need a place to store the images used to draw the map. Drop a TDXImageList on your form and give it these properties:

DXDrawDXDraw
NameTileMaps

Add images to the Items property. (You will figure out how, it's self-explanatory.) There are four images to add. They will be numbered 0 to 3.

Next we will need a Timer to tell the computer when to draw. Don't use the Timer that ships with Delphi, as it's too slow for game purposes. The DXTimer is much faster. I once clocked the two timers against each others -- the standard timer went 19 times per second while the DXTimer went about 1,200 times a second. Drop a DXTimer on your form and give it these properties:

EnabledFalse
Interval0
NameDXTimer

Next we'll need a way to scroll our screen. For this we'll use the DXInput component. Drop a DXInput component on your form and give it these properties:

NameDXInput
Joystick
EnabledFalse
Mouse
BindInputStateTrue
EnabledTrue

Now place a panel on the form. You only have to clear the caption and call it "Panel." Set the Align property to "alBottom." On this panel place a button and call it whatever you want. It will be used to shut down the application.

Now all the cut-and-paste programming is done!

BEYOND CUT AND PASTE

Now switch over to the code window and put this declaration in the private section of your form:

procedure DrawTiles(StartX, StartY : Integer);

This is the procedure in which we draw the tiles, the tiling engine. As you can see, it requires two values to know where to start drawing the tiles. Make these type declarations above the form declaration:

Type TTile = Record
  TileImage : Integer;
end;

Type TTileMap = Record
  Map : array[1..50,1..50] of TTile;
end;

Also make these global declarations:

Map : TTileMap;
StartX, StartY : Integer;

This creates the information for a map about 50x50 tiles of size. Couldn't we just make an array of integers instead of a record? I hear you ask. Maybe. I made it a record so we can expand it later without changing a lot of code. This way we get a flexible system to build on for later articles.

In the onClick event of the button, write:

Close;

This is for exiting the application. Since we're in full-screen mode we won't have access to the system menu or system buttons.

Now implement this procedure the DrawTiles procedure we declared earlier:

procedure TMainForm.DrawTiles(StartX, StartY : Integer);
var PosX, PosY, LoopX, LoopY, XTile, YTile, NumOfTilesY,
  TileImage : Integer;

begin
  PosY := 0;
  PosX := 1;
  NumOfTilesY := 1;
  for LoopY := 1 to 50 do
  begin
    XTile := 1;
    YTile := LoopY;
    for LoopX := 1 to NumOfTilesY do
    begin
      TileImage := Map.Map[XTile, YTile].TileImage;
      TileMaps.Items[TileImage].Draw(DXDraw. Surface,
        StartX + PosX + LoopX * 64, StartY + LoopY * 16, 0);
      XTile := XTile + 1;
      YTile := YTile - 1;
    end;
    NumOfTilesY := NumOfTilesY + 1;
    PosX := PosX - 32;
    PosY := PosY + 16;
  end;
  NumOfTilesY := NumOfTilesY - 2;
  PosX := PosX + 64;
  for LoopY := 1 to 49 do
  begin
    XTile := LoopY;
    YTile := 3;
    for LoopX := 1 to NumOfTilesY do
    begin
      TileImage := Map.Map[XTile, YTile].TileImage;
      TileMaps.Items[TileImage].Draw(DXDraw. Surface, StartX +
        PosX + LoopX * 64, StartY + PosY + LoopY * 16, 0);
      XTile := XTile + 1;
      YTile := YTile - 1;
    end;
    NumOfTilesY := NumOfTilesY - 1;
    PosX := PosX + 32;
  end;
end;

This procedure draws the tiles starting at the StartX, StartY coordinates. TileImage holds the current tile type to draw. It's recalculated for each tile, as you can see. To access this procedure we need to put this code in the onTimer event for DXTimer:

if not DXDraw.CanDraw then Exit;
DXDraw.Surface.Fill(0);
Panel.Caption := 'FPS:' + InttoStr(DXTimer.FrameRate);
DrawTiles(StartX, StartY);
DXDraw.Flip;
DXInput.Update;
If isLeft in DXInput.States then StartX := StartX + 16;
If isRight in DXInput.States then StartX := StartX - 16;
If isUp in DXInput.States then StartY := StartY + 8;
If isDown in DXInput.States then StartY := StartY - 8;

First the procedure checks to see if it can draw on the surface. Then it fills the surface with black paint. This erases the previous frame content. The frame rate is then passed to the Caption of the panel. Then it calls the DrawTiles procedure, passing StartX and StartY as the starting position. It flips the new image to the screen. After that it updates the input states. It also tests a number of buttons to figure out how to move the map. Be sure to add this to the onCreate event for the form:

StartX := 1;
StartY := 1;
DXTimer.Enabled := True;

SUPPORT FOR CUSTOM MAPS

This is all we need to display the map on our screen. But what about custom maps? Start by adding this global declaration:

MapFile : file of TTileMap;

Put a button on the panel and set its caption to "Open." Also place an OpenDialog component on your form. Give it the name OpenDlg and the filter: MapFile(*.map) | *.map. Add this code to the onClick event of the Open button:

if OpenDlg.Execute then
begin
  AssignFile(MapFile, OpenDlg.FileName);
  Reset(MapFile);
  Read(MapFile,Map);
end;

This reads the map from file and draws it.

That's it! Now you've got a basic isometric tiling engine up and running. In the next article (in about a month) we will take a look at animated tiles and world positioning of the cursor.

The source for this tutorial can be downloaded here. E-mail me if you have questions about this article. That's all for now! See you next month!



Server Response from: ETNASC03