Tutorial de SDL – Parte 39 – Ladrilhamento

Continuando nossa série de artigos traduzidos do site lazyfoo, agora veremos como posicionar várias imagens lado a lado, como ladrilhos.
Ladrilhamento é uma forma de criar níveis a partir de peças reutilizáveis de tamanho uniforme. Nesse artigo, iremos criar uma nível de tamanho 1280×960 a partir de um ladrilho de tamanho 160×120.

Digamos que queremos criar um nível como esse:

Podemos criar um único nível bem grande ou poderíamos usar um conjunto de 12 peças:

E então  criar um nível a partir desses peças o que permite economizar memória e tempo pela reutilização das peças. É por isso que nos primórdios do desenvolvimento de jogos motores de ladrilhamento eram tão populares em sistema com poucos recursos e ainda são usados hoje em alguns jogos.

//Using SDL, SDL_image, standard IO, strings, and file streams
#include
#include
#include
#include
#include

Nos artigos anteriores, fazíamos leitura e escrita em arquivos com SDL_RWOps. Aqui usaremos fstream, que é parte da bibliotecas padrão do C++ e é relativamente fácil de usar com arquivos de texto.

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

Aqui definimos algumas constantes. Estaremos usando rolagem, então teremos constantes tanto para a tela quanto para o nível. Também teremos constantes para definir os ladrilhos e os tipos de ladrilhos.

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

Aqui temos a nossa classe que define um ladrilho, com uma construtor que define a posição e o tipo, uma renderizador que usa uma câmera, e alguns métodos de acesso para ler o tipo do ladrilho e a caixa de colisão. Em termos de atributos, temos uma caixa de colisão e o indicador do tipo.

Normalmente é uma boa ideia ter a posição e a caixa de colisão separadas ao realizar detecção de colisão, mas por questão de simplicidade usaremos a caixa de colisão para guardar a posição.

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

Aqui temos a classe Dot novamente, agora com a habilidade de checar por colisões entre os ladrilhos quando eles se movem.

//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[] );

Nossa função de carregamento de mídia também irá inicializar os ladrilhos de forma que precisaremos informar eles como argumento.

Também temos uma função touchesWall que verifica a caixa de colisão contra todas as paredes de um conjunto de ladrilhos que será usada quando precisarmos checar um ponto contra todo o conjunto de ladrilhos. Finalmente, a função setTiles carrega e configura os ladrilhos.

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

O construtor inicializa a posição, dimensões e tipo de ladrilho.

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 ] );
    }
}

Quando renderizamos queremos apenas exibir os ladrilhos que estiverem no ponto de vista da câmera:

Assim, verificamos que o ladrilho colide com a câmera antes de renderiza-lo. Observe que também renderizamos o ladrilho relativamente à câmera.

int Tile::getType()
{
    return mType;
}
SDL_Rect Tile::getBox()
{
    return mBox;
}

E aqui temos os métodos de acesso para ler o tipo do ladrilho e a caixa de colisão.

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

Quando movemos o ponto verificamos se estamos saindo do nível ou atingindo a parede de um ladrilho. Caso afirmativo, corrigimos o movimento.

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

Aqui temos o código de renderização largamente copiado do artigo sobre rolagem da câmera.

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

Na nossa função de carregamento, não apenas carregamos as texturas, mas também o conjunto de ladrilhos.

bool setTiles( Tile* tiles[] )
{
    //Success flag
    bool tilesLoaded = true;
    //The tile offsets
    int x = 0, y = 0;
    //Open the map
    std::ifstream map( "39_tiling/lazy.map" );
    //If the map couldn't be loaded
    if( map == NULL )
    {
        printf( "Unable to load map file!\n" );
        tilesLoaded = false;
    }
Próximo ao topo da função setTiles declaramos deslocamentos x/y que definem onde serão posicionados os ladrilhos. A medida que carregamos mais ladrilhos, mudamos a posição x/y da esquerda para a direita e de cima para baixo.
Então abrimos o arquivo lazy.map que é apenas um arquivo de texto com o seguinte conteúdo:
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

Usando fstream podem ler os texto de um arquivo de forma parecida de como lemos a entrada do teclado com iostream. Antes de podermos continuar, temos que checar se o mapa foi carregado corretamente, verificando se não foi retornado o valor NULL. Se afirmativo, abortamos a execução do programa; caso contrário, continuamos o carregamento de arquivo.

Nota: dependendo de qual compilador você estiver usando (como certas versões do Visual Studio), este código pode não compilar. Ao invés de checar se map == NULL, você terá que checar se !map.is_open().

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

Se o arquivo tiver sido carregado com sucesso, temos um loop que lê todos os números de arquivo de texto. Lemos um número na variável tileType e então verificamos se ocorreu um erro na leitura. Se ocorreu, abortamos a execução. Caso contrário, verificamos se o número do tipo é válido. Se for válido, criamos um novo ladrilho do tipo lido, se não for válido exibimos um erros e paramos de carregar ladrilhos.

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

Depois de carregar um ladrilho, movemos para a próxima posição à direita. Se atingirmos o fim da linha de ladrilhos, movemos para baixo na próxima linha.

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

Depois que todos os ladrilhos tiverem sido carregado, configuramos os retângulos de corte para os ladrilhos. Finalmente, fechamos o arquivo do mapa e retornamos.

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

A função touchesWall verifica uma caixa de colisão dada contra ladrilhos do tipo TILE_CENTER, TILE_TOP, TILE_TOPRIGHT, TILE_RIGHT, TILE_BOTTOMRIGHT, TILE_BOTTOM, TILE_BOTTOMLEFT, TILE_LEFT, e TILE_TOPLEFT que são os ladrilhos da parede. Se você olhar lá atrás quando definimos essas constantes, verá que elas foram numeradas com números próximos entre si, de forma que tudo que temos que fazer é verificar se o tipo está entre TILE_CENTER e TILE_TOPLEFT.

Se a caixa de colisão fornecida colide em algum ladrilho que esteja na parede essa função retorna true,

        //The level tiles
        Tile* tileSet[ TOTAL_TILES ];
        //Load media
        if( !loadMedia( tileSet ) )
        {
            printf( "Failed to load media!\n" );
        }

Na função principal logo antes de carregarmos os arquivo de mídia declaramos nosso array de ponteiros para os ladrilhos.

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

Nosso loop principal é basicamente o mesmo com alguns ajustes. Quando movemos Dot passamos pelo conjunto de ladrilhos e ajustamos a câmera sobre Dot. Então renderizamos o conjunto de ladrilhos e finalmente renderizamos Dot sobre o nível.

Baixe os arquivos de mídia e de código fonte desse exemplo aqui.