Integrando mapas do Tiled com o Phaser

(For the English version, click here)

Introdução

Neste post iremos analisar como integrar mapas feitos no Tiled em jogos feitos com Phaser, que é um framework JavaScript pra jogos em HTML5; é uma abordagem interessante para dar mais autonomia ao game designer, que poderá trabalhar no design das fases separadamente e ver o resultado praticamente em tempo real.

Alguns links úteis sobre Phaser e Tiled:

Antes de mais nada, vamos ver como fazer um mapa simples com layers(camadas) de tiles e de objetos, já tendo em vista a integração com o Phaser. Os tilesets utilizados foram obtidos aqui. Vamos assumir que você tenha um projeto básico do Phaser já configurado com um estado e um jogador, e as funções preload(), create() e update(), e também conhecimento básico do funcionamento do Tiled.

O post é dividido em 4 partes:

  • Criando um mapa no Tiled
  • Importando o mapa no Phaser
  • Adicionando layers de objetos no Tiled
  • Carregando layer de objetos no Phaser

Todos os tiles têm 16x16 pixels, e todas as numerações dos tiles são referentes ao exemplo em questão; no seu projeto haverá valores diferentes, cabendo a você fazer as devidas adaptações.

Criando um mapa no Tiled

Vamos criar um mapa com 20x15 tiles, como os tiles possuem um tamanho de 16x16 pixels isso resulta em um tamanho em pixels de 320x240.

Feito isso, vamos adicionar uma imagem para usarmos como tileset. Basta clicar no botão Novo Tileset no canto inferior direito, localizar o arquivo com o tileset e configurar o tamanho para 16x16 pixels.

Feito isso, vamos criar um mapa com 2 tile layers, Walls e Background. Os layers são ordenados de forma que o layer de cima estará mais "à frente" na tela, e é possível usar as setas abaixo da lista de layers para fazer a ordenação. Um possível mapa com os layers é o mostrado abaixo:

Para criar os layers, basta clicar no botão sublinhado e escolher Tile Layer. Importante: depois que você estiver satisfeito com o seu mapa, salve-o no formato .json para que o mesmo possa ser importado no Phaser.

Importando o mapa no Phaser

Para esta parte, assuma que o estado tenha sido definido da forma abaixo e que todos os arquivos existem nos seus respectivos paths:

var GameState = function(game) {};  

No método preload() do estado, vamos carregar tanto o arquivo .json do mapa quanto o tileset do mesmo:

GameState.prototype.preload = function() {  
    // outros assets, etc
    this.game.load.image('mapTiles', 'Assets/spritesheets/tiles.png');
    this.game.load.tilemap('level1', 'Assets/maps/level1.json', null, Phaser.Tilemap.TILED_JSON);
}

Depois disso, vamos criar efetivamente um objeto com o nosso mapa no estado, em create(), em 4 passos:

  1. Criar o objeto que conterá o mapa em si;
  2. Associar o asset do Phaser que contém o tileset com o nome do tileset que foi especificado no Tiled;
  3. Criar os layers, ordenando do background para o foreground;
  4. Especificar quais tiles devem ter um colisor, para que possamos colidir o jogador com os mesmos. Os parâmetros são uma lista de tiles, a palavra-chave true (para indicar que estamos ligando a colisão) e o layer onde deverão ocorrer tais colisões.
GameState.prototype.create = function() {  
    // outros objetos, etc
    this.level1 = this.game.add.tilemap('level1'); // passo 1
    this.level1.addTilesetImage('tiles', 'mapTiles'); // passo 2

    // passo 3
    this.bgLayer = this.level1.createLayer('Background');
    this.wallsLayer = this.level1.createLayer('Walls');

    // passo 4 será descrito logo mais
}

Sobre o passo 4, perceba que todos os tiles são identificados por um número, começando do tile superior direito e aumentando da direita pra esquerda, continuando a contagem quando passa para a próxima linha. No nosso exemplo, a numeração ficaria a seguinte:

Dito isso, como a grande maioria dos tiles efetivamente devem colidir, vamos especificar as colisões de maneira diferente: ao invés de dizer quais devem colidir, vamos dizer quais não devem colidir:

    // passo 4, ainda dentro do create()
    this.level1.setCollisionByExclusion([9, 10, 11, 12, 17, 18, 19, 20], true, this.wallsLayer);

