Fundamentos: Game estilo Frogger com Phaser 3 - Parte 1

Cover image

Esse artigo faz parte de uma série de tutoriais referente a criação de um game estilo frogger, você está na parte 1.

Caso queira ver o jogo final clique aqui, o repositório com todos os códigos do jogo com posts separados por commit pode ser encontrado aqui.

Essa e uma série finalizada, você pode ver todos os posts aqui:

Público Alvo

Esta série de tutoriais irá fazer mais sentido caso você tenha algum conhecimento com javascript, não precisa ter nenhum conhecimento sobre o Phaser. Para melhor experiência o ideal e você ter conhecimentos básicos sobre:

  • HTML
  • CSS
  • Javascript (com es6+)

Metas de aprendizado

O objetivo dessa série de tutoriais é ensinar os fundamentos do Phaser, tais como:

  • Estrutura Básica
  • Carregamento de Imagens
  • Carregamento de Áudios
  • Colisões
  • Manipulação de sprite / imagem / áudio
  • Eventos no teclado
  • Movimento ( Jogador e Inimigo )
  • Efeitos de câmera

NOTA: Pretendo criar novas séries de tutoriais sobre phaser (cada vez mais avançado) até chegarmos na criação de um mmorpg!

Configurando o Ambiente e Pré-Requisitos

Antes de irmos direto ao código, precisamos configurar o nosso ambiente, os pré-requisitos são mínimos:

  • Editor de código, Irei usar o VSCode (recomendado);
  • Servidor web local, não precisa de uma configuração complexa. Utilizando o VSCode você pode usar a extensão Live Server para criar o necessário, durante o tutorial irei entrar em mais detalhes;
  • Phaser, por questões óbvias;

Primeiro Passo

Baixe aqui os arquivos iniciais do projeto. Você verá uma pasta na seguinte estrutura:

Estrutura de Pasta

Explicando:

  • Assets: Imagens e áudios que serão utilizados no jogo.
  • CSS: Arquivo de css contendo alguns estilos simples, o importante aqui é a tag “canvas” que está com a propriedade “width” em 100%, isso ajuda a dar uma certa responsividade na tela do jogo, não é o método mais adequado, mas para esse projeto será o mais simples possível, pois o objetivo é ensinar os fundamentos do phaser, caso você queira ver o projeto com as dimensões reais, apenas exclua essa linha.
  • JS: Contém apenas o arquivo phaser.min.js na versão 3.22.0, ele e o responsável por nos fornecer todos os recursos do Phaser.

Inicializando o Projeto

Agora que já estamos com os arquivos iniciais do projeto, podemos abri-lo com o VSCode (ou algum editor de sua preferência). A primeira coisa a se fazer é iniciar o servidor web local e o modo mais rápido e simples para essa série de tutoriais e utilizar a extensão chamada Live Server(link), caso você ainda não tenha ele instalado basta você ir na aba de extensões e buscar por “live server” e clicar no botão de instalar.

Instalando o Live Server

O motivo de precisar de um servidor web local

Simplificando, por razões de segurança o Phaser irá fazer requisições utilizando o protocolo http:// para carregar os recursos do jogo, tais como imagens, arquivos de áudio, json etc…

Se apenas abríssemos o index.html no nosso navegador, utilizaríamos o file:// e consequentemente não sendo possível do Phaser fazer as requisições para carregar os recursos do jogo.

Iniciando o servidor web local

Agora que já estamos com tudo configurado, basta que com o VSCode aberto na pasta do nosso projeto, executar a extensão do live server, pode ser feita da seguinte maneira:

Com o arquivo index.html aberto, clique com o botão direito do mouse e depois em “Open with live server”, como na imagem abaixo:

Executando o Live Server

Pronto! agora é só ir no seu navegador e caso ele não tenha aberto automaticamente, ir na seguinte url: http://localhost:PORTA/ , o meu está configurado na porta 8080 mas se eu não me engano por padrão o live server executa na 5500, para você saber qual porta ele está rodando basta verificar no canto direito inferior do seu VSCode, que no meu caso mostra a porta 8080 ,ou seja, esta rodando na seguinte url http://localhost:8080 como na imagem abaixo:

