Skip to content

Latest commit

 

History

History
580 lines (456 loc) · 16.8 KB

README.md

File metadata and controls

580 lines (456 loc) · 16.8 KB

[<-back]

Tiling

Tiling is a way of making levels out of uniformly sized reusable pieces. In this tutorial we'll be making a 1280x960 sized level of out only a 160x120 sized tile set.

preview


Say if we want to make a level like this:

level

We could make one huge level or we could create a tile set of 12 pieces:

tiles

And then create a level out of those pieces allowing us to save memory and save time by reusing pieces. This is why back in the early days of gaming tiling engines were so popular on low resource systems and are still used today in some games.


In our previous tutorials we did our file reading and writing with SDL RWOps. Here we'll be using fstream which is part of the standard C++ library and is relatively easy to use with text files.

//  Using SDL, SDL_image, standard IO, strings, and file streams
#include <SDL.h>
#include <SDL_image.h>
#include <stdio.h>
#include <string>
#include <fstream>

Here we're defining some constants. We'll be using scrolling so we have constants for both the screen and the level. We'll also have constants to define the tiles and the tile types.

//  Screen dimension constants
const int SCREEN_WIDTH      = 640;
const int SCREEN_HEIGHT     = 480;

//  The dimensions of the level
const int LEVEL_WIDTH       = 1280;
const int LEVEL_HEIGHT      = 960;

//  Tile constants
const int TILE_WIDTH        = 80;
const int TILE_HEIGHT       = 80;
const int TOTAL_TILES       = 192;
const int TOTAL_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 is our tile class with a constructor that defines position and type, a renderer that uses a camera, and some accessors to get the tile's type and collision box. In terms of data members we have a collision box and type indicator.

Normally it's a good idea to have position and collider separate when doing collision detection, but for the sake of simplicity we're using the collider to hold position.

//  The tile
class Tile
{
    public:
        //  Initializes position and type
        Tile( int x, int y, int tileType );

        //  Shows the tile
        void render( SDL_Rect& camera );

        //  Get the tile type
        int getType();

        //  Get the collision box
        SDL_Rect getBox();

    private:
        //  The attributes of the tile
        SDL_Rect mBox;

        //  The tile type
        int mType;
};

Here is the dot class yet again, now with the ability to check for collision against the tiles when moving.

//The dot that will move around on the screen
class Dot
{
    public:
        //  The dimensions of the dot
        static const int DOT_WIDTH  = 20;
        static const int DOT_HEIGHT = 20;

        //  Maximum axis velocity of the dot
        static const int DOT_VEL    = 10;

        //  Initializes the variables
        Dot();

        //  Takes key presses and adjusts the dot's velocity
        void handleEvent( SDL_Event& e );

        //  Moves the dot and check collision against tiles
        void move( Tile *tiles[] );

        //  Centers the camera over the dot
        void setCamera( SDL_Rect& camera );

        //  Shows the dot on the screen
        void render( SDL_Rect& camera );

    private:
        //  Collision box of the dot
        SDL_Rect mBox;

        //  The velocity of the dot
        int mVelX, mVelY;
};

Our media loading function will also be initializing tiles so it needs to take them in as an argument.

We also have the touchesWall function that checks a collision box against every wall in a tile set which will be used when we need to check the dot against the whole tile set. Finally the setTiles function loads and sets the tiles.

//  Starts up SDL and creates window
bool init();

//  Loads media
bool loadMedia  ( Tile* tiles[] );

//  Frees media and shuts down SDL
void close      ( Tile* tiles[] );

//  Box collision detector
bool checkCollision( SDL_Rect a, SDL_Rect b );

//  Checks collision box against set of tiles
bool touchesWall( SDL_Rect box, Tile* tiles[] );

//  Sets tiles from tile map
bool setTiles( Tile *tiles[] );

The tile constructor initializes position, dimensions, and type.

