Last Updated 2/26/06
Tiling is a common method to create flexible level engines. Here you'll learn to load a tile map to place the tiles and how to interact with and show tiles.
Here we're going to use the 12 tiles from this sprite sheet:
(tiles are 50% actual size)
To create this level:
(level is also 50% actual size)
Notice that one benefit of using tiles is that we save RAM. Instead of using a 1280 x 960 image, we only use a 320 x 240 image. This saves 4 1/2 megabytes of RAM.
//Tile variables const int TILETYPE_FLOOR = 1; const int TILETYPE_WALL = 2; const int TILE_WIDTH = 80; const int TILE_HEIGHT = 80; const int TOTAL_TILES = 192; const int TILE_SPRITES = 12; //The different tile sprites const int TILE_RED = 0; const int TILE_GREEN = 1; const int TILE_BLUE = 2; const int TILE_CENTER = 3; const int TILE_TOP = 4; const int TILE_TOPRIGHT = 5; const int TILE_RIGHT = 6; const int TILE_BOTTOMRIGHT = 7; const int TILE_BOTTOM = 8; const int TILE_BOTTOMLEFT = 9; const int TILE_LEFT = 10; const int TILE_TOPLEFT = 11;
Here's two sets of global constants having to do with the tiles.
The first set constants are things like the tile types we use (which I'll talk about later), tile dimensions, how many tiles we're going to use and how many kinds of tiles we have.
The second set is just symbolic constants for all the different tiles we have.
//The tile class Tile { private: //The attributes of the tile SDL_Rect box; //The tile type int type; //The tile image SDL_Rect *image; public: //Initializes the variables Tile( int x, int y, int TYPE, SDL_Rect *img ); //Shows the tile void show(); //Get the tile type int get_type(); //Get the collision box SDL_Rect &get_box(); };
Here's the break down of the tile class.
"box" is the offsets and dimensions of the tile and it also functions as a collision box. "type" is the obviously what type of tile the tile is. "image" points to the clipping rectange of the tile's sprite from the tile sheet.
Then we have the constuctor which sets the tile's offsets, type and tile image. Then the show() function shows the tile on the screen. Lastly we have get_type() and get_box() which simply retrieve the tile's type and collision box.
//The dot class Dot { private: //The dot's collision box SDL_Rect box; //The velocity of the dot int xVel, yVel; public: //Initializes the variables Dot(); //Takes key presses and adjusts the dot's velocity void handle_input(); //Moves the dot void move( Tile *tiles[] ); //Shows the dot on the screen void show(); //Sets the camera over the dot void set_camera(); };
Here's our friend the dot class. It's essentially the same thing as it was in the scrolling tutorial, only now the move() function takes in tiles so we can interact with them.
Tile::Tile( int x, int y, int TYPE, SDL_Rect *img ) { //Get the offsets box.x = x; box.y = y; //Get the tile type type = TYPE; //Get the image image = img; //Set the collision box box.w = img->w; box.h = img->h; }
Here's the Tile contructor. Nothing much to explain here, it just sets the tile at the given offsets, gets the tile type, gets the tile image, and sets the dimensions.
void Tile::show() { //If the tile is on screen if( check_collision( camera, box ) == true ) { //Show the tile apply_surface( box.x - camera.x, box.y - camera.y, tileSheet, screen, image ); } }
Now it's time to show the tile on the screen.
First we check if the tile is on the screen. So if the red rectangle is the camera:
Only the lighted tiles will get shown. It makes sense since why would you apply a surface if it won't be seen?
When we apply the tile image, we show the tile from the tile sheet relative to the camera so the tiles will scroll.
int Tile::get_type() { return type; } SDL_Rect &Tile::get_box() { return box; }
These functions right here simply retrieve the tile's type and collision box.
bool set_tiles( Tile *tiles[] ) { //The tile offset int x = 0, y = 0; //Open the map std::ifstream map( "lazy.map" ); //If the map couldn't be loaded if( map == NULL ) { return false; }
Here's our function to set the tiles.
At the top we have two offset variables. They're used to keep track of where to place the tiles. Then we open up our map file.
//Initialize the tiles for( int t = 0; t < TOTAL_TILES; t++ ) { //Determines what kind of tile will be made int tile = -1; //Read tile from map file map >> tile; //If the was a problem in reading the map if( map.fail() == true ) { return false; }
Now it's time to go through and set the tiles.
First we read an integer from the file.
Here's the contents of the "lazy.map" file:
00 01 02 00 01 02 00 01 02 00 01 02 00 01 02 00
01 02 00 01 02 00 01 02 00 01 02 00 01 02 00 01
02 00 11 04 04 04 04 04 04 04 04 04 04 05 01 02
00 01 10 03 03 03 03 03 03 03 03 03 03 06 02 00
01 02 10 03 08 08 08 08 08 08 08 03 03 06 00 01
02 00 10 06 00 01 02 00 01 02 00 10 03 06 01 02
00 01 10 06 01 11 05 01 02 00 01 10 03 06 02 00
01 02 10 06 02 09 07 02 00 01 02 10 03 06 00 01
02 00 10 06 00 01 02 00 01 02 00 10 03 06 01 02
00 01 10 03 04 04 04 05 02 00 01 09 08 07 02 00
01 02 09 08 08 08 08 07 00 01 02 00 01 02 00 01
02 00 01 02 00 01 02 00 01 02 00 01 02 00 01 02
It's just a bunch of numbers. Each number corresponds with a type of tile. It's a simple, maybe even crude way to save a tile map, but it works.
//Create tile if( tile == TILE_RED ) { tiles[ t ] = new Tile( x, y, TILETYPE_FLOOR, &clips[ TILE_RED ] ); } else if( tile == TILE_GREEN ) { tiles[ t ] = new Tile( x, y, TILETYPE_FLOOR, &clips[ TILE_GREEN ] ); } else if( tile == TILE_BLUE ) { tiles[ t ] = new Tile( x, y, TILETYPE_FLOOR, &clips[ TILE_BLUE ] ); }
Now that we read the number from the file, we set the proper tile.
These 3 kinds of tiles are floor tiles, which basically means the dot can move over them.
else if( tile == TILE_CENTER ) { tiles[ t ] = new Tile( x, y, TILETYPE_WALL, &clips[ TILE_CENTER ] ); } else if( tile == TILE_TOP ) { tiles[ t ] = new Tile( x, y, TILETYPE_WALL, &clips[ TILE_TOP ] ); } else if( tile == TILE_TOPRIGHT ) { tiles[ t ] = new Tile( x, y, TILETYPE_WALL, &clips[ TILE_TOPRIGHT ] ); } else if( tile == TILE_RIGHT ) { tiles[ t ] = new Tile( x, y, TILETYPE_WALL, &clips[ TILE_RIGHT ] ); } else if( tile == TILE_BOTTOMRIGHT ) { tiles[ t ] = new Tile( x, y, TILETYPE_WALL, &clips[ TILE_BOTTOMRIGHT ] ); } else if( tile == TILE_BOTTOM ) { tiles[ t ] = new Tile( x, y, TILETYPE_WALL, &clips[ TILE_BOTTOM ] ); } else if( tile == TILE_BOTTOMLEFT ) { tiles[ t ] = new Tile( x, y, TILETYPE_WALL, &clips[ TILE_BOTTOMLEFT ] ); } else if( tile == TILE_LEFT ) { tiles[ t ] = new Tile( x, y, TILETYPE_WALL, &clips[ TILE_LEFT ] ); } else if( tile == TILE_TOPLEFT ) { tiles[ t ] = new Tile( x, y, TILETYPE_WALL, &clips[ TILE_TOPLEFT ] ); } //If we don't recognize the tile type else { return false; }
Then these types of tiles are wall tiles which means the dot can't go through them.
If the number read in from the file doesn't match one of our kind of tiles we return false.
//Move to next tile spot x += TILE_WIDTH; //If we've gone too far if( x >= LEVEL_WIDTH ) { //Move back x = 0; //Move to the next row y += TILE_HEIGHT; } }
At the end of our tile placement loop we move over to the next tile spot.
//Close the file map.close(); //If the map was loaded fine return true; }
When we're done with a file, we have to remember to close it by calling the close() function.
bool touches_wall( SDL_Rect &box, Tile *tiles[] ) { //Go through the tiles for( int t = 0; t < TOTAL_TILES; t++ ) { //If the tile is a wall if( tiles[ t ]->get_type() == TILETYPE_WALL ) { //If the collision box touches the wall tile if( check_collision( box, tiles[ t ]->get_box() ) == true ) { return true; } } } //If no wall tiles were touched return false; }
touches_wall() goes through the set of tiles and returns true if it finds a wall tile that collides with the given collision box.
void Dot::move( Tile *tiles[] ) { //Move the dot left or right box.x += xVel; //If the dot went too far to the left or right or touched a wall if( ( box.x < 0 ) || ( box.x + DOT_WIDTH > LEVEL_WIDTH ) || touches_wall( box, tiles ) ) { //move back box.x -= xVel; } //Move the dot up or down box.y += yVel; //If the dot went too far up or down or touched a wall if( ( box.y < 0 ) || ( box.y + DOT_HEIGHT > LEVEL_HEIGHT ) || touches_wall( box, tiles ) ) { //move back box.y -= yVel; } }
The only change we made to the Dot class is that we check if the dot collides with any wall tiles when we move it.
//Quit flag bool quit = false; //The dot Dot myDot; //The tiles that will be used Tile *tiles[ TOTAL_TILES ]; //The frame rate regulator Timer fps; //Initialize if( init() == false ) { return 1; } //Load the files if( load_files() == false ) { return 1; } //Clip the tile sheet clip_tiles(); //Set the tiles if( set_tiles( tiles ) == false ) { return 1; }
Here's the top of our main() function.
Near the top we make an array of pointers to Tiles. Then we intialize and load image files, and we set the clip rectangles for the tile sheet with clip_tiles(). After that we the create and set our tiles with set_tiles().
//While the user hasn't quit while( quit == false ) { //Start the frame timer fps.start(); //While there's events to handle while( SDL_PollEvent( &event ) ) { //Handle events for the dot myDot.handle_input(); //If the user has Xed out the window if( event.type == SDL_QUIT ) { //Quit the program quit = true; } } //Move the dot myDot.move( tiles ); //Set the camera myDot.set_camera(); //Show the tiles for( int t = 0; t < TOTAL_TILES; t++ ) { tiles[ t ]->show(); } //Show the dot on the screen myDot.show(); //Update the screen if( SDL_Flip( screen ) == -1 ) { return 1; } //Cap the frame rate while( fps.get_ticks() < 1000 / FRAMES_PER_SECOND ) { //wait } }
Here's our main loop. Nothing much to explain here, just wanted to show everything in action.
void clean_up( Tile *tiles[] ) { //Free the surfaces SDL_FreeSurface( dot ); SDL_FreeSurface( tileSheet ); //Free the tiles for( int t = 0; t < TOTAL_TILES; t++ ) { delete tiles[ t ]; } //Quit SDL SDL_Quit(); }
Since we dynamically allocated our Tile objects, we have to remember to free them in our clean_up() function.