[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:
Sprite:
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.



