Tile::Tile( int x, int y, int tileType )
{
    //  Get the offsets
    mBox.x = x;
    mBox.y = y;

    //  Set the collision box
    mBox.w = TILE_WIDTH;
    mBox.h = TILE_HEIGHT;

    //  Get the tile type
    mType = tileType;
}

Rendering

When we render we only want to show tiles that are in the camera's sight:

shown

So we check if the tile collides with the camera before rendering it. Notice also that we render the tile relative to the camera.

void Tile::render( SDL_Rect& camera )
{
    //  If the tile is on screen
    if  ( checkCollision( camera, mBox ) )
    {
        //  Show the tile
        gTileTexture.render(
            mBox.x - camera.x   ,
            mBox.y - camera.y   ,
            &gTileClips[ mType ]
        );
    }
}

And here are the accessors to get the tile's type and collision box.

int Tile::getType()
{
    return mType;
}

SDL_Rect Tile::getBox()
{
    return mBox;
}

When we move the dot we check if it goes off the level or hits a wall tile. If it does we correct it.

void Dot::move( Tile *tiles[] )
{
    //  Move the dot left or right
    mBox.x += mVelX;

    //  If the dot went too far to the left or right or touched a wall
    if  ( ( mBox.x < 0 ) || ( mBox.x + DOT_WIDTH > LEVEL_WIDTH ) || touchesWall( mBox, tiles ) )
    {
        //  move back
        mBox.x -= mVelX;
    }

    //  Move the dot up or down
    mBox.y += mVelY;

    //  If the dot went too far up or down or touched a wall
    if  (
            ( mBox.y < 0 )                          ||
            ( mBox.y + DOT_HEIGHT > LEVEL_HEIGHT )  ||
            touchesWall( mBox, tiles )
        )
    {
        //  move back
        mBox.y -= mVelY;
    }
}

Here is the rendering code largely lifted from the scrolling/camera tutorial.

void Dot::setCamera( SDL_Rect& camera )
{
    //  Center the camera over the dot
    camera.x = ( mBox.x + DOT_WIDTH  / 2 ) - SCREEN_WIDTH  / 2;
    camera.y = ( mBox.y + DOT_HEIGHT / 2 ) - SCREEN_HEIGHT / 2;

    //  Keep the camera in bounds
    if  ( camera.x < 0 )
    { 
        camera.x = 0;
    }
    if  ( camera.y < 0 )
    {
        camera.y = 0;
    }
    if  ( camera.x > LEVEL_WIDTH - camera.w )
    {
        camera.x = LEVEL_WIDTH - camera.w;
    }
    if  ( camera.y > LEVEL_HEIGHT - camera.h )
    {
        camera.y = LEVEL_HEIGHT - camera.h;
    }
}

void Dot::render( SDL_Rect& camera )
{
    //  Show the dot
    gDotTexture.render( mBox.x - camera.x, mBox.y - camera.y );
}

In our loading function we not only load the textures but also the tile set.

bool loadMedia( Tile* tiles[] )
{
    //  Loading success flag
    bool success = true;

    //  Load dot texture
    if  ( !gDotTexture.loadFromFile( "39_tiling/dot.bmp" ) )
    {
        printf( "Failed to load dot texture!\n" );
        success = false;
    }

    //  Load tile texture
    if  ( !gTileTexture.loadFromFile( "39_tiling/tiles.png" ) )
    {
        printf( "Failed to load tile set texture!\n" );
        success = false;
    }

    //  Load tile map
    if  ( !setTiles( tiles ) )
    {
        printf( "Failed to load tile set!\n" );
        success = false;
    }

    return success;
}

Near the top of the setTiles function we declare x/y offsets that define where we'll be placing the tiles. As we load in more tiles we'll be shifting the x/y position left to right and top to bottom.

We then open the lazy.map file which is just a text file with the following contents:

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

Using fstream we can read text from a file much like we would read keyboard input with iostream. Before we can continue we have to check if the map loaded correctly. If it failed we abort and if not we continue loading the file.

