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.