Creating tile-based terrains (I)

After creating one or more characters, we have to build some kind of world where characters can interact. In two-dimensional games, the terrain of the scenarios is composed by a large amount, usually thousands, of small pieces known as tiles. In general terms, tiles can have diverse shapes, but within the boundaries of a particular game, they have all the same shape and size. You can see these three examples of tiles, which are in use in the Dagovar games:

Tile Combat Dolls
Tile Desert Vixens 2
Tile Desert Vixens 3

In the first example, we are seeing a hexagonal tile. This type of tile is intended for games where characters have been set to walk in six directions. These kind of games are rare, being the old Fallout games an example of them.

In the second example, we are seeing an asymmetric tile which is also intended for games where characters have been set to walk in six directions. However, this type of tile is more conventional and I used it in Desert Vixens 1 and 2 because it fits better with the walls.

In the third example, we have a symmetric (isometric) tile, which is intended for games where characters heve been set to walk in eight or four directions. This type of tile has a more regular shape than the previous one and it simplifies greatly the algorithms used to manage the tile-based terrain.

Creating a hex-based terrain

The first thing that we have to do when programming a source code that manages tiles to form a terrain with them, is to declare two constants that will inform the program about the size of the tile. However, take into account that the size that we declare on the program is not exactly the size of the tile 'piece', since tiles are interleaved with each other. For instance, the hexagonal tile shown in the previous example, is 48 x 27 pixels in size, but the declaration of the constants in the program is:

Public Const HEIGHT_TILES = 20
Public Const WIDTH_TILES = 48

Why have I set the height of the tile to 20 instead of 27? Just because each of the tiles interleave with the others 7 pixels, vertically, when they are arranged together. Horizontally, they do not interleave, so the declared width is the same than the original width of the tile. The picture below shows how these hexagonal tiles arrange together:

Hexagonal tiles

We have to write a good amount of variables before starting to write the algorithm that will arrange together the large amount of tiles required. The first thing that we have to think about is the size of the map, which will be defined by the number of tiles that the sides of the map have along it. In Desert Vixens games I have set the maps to have the same dimension on each of their four sides, so the maps are always square-shaped; this helps to simplify things.

If you have read the chapter about creating a character, you will remember that I talked about entities and user-defined types. We have to use these here too, and with even more reason, since creating a scenario is much more difficult than creating a poor character. Since we are working with 2D maps, the first thing that comes into mind is that we have to manage 2D coordinates, so I declared an user-defined type which would allow me to manage coordinates as they were a regular type of variable defined in Visual Basic 'from factory'. Let us see it:

Public Type Coordinate
intX As Integer
intY As Integer
End Type

How beautiful. This will be the foundation for working with our tile-based terrains. Now, we have to create another user-defined type to store all the data required for any map or terrain in general; so I am considering a map as an entity on itself. Let us see the definition:

Public Type Map
bytTilesDimX As Byte
bytTilesDimY As Byte
bytType As Byte
NW_Corner As Coordinate
NE_Corner As Coordinate
SE_Corner As Coordinate
SW_Corner As Coordinate
End Type

The first two variables are intended to store the dimensions of the sides of the map. Unlike in Desert Vixens games, in Combat Dolls games the maps have independent vertical and horizontal sizes. The third variable is intended to store the type of terrain that will be used: wasteland, desert or forest. The last four variables are of the type that I previously defined as a coordinate; these variables will store the coordinates of the four corners of the map. As you can guess, the third variable can be optional - it is used for, lets say, 'aesthetical' purposes -, but the rest of them are necessary, since they define fundamental properties of any map.

Now that we have created an user-defined type for the 'entity map', it would be logical to declare now an user-defined type for the 'entity tile'. Let us see it:

Public Type Tile
bytDefTerrain As Byte
NW_Corner As Coordinate
NE_Corner As Coordinate
SE_Corner As Coordinate
SW_Corner As Coordinate
Position As Coordinate
bytNumFrameTerrain As Byte
End Type

In the declaration of a tile entity shown in the previous example, the first variable stores which picture (terrain definition) the tile will display (for example soil, grass, sand, water, etc...). The following four variables will store the coordinates of the four corners of the tile; you can see how the tile is like a map in a much smaller scale. The sixth variable stores the position of the tile in the map, using a coordinate, like we were playing a classical Sea Battle game. The seventh variable stores which variation will be applied to the picture of that tile; variations on tile pictures are used to avoid an excessive repetitive effect of tile pictures that form the terrain.

One more user-defined type that we have to define is the one used for the different types of terrains that maps can have. These are what I call the terrain definitions.

Public Type Terrain_Def
bytId As Byte
Pos_Surface As Coordinate
End Type