Verificando porta do Live Server

No momento só estamos vendo uma tela vazia, isso e porque não instanciamos o Phaser ainda.

Instanciando o Phaser

O primeiro passo e dentro da pasta src/js criarmos um arquivo index.js, no index.html ele já está sendo chamado na tag script.

Agora precisamos chamar o Phaser dentro do nosso arquivo index.js, e para isso ele precisa de algumas configurações, para saber como será o nosso jogo. Uma boa prática e envolver ele com a função onload do objeto window do nosso navegador que executa imediatamente após a página ser carregada.

NOTA: Para esclarecer melhor, vou sempre que possível comentar cada linha do código

Ficando dessa forma o nosso index.js:

// passa as configurações do jogo
const config = {
  type: Phaser.AUTO, // preferência por WebGL, caso não, irá usar o Canvas
  width: 420, // largura da tela do jogo
  height: 462, // altura da tela do jogo
  pixelArt: true, // mantem os graficos nitidos, sem essa propriedade o jogo fica meio "borrado"
  banner: false, // desativa o log ao iniciar o phaser (na aba console do navegador)
}
 
window.onload = () => new Game(config);

Agora precisamos de fato criar a classe Game que estamos chamando na função onload.

NOTA: Por motivos didáticos iremos manter todos os códigos do jogo no arquivo index.js, mas o ideal é usarmos o webpack por exemplo para fazer o bundle do nosso jogo, se você não sabe o que é isso, não se preocupe. Primeiro vamos aprender os fundamentos do Phaser.

Com a classe Game criada, o nosso index.js ficará assim:

// passa as configurações do jogo
const config = {
  type: Phaser.AUTO, // preferência por WebGL, caso não, irá usar o Canvas
  width: 420, // largura da tela do jogo
  height: 462, // altura da tela do jogo
  pixelArt: true, // mantém os gráficos nítidos, sem essa propriedade o jogo fica meio "borrado"
  banner: false, // desativa o log ao iniciar o phaser (na aba console do navegador)
}

// cria uma instancia do jogo, assim que a pagina e totalmente carregada
window.onload = () => new Game(config);
 
// classe que inicia o jogo
class Game extends Phaser.Game {
  constructor(config) {
    super(config);
  }
}

Se você olhar o seu navegador, verá que o Phaser já está sendo iniciado e a tela está preta. Agora é hora de darmos vida ao nosso jogo!

Criando uma Scene (cena)

O conceito de Scene no Phaser foi uma das grandes novidades em comparação à versão anterior, de forma simplificada uma Scene no Phaser e referente a uma tela do seu jogo, ou seja, um jogo simples pode ter as seguintes Scenes:

  • Tela de Menu
  • Tela do Principal (onde jogamos em si)
  • Tela de Game Over

Podemos fazer mais coisas com as Scenes, mas isso será mais aprofundado em tópicos mais avançados que pretendo lançar aqui no blog.

Agora com a Scene adicionada ao nosso index.js, ele ficará assim:

// passa as configurações do jogo
const config = {
  type: Phaser.AUTO, // preferência por WebGL, caso não, irá usar o Canvas
  width: 420, // largura da tela do jogo
  height: 462, // altura da tela do jogo
  pixelArt: true, // mantém os gráficos nítidos, sem essa propriedade o jogo fica meio "borrado"
  banner: false, // desativa o log ao iniciar o phaser (na aba console do navegador)
}

// cria uma instancia do jogo, assim que a pagina e totalmente carregada
window.onload = () => new Game(config);
 
// classe que inicia o jogo
class Game extends Phaser.Game {
  constructor(config) {
    super(config);
  }
}

// classe referente a cena principal do jogo
class MainScene extends Phaser.Scene {
  // "key" e o identificador único de uma Scene, assim não corremos riscos de ter Scenes repetidas
  constructor(key) {
    super(key);
  }
}

