Início > Tutoriais, XNA > [XNA] TileMap

[XNA] TileMap

Antes de começarmos a programar tilemaps vamos ver algumas definições.

Tiles:

Tiles são pequenos pedaços de imagens que servem para criar uma nova imagem composta conhecida como layer, utilizadas para criar o cenário de um jogo. Essa técnica é chamada de TileMaps, no qual, através de uma imagem contendo diversos pedacinhos de imagens (tiles), se pode criar um cenário para um jogo. Exemplos clássicos do uso de TileMaps, são em jogos de RPG (Role Playing Game). Os jogos de RPG geralmente possuem cenários extensos o qual se fosse feito com uma única imagem ocuparia muito espaço de memória e naquela época em que surgiram, as memórias eram bem limitadas. Então a técnica de dividir o cenário em tiles e recriá-lo em tempo de processamento fez essa técnica ser muito utilizada até hoje em dia.

Como esta técnica funciona?

A primeira etapa é definir os tiles, ou seja, seu tamanho e suas imagens dentro dele.

A segunda parte é montar o Tilemap (também conhecido como bricks), ou seja, definir a estrutura que vai descrever aquele cenário do nosso jogo.

E por fim em tempo de execução nosso algoritmo deve substituir os números da matriz de tilemap pelas posições correspondentes do nosso tile. Esse cenário é conhecido como layer.

Projeto:

Então para fixarmos a parte teórica vamos implementar um mapa básico para um jogo do Pacman, inicie um novo projeto e coloque o nome de Pacman.

Criado o projeto vamos então criar uma classe com o nome Tile.cs que servirá para definirmos as propriedades dos tiles do jogo. Veja como vai ficar a classe.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
namespace TileMap
{
    /// <summary>
    /// Controla o estado entre os tiles para ver se existe ou não colisão
    /// </summary>
    public enum TileCollision
    {
        /// <summary>
        /// Define que o tile á passavel
        /// </summary>
        Passable,
        /// <summary>
        /// Define que o tile é um sólido impassavel, ou seja,
        /// existe colisão caso tentem passar por este tile.
        /// </summary>
        Impassable
    };

    /// <summary>
    /// Definição da classe tile
    /// </summary>
    public class Tile
    {
        public Texture2D texture;
        public TileCollision collision;
        public readonly Vector2 size;
        /// <summary>
        /// Construtor de um novo tile
        /// </summary>
        public Tile(Texture2D texture, TileCollision collision)
        {
            this.texture = texture;
            this.collision = collision;
            this.size = new Vector2(this.texture.Width, this.texture.Height);
        }
    }
}

Essa classe define a imagem do tile, seu tamanho que é conforme o tamanho da imagem e se existe ou não colisão.

Agora clique com o botão direito e faça download das imagens abaixo.

Tiles:

block00.png

block01.png

block02.png

block03.png

Sprite:

ghost.png

Logo após fazer o download das imagens as carregue em seu projeto conforme o Solution Explorer abaixo.


Agora na nossa classe Game1.cs vamos carregar nossas imagens, mas para isso vamos criar uma lista da nossa classe Tile e um Texture2D para carregar a imagem do fantasmas (personagem do nosso jogo). Adicione então as seguintes variáveis:

// Texture a ser utilizada para o personagem do tutorial
Texture2D character;
// Lista de tiles para montar o cenário
List<Tile> tiles;

E então no método LoadContent vamos carregar as imagens.

protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);
    // Criando a lista de tiles
    tiles = new List<Tile>();
    // Adicionando os tiles a lista
    tiles.Add(new Tile(Content.Load<Texture2D>(@"Tiles\block00"), TileCollision.Passable));
    tiles.Add(new Tile(Content.Load<Texture2D>(@"Tiles\block01"), TileCollision.Passable));
    tiles.Add(new Tile(Content.Load<Texture2D>(@"Tiles\block02"), TileCollision.Passable));
    tiles.Add(new Tile(Content.Load<Texture2D>(@"Tiles\block03"), TileCollision.Impassable));
    // Carrega a textura do personagem
    character = Content.Load<Texture2D>(@"Sprites\ghost");
}

Com nossas imagens carregadas precisamos agora criar a estrutura do nosso mapa. E nosso mapa vai ser da seguinte forma:

Propriedades do mapa:

  • Matriz de números inteiros de 28 x 25
  • Valor0 para block00
  • Valor1 para block01
  • Valor2 para block02
  • Valor3 para block03 e somente este bloco é impassível.

Abaixo então de onde colocamos nossas variáveis declare nosso mapa conforme o código abaixo.

/// <summary>

/// Este é o brick ou seja nossa estrutura do mapa para nosso jogo
/// Os números serão substituidos pelos tiles adcionados a nossa lista
/// </summary>
int[,] map = {
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,2,2,2,2,2,2,2,2,2,2,2,2,3,3,2,2,2,2,2,2,2,2,2,2,2,2,3},
{3,1,3,3,3,3,3,2,3,3,3,3,2,3,3,2,3,3,3,3,2,3,3,3,3,3,1,3},
{3,2,3,3,3,3,3,2,3,3,3,3,2,3,3,2,3,3,3,3,2,3,3,3,3,3,2,3},
{3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3},
{3,2,3,3,3,3,3,2,3,2,3,3,3,3,3,3,3,3,2,3,2,3,3,3,3,3,2,3},
{3,2,2,2,2,2,2,2,3,2,2,2,2,3,3,2,2,2,2,3,2,2,2,2,2,2,2,3},
{3,3,3,3,3,3,3,2,3,3,3,3,2,3,3,2,3,3,3,3,2,3,3,3,3,3,3,3},
{0,0,0,0,0,0,3,2,3,2,2,2,2,2,2,2,2,2,2,3,2,3,0,0,0,0,0,0},
{0,3,3,3,3,3,3,2,3,2,3,3,3,0,0,3,3,3,2,3,2,3,3,3,3,3,3,0},
{0,0,0,0,0,0,0,2,2,2,3,0,0,0,0,0,0,3,2,2,2,0,0,0,0,0,0,0},
{0,3,3,3,3,3,3,2,3,2,3,3,3,3,3,3,3,3,2,3,2,3,3,3,3,3,3,0},
{0,0,0,0,0,0,3,2,3,2,2,2,2,2,2,2,2,2,2,3,2,3,0,0,0,0,0,0},
{3,3,3,3,3,3,3,2,3,2,3,3,3,3,3,3,3,3,2,3,2,3,3,3,3,3,3,3},
{3,2,2,2,2,2,2,2,2,2,2,2,2,3,3,2,2,2,2,2,2,2,2,2,2,2,2,3},
{3,2,3,3,3,3,3,2,3,3,3,3,2,3,3,2,3,3,3,3,2,3,3,3,3,3,2,3},
{3,2,3,3,3,3,3,2,3,3,3,3,2,3,3,2,3,3,3,3,2,3,3,3,3,3,2,3},
{3,2,2,2,2,2,3,2,2,2,2,2,2,0,0,2,2,2,2,2,2,3,2,2,2,2,2,3},
{3,3,3,3,3,2,3,2,3,2,3,3,3,3,3,3,3,3,2,3,2,3,2,3,3,3,3,3},
{3,3,3,3,3,2,3,2,3,2,3,3,3,3,3,3,3,3,2,3,2,3,2,3,3,3,3,3},
{3,2,2,2,2,2,2,2,3,2,2,2,2,3,3,2,2,2,2,3,2,2,2,2,2,2,2,3},
{3,2,3,3,3,3,3,2,3,3,3,3,2,3,3,2,3,3,3,3,2,3,3,3,3,3,2,3},
{3,1,3,3,3,3,3,2,3,3,3,3,2,3,3,2,3,3,3,3,2,3,3,3,3,3,1,3},
{3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
};

NOTA: O mapa não precisa ser inicializado desta maneira, você pode carregar sua matriz através de um arquivo.

Antes de desenharmos nosso mapa e personagem vamos definir uma váriavel de posição, velocidade e direção para nosso personagem. Veja abaixo como fica a declaração das variáveis

// Posição do personagem

Vector2 position = new Vector2(324.0f, 408.0f);
// Velocidade do personagem
float speed = 2.0f;
// Direção de movimento do personagem
Vector2 direction = new Vector2(0.0f, 0.0f);

