The Allegro Wiki is migrating to github at https://github.com/liballeg/allegro_wiki/wiki

Allegro tile maps

From Allegro Wiki
Revision as of 17:55, July 15, 2007 by ReyBrujo (talk | contribs) (AllegroTileMaps moved to Allegro tile maps: Twiki -> MediaWiki title format)
Jump to: navigation, search

Tile maps: Storing, Drawing, Saving and Loading


Introduction

This article talks mostly about drawing tile maps but I will talk a little about loading and saving too.

Some of the examples are taken from a RPG game I am working on with some friends. It uses the allegro library for drawing, but some of this stuff should apply to tile maps in general.


Data Structures

To store your maps in memory you need a nice data structure. Bad ones will be harder to change or expand upon later.

A tile map is basically a bitmap with tiles as its most basic unit instead of pixels therefore I like to use a tile map data structure originally based loosy on the allegro bitmap data structure, because I hate how most tile map article have the width and height of their maps set in stone.

here is a basic single layer map:

<highlightSyntax language="cpp">struct tile_map {

   struct tile *data; // an array of tiles sized w*h
   int w,h; // width and height
   int tile_spacing_w,tile_spacing_h; 
   // this is how far apart each tile is drawn. (when we started our
   // game we used 16x16 most of time but later we decided to
   // change over to 32x32, having these variable made it very easy to
   // adjust the maps we had already done.)

};

</highlightSyntax>

The tile struct should hold information on how to draw each tile, but what you put in the tile struct really depends on the features you want in your tile map system.

In our game we have:

<highlightSyntax language="cpp">struct tile

{
   BITMAP *pict;     // pointer to the bitmap of the tile
   int flags;             // bit flags controlling special tile effects
   int trans;             // how translucent is this tile
//also has some animation information and other drawing stuff
};
</highlightSyntax>

However while a map with the above two data structures is okay.. it is not very flexible and you will end up with quite boring maps.

so the next step is to have multiple layers. A good tile map is like an onion it has layer over layer over layer... this gives the map depth and allows you to create better maps.

for example: the bottom layer could hold the basic floor tiles, and next walls/cliffs, then have a layer for a pond of translucent water that lets you see the rocks and mud bellow it and finally create a layer on top for trees and rocks.

How many total layers should a map have? I don't believe there should be a set number. Different map require a different number of layers. So here is the changed data structure:

<highlightSyntax language="cpp">struct map

{
   struct layer *data;
   int total_layers;
};
struct layer
 {
   int x,y; // where to start drawing this layer (this should be relative to the map's x and y)
   struct tile *data;
   int w,h; // in our game different layers can have different sizes and different tile spacing.
   int tile_spacing_w,tile_spacing_h;
};
struct tile
{
   BITMAP *pict;
   int flags;
   int trans;
};
</highlightSyntax>

In our game we used classes instead of structs but only difference is our drawing code and load/save functions where inside the classes instead of outside if we had used structs.


Drawing tile maps

Now the main thing that everyone wants when drawing a tile map is to be able to scroll it pixel by pixel. I have seen four different methods of doing this:

  1. . Total redraw - each frame draw the whole map and add a x and y to each tile's location then change the x and y to scroll the map.
  2. . Hardware scrolling - draw sections of the map to video memory and use hardware to scroll the screen around... redrawing only when the player reaches a border..
  3. . Buffered scrolling - draw the map to a buffer larger than the screen as the player moves around copy from the buffer to the screen..redrawing only the edges of the buffer.
  4. . Partial redraw - each frame draw the only section of the map the player is over.
Total redraw

This is the easiest to do and understand.. also the slowest (even more so if you have animation and other special effects like translucency)

<highlightSyntax language="cpp"> for(l=0;l< total layers in map; l++)

 for(j=0;j<current layer.h;j++)
  for(i=0;i<current layer.w; i++)
   {
    draw the tile at (scroll_x +layer.x+layer.tile_spacing_w*i), (scroll_y +layer.y+layer.tile_spacing_h*j)
   }
  }
 }
}
</highlightSyntax>

and this will work

One other thing because the map must be redraw each frame you will need to use either page flipping or a double buffer system, because dirty rectangles just will not work for your game sprites.

Hardware scrolling

This is most likely the fastest... but I don't like it. Being hardware dependent, it works on some machines and not others. It also can't really handle tile map animation very well.

in the dos it requires that you that use either Mode X in dos or some machines can do it with vesa too but vesa scrolling is not a good as it requires you to scroll 4 pixels at a time when scrolling horizontally..

with allegro in dos you can do this:

<highlightSyntax language="cpp">set_gfx_mode(GFX_MODEX,320,200,640,400); // we request a virtual screen twice that of the screen

draw to the screen using VIRTUAL_W and VIRTUAL_H instead of SCREEN_W and SCREEN_H for size checking.

then use scroll_screen(x,y); to move around inside the 640x400 area but! you will need to redraw when the player try to walk off the edge of the 640x480 area.

the one nice thing about this method is it works well with dirty rectangles for your sprites.

Buffered scrolling

This is basically a software version of the above method.

If you do use the above method I would suggest you add this one too for machines and/or operating systems that don't support Mode X or vesa.

basically if you're screen size is 320x200 then create a bitmap the size of 640x480 <highlightSyntax language="cpp">BITMAP *buffer=create_bitmap(640,480); </highlightSyntax> then draw your map to the buffer

and then instead of scroll_screen(x,y) do this <highlightSyntax language="cpp">blit(buffer,screen,x,y,0,0,SCREEN_W,SCREEN_H); </highlightSyntax>