As you can see, each terrain definition will have an Id and a coordinate assigned to it. The coordinate is needed to tell the program in which position the graphic corresponding to a certain terrain definition can be found on the graphical file. If you do not understand what I am talking about, I encourage you to look at this article, in the section dedicated to Bitblt: API Functions For Games.

The following are the declarations for the variables that will store the data of the map, tiles and terrain definitions. Since there is only a map, its data is stored in a simple variable. Tiles are stored in a dynamic array, so we can create maps with different sizes. And finally, terrain definitions are stored in a fixed array, whose number of cells is defined by a constant that stores the number of terrain definitions that we will use in our game.

Public The_Map As Map
Public Tiles() As Tile
Public Terrain_Defs(1 To NUM_TERRAIN_DEFS) As Terrain_Def

Now it is time to see the algorithm that creates the hex-based terrain. We will start by giving the map a certain size; the following code will choose between three possible sizes:

If optSmall.Value = True Then
The_Map.bytTilesDimX = 40
The_Map.bytTilesDimY = 96
ElseIf optMedium.Value = True Then
The_Map.bytTilesDimX = 50
The_Map.bytTilesDimY = 120
ElseIf optLarge.Value = True Then
The_Map.bytTilesDimX = 60
The_Map.bytTilesDimY = 144
End If

Then, we have to initialize all the data of the map, terrain definitions and tiles. Firstly, we initialize the values of the map. The corners are given in this order: NW, NE, SE, SW. Using the north-western corner as the reference point, we can easily calculate the coordinates of the other corners, by just knowing the width and height of the map, measured in tiles, and the width and height of the tiles, measured in pixels.

The_Map.NW_Corner.intX = 0
The_Map.NW_Corner.intY = 0
The_Map.NE_Corner.intX = The_Map.bytTilesDimX * WIDTH_TILES
The_Map.NE_Corner.intY = 0
The_Map.SE_Corner.intX = The_Map.bytTilesDimX * WIDTH_TILES
The_Map.SE_Corner.intY = The_Map.bytTilesDimY * HEIGHT_TILES
The_Map.SW_Corner.intX = 0
The_Map.SW_Corner.intY = The_Map.bytTilesDimY * HEIGHT_TILES

Then, we have to load the different terrain definitions that can be used on a map:

Terrain_Defs(1).bytId = 1
Terrain_Defs(1).Pos_surface.intX = 0
Terrain_Defs(1).Pos_surface.intY = 0
Terrain_Defs(2).bytId = 2
Terrain_Defs(2).Pos_surface.intX = 96
Terrain_Defs(2).Pos_surface.intY = 0
Terrain_Defs(3).bytId = 3
Terrain_Defs(3).Pos_surface.intX = 0
Terrain_Defs(3).Pos_surface.intY = 27
Terrain_Defs(4).bytId = 4
Terrain_Defs(4).Pos_surface.intX = 96
Terrain_Defs(4).Pos_surface.intY = 27
Terrain_Defs(5).bytId = 5
Terrain_Defs(5).Pos_surface.intX = 0
Terrain_Defs(5).Pos_surface.intY = 54
Terrain_Defs(6).bytId = 6
Terrain_Defs(6).Pos_surface.intX = 96
Terrain_Defs(6).Pos_surface.intY = 54

As you can see, we have here six different terrain definitions, though a map will use only two of them at the same time. Now we continue initializing more variables. The size of the map is calculated by multiplying the lenght in tiles of the map width and height, which gives as a result the total number of tiles that the map has; that is what I call the map size. The array of tiles is then resized to the given number of tiles, and finally, by using a loop that will run that same number of iterations, we initialize to zero all the data contained on the array of tiles.

Dim intMapSize As Integer
intMapSize = CInt(The_Map.bytTilesDimX) * CInt(The_Map.bytTilesDimY)
ReDim Tiles(1 To intMapSize)
Dim i As Integer
For i = 1 To intMapSize
Tiles(i).bytDefTerrain = 0
Tiles(i).bytNumFrameTerrain = 0
Tiles(i).NW_Corner.intX = 0
Tiles(i).NW_Corner.intY = 0
Tiles(i).NE_Corner.intX = 0
Tiles(i).NE_Corner.intY = 0
Tiles(i).SE_Corner.intX = 0
Tiles(i).SE_Corner.intY = 0
Tiles(i).SW_Corner.intX = 0
Tiles(i).SW_Corner.intY = 0
Tiles(i).Position.intX = 0
Tiles(i).Position.intY = 0
Next i

The following source code is the one which constructs the terrain by assembling the thousands of tiles that form it. It is a complex, intricate algorithm. It firstly calculates the position that each tile has to have on the map, and then it calculates the coordinates of the four corners for each of the numerous tiles. There are two ways of managing the array of tiles that form a map; one can use a two-dimensional array, which is resized with the width and height of the map given in tiles; or one can use an one-dimensional array, which is resized with the size of the map, that is, the total number of tiles that form the map. I have chosen this second method for my applications.