Com todas essas variáveis declaradas, agora podemos desenhar para ver o resultado e depois trabalhar na movimentação e colisão do personagem. Veja o método de desenho como fica.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Black);
    spriteBatch.Begin();
    // Desenhando o mapa do jogo
    for (int i = 0; i < map.GetLength(0); i++)
    {
        for (int j = 0; j < map.GetLength(1); j++)
        {
             spriteBatch.Draw(tiles[map[i, j]].texture,
                new Vector2(j * tiles[map[i, j]].size.X, i * tiles[map[i, j]].size.X),
                Color.White);
        }
    }
    // Desenhando o personagem
    spriteBatch.Draw(character, position, Color.White);
    spriteBatch.End();
    base.Draw(gameTime);
}

Se você executar o programa agora poderá ver o mapa e o nosso personagem desenhado na tela, mas sem movimento. Para criarmos seu movimento precisamos fazer como já fizemos em diversos tutoriais.

protected override void Update(GameTime gameTime)
{
    // Recebe o estado do teclado
    KeyboardState keyboardState = Keyboard.GetState();
    // Verifica se o jogador pressionou seta para cima
    if (keyboardState.IsKeyDown(Keys.Up))
    {
        this.direction.Y = -1;
    }
    // Verifica se o jogador pressionou seta para baixo
    if (keyboardState.IsKeyDown(Keys.Down))
    {
        this.direction.Y = 1;
    }
    // Verifica se o jogador pressionou seta para direita
    if (keyboardState.IsKeyDown(Keys.Right))
    {
        this.direction.X = 1;
    }
    // Verifica se o jogador pressionou seta para esqueda
    if (keyboardState.IsKeyDown(Keys.Left))
    {
        this.direction.X = -1;
    }
    // Atualiza a posição do personagem
    this.position += this.direction * this.speed;
    base.Update(gameTime);
}

Legal, se executar o nosso joguinho agora verá poderá movimentar o fantasma, porém ele esta atravessando a parede e andando em diagonal, ou seja, precisamos corrigir isso. Para corrigirmos isso vamos ter que verificar se existe colisão do personagem com o tile que ele esta no momento. Então vamos criar um método para verificar isso. Abaixo do método de desenho, digite o seguinte código:

/// <summary>
/// Método que detecta colisão
/// </summary>
/// <param>posição y do tile</param>
/// <param>posição x do tile</param>
/// <returns></returns>
public bool CheckTileCollision(int x, int y)
{
    if (tiles[map[y, x]].collision != TileCollision.Impassable)
    {
        return false;
    }
    return true;
}

Agora temos um método que a partir de um ponto X e Y de tile ele verifica se o tile não é passável. Vamos então utilizar esse método após tentarmos movimentar nosso personagem no método Update(). Digite o seguinte código antes de atualizarmos a posição do personagem:

protected override void Update(GameTime gameTime)
{
    .
    .
    .
    .
    // Define a nova posição do personagem
    Vector2 newPosition = this.position;
    newPosition.X += this.direction.X * speed;
    newPosition.Y += this.direction.Y * speed;
    // Define os tiles vizinhos do personagem conforme a nova posição
    int left = (int)(newPosition.X / 24);
    int top = (int)(newPosition.Y / 24);
    int right = (int)((newPosition.X + (character.Width - 1)) / 24);
    int bottom = (int)((newPosition.Y + (character.Height - 1)) / 24);
    // Verifica colisão do personagem com todos os tiles vizinhos definidos ao deslocar-se
    if (this.direction.Y == -1) // Se o personagem esta indo para Cima
    {
        if (CheckTileCollision(left, top) || CheckTileCollision(right, top))
        {
            this.direction.Y = 0;
        }
    }
    else if (this.direction.Y == 1) // Se o personagem esta indo para baixo
    {
        if (CheckTileCollision(left, bottom) || CheckTileCollision(right, bottom))
        {
            this.direction.Y = 0;
        }
    }
    if (this.direction.X == 1) // Se o personagem esta indo para direita
    {
        if (CheckTileCollision(right, top) || CheckTileCollision(right, bottom))
        {
            this.direction.X = 0;
        }
    }
    else if (this.direction.X == -1) // Se o personagem esta indo para esquerda
    {
        if (CheckTileCollision(left, top) || CheckTileCollision(left, bottom))
        {
            this.direction.X = 0;
        }
    }
    // Verifica se o personagem passou a tela pela esquerda
    if (this.position.X < 24.0)
    {
        this.position.X = 624.0f;
    }
    // Verifica se o personagem passou a tela pela direita
    if (this.position.X > 624.0)
    {
        this.position.X = 24.0f;
    }
    // Atualiza a posição do personagem
    this.position += this.direction * this.speed;
    base.Update(gameTime);
}

