Conheça a iniciativa da Biblioteca dos Desenvolvedores
Índice da Biblioteca
Área dos Usuários
Fórum de Discussão
30 - Scrolling de Tiles
No tutorial 26 "Utilizando Tiles" já foi explicado conceitos básicos sobre tiles e sua aplicação. Nesse tutorial, estarei mostrando uma técnica chamada SCROLLING de TILES (Não é importante que você tenha lido o tutorial 26 antes de continuar neste tutorial, pois estarei mostrando uma abordagem diferente sobre o assunto). Para imprimir tiles na tela, é usado um mapa para representar a posição de cada tile. Para preenchermos uma tela de 640x480, e usando tiles de 32x32, precisaríamos de 20 tiles na horizontal e 15 na vertical. Com SCROLLING DE TILES, podemos ter o mapa do tamanho que quisermos (não preciso nem dizer que o mapa é representado por uma matriz multidimensional). SCROLING DE TILES é amplamente usado em jogos 2d, como Mario ou Metroid.
Portanto, poderemos ter um mapa de tamanho qualquer, não importa a altura nem a largura, nosso algoritmo deve imprimir os tiles na tela perfeitamente. Você pode estar pensando: "é só imprimir todos os tiles na tela". Nada impede que você faça isso, mas seu jogo ficará extremamente lento. O correto é imprimir somente a área correspondente ao tamanho da tela.
Básico
O tutorial 26 mostrou um jeito fácil de imprimir tiles na tela. Nessa parte básica, vou mostrar outra forma de obter o mesmo resultado, ou seja, imprimir tiles carregando o mapa a partir de um arquivo. Primeiro, precisamos declarar algumas constantes e estruturas básicas:
CÓDIGO...
/* Tamanho do tile */
#define TILEWH 32
typedef struct
{
BITMAP * bitmap;
} SPRITE;
typedef struct
{
SPRITE * sprite;
} TILE;
typedef struct
{
int w;
int h;
TILE ** tiles;
} MAP;
typedef struct
{
int w;
int h;
} MAPINFO;
FIM DE CÓDIGO...
A primeira estrutura, SPRITE, armazenará informações do tile atual, que no nosso caso vai ser somente estático (imagem fixa).
A segunda estrutura, TILE, armazenará informações sobre que tipo de SPRITE iremos imprimir na tela.
A estrutura MAP armazenará o mapa, contendo a posição x e y da cada tile.
MAPINFO é um estrutura adicional e será explicada mais à frente.
Os Tiles
Usarei tiles de 32x32px de tamanho neste tutorial. Portanto, o arquivo Bitmap deve ser múltiplo de 32.
Poderíamos carregar os tiles manualmente, mas um processo automatizado é sempre melhor, então, criaremos uma função para carregar os tiles a partir de um arquivo Bitmap.
A função irá retornar um vetor de SPRITE.
CÓDIGO...
/* Função para carga de tiles */
SPRITE * load_tiles(const char * filename)
{ /* Armazenando o bitmap em uma variável temporária */
BITMAP * tmp = load_bitmap(filename, NULL);
/* Verificando se o bitmap foi carregado */
if (tmp == NULL)
return NULL;
/* Pegando o número de tiles na horizontal e na vertical */
int xtiles = tmp->w / TILEWH;
int ytiles = tmp->h / TILEWH;
/* Faça enquanto houver tiles na horizontal e na vertical */
for (y = 0; y < ytiles; y++)
{
for (x = 0; x < xtiles; x++)
{ /* Criando o BITMAP que vai armazenar o tile */
spr[i].bitmap = create_bitmap(TILEWH, TILEWH);
/* Copiando o pedaço da imagem para o SPRITE */
blit(tmp, spr[i].bitmap, x * TILE, y * TILE, 0, 0, TILE, TILE);
/* Próximo índice */
i++;
}
}
/* Libere a memória alocada para o bitmap */
destroy_bitmap(tmp);
/* Retorne o vetor de SPRITE */
return spr;
}
FIM DE CÓDIGO...
Explicando
No código acima, criamos uma rotina de carga de tiles. Um arquivo Bitmap é requerido.
Para obtermos o número de tiles na horizontal e na vertical, basta pegar a altura e largura do Bitmap e dividir pelo tamanho do tile.
int xtiles = tmp->w / TILEWH;
int ytiles = tmp->h / TILEWH;
Calculamos o total de tiles no bitmap
int total = xtiles * ytiles;
Já temos condição de criar o vetor de SPRITE, que armazenará os tiles:
E o final: carregar os tiles do bitmap "tmp" no vetor.
int x;
int y;
int i = 0;
for (y = 0; y < ytiles; y++)
{
for (x = 0; x < xtiles; x++)
{
spr[i]->bitmap = create_bitmap(TILE, TILE);
blit(tmp, spr[i]->bitmap, x * TILE, y * TILE, 0, 0, TILE, TILE);
i++;
}
}
Primeiro, declaramos uma variável i, que armazenará o índice no vetor de SPRITE (nosso vetor de SPRITE é linear).
Os dois laços de repetição FOR são usados para percorrer o bitmap "tmp" de cima para baixo e da esquerda para a direita.
Para cada vez que o laço y for executado, o laço x será executado N vezes.
Os tiles ficariam organizados nessa ordem no vetor. Para finalizar, criamos realmente o BITMAP da estrutura SPRITE e "blitamos" o tile nele:
spr[i]->bitmap = create_bitmap(TILEWH, TILEWH);
blit(tmp, spr[i]->bitmap, x * TILEWH, y * TILEWH, 0, 0, TILEWH, TILEWH);
Lembrando que a posição do tile é definida pelos parâmetros x e y dos laços.
Como nossos TILES tem 32x32 de tamanho, fica fácil encontrar sua posição no BITMAP tmp, pois é só multiplicar por 32.
O Mapa
Normalmente, tudo em um jogo é colocado dentro de arquivos, no caso de mapas, é mais simples e você ainda tem a possibilidade de criar outros mapas para seu jogo. Pode-se usar programas como o Mappy ou Tile Studio, mas a verdade é que VOCÊ deve fazer seu próprio editor de mapas. O mapa pode ser armazenado em arquivos texto ou binário.
Para usarmos arquivos texto, precisaríamos criar um ANALISADOR LÉXICO para ler os tokens. Escrever um analisador léxico (descente) não é fácil, e outra coisa, o mapa ficaria exposto (com qualquer editor de texto poderíamos editar o mapa). Com arquivos binários, é mais simples e para alterar o mapa será necessário um editor hexadecimal.
Usaremos o Mappy para criar os mapas, mas não usaremos sua API para imprimi-lo na tela. Baixe o Mappy no endereço:
Basta descompactá-lo e executar normalmente. Abra-o e uma tela como essa será apresentada:
Para criar um mapa, basta ir ao menu "File / New Map". Altere o tamanho do tile se quiser, mas a configuração padrão é a de tiles de 32x32. Mais abaixo, coloque o tamanho na horizontal (tiles wide) e na vertical (tiles high), lembrando que isso significa o tamanho da tela em tiles, não em pixels. Crie um mapa com tiles de 32x32 e a tela com 40 tiles na horizontal e 15 na vertical.
Depois de criar o mapa, é só importar o tiles. Baixe os seguintes tiles (clique em cima da imagem para fazer o download):
Agora, para importar, vá no menu "File / Import", e escolha um arquivo .bmp contendo os tiles que você baixou ou os de sua escolha. Selecione o tile na janela à direita e depois pinte o mapa. Faça um mapa parecido com este:
Depois que o mapa estiver pronto, precisaremos gerar um arquivo.
Como não usaremos a API do Mappy, precisaremos exportar para um outro formato de arquivo que seja fácil de abrirmos.
Vá no menu "Custom / Export binary file".
Uma janela abrirá, basta clicar em OK e outra janela abrirá.
Nela diz se você quer ajustar os valores dos tiles (pode-se ajustar tanto para cima quanto para baixo).
O valor "-1" é o padrão, mas devemos alterar para "0" pelo seguinte fato:
os tiles vazios são representados pelo número "0", e o nosso primeiro tile é representado pelo número "1", se colocarmos o valor de ajuste como "-1", nosso primeiro tile será "0", ou seja, ele será um tile vazio!
Ao final, um arquivo .map será criado. Segue-se a estrutura desse tipo de arquivo:
+----------------------+
|
Cabeçalho |
+----------------------+
| Dados sobre a fase |
| |
| |
| |
+----------------------+
As primeiras informações no arquivo referem-se ao tamanho do mapa (em tiles) na horizontal e na vertical.
Usaremos a estrutura MAPINFO para obter essas informações para depois ler o mapa. Segue a rotina de carga de mapa:
CÓDIGO...
/* Rotina para carga de mapa */
MAP * load_map(const char * filename, SPRITE * spr)
{ /* Abrindo o arquivo de mapa */
No código acima, um arquivo de mapa é requerido e o vetor de SPRITE contendo os tiles.
Primeiramente, pegamos as informações referentes ao mapa (altura e largura em tiles). A partir daí, é só carregar o mapa. A leitura do mapa é feita sequencialmente, quem define a posição de cada tile são os laços de repetição FOR.
Lemos um inteiro de cada vez e verificamos se o seu valor não é 0. Se for 0, o valor NULL é atribuído, senão, atribua o ponteiro para o vetor de SPRITE (lembrando que na rotina de carga de tiles, os SPRITEs são carregados linearmente, começando pelo índice 0, que é o primeiro tile).
Obs.: No Mappy, o primeiro tile representa o índice 1, por isso a subtração por 1.
Imprimindo o mapa
A rotina de impressão de mapa deste tutorial difere um pouco do tutorial 26. Se você reparar bem, o código mostrado até agora parece ser uma extensão do allegro. Eis a rotina de impressão de mapa:
CÓDIGO...
/* Rotina de impressão de mapa */
void draw_map(BITMAP * bitmap, MAP * map, int xpos, int ypos)
{ /* Posição inicial */
int xbegin = abs(xpos / TILEWH);
int ybegin = abs(ypos / TILEWH);
/* Verificando se não ultrapassa o mapa */
if (xpos > 0)
xbegin = 0;
if (ypos > 0)
ybegin = 0;
/* Posição final */
int xend = SCREEN_W / TILEWH + xbegin + 1;
int yend = SCREEN_H / TILEWH + ybegin + 1;
/* Verificando se não ultrapassa o mapa */
if (xend > map->w)
xend = map->w;
if (yend > map->h)
yend = map->h;
int x;
int y;
/* Imprima o mapa! */
for (y = ybegin; y < yend; y++)
for (x = xbegin; x < xend; x++)
if (map->tiles[y][x].sprite)
draw_sprite(bitmap, map->tiles[y][x].sprite->bitmap, x * TILEWH + xpos, y * TILEWH + ypos);
}
FIM DE CÓDIGO...
Explicando
A rotina acima se propõe a imprimir um MAPA no "BITMAP bitmap". As posições x e y são também requeridas. Primeiro, pegamos as posições iniciais x e y de pintura da tela. Como dito anteriormente, a tela fica sendo dividida em tiles, então, devemos pegar a posição do tile e não a posição na tela, portanto, devemos dividir as posições x e y pelo tamanho do tile que estamos usando (32x32).
Se xpos for 16, dividido por 32, fica sendo 0,5. Arredondando dá 0. Então essa é a posição x inicial. O mesmo para ypos (abs() foi usando pois não adianta imprimir antes da posição 0,0 da tela). os dois IF's verificam se as posições xpos e ypos são maiores que 0, então, a posição inicial x e y ficam sendo 0.
Já encontramos as posições iniciais x e y, agora falta encontrar as posições finais. As posições finais são fáceis de encontrar: basta pegar o tamanho da tela em tiles (tanto na horizontal quanto na vertical) e somar com o x e y inicial. O "+ 1" é para que a rotina de impressão imprima até para fora da tela (somente 1 tile a mais). Sem isso, os tiles não serão impressos até que preencham a tela toda.
Sabendo as posições iniciais e finais, basta imprimir na tela. Antes de imprimir, verificamos se o SPRITE é nulo (NULL), e se não for, imprimimos ele na tela. Encontramos a posição correta de cada tile multiplicando pelo tamanho do mesmo, ou seja:
void draw_map(BITMAP * bitmap, MAP * map, int xpos, int ypos)
{
int xbegin = abs(xpos / TILEWH);
int ybegin = abs(ypos / TILEWH);
if (xpos > 0)
xbegin = 0;
if (ypos > 0)
ybegin = 0;
int xend = SCREEN_W / TILEWH + xbegin + 1;
int yend = SCREEN_H / TILEWH + ybegin + 1;
if (xend > map->w)
xend = map->w;
if (yend > map->h)
yend = map->h;
int x;
int y;
for (y = ybegin; y < yend; y++)
for (x = xbegin; x < xend; x++)
if (map->tiles[y][x].sprite)
draw_sprite(bitmap, map->tiles[y][x].sprite->bitmap, x * TILEWH + xpos, y * TILEWH + ypos);
}
FIM DE CÓDIGO...
Fim
Chegamos ao final deste EXTENSO tutorial. Se houver alguma dúvida, crítica ou sugestão sobre o tutorial é só ir no fórum, ficarei muito grato. Num próximo tutorial, estarei escrevendo sobre colisão com o cenário (um dos assuntos mais comentados sobre desenvolvimento de jogos).
Mais uma coisa: se você quer ver a rotina de impressão de mapa trabalhar, altere o código da seguinte forma:
for (y = ybegin + 1; y < yend - 1; y++)
for (x = xbegin + 1; x < xend - 1; x++)
if (map->tiles[y][x].sprite)
draw_sprite(bitmap, map->tiles[y][x].sprite->bitmap, x * TILEWH + xpos, y * TILEWH + ypos);
Você pode incrementar o cenário, imprimindo uma imagem de fundo atrás do mapa, como um céu ou uma caverna.
Espero que tenham gostado do tutorial e até mais...