In the first part of the algorithm, which calculates the position that each tile will have on the map, you can see two separated blocks of sentences; that is because position is calculated differently depending on wether a certain tile is a multiple of the map width or not. For example, if the width of the map is 40 tiles, the tiles which are number 40, number 80, number 120, and so on..., would have their position calculated by the first block of sentences, since these tiles are multiples of the map width, because they are the last tile on their row; the rest of the tiles will have their position calculated by the second block of sentences.

In the second part of the algorithm, which calculates the coordinates of the four corners for each of the tiles, you can see a function called Es_Impar; it is used to determine if a given tile is located on an even or an odd row (rows are determined by vertical - Y - position). This is done this way because in hex-based maps, as you can see on that graphic shown above on this page, the different rows of tiles have to be interleaved; and because of this, there are separate blocks of sentences that will apply the corresponding alterations on the coordinate X of the NO (north-western) corner of the tiles, depending on wether these are located on an even or an odd row. After finding the coordinates of the north-western corner of each tile, the coordinates of the remaining corners are easy to calculate, since we know the size of the tiles, measured in pixels. You can see also how the function Is_Odd is implemented, by looking at this article: Algorithms.

For i = 1 To intMapSize
If i / The_Map.bytTilesDimX = Int(i / The_Map.bytTilesDimX) Then
Tiles(i).Position.intX = The_Map.bytTilesDimX
Tiles(i).Position.intY = i / The_Map.bytTilesDimX
Tiles(i).Position.intX = i - ((Int(i / The_Map.bytTilesDimX)) * The_Map.bytTilesDimX)
Tiles(i).Position.intY = (Int((i / The_Map.bytTilesDimX) + 1))
End If
If Is_Odd(Tiles(i).Position.intY) = True Then
Tiles(i).NW_Corner.intX = (Tiles(i).Position.intX - 1) * WIDTH_TILES
Tiles(i).NW_Corner.intX = ((Tiles(i).Position.intX - 1) * WIDTH_TILES) + (WIDTH_TILES / 2)
End If
Tiles(i).NW_Corner.intY = (Tiles(i).Position.intY - 1) * HEIGHT_TILES
Tiles(i).NE_Corner.intX = Tiles(i).NW_Corner.intX + WIDTH_TILES
Tiles(i).NE_Corner.intY = Tiles(i).NW_Corner.intY
Tiles(i).SE_Corner.intX = Tiles(i).NE_Corner.intX
Tiles(i).SE_Corner.intY = Tiles(i).NE_Corner.intY + HEIGHT_TILES
Tiles(i).SW_Corner.intX = Tiles(i).NW_Corner.intX
Tiles(i).SW_Corner.intY = Tiles(i).SE_Corner.intY
Next i

Until here, I have shown a basic implementation on how a hex-based terrain can be constructed. Of course, this is just the foundation of a map. Combat Dolls games use this same algorithm, but there it is mixed with additional source code to manage all the elements that the game has. For instance, variables used to store which object - if any - is placed on a certain tile, or variables that indicate if there is any character or a corpse - poor one - located in a certain tile, or variables that store how much life has the object placed on a certain tile (if the value of this variable becomes 0, the object is destroyed). And also all the algorithms that manage the minimap, which is created apart as some kind of auxiliar map, having its own user-defined types and being created at the same time than the main map...

As you can see, some of these variables can be optional, but others, like the corners of the tiles, are fundamental to create the terrain. Later, during gameplay, the corners of the tiles will be reference points needed for many operations related with positioning and movement. But all of this is another story and I will not enter on it now...

After the array of tiles is correctly assembled to create the terrain, additional algorithms will go trough all the tiles with a loop, assigning random terrain definitions to these, so they will be assigned graphics showing grass, soil or sand. This is done by using random terrain generation. Random terrain, as you can easily realize, is not merely random, but subject to a wide serie of rules, controlled by complex algorithms. To know about random terrain generation, you can check this article: Random Maps. In the next chapter, I will show how to assemble an isometric terrain. Join me for the adventure...

About Dagovar

~ About Dagovar ~

Defining characters

~ Defining characters ~

Desert Vixens 2 maps

~ Desert Vixens 2 maps ~

Desert Vixens 3 maps

~ Desert Vixens 3 maps ~

Random maps

~ Random maps ~


~ Screenshots ~


~ Algorithms ~

API functions for games

~ API functions for games ~

Source code examples

~ Source code examples ~

<< Return To Index ~ ~ Next Chapter >>

Privacy Policy