just like in the hardware method you need to redraw if the player walks off the edge of the map

this could be done with dirty rectangles if you drew the sprites on the buffer then just did one blit to the screen each frame... but I doubt it would gain you much speed, as you are already using a buffer.

Partial redraw

This is my current favorite. Like the total redraw method you redraw the screen each frame... but you just redraw the part of the map that is visible on the screen.

its not that much faster for simple one layer map, but the speed comes when you have complex maps with multi-layers and special tile effects that can use a lot of the cpu time.

to do this you need three things for each layer: the x and y of where to draw the first visible tile on the map. the x and y of the first visible tile in the tile map the x and y of the last visible tile in the tile map

take the x,y of the first tile (eg the top left corner of the map)

for example -100,-132 invert it: -(-100)=100 -(-132)=132 divide by the tile_spacing (32 in this example) 100/32=3; 132/32=4; 3,4 is the first visible tile now go back and find the remainder of the first variables (%) (-100)% 32 = -4 (-132)% 32 = -4 so -4,-4 is where to draw to the screen then to get the last tile take 3,4 and add (SCREEN_W-draw_first_tile_here_x)/tile_spacing_w,(SCREEN_H-draw_first_tile_here_y)/tile_spacing_h (but you must round up!!!!!!) (320-(-4))/32+1=11; (200-(-4))/32+1=7; the last tile to draw is 11,7

<highlightSyntax language="cpp"> for(j=first_tile_in_map_to_draw_y, j<last_tile_in_map_to_draw_y;j++)

for(i=first_tile_in_map_to_draw_x,i<last_tile_in_map_to_draw_x;i++)
 {
   draw the tile at i*tile_spacing_w+draw_first_tile_here_x, *tile_spacing_h+draw_first_tile_here_y
 }

}

</highlightSyntax>

We use this method in our game.

to speed up the inner loops of your tile drawing function try and change all * to + if you can

for example before loop create a sx and sy variable <highlightSyntax language="cpp"> sx=first_tile_in_map_to_draw_y*tile_spacing_w+draw_first_tile_here_x; sy=first_tile_in_map_to_draw_y*tile_spacing_h+draw_first_tile_here_h;

for(j=first_tile_in_map_to_draw_y, j<last_tile_in_map_to_draw_y;j++,sy+=tile_spacing_h)

for(i=first_tile_in_map_to_draw_x,temp_sx=sx,i<last_tile_in_map_to_draw_x;i++,temp_sx+=tile_spacing_w) 
{
   draw the tile at temp_sx, sy
}

}

</highlightSyntax>

there is on more thing about drawing tiles and that is some engines (like ours) don't require all the tiles to be of the same size. to handle bigger tiles so they overlap nicely (like big trees for example) you need to make a small change to the draw function and that is to draw them at temp_sx+offset_x, sy+offset_y where offset_x is equal to tile_spacing_w-the_tiles_bitmap->w and the offset_y is equal to the tile_spacing_w-the_tiles_bitmap->h.


Saving and Loading tile maps

there are so many way of doing this that I couldn't even begin to list them..

however I will show you the method we use

we hold all the bitmaps of the tiles for each map in a tileset we store the tile set as a datafile. and the problem was each time with changed a tileset the whole map had to be rebuild from scratch, but then we came up with this method. As we go to save the map, for the bitmap of each tile we find the index of it in the datafile we then save grab the datafile propriety 'name' for the name of the tile and we save this as a string. then if the datafile order get changed it doesn't matter.

As you can imagine this makes map larger than needed on the disk but thankfully file compression takes care of that. if anyone using allegro I strongly suggest that you use the packfile rountines.

The easiest way of saving a tilemap is to draw it to the file (using method 1)

<highlightSyntax language="cpp"> PACKFILE *f

f=pack_fopen(filename,"p");

</highlightSyntax>

first you should write a id string to the top of the file using pack_fwrite so you be able to tell if a file is of your type or not.

the next step is to write the number of layer and any other map variables. We used pack_iputl for this but you can also use pack_mputl it doesn't really matter as long as you stuck to one of them.

then loop through each layer: { f=pack_fopen_chunk(f,FALSE); // this makes sure information from one layer is separate from

                                                  // another

write the w,h and any other layer variables from in the layer here

then loop through each tile in the map (from 0 to w*h) { once again use f=pack_fopen_chunk(f,FALSE); write the name of the tile's bitmap in the datafile in string form by first writing the length of the string with a pack_iputl then the string it self with a pack_fwrite also write the flags and any other tile variables to the file. f=pack_fclose_chunk(f); } f=pack_close_chunk(f); }

Reading the map from the disk is as easy as coping the above function and changing all the writes to reads and the puts to gets...

There is however there is one small problem, and that is what happens to your old maps when you want to make a change to the format?

I would suggest that you save a version number with each map. That way when you change the format you can either not load an old version and return with an error (better than crashing), or like us you can keep all the old versions of your load_map function and call the right one depending on which format the map you are loading in.


Conclusion

There is no perfect map structure, format, drawing function, loading or saving function.

It all depends on the features of your game engine as to what is best to use. Here is the thing, the more general and more flexible a tile map system is, the slower it will be. There is no other way about it. You can't download a tile map engine and expect it to do all the cool stuff at the same speed as one you build yourself.

I hope you found this useful, if I missed anything out or messed anything up, or if you need some help with tile maps, or even if you want to thank me for saving your life by writing this article please email me at whitedoor@gmail.com


Credits

Written by: Nathan Smith (2000)