Pronto, criamos a nossa classe, agora precisamos adicionar ela ao nosso jogo.

Adicionando uma Scene

Dentro da classe Game, que é a classe responsável por iniciar e gerenciar o nosso jogo, iremos instanciar a classe MainScene.

NOTA: Caso você esteja um pouco confuso com a parte do código onde tem “extends Phaser.Game e extends Phaser.Scene”, eu falo um pouco mais no final desse artigo.

Agora o nosso index.js ficará assim:

// passa as configurações do jogo
const config = {
  type: Phaser.AUTO, // preferência por WebGL, caso não, irá usar o Canvas
  width: 420, // largura da tela do jogo
  height: 462, // altura da tela do jogo
  pixelArt: true, // mantém os gráficos nítidos, sem essa propriedade o jogo fica meio "borrado"
  banner: false, // desativa o log ao iniciar o phaser (na aba console do navegador)
}

// cria uma instancia do jogo, assim que a pagina e totalmente carregada
window.onload = () => new Game(config);
 
// classe que inicia o jogo
class Game extends Phaser.Game {
  constructor(config) {
    super(config);
  }

  this.scene.add('Main', new MainScene('Main')); // adiciona essa cena ao jogo
  this.scene.start('Main'); // inicia a cena selecionada
}

// classe referente a cena principal do jogo
class MainScene extends Phaser.Scene {
  // "key" e o identificador único de uma Scene, assim não corremos riscos de ter Scenes repetidas
  constructor(key) {
    super(key);
  }
}

Note que eu adiciono a Scene ao jogo e logo depois preciso dizer qual Scene vou iniciar o jogo, podemos ter várias Scenes adicionadas ao jogo, mas sempre precisaremos informar qual é a Scene para iniciar o jogo.

Se olharmos no navegador, não mudou muita coisa, agora é hora de adicionamos um objeto ao nosso jogo.

Adicionando o background do jogo

Começamos adicionando os 4 principais métodos de uma Scene: init, preload, create e update. Irei detalhar cada uma ao decorrer da série.

Agora o nosso index.js ficará assim:

// passa as configurações do jogo
const config = {
  type: Phaser.AUTO, // preferência por WebGL, caso não, irá usar o Canvas
  width: 420, // largura da tela do jogo
  height: 462, // altura da tela do jogo
  pixelArt: true, // mantém os gráficos nítidos, sem essa propriedade o jogo fica meio "borrado"
  banner: false, // desativa o log ao iniciar o phaser (na aba console do navegador)
}

// cria uma instancia do jogo, assim que a pagina e totalmente carregada
window.onload = () => new Game(config);
 
// classe que inicia o jogo
class Game extends Phaser.Game {
  constructor(config) {
    super(config);
  }

  this.scene.add('Main', new MainScene('Main')); // adiciona essa cena ao jogo
  this.scene.start('Main'); // inicia a cena selecionada
}

// classe referente a cena principal do jogo
class MainScene extends Phaser.Scene {
  // "key" e o identificador único de uma Scene, assim não corremos riscos de ter Scenes repetidas
  constructor(key) {
    super(key);
  }

  /*
    Primeiro metodo a ser executado
    normalmente utilizamos para iniciar o estado da nossa Scene,
    como hp do jogador, ou o level que estamos
  */
  init () { }

  /*
    Segundo metodo a ser executado
    aqui nós colocamos todos os recursos do jogo, como imagens e sons para o jogo,
    note que esse metodo só ira ser finalizado quando todos os recursos forem importados para o Phaser
  */
  preload () { }

  /*
    Terceiro metodo a ser executado
    utilizamos para criar os objetos do nosso jogo, como por exemplo o jogador
    e onde ele irá ficar posicionado na tela
  */
  create () { }

  /*
    Quarto metodo a ser executado
    irá ficar em loop executando em média 60fps, basicamente e a função que 
    realmente da "vida" ao jogo, aqui colocamos a lógica de movimentação e
    colisão do jogador por exemplo.
  */
  update () { }
}

