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.