Importante: estes números de tiles só são referentes ao tileset que estamos utilizando como exemplo, outros tilesets terão outras numerações e outros tiles que devem ou não colidir. Outros métodos de selecionar quais tiles devem colidir podem ser vistos na documentação do Phaser aqui.

Agora basta dizer para o Phaser que o jogador deve colidir com o layer Walls. Adicione a seguinte chamada em update():

    this.game.physics.arcade.collide(this.player, this.wallsLayer);

Feito isso, carregando o jogo, este deve ser o resultado:

Adicionando layers de objetos no Tiled

Vamos agora adicionar um layer de objetos para que o jogador possa interagir com os mesmos, que será nomeado Items.

Logo após, vamos adicionar um tileset de objetos, que é feito da mesma maneira do que o tileset que tem os tiles: basta especificar o tamanho dos tiles e a imagem que contém o tileset.

Feito isso, clique na aba Objetos, logo acima dos tilesets, e selecione o layer criado, Items. Agora é só apertar no botão Inserir Tile, e ir inserindo os objetos, de acordo como demonstrado no GIF abaixo:

Depois disso, clique em cada objeto na lista à direita, e clique duas vezes no espaço ao lado da marcação para dar um nome para os objetos, sendo que o mesmo nome deve ser dado para objetos iguais; no nosso exemplo, irão se chamar "diamond".

Com tudo isso feito, nosso trabalho no Tiled terminou, vamos voltar ao código e carregar tais objetos.

Carregando layer de objetos no Phaser

Vamos na função create() executar os seguintes passos

    this.diamonds = this.game.add.physicsGroup(); // Passo 1
    this.level1.createFromObjects('Items', 'diamond', 'items', 5, true, false, this.diamonds); // Passo 2

    // Passo 3
    this.diamonds.forEach(function(diamond){
        diamond.body.immovable = true;
        diamond.animations.add('spin', [4, 5, 6, 7, 6, 5], 6, true);
        diamond.animations.play('spin');
    });    

No passo 1, criamos um grupo para conter esses objetos, e no passo 2 e criamos os mesmos utilizando a função createFromObjects() do mapa. Os parâmetros completos da função podem ser vistos na documentação, mas os fundamentais são os 4 primeiros e o último:

  • Primeiro: nome do layer de objetos do Tiled
  • Segundo: nome do objeto do layer do Tiled que será instanciado
  • Terceiro: nome do spritesheet carregado no preload() que contém as imagens dos objetos
  • Quarto: qual o frame do spritesheet que será utilizado para o objeto.
  • Último: nome do grupo onde os objetos serão adicionados.

No passo 3, vemos como aplicar uma função a cada elemento do grupo como pós-processamento, para que todos os objetos criados tenham o comportamento esperado. No exemplo, estamos fazendo os objetos não serem afetados por forças externas (body.immovable = true) e adicionando uma animação ao objeto. Um detalhe é que as animações são feitas com spritesheets no Phaser, e os frames nos spritesheets começam a contar a partir 0, ao invés de 1 como os tilesets.

Feito isso, recarregando o jogo, veremos os objetos:

Depois de todo esse trabalho, só resta mesmo fazer uma maneira do jogador interagir com estes objetos. Para tal fim, vamos adicionar o código para colisão na função update(), juntamente com uma função de tratamento:

    // dentro da função update()
    this.game.physics.arcade.overlap(this.player, this.diamonds, this.diamondCollect, null, this);

GameState.prototype.diamondCollect = function(player, diamond){  
    diamond.kill();
}

Tal código estabelece a colisão entre o jogador e o grupo de diamantes, com a função this.diamondCollect para tratar a colisão. Na função de tratamento, apenas desativamos o objeto diamond. O resultado deve ser o seguinte:

Está sendo utilizada a função overlap() ao invés de collide() neste caso para evitar que o jogador perca velocidade ao colidir com o diamante, gerando um efeito indesejado no gameplay.

Com isso, já teremos um mapa do Tiled integrado no Phaser, juntamente com todos os objetos necessários.