Conheça a iniciativa da Biblioteca dos Desenvolvedores  
Índice da Biblioteca  
Área dos Usuários  
Fórum de Discussão  
Forúm
 
 

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;

    /* Total de tiles no bitmap */

    int total = xtiles * ytiles;

    /* Criando o vetor de SPRITE */

    SPRITE * spr = (SPRITE *) malloc(sizeof(SPRITE) * total);

    int x;
    int y;

    /* Índice para o vetor de SPRITE */

    int i = 0;

    /* 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:

SPRITE * spr = (SPRITE *) malloc(sizeof(SPRITE) * total);

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.

Na prática ficaria:

+-------+-------+-------+
|       |       |       |
|   1   |   2   |   3   |
+-------+-------+-------+

Isso se tivéssemos um BITMAP de 96x32. Se tivéssemos um BITMAP de 96x96:

+-------+-------+-------+
|       |       |       |
|   1   |   2   |   3   |
+-------+-------+-------+
|       |       |       |
|   4   |   5   |   6   |
+-------+-------+-------+
|       |       |       |
|   7   |   8   |   9   |
+-------+-------+-------+

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:

http://www.tilemap.co.uk/mappy.php

Basta descompactá-lo e executar normalmente. Abra-o e uma tela como essa será apresentada:

Mappy

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

    FILE * file = fopen(filename, "rb");

    /* Verificando se ele existe */

    if (!file)
        return NULL;

     /* Alocando memória para o mapa */

    MAP * map = (MAP *) malloc(sizeof(MAP));

    /* Pegando informaçõe sobre o mapa */

    MAPINFO info;

    fread(& info, sizeof(MAPINFO), 1, file);

    /* Altura e largura do mapa */

    map->w = info.w;
    map->h = info.h;

    /* Crie a matriz na vertical */

    map->tiles = (TILE **) malloc(sizeof(TILE) * map->h);

    int x;
    int y;

    int value;

    for (y = 0; y < map->h; y++)
    {
        /* Crie a matriz na horizontal */

        map->tiles[y] = (TILE *) malloc(sizeof(TILE) * map->w);

        for (x = 0; x < map->w; x++)
        {
            /* Lendo o tipo de tile */

            fread(& value, sizeof(int), 1, file);

             /* Se o valor do tile for diferente de 0, atribua o SPRITE, senão, NULL */

            map->tiles[y][x].sprite = value != 0 ? & spr[value - 1] : NULL;
        }
    }

    /* Feche o handle de arquivo */

    fclose(file);

    /* Retorne o mapa */

    return map;
}


FIM DE CÓDIGO...

Explicando

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:

Tile: 0,0 - Posição: 0, 0
Tile: 0,1 - Posição: 0, 32
...
Tile: 1,1 - Posição: 32, 32
...

Acrecentamos também xpos e ypos à posição do tile, para não dar um efeito de MOSAICO na tela.

O Código Completo

Para executar o código abaixo, baixe o seguinte arquivo : mapa.map


CÓDIGO...

#include <allegro.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define TILEWH 32

#define SPEED 1

typedef struct
{
    BITMAP * bitmap;

} SPRITE;

typedef struct
{
    SPRITE * sprite;

} TILE;

typedef struct
{
    TILE ** tiles;

    int w;
    int h;

} MAP;

typedef struct
{
    int w;
    int h;

} MAPINFO;

BITMAP * buffer;

SPRITE * load_tiles(const char *);

MAP * load_map(const char *, SPRITE *);
void draw_map(BITMAP *, MAP *, int, int);

int main(int argc, char ** argv)
{
    allegro_init();

    install_keyboard();

    set_color_depth(16);
    set_gfx_mode(GFX_AUTODETECT_WINDOWED, 640, 480, 0, 0);

    buffer = create_bitmap(SCREEN_W, SCREEN_H);

    SPRITE * spr = load_tiles("tiles.bmp");

    if (!spr)
    {
        allegro_message("Falha ao carregar SPRITES!");

        return 1;
    }

    MAP * map = load_map("mapa.map", spr);

    if (!map)
    {
        allegro_message("Falha ao carregar MAPA!");

        return 1;
    }

    int xmap = 0;
    int ymap = 0;

    int x = 0;
    int y = 0;

    while (!key[KEY_ESC])
    {
        if (key[KEY_LEFT])
            xmap += SPEED;

        if (key[KEY_RIGHT])
            xmap -= SPEED;

        clear(buffer);

        draw_map(buffer, map, xmap, 0);

        blit(buffer, screen, 0, 0, 0, 0, SCREEN_W, SCREEN_H);
    }

    destroy_bitmap(buffer);

    allegro_exit();

    return 0;
}
END_OF_MAIN();

SPRITE * load_tiles(const char * filename)
{
    BITMAP * tmp = load_bitmap(filename, NULL);

    if (!tmp)
        return NULL;

    int xtiles = tmp->w / TILEWH;
    int ytiles = tmp->h / TILEWH;

    int total = xtiles * ytiles;

    SPRITE * spr = (SPRITE *) malloc(sizeof(SPRITE *) * total);

    int x;
    int y;

    int i = 0;

    for (y = 0; y < ytiles; y++)
    {
        for (x = 0; x < xtiles; x++)
        {
            spr[i].bitmap = create_bitmap(TILEWH, TILEWH);

            blit(tmp, spr[i].bitmap, x * TILEWH, y * TILEWH, 0, 0, TILEWH, TILEWH);

            i++;
        }
    }

    destroy_bitmap(tmp);

    return spr;
}

MAP * load_map(const char * filename, SPRITE * spr)
{
    FILE * file = fopen(filename, "rb");

    if (!file)
        return NULL;

    MAP * map = (MAP *) malloc(sizeof(MAP));

    MAPINFO info;

    fread(& info, sizeof(MAPINFO), 1, file);

    map->w = info.w;
    map->h = info.h;

    map->tiles = (TILE **) malloc(sizeof(TILE **) * map->h);

    int x;
    int y;

    int value;

    for (y = 0; y < map->h; y++)
    {
        map->tiles[y] = (TILE *) malloc(sizeof(TILE *) * map->w);

        for (x = 0; x < map->w; x++)
        {
            fread(& value, sizeof(int), 1, file);

            map->tiles[y][x].sprite = value != 0 ? & spr[value - 1] : NULL;
        }
    }

    fclose(file);

    return map;
}

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...

 


Download do exemplo 1 - Clique Aqui

Download do exemplo 2 - Clique Aqui



Contribuidor
Arabasso
04/02/2009


 

« Anterior

 

Próximo »

 
 

01/06/2007 (C) Copyright. Todos os Direitos Reservados. Leia a política de privacidade do portal.
É proibida a cópia de conteúdo deste site de acordo com a Lei Brasileira de Direitos Autorais.