bool setTiles( Tile* tiles[] )
{
    //  Success flag
    bool tilesLoaded = true;

    //  The tile offsets
    int x = 0, y = 0;

    //  Open the map
    std::ifstream map( "./lazy.map" );

    //  If the map couldn't be loaded
    if  ( map.fail() )
    {
        printf( "Unable to load map file!\n" );
        tilesLoaded = false;
    }

If the file loaded successfully we have a for loop that reads in all the numbers from the text file. We read a number into the tileType variable and then check if the read failed. If the read failed, we abort.

If not we then check if the tile type number is valid. If it is valid we create a new tile of the given type, if not we print an error and stop loading tiles.

    else
    {
        //  Initialize the tiles
        for ( int i = 0; i < TOTAL_TILES; ++i )
        {
            //  Determines what kind of tile will be made
            int tileType = -1;

            //  Read tile from map file
            map >> tileType;

            //  If the was a problem in reading the map
            if  ( map.fail() )
            {
                //  Stop loading map
                printf( "Error loading map: Unexpected end of file!\n" );
                tilesLoaded = false;
                break;
            }

            //  If the number is a valid tile number
            if  (
                    ( tileType >= 0 )                   &&
                    ( tileType < TOTAL_TILE_SPRITES )
                )
            {
                tiles[ i ] = new Tile( x, y, tileType );
            }
            //  If we don't recognize the tile type
            else
            {
                //  Stop loading map
                printf( "Error loading map: Invalid tile type at %d!\n", i );
                tilesLoaded = false;
                break;
            }

After loading a tile we move to the text tile position to the right. If we reached the end of a line of tiles, we move down to the next row.

            //  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;
            }
        }

After all the tiles are loaded we set the clip rectangles for the tile sprites. Finally we close the map file and return.

        //  Clip the sprite sheet
        if  ( tilesLoaded )
        {
            gTileClips[ TILE_RED        ].x = 0;
            gTileClips[ TILE_RED        ].y = 0;
            gTileClips[ TILE_RED        ].w = TILE_WIDTH;
            gTileClips[ TILE_RED        ].h = TILE_HEIGHT;

            gTileClips[ TILE_GREEN      ].x = 0;
            gTileClips[ TILE_GREEN      ].y = 80;
            gTileClips[ TILE_GREEN      ].w = TILE_WIDTH;
            gTileClips[ TILE_GREEN      ].h = TILE_HEIGHT;

            gTileClips[ TILE_BLUE       ].x = 0;
            gTileClips[ TILE_BLUE       ].y = 160;
            gTileClips[ TILE_BLUE       ].w = TILE_WIDTH;
            gTileClips[ TILE_BLUE       ].h = TILE_HEIGHT;

            gTileClips[ TILE_TOPLEFT    ].x = 80;
            gTileClips[ TILE_TOPLEFT    ].y = 0;
            gTileClips[ TILE_TOPLEFT    ].w = TILE_WIDTH;
            gTileClips[ TILE_TOPLEFT    ].h = TILE_HEIGHT;

            gTileClips[ TILE_LEFT       ].x = 80;
            gTileClips[ TILE_LEFT       ].y = 80;
            gTileClips[ TILE_LEFT       ].w = TILE_WIDTH;
            gTileClips[ TILE_LEFT       ].h = TILE_HEIGHT;

            gTileClips[ TILE_BOTTOMLEFT ].x = 80;
            gTileClips[ TILE_BOTTOMLEFT ].y = 160;
            gTileClips[ TILE_BOTTOMLEFT ].w = TILE_WIDTH;
            gTileClips[ TILE_BOTTOMLEFT ].h = TILE_HEIGHT;

            gTileClips[ TILE_TOP        ].x = 160;
            gTileClips[ TILE_TOP        ].y = 0;
            gTileClips[ TILE_TOP        ].w = TILE_WIDTH;
            gTileClips[ TILE_TOP        ].h = TILE_HEIGHT;

            gTileClips[ TILE_CENTER     ].x = 160;
            gTileClips[ TILE_CENTER     ].y = 80;
            gTileClips[ TILE_CENTER     ].w = TILE_WIDTH;
            gTileClips[ TILE_CENTER     ].h = TILE_HEIGHT;

            gTileClips[ TILE_BOTTOM     ].x = 160;
            gTileClips[ TILE_BOTTOM     ].y = 160;
            gTileClips[ TILE_BOTTOM     ].w = TILE_WIDTH;
            gTileClips[ TILE_BOTTOM     ].h = TILE_HEIGHT;

            gTileClips[ TILE_TOPRIGHT   ].x = 240;
            gTileClips[ TILE_TOPRIGHT   ].y = 0;
            gTileClips[ TILE_TOPRIGHT   ].w = TILE_WIDTH;
            gTileClips[ TILE_TOPRIGHT   ].h = TILE_HEIGHT;

            gTileClips[ TILE_RIGHT      ].x = 240;
            gTileClips[ TILE_RIGHT      ].y = 80;
            gTileClips[ TILE_RIGHT      ].w = TILE_WIDTH;
            gTileClips[ TILE_RIGHT      ].h = TILE_HEIGHT;

            gTileClips[ TILE_BOTTOMRIGHT ].x = 240;
            gTileClips[ TILE_BOTTOMRIGHT ].y = 160;
            gTileClips[ TILE_BOTTOMRIGHT ].w = TILE_WIDTH;
            gTileClips[ TILE_BOTTOMRIGHT ].h = TILE_HEIGHT;
        }
    }

    //  Close the file
    map.close();

    //  If the map was loaded fine
    return tilesLoaded;
}