O que fizemos então criamos uma variável que recebe a nova posição que o personagem vai receber, então verificamos esta posição dentro do nosso mapa de tiles se ela ocupa algum espaço que existe tiles impassíveis com a função CheckTileCollision(int x, int y)

Se você acompanhou corretamente todos os passos e não digitou nada errado e resultado é para ser um fantasma se movimentando na tela colidindo com o cenário, conforme imagem abaixo.


Para fazer download do código completo clique aqui.

Abraços a todos e espero que tenham gostado. Até a próxima.

  1. Luana
    05/16/2010 às 2:39 pm | #1

    Olá, fiquei com uma dúvida sobre este tutorial…Tentei fazê-lo, até consegui em partes, mas gostaria de saber como eu faço pra controlar a movimentação do personagem, isto é, sem que ele ande infinitamente só de eu pressionar uma tecla, mas sim que ande conforme eu pressionar a tecla.Tentei adaptar códigos que eu fiz, mas aí a colisão não funciona.

    • Kleber Andrade
      05/16/2010 às 7:09 pm | #2

      Olá Luana, então eu vou tentar fazer um exemplo aqui para seu caso, mas não é tão diferente, é só você mudar a posição e checar a colisão. Mas eu vou fazer um tutorial para você.
      Abraços,

  2. Luana
    05/17/2010 às 1:29 pm | #3

    Obrigada. :D Estes tutoriais tem me ajudado muito nos últimos dias.Estou tendo uma cadeira de xna na faculdade, mas o meu professor não é tão bom xD Vou esperar então^^

  3. Juan Pablo
    09/02/2010 às 3:55 pm | #4

    é so fazer um contador ….

    ex:
    int cont;
    cont = 0;
    instacie o teclado …depois:

    if(ks.isKey(Keys.Left) && i == 0)
    {
    movePersonagem.X += 3;
    i = 1;
    }

    else if(i == 1)
    {
    i = 0 ;
    }

    so fazer isso com todas as direçoes..abraços

  4. Juan Pablo
    09/02/2010 às 3:59 pm | #5

    Obs. qualquer duvida mande-me um email….abraços

  5. guilherme pelanda onofre
    09/19/2011 às 5:44 pm | #6

    Olá, tenho uma dúvida em como criar um tile animado
    como água se mexendo, lava entre outros.
    Procurei em vários lugares e nada.
    Se poder me ajudar, agradeço.

    • Kleber Andrade
      09/19/2011 às 11:24 pm | #7

      Amigo é só você criar um sprite animado.

      No meu blog tem uns tutoriais para isso.

      Abraços

      • guilherme pelanda onofre
        09/20/2011 às 5:50 pm | #8

        Então devo carregar a textura do tile(com vários frames) e utilizar a classe AnimatedSprite do seu Exemplo para executar esta?

  6. guilherme pelanda onofre
    09/21/2011 às 4:00 pm | #9

    Kleber deu certo!
    Mas tive que acrescentar na classe Tile um AnimatedSprite e
    No método Draw do Game1 acrecentei um IF para fazer uma verificação(se é animação ou textura), dentro do looping de desenho do Mapa.
    Não sei se existe uma forma mais simple(ou se fiz de maneira correta).
    Se tiver uma outra sujestão, fico no aguardo.
    por hora Valeu mesmo pela dica, Obrigado e admiro muito o seu fórum!

  1. Nenhum trackbacks ainda.

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Sair / Alterar )

Imagem do Twitter

You are commenting using your Twitter account. Sair / Alterar )

Foto do Facebook

You are commenting using your Facebook account. Sair / Alterar )

Connecting to %s

Seguir

Obtenha todo post novo entregue na sua caixa de entrada.

Join 29 other followers