DelphiX game tutorial - continued!

By: Michael Dundee

Abstract: Learn the basics of tile animation. By Michael Dundee.

Hello again! It has been a while but now I'm back! The last time we looked at how to create the isometrically tiled world. This time we will look at how to determine the position of the cursor and units. We will also go through basic animation of tiles. For this purpose a new set of graphics have been produced. You can get the whole package with source, graphics and the new converter at CodeCentral. If you don't have the source from my old article then you can download it here. This source is needed since we will be building on it this article. Ok, enough about that! Let's get to the topic.

THEORETICAL OVERVIEW

ANIMATION

Animating an image in DelphiX is a really simple task. The images supplied for this article have a little different look than the old ones. First of all I've expanded the number of tiles to 16. This makes the world more diverse, which is probably needed since the old ones were quite boring. Well anyway, three of these tiles are animated. If you open the bitmaps you will notice that the frames are placed side by side. Like this:

Of course it doesn't have the red borders and the numbers. When you load the bitmap into the DXImageList component, there are two properties called PatternHeight and PatternWidth. These are set to the height and width of a single frame. We can now, with the Pattern Number part of the Draw Method, decide which frame to show The first frame is number 0, the second frame number 1, and so on. By changing a variable every time the map is redrawn, we can get the frames to be shown one after the other and thus get an animated tile. Like this:

That is the basics of animation! Of course it gets more complicated when you animate units since they have different animations for different actions. But this is really all there is to it!

WORLD POSITIONING

Now this is the tricky part. On a screen the coordinate system is quite easy, it looks like this:

But this coordinate system doesn't work very well with the isometric tile. By rotating and skewing the coordinate system we get a more suitable coordinate system. Like this:

In Delphi all positions are given according to the screen coordinate system. But the tiles use the isometric coordinate system or the world coordinate system which is the one in the second image. The hard part is converting between these two coordinate systems.

This is where those math classes you took come in handy. Finally, functions are really useful. (And, I don't mean functions as in programming.) A row of tiles is placed along, or follows, the function f(X)=0.46875X or f(X)=0.46875X depending on which axis. (Why all the decimals? Remember in the last article we used the tile height of 30, which divided by 64 is 0.46875. If we use 0.5 instead, which doesn't seem to be too much different, we will get a slight deviation in the positioning system. The cursor will start selecting tiles beside it, in some cases as many as three tiles in the wrong direction.) The X in these function refers to the screen coordinate system. These two functions look like this:

The shaded area is a value above and below the function that the tile is within. Where two shaded areas cross each other we have the shape of a tile. What we have to do in order to figure out which tile the cursor is in is to determine when the tile is inside the shaded area. So, if the Y value of the cursor is between -0.46875X+14 and -0.46875X-14 then the cursor is on the same WorldY as the tile. If the Y value is between 0.46875X+14 and 0.46875X-14 then the cursor is on the same WorldX as the tile. If both these happen at the same time then the cursor is on the tile (where the shaded areas cross). This is just for one tile. What about 100x100 tiles? This picture might give you an idea of how to solve this.

First of all, every row is offset one tile height (30 pixels) from the one above. So, what we do is enumerate through all WorldX rows until we strike the right one. Then we take the number of that row and place it in the TileX Integer. Next, we enumerate through all the WorldY rows and place the right number in the TileY Integer. Finally, we return the two values to the main program. Now, the main program can use these values to get tile info, draw the marker, etc. Ok? Let's implement it in our tiling engine.

PRACTICAL IMPLEMENTATION

ANIMATION

First of all, delete all the old images from the image list and add the new ones. Do not add "Marker.bmp" at this point. For each image change these properties:

PatternHeight = 32
PatternWidth = 64

Place a new DXImageList on your form and call it Markers. Add "Marker.bmp" to this list. Then put this in the global declarations:

Frame: Integer;
TileX, TileY: Integer;

In the OnCreate event for the form add this:

Frame = 0;

And in the OnTimer event of the DXTimer add this:

if (Frame = 5) then
  Frame=0
else
  Frame = Frame + 1;
end;

And in the DrawTiles procedure we replace 0 with Frame in the calls to the Draw procedure. Like this:

TileMaps.Items[TileImage].Draw(DXDraw.Surface, 
  StartX + PosX + LoopX * 64, StartY + LoopY * 16, Frame);

Remember that this statement occurs twice in the DrawTiles procedure. Remember to change both, or you will have half your map not animating. Now save and test your program. You cannot use the old editor because of changes to the file structure. But you can download a converter from CodeCentral. This converter converts four-bit bmps to .map files. This way you can create a 50x50 pixel map in an image editor and save it as a 4bit (16 color) bitmap. And then convert it for use with the tiling engine. If your frame rate is too high then the animation may look bad. Just set Interval of the Timer to 33 to fix this. This will give you a frame rate of 30 fps. Well, that's all for animation! Now let's get to the hard part!

WORLD POSITIONING

First of all we need to make some declarations. Add this to the type declaration of TTile:

Marked: Boolean;

Also declare this procedure:

procedure CheckTiles(X, Y: Integer; var XTile, YTile: Integer);

Next create the procedure with this code in it:

procedure TMainForm.CheckTiles(X, Y: Integer; var XTile, YTile: Integer);
var LoopX, LoopY: Integer;
begin
  for LoopY := 1 to 50 do
  begin
    if (Y - ((0.46875) * (X - StartX) + StartY + (LoopY * 30)) < 15) and (Y - ((0.46875) * (X - StartX) + StartY + (LoopY * 30)) > (-15)) then
    YTile := LoopY;
  end;
  for LoopX := 1 to 50 do
  begin
    if (-(0.46875) * (X - StartX) + StartY + (LoopX * 30) - Y < 15) and ((-0.46875) * (X - StartX) + StartY + (LoopX * 30)- Y > (-15)) then
    XTile := LoopX;
  end;
end;

Then in the OnMouse move event of the DXDraw component add this:

Map.Map[TileX, TileY].Marked := False;
CheckTiles(X - 96, Y, TileX, TileY);
Map.Map[TileX, TileY].Marked := True;

This unmarks the old tile and marks the new one. Then add this line under the Draw function in the DrawTiles procedure. Be sure to add it under both function calls, as each one is just for half the map.

if Map.Map[XTile, YTile].Marked then
  begin
    Markers.Items[0].Draw(DXDraw.Surface, StartX + PosX + LoopX * 64, StartY + PosY + LoopY * 15 - 2, 0);
  end;

This draws a marker on the marked tile making it easier to see what tile is marked. Change this line in the OnTimer event:

Panel.Caption := 'FPS: ' + IntToStr(DXTimer.FrameRate);

to:

Panel.Caption := 'FPS: ' + IntToStr(DXTimer.FrameRate) + ' TileX: ' + IntToStr(TileX) + 'TileY: ' + IntToStr(TileY);

This lets us see the current world coordinate of the cursor. Using this world coordinate we can later extract info about tiles, units, buildings, etc. Well, that's all! Next time we will start placing props and moving units. Feel free to e-mail comments and questions to me.


Server Response from: ETNASC01