The touchesWall function checks a given collision box against tiles of type TILE_CENTER, TILE_TOP, TILE_TOPRIGHT, TILE_RIGHT, TILE_BOTTOMRIGHT, TILE_BOTTOM, TILE_BOTTOMLEFT, TILE_LEFT, and TILE_TOPLEFT which are all wall tiles. If you check back when we defined these constants, you'll see that these are numbered right next to each other so all we have to do is check if the type is between TILE_CENTER and TILE_TOPLEFT.

If the given collision box collides with any tile that is a wall this function returns true,

bool touchesWall( SDL_Rect box, Tile* tiles[] )
{
    //  Go through the tiles
    for ( int i = 0; i < TOTAL_TILES; ++i )
    {
        //  If the tile is a wall type tile
        if  (
                ( tiles[ i ]->getType() >= TILE_CENTER  )   &&
                ( tiles[ i ]->getType() <= TILE_TOPLEFT )
            )
        {
            //  If the collision box touches the wall tile
            if  ( checkCollision( box, tiles[ i ]->getBox() ) )
            {
                return true;
            }
        }
    }

    //  If no wall tiles were touched
    return false;
}

In the main function right before we load the media we declare our array of tile pointers.

        //  The level tiles
        Tile* tileSet[ TOTAL_TILES ];

        //  Load media
        if  ( !loadMedia( tileSet ) )
        {
            printf( "Failed to load media!\n" );
        }

Our main loop is pretty much the same with some adjustments. When we move the dot we pass in the tile set and then set the camera over the dot after it moved. We then render the tile set and finally render the dot over the level.

            //  While application is running
            while   ( !quit )
            {
                //  Handle events on queue
                while   ( SDL_PollEvent( &e ) != 0 )
                {
                    //  User requests quit
                    if  ( e.type == SDL_QUIT )
                    {
                        quit = true;
                    }

                    //  Handle input for the dot
                    dot.handleEvent( e );
                }

                //  Move the dot
                dot.move( tileSet );
                dot.setCamera( camera );

                //  Clear screen
                SDL_SetRenderDrawColor  ( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
                SDL_RenderClear         ( gRenderer );

                //  Render level
                for ( int i = 0; i < TOTAL_TILES; ++i )
                {
                    tileSet[ i ]->render( camera );
                }

                //  Render dot
                dot.render( camera );

                //  Update screen
                SDL_RenderPresent( gRenderer );
            }

[<-back]