Dentro do método preload, adicionaremos a imagem do nosso background, ficando dessa forma:

preload () {
  // carregando arquivo do tipo imagem
  this.load.image('background', './src/assets/background.png');
}

Podemos separar a funcionalidade que carrega o nosso arquivo de imagem da seguinte forma:

  • this: está referenciando a MainScene
  • load: classe responsável por carregar os recursos do jogo
  • image: método responsável por carregar um arquivo do tipo imagem, possuindo 2 parâmetros principais:

1) a chave única que esse arquivo irá ter dentro do jogo

2) a localização do arquivo no nosso diretório

Note que no navegador ainda não conseguimos ver a imagem na tela, isso é obrigação do método create, então adicionaremos a funcionalidade responsável por isso, ficando dessa forma:

create () {
  // criando o objeto do tipo imagem
  const background = this.add.image(
    0, // X referente a tela do jogo
    0, // Y referente a tela do jogo
    'background', // chave da textura
  );
  // setando a origin do objeto para 0 (x, y)
  background.setOrigin(0);
}

Finalmente conseguimos ver alguma coisa na tela! a imagem de fundo já está aparecendo, podemos separar a funcionalidade que cria o nosso arquivo do tipo imagem da seguinte forma (não vou mais falar do this, pois já sabemos que ele e o MainScene) :

  • add: classe responsável por adicionar recursos do jogo na tela
  • image: método responsável por adicionar uma imagem na tela do jogo, possuindo 3 principais parâmetros:

1) Posição X referente a tela do jogo

2) Posição Y referente a tela do jogo

3) Chave da textura do jogo (lembra que carregamos uma imagem e adicionamos uma chave única para ela? e aqui que referenciamos! )

Entendendo as posições X e Y

Pode notar que quando eu criei o objeto background eu definir que a posição X e Y dele seria 0, ou seja, ele irá ser renderizado no canto esquerdo superior.

Simplificando, se adicionarmos valores maiores que 0 ao eixo X, o objeto irá ser renderizado mais para direita, se adicionarmos valores maiores que 0 no eixo Y o objeto irá ser renderizado mais para baixo, e o mesmo acontece se adicionarmos valores mais próximo de 0, no eixo X irá ir mais para esquerda, e no eixo Y irá ir mais para cima.

Posições X e Y

Entendendo Origin

Note que executo o método setOrigin para passando o parâmetro 0.

Isso fez com que o eixo do objeto fosse setado para 0, tanto na posição X, quanto na posição Y. Por padrão um objeto do tipo image vem com a origin em 0.5, ou seja, o objeto fica centralizado em seu eixo. No caso do background, mesmo eu setando a posição dele para X=0 e Y=0, caso eu não coloque o origin dele tambem para 0, ele vai ficar centralizado no seu eixo e irá ter um comportamento que nós não queremos para um background (você pode comentar o código, penas para ver como fica), a imagem abaixo pode deixar mais claro a situação:

Posições X e Y

Conclusão

Nessa primeira parte vimos como iniciar um projeto com Phaser, carregar e adicionar recursos ao jogo, nós vemos na parte 2!

Bônus

Como havia dito em alguma parte do texto acima, irei detalhar mais um pouco o que é o “extends Phaser…”

Quando desenvolvo jogos com o Phaser, costumo utilizar a sintaxe de classe, pois acho que fica mais organizado e limpo o código, quando eu crio por exemplo a classe MainScene e uso o extends, eu basicamente quero dizer que minha classe MainScene será uma filha da classe Phaser.Scene, portanto irá herdar todas as propriedades e métodos da classe Scene, o mesmo eu faço com a classe Game que herda da classe Phaser.Game, dessa forma não ficamos presos diretamente a classe principal do Phaser, então conseguimos criar por exemplo Scenes customizadas para o nosso jogo, que além de ter tudo que uma Scene do Phaser possui, irá ter mais funcionalidades que iremos adicionar.

Também deixo links úteis para você se aprofundar nos estudos: