Tutorial 8: Map Tiles
Introduction
Have you ever played a strategy game like “Warcraft” or “Command & Conquer”?. Did you notice the map navigation? Do you know how did they (game developers) build such type of maps?.
These questions and more will be answered in this tutorial.
We will explain the concept of map tiles, then we will build a map taken from image slices, finally we will show how to navigate through the map.
Map tiles
Strategy games are built on maps, without maps the game will not be interesting. The map can be loaded to OpenGL in tiles or slices. Take Fig.1 as an example we have an image for a map of size 1280×1280 sliced to 100 tiles. The map will be loaded to OpenGL by taking each tile and put it on the corresponding screen position. The map contains the following types of tiles
- Grass, example tiles 49, 50
- Rock, example tiles 38, 53
- Water, example tiles 99, 100.
- Tree, example tiles 22, 23
So, why we did divide the map into tiles, why don’t we load it to OpenGL as one big tile?
We do that for many reasons, the most important reasons is that
- We can minimize the memory space needed for storing the tiles, example see water tiles 99,100 they are typical, so we can remove one of them and use one tile instead of two also tile 90 can be removed. Another example look at tile number 49, 93 isn’t they typical? We can remove one of them too. In Fig.1 the tile size is 128, it can be reduced to 32×32 or 64×64, the repeated tiles will be much more then 128×128 size.
- We will use an obstruction map ( we will do that in further tutorials) , this map will be used to do the path finding algorithms. So for example, the avatar of the game will not walk on Rock tiles but it can walk on grass tiles.
Creating the map tiles
Suppose we have the map in Fig.1 and we need to slice it down to 400 tiles, We need to cut the image into 400 slices, 64×64 size each. This can be done with Adobe Photoshop or Adobe ImageReady.
Open Photoshop and then open the map image, use the slice tool to do one big slice holds the entire map, then divide this slice down into 20 horizontal slices 64 pixels each, this can be done by right clicking onto the map and select “Divide slice” from the context window. The output will be as shown in Fig.2. Then use the same procedure to divide the vertical slices.
To save the created slices, click File-> Save for web from the menu bar. Then use the JPG extension for the created slices. Finally define the path in which you will save your slices. The program will save 400 image files named sequentially from 1 to 400.
After the slices are saved into the hard disk with the .JPG extension, we need to change the image slices extension from .JPG to .BMP so it can be used in OpenGL.
To do that, we need to use third party software, which will convert the entire set of images together, instead of converting them one by one by paint brush or Photoshop. The software to do that named Imageicon and can be downloaded for free from the following website
http://www.bestfreewaredownload.com/freeware/t-free-imagicon-freeware-eignnsns.html
Drag and drop the files to the Imageicon and convert them to .BMP extension then they will be ready to be used in OpenGL.
Loading the map tiles to OpenGL
Now, we will get into the core of implementing maps in OpenGL. In previous tutorials, we explained the concept of textures. The map tiles will be loaded to OpenGL as textures, we will create gird of 400 squares 20×20 squares the width and height of the square will be 64 pixels, then we will cover each square with its corresponding tile. We will define a matrix of size 400 it will hold the entire collection of tiles
int g_map_width=20; int g_map_height=20; GLuint map_tiles[400];
Then we will use a function to load the tiles (all tiles are saved into a folder named map_slices located in the same program directory, the first file named map_1.bmp, the second file named map_2.bmp, ….. the last one named map_400.bmp), the structure of this function will be as follow
void LoadAllSlices() { for(int i=0;i < g_map_height*g_map_width;i++) { std::stringstream str_number; str_number << i+1; string fileName="map_slices\map_"+str_number.str()+".bmp"; map_tiles[i]=LoadTexture((char*)fileName.c_str()); } }
The function will loop 20×20 or 400 times, and load the tiles with the use of their names. We will convert the number i to string then we will concatenate the string with the remaining file name string to structure the entire file name. The LoadTexture function will take the current file name as an argument and load it to the proper place in the map_tiles matrix.
Displaying the map tiles
To display the map tiles, we need to define some concepts. The tiles will not be loaded entirely instead we will just render 10×10 tiles with 64 pixels wide for each tile, the OpenGL window size is 640×640, so it fits only 10×10 tiles and no need to load the entire 20×20 tiles. Fig.3 shows this concept.
To display a tile we use the following pseudo-code
int x,y; // Display from top to bottom for(y = 0; y < 10; y++) { // Display from left to right for(x = 0; x < 10; x++) { // Your display function here DisplayTile(x, y); } }
Since the tile array is not defined by x, y but defined by one dimension from 0 to 399. so how to get the corresponding element in the “map_tiles” array by using x, y? we can use the following formula
DisplayTile(map_tiles[x+y*10])
To display only 10×10 tiles we need to define some global variables
int g_map_width=20; int g_map_height=20; int g_tile_wide=10; int g_tile_high=10; int g_XPos=0; int g_YPos=0;
The g_map_width, g_map_height holds the width and height of the entire tiles. The g_tile_wide and g_tile_high holds the size of the tiles to be displayed currently on the screen. The g_XPos and g_YPos holds the position of the first tile to be displayed. See Fig.3 for more details.
Now we understood the main concepts behind displaying a map, it is time for navigating the code. The code for displaying tiles is shown by the following code segment
void RenderMapSlices() { for(int y=0;y < g_tile_high;y++) { for(int x=0;x < g_tile_wide;x++) { glBindTexture(GL_TEXTURE_2D, map_tiles[(x+g_XPos)+(y+g_YPos)*g_map_width]); int pixel_x=x*tile_size; int pixel_y=y*tile_size; glBegin(GL_QUADS); glTexCoord2f(0.0f,1.0f); glVertex2f(pixel_x,pixel_y); glTexCoord2f(1.0f,1.0f); glVertex2f(pixel_x+tile_size,pixel_y); glTexCoord2f(1.0f,0.0f); glVertex2f(pixel_x+tile_size,pixel_y+tile_size); glTexCoord2f(0.0f,0.0f); glVertex2f(pixel_x,pixel_y+tile_size); glEnd(); } } }
We loop through the tiles to be displayed. The texture will be bind at the position related to the current g_XPos and g_Ypos. The pixel position of the tile will be defined by multiply the current position of tile by the tile size. Finally an imaginary triangle will be drawn and the texture will be mapped to it.
Navigating through the map
If the cursor hits the top of the map, it will be explored form the top until the top of the map is reached. The same procedure is applied to the right, left, and the bottom of the map. When the cursor hits the right of the map g_XPos will be increased, when it hits the left of the map g_XPos will be decreased, when it hits the top of the map, g_YPos will be decreased, when it hits the bottom of the map, g_YPos will be increased, we just need to make sure that g_XPos and g_YPos are within the boundary of the map. The following code does the above function
void MouseMotion(int x,int y) { int mouse_tile_XPos=x/tile_size; int mouse_tile_YPos=y/tile_size; if(mouse_tile_XPos == g_tile_wide-1) { g_XPos++; if(g_XPos > =g_map_width-g_tile_wide) g_XPos=g_map_width-g_tile_wide; } else if(mouse_tile_XPos ==0) { g_XPos--; if(g_XPos < 0) g_XPos=0; } else if(mouse_tile_YPos == g_tile_high-1) { g_YPos++; if(g_YPos > = g_map_height-g_tile_high) g_YPos=g_map_height-g_tile_high; } else if(mouse_tile_YPos == 0) { g_YPos--; if(g_YPos < 0) g_YPos=0; } }
The code takes the position of the mouse cursor and maps it to the tile scale. The code increases/decreased g_XPos, g_YPos, and then it makes sure that they are within the boundary of the displaying window.
Rendering the Minimap
The minimap is rendered in the strategy games to show the current position of the player on the map. Two ways to render the minimap
- Resizing each tile to a very small size, and displaying the minimap with the same concept shown in displaying the entire map.
- Use proxy code, this is done by assigning a color for each tile, for example green dots means a grass tile, black dots means a rock tile, and so on
In this tutorial, the first approach will be used. A rectangle showing the current position of the player will be drawn to show the current position of the player on the map.
The following function displays the minimap
void RenderMiniMap() { int y_offset=650; int dot_size=8; for(int y=y_offset;y < y_offset+g_map_height;y++) { for(int x=0;x < g_map_width;x++) { int pixel_x=x*dot_size; int pixel_y= y_offset + (y-y_offset)*dot_size; glBindTexture(GL_TEXTURE_2D, map_tiles[x+(y-y_offset)*g_map_width]); glBegin(GL_QUADS); glTexCoord2f(0.0f,1.0f); glVertex2f(pixel_x,pixel_y); glTexCoord2f(1.0f,1.0f); glVertex2f(pixel_x+dot_size,pixel_y); glTexCoord2f(1.0f,0.0f); glVertex2f(pixel_x+dot_size,pixel_y+dot_size); glTexCoord2f(0.0f,0.0f); glVertex2f(pixel_x,pixel_y+dot_size); glEnd(); } } //render the small rectangle to show your pos on the map int rect_pixel_x=g_XPos*dot_size; int rect_pixel_y=y_offset+g_YPos*dot_size; int rect_pixel_width=g_tile_wide*dot_size; int rect_pixel_height=g_tile_high*dot_size; glPolygonMode (GL_FRONT_AND_BACK, GL_LINE); glBegin(GL_QUADS); glVertex2f(rect_pixel_x,rect_pixel_y); glVertex2f(rect_pixel_x+rect_pixel_width,rect_pixel_y); glVertex2f(rect_pixel_x+rect_pixel_width,rect_pixel_y+rect_pixel_height); glVertex2f(rect_pixel_x,rect_pixel_y+rect_pixel_height); glEnd(); }
Two new variables are used, y_offset is the y position of the map. X offset will be zero, so the map position will be to the bottom-left of the map.
The dot_size=8 is the size of the tile after resizing. This means every tile is resized from 64 pixels to 8.
We loop through the tiles as explained before, and draw the tiles in the specified x, y positions.
Finally a rectangle will be drawn to show the current position of the player in the game. The final result of our program should be as shown in Fig. 4
Source Code
To download a sample code of the above tutorial click here. To download the map slices click here.