Boa tarde, queridos colegas!
Meu nome é Alexander, sou desenvolvedor de jogos HTML5.
Em uma das empresas para as quais enviei meu currículo, fui solicitado a concluir uma tarefa de teste. Concordei e, após 1 dia, enviei como resultado o jogo desenvolvido de acordo com o TOR HTML5.

Como estou treinando em programação de jogos, bem como para um uso mais eficiente do meu código, decidi que seria útil escrever um artigo de treinamento sobre o projeto concluído. E como o teste concluído recebeu uma avaliação positiva e levou a um convite para uma entrevista, provavelmente minha decisão tem o direito de existir e, possivelmente, ajudará alguém no futuro.
Este artigo dará uma idéia da quantidade de trabalho suficiente para concluir com êxito a tarefa de teste média para a posição HTML5 do desenvolvedor. O material também pode ser de interesse para quem deseja se familiarizar com a estrutura da Phaser. E se você já estiver trabalhando com a Phaser e escrevendo em JS - veja como desenvolver um projeto no TypeScript.
Portanto, no cat, há muito código TypeScript!
1. Introdução
Damos uma breve declaração do problema.
- Vamos desenvolver um jogo HTML5 simples - um sapador clássico.
- Como principais ferramentas, usaremos o phaser 3, typescript e webpack.
- O jogo será projetado para a área de trabalho e será executado no navegador.
Fornecemos links para o projeto final.
Links para a demonstração e fonte E lembre-se da mecânica do sapador, se de repente alguém esquecer as regras do jogo. Mas, como esse é um caso improvável, as regras são colocadas sob o spoiler :)
Regras do SapperO campo de jogo consiste em células dispostas em uma mesa. Por padrão, quando o jogo é iniciado, todas as células são fechadas. Bombas são colocadas em algumas células.
Ao clicar com o botão esquerdo em uma célula fechada, ela é aberta. Se houve uma bomba em uma célula aberta, o jogo termina em derrota.
Se não havia bomba na célula, um número é exibido dentro dela, indicando o número de bombas que estão nas células vizinhas em relação à corrente aberta. Se não houver bombas por perto, a célula parecerá vazia.
Clicar com o botão direito do mouse em uma célula fechada define uma sinalização nela. A tarefa do jogador é organizar todas as bandeiras disponíveis para que ele marque todas as células minadas. Depois de colocar todas as bandeiras, o jogador pressiona o botão esquerdo do mouse em uma das células abertas para verificar se ganhou.
Em seguida, vamos diretamente ao próprio manual. Todo o material é dividido em pequenas etapas, cada uma das quais descreve a implementação de uma tarefa específica em um curto espaço de tempo. Assim, realizando pequenos objetivos passo a passo, no final, criaremos um jogo completo. Use o índice se decidir ir rapidamente para uma etapa específica.
1. Preparação
1.1 Modelo de Projeto
Faça o download do
modelo de projeto phaser padrão . Este é o modelo recomendado pelo autor da estrutura e oferece a seguinte estrutura de diretórios:
Para o nosso projeto, não precisamos do arquivo
index.js
atual; portanto, exclua-o. Em seguida, crie o diretório
/src/scripts/
e coloque o arquivo
index.ts
vazio nele. Adicionaremos todos os nossos scripts a esta pasta.
Também é importante ter em mente que, ao criar um projeto para produção, um diretório
dist
será criado na raiz, no qual a compilação do release será colocada.
1.2 Configuração da compilação
Usaremos o webpack para montagem. Como nosso modelo foi originalmente preparado para trabalhar com JavaScript e escrevemos no TypeScript, precisamos fazer pequenas alterações na configuração do coletor.
No
webpack/base.js
adicione a chave de
entry
, que indica o ponto de entrada ao criar nosso projeto, bem como a configuração do
ts-loader
que descreve as regras para a criação de scripts TS:
Também precisaremos criar o arquivo tsconfig.json na raiz do projeto. Para mim, tem o seguinte conteúdo:
{ "compilerOptions": { "module": "commonjs", "lib": [ "dom", "es5", "es6", "es2015", "es2017", "es2015.promise" ], "target": "es5", "skipLibCheck": true }, "exclude": ["node_modules", "dist"] }
1.3 Instalando módulos
Instale todas as dependências do package.json e adicione os módulos typescript e ts-loader a eles:
npm i npm i typescript --save-dev npm i ts-loader --save-dev
Agora o projeto está pronto para iniciar o desenvolvimento. Temos 2 comandos à nossa disposição que já estão definidos na propriedade
scripts
no arquivo
package.json
.
- Crie um projeto para depuração e abra em um navegador por meio de um servidor local
npm start
- Execute a compilação para venda e coloque a compilação de lançamento na pasta dist /
npm run build
1.4 Preparação de ativos
Todos os recursos deste jogo são honestamente baixados do
OpenGameArt (versão 61x61) e possuem as licenças mais amigáveis, denominadas
Sinta-se livre para usar , o que a página com o pacote nos informa cuidadosamente). A propósito, o código apresentado no artigo tem a mesma licença! ;)
Eu apaguei a imagem do relógio do conjunto baixado e renomeei o restante dos arquivos para obter nomes de quadros fáceis de usar. A lista de nomes e os arquivos correspondentes são exibidos na tela abaixo.
A partir dos sprites resultantes, criaremos um atlas do formato
Phaser JSONArray
no programa
TexturePacker (há uma versão gratuita mais do que suficiente, ainda não consegui trabalhar) e colocaremos os arquivos
spritesheet.json
e
spritesheet.png
gerados no diretório
src/assets/
project.

2. Criando cenas
2.1 Ponto de entrada
Iniciamos o desenvolvimento criando o ponto de entrada descrito na configuração do webpack.
Como o jogo que temos foi projetado para a área de trabalho e preencherá a tela inteira, usamos com ousadia toda a largura e altura do navegador nos campos de
width
e
height
.
Atualmente, o campo de
scene
é um array vazio e vamos corrigi-lo!
2.2 Cena Inicial
Crie a classe da primeira cena no
src/scripts/scenes/StartScene.ts
:
export class StartScene extends Phaser.Scene { constructor() { super('Start'); } public preload(): void { } public create(): void { } }
Para herança válida de
Phaser.Scene
passamos o nome da cena como um parâmetro para o construtor da classe pai.
Esta cena combinará a funcionalidade de pré-carregamento de recursos e a tela inicial, convidando o usuário ao jogo.
Normalmente, em meus projetos, um jogador passa por duas cenas antes de chegar à inicial, nesta ordem:
Boot => Preload => Start
Mas, neste caso, o jogo é tão simples, e há tão poucos recursos que não há razão para colocar a pré-carga em uma cena separada, e mais ainda o carregador de
Boot
inicial separado.
Carregaremos todos os ativos no método de
preload
-
preload
. Para poder trabalhar com o atlas criado no futuro, precisamos executar 2 etapas:
- obtenha os arquivos
png
e json
atlas usando o require
:
- carregue-os no método de
preload
- preload
da cena inicial:
2.3 Textos da cena inicial
Há duas coisas a serem feitas na cena inicial:
- diga ao jogador como iniciar o jogo
- iniciar o jogo por iniciativa do jogador
Para cumprir o primeiro ponto, primeiro crie duas enumerações no início do arquivo de cena para descrever os textos e seus estilos:
E, em seguida, crie os dois textos como objetos no método
create
. Deixe-me lembrá-lo de que o método de
create
de cenas na
Phaser
será chamado somente após o carregamento de todos os recursos no método de
preload
-
preload
, e isso é bastante adequado para nós.
Em outro projeto maior, poderíamos levar os textos e os estilos para arquivos json locale ou para configurações separadas, mas, como agora temos apenas duas linhas, considero essa etapa redundante e, neste caso, sugiro não complicar nossas vidas, nos limitando a listagens no início do arquivo de cena.
2.4 Transição para o nível do jogo
A última coisa que faremos nesta cena antes de prosseguir é rastrear o evento de clique do mouse para iniciar o jogador no jogo:
Cena de 2,5 níveis
A julgar pelo parâmetro
"Game"
passado para o método
this.scene.start
você já imaginou que era hora de criar uma segunda cena, que processaria a lógica principal do jogo. Crie o
src/scripts/scenes/GameScene.ts
:
export class GameScene extends Phaser.Scene { constructor() { super('Game'); } public create(): void { } }
Nesta cena, não precisamos do método de
preload
-
preload
, porque já carregamos todos os recursos necessários na cena anterior.
2.6 Configurando cenas no ponto de entrada
Agora que as duas cenas foram criadas, adicione-as ao nosso ponto de entrada
src/scripts/index.ts
:
3. Objetos do jogo
Portanto, a classe
GameScene
implementará a lógica no nível do jogo. E o que esperamos do nível do jogo de sapadores? Visualmente, esperamos ver um campo de jogo com células fechadas. Sabemos que o campo é uma tabela, o que significa que possui um determinado número de linhas e colunas, em várias das quais bombas são confortavelmente colocadas. Assim, temos informações suficientes para criar uma entidade separada que descreve o campo de jogo.
3.1 Tabuleiro de jogo
Crie o
src/scripts/models/Board.ts
no qual colocamos a classe
Board
:
import { Field } from "./Field"; export class Board extends Phaser.Events.EventEmitter { private _scene: Phaser.Scene = null; private _rows: number = 0; private _cols: number = 0; private _bombs: number = 0; private _fields: Field[] = []; constructor(scene: Phaser.Scene, rows: number, cols: number, bombs: number) { super(); this._scene = scene; this._rows = rows; this._cols = cols; this._bombs = bombs; this._fields = []; } public get cols(): number { return this._cols; } public get rows(): number { return this._rows; } }
Vamos fazer da classe o sucessor do Phaser.Events.EventEmitter para acessar a interface para registrar e chamar eventos, que precisaremos no futuro.
Uma matriz de objetos da classe
Field
será armazenada na propriedade privada
_fields
. Implementaremos esse modelo mais tarde.
Configuramos as propriedades numéricas privadas
_rows
e
_cols
para indicar o número de linhas e colunas do campo de jogo. Crie getters públicos para ler
_rows
e
_cols
.
O campo
_bombs
nos diz o número de bombas que precisarão ser geradas para o nível. E no parâmetro
_scene
passamos uma referência ao objeto da cena do jogo
GameScene
, na qual criaremos uma instância da classe
Board
.
É importante notar que transferimos o objeto de cena para o modelo apenas para transmissão posterior às vistas, onde o usaremos apenas para exibir a vista. O fato é que o phaser usa diretamente o objeto de cena para renderizar sprites e, portanto, obriga a fornecer um link para a cena atual ao criar pré-fabricados para sprites, que iremos desenvolver no futuro. E por nós mesmos, aceitaremos o acordo de que transferiremos o link para a cena apenas para uso posterior como um mecanismo de exibição e concordaremos que não chamaremos diretamente os métodos personalizados da cena em modelos e visualizações.
Depois de decidirmos sobre a interface de criação do tabuleiro, proponho inicializá-la na cena do nível, finalizando a classe
GameScene
:
Tomamos os parâmetros do quadro para constantes no início do arquivo de cena e os passamos para o construtor do
Board
ao criar uma instância desta classe.
3.2 Modelo Celular
O quadro consiste em células, que você deseja exibir na tela. Cada célula deve ser colocada na posição correspondente, determinada pela linha e coluna.
As células também são selecionadas como uma entidade separada. Crie o
src/scripts/models/Field.ts
no qual colocaremos a classe que descreve a célula:
import { Board } from "./Board"; export class Field extends Phaser.Events.EventEmitter { private _scene: Phaser.Scene = null; private _board: Board = null; private _row: number = 0; private _col: number = 0; constructor(scene: Phaser.Scene, board: Board, row: number, col: number) { super(); this._init(scene, board, row, col); } public get col(): number { return this._col; } public get row(): number { return this._row; } public get board(): Board { return this._board; } private _init(scene: Phaser.Scene, board: Board, row: number, col: number): void { this._scene = scene; this._board = board; this._row = row; this._col = col; } }
Cada célula deve ter métricas de linha e coluna em que está localizada. Nós configuramos os parâmetros
_board
e
_scene
para definir links para objetos do quadro e da cena. Implementamos getters para ler os
_row
,
_col
e
_board
.
3.3 Visualização de célula
A célula abstrata é criada e agora queremos visualizá-la. Para exibir uma célula na tela, você precisa criar sua exibição. Crie o
src/scripts/views/FieldView.ts
e coloque a classe de exibição nele:
import { Field } from "../models/Field"; export class FieldView extends Phaser.GameObjects.Sprite { private _model: Field = null; constructor(scene: Phaser.Scene, model: Field) { super(scene, 0, 0, 'spritesheet', 'closed'); this._model = model; this._init(); this._create(); } private _init(): void { } private _create(): void { } }
Observe que fizemos dessa classe o descendente de
Phaser.GameObjects.Sprite
. Em termos de fases, essa classe se tornou uma pré-fabricada de sprites. Ou seja, obtive a funcionalidade do objeto de jogo do sprite, que expandiremos ainda mais com nossos próprios métodos.
Vejamos o construtor desta classe. Aqui, primeiro, devemos chamar o construtor da classe pai com os seguintes conjuntos de parâmetros:
- link para o objeto de cena (como avisei na seção 3.1: phaser exige que vinculemos à cena atual para renderizar sprites)
- coordenadas
x
e y
na tela - a chave da string para a qual o atlas está disponível, que carregamos no método de
preload
- preload
da cena inicial - a chave de sequência de quadros neste atlas que você deseja selecionar para exibir o sprite
Defina uma referência ao modelo (ou seja, uma instância da classe
Field
) na propriedade
_model
privada.
Também prudentemente iniciamos 2
_create
_init
e
_create
atualmente vazios, que implementaremos um pouco mais tarde.
3.4 Criando um sprite em uma classe de exibição
Então, a visão foi criada, mas ela ainda não sabe desenhar um sprite. Para colocar o sprite com o quadro que precisamos na tela, você precisará modificar nosso próprio método
_create
privado:
3.5 Posicionamento de Sprite
No momento, todos os sprites criados serão colocados nas coordenadas (0, 0) da tela. Também precisamos colocar cada célula em sua posição correspondente no quadro. Ou seja, para o local que corresponde à linha e coluna desta célula. Para fazer isso, precisamos escrever um código para calcular as coordenadas de cada instância da classe
FieldView
.
Adicione a propriedade
_position
à classe, responsável pelas coordenadas finais da célula no campo de jogo:
Como queremos alinhar a placa e, consequentemente, as células nela, em relação ao centro da tela, também precisamos da propriedade
_offset
, indicando o deslocamento dessa célula específica em relação às bordas esquerda e superior da tela. Adicione-o com um getter privado:
Assim, nós:
- Tem a largura total da tela em
this._scene.cameras.main.width
. - Obtivemos a largura total do quadro multiplicando o número de células pela largura de uma célula:
this._board.cols * this.width
. - Tirando a largura da placa da largura da tela, conseguimos um lugar na tela, não ocupado pela placa.
- Dividindo o número resultante por 2, obtivemos o valor do recuo à esquerda e à direita do quadro.
- Ao mudar cada célula pelo valor desse recuo, garantimos o alinhamento de toda a placa ao longo do eixo
x
.
Realizamos ações absolutamente semelhantes para obter deslocamento vertical.
Resta adicionar o código necessário no método
_init
:
As propriedades
this.x
,
this.y
,
this.width
e
this.height
aqui são as propriedades herdadas da classe pai
Phaser.GameObjects.Sprite
. Alterar as propriedades de
this.x
e
this.y
leva ao posicionamento correto do sprite na tela.
3.6 Criando uma instância do FieldView
Crie uma exibição na classe
Field
:
3.7 Exibir campos do quadro.
Vamos voltar à classe
Board
, que é essencialmente uma coleção de objetos
Field
e criará células.
_create
código de criação da placa em um método
_create
separado e chamaremos esse método do construtor. Sabendo que no método
_create
não apenas criaremos células,
_createFields
o código para criar células em um método
_createFields
separado.
É nesse método que criaremos o número desejado de células em um loop aninhado:
É hora da primeira vez de executar o assembly para depuração com o comando
npm start
Certifique-se de que, no centro da tela, esperamos ver 64 células em 8 linhas.
3.8 Fazendo bombas
Anteriormente, relatei que no método
_create
da classe
Board
, não apenas criaremos campos. O que mais? Também haverá a criação de bombas e a definição das células criadas para o número de bombas vizinhas. Vamos começar pelas próprias bombas.
Precisamos colocar N bombas no tabuleiro em células aleatórias. Descrevemos o processo de criação de bombas com um algoritmo aproximado:
A cada iteração do loop, obteremos uma célula aleatória da propriedade
this._fields
até criarmos tantas bombas quanto indicado no campo
this._bombs
. Se a célula recebida estiver vazia, instalaremos uma bomba nela e atualizaremos o contador das bombas necessárias para a geração.
Para gerar um número aleatório, usamos o método estático
Phaser.Math.Between
.
Não esqueça de escrever a chamada para
this._createBombs();
no arquivo
Board.ts
this._createBombs();
no final do método
_create
Como você já percebeu, para que esse código funcione corretamente, você precisa refinar a classe
Field
adicionando o getter
empty
e o método
setBomb
a
setBomb
.
Adicione um campo
_value
privado à
_value
Field, que regulará o conteúdo da célula. Aceitamos os seguintes acordos.
Seguindo essas regras, desenvolveremos métodos na classe
Field
que funcionam com a propriedade
_value
:
3.9 Valores de configuração
As bombas estão organizadas e agora temos todos os dados para definir os valores numéricos em todas as células que precisam dele.
Deixe-me lembrá-lo de que, de acordo com as regras do sapador, a célula deve ter o número que corresponde ao número de bombas localizadas próximas a essa célula. Com base nessa regra, escrevemos o pseudocódigo correspondente.
Na classe
Board
, crie um novo método e traduza o pseudocódigo especificado em código real:
Vamos ver qual das interfaces que usamos não está implementada. Você precisa adicionar o método
getClosestFields
para obter as células vizinhas.
Como identificar células vizinhas?
Por exemplo, considere qualquer célula da placa que não esteja na borda, ou seja, não na linha extrema nem na coluna extrema. Essas células têm um número máximo de vizinhos: 1 na parte superior, 1 na parte inferior, 3 à esquerda e 3 à direita (incluindo células na diagonal).
Assim, em cada uma das células vizinhas, os indicadores
_row
e
_col
não diferem em mais de 1. Isso significa que podemos especificar antecipadamente a diferença entre os parâmetros
_row
e
_col
com o campo atual. Adicione uma constante no início do arquivo à descrição da classe:
E agora podemos adicionar o método ausente, no qual percorreremos esse array:
Não se esqueça de verificar a variável de
field
a cada iteração, pois nem todas as células na placa têm 8 vizinhos. Por exemplo, a célula superior esquerda não terá vizinhos à sua esquerda e assim por diante.
Resta implementar o método
getField
e adicionar todas as chamadas necessárias ao método
_create
na classe
Board
4. Manipulando Eventos de Entrada
4.1 Rastreando eventos de clique do mouse
No momento, o painel está completamente inicializado, possui bombas e há células com números, mas todas estão atualmente fechadas e não há como abri-las. Vamos corrigir isso e implementar a abertura de células clicando no botão esquerdo do mouse.Primeiro, precisamos rastrear esse mesmo clique. Na classe, FieldView
adicione o _create
seguinte código ao final do método :
No phaser, você pode se inscrever em objetos do namespace para diferentes eventos Phaser.GameObjects
. Em particular, assinaremos o evento click ( pointerdown
), a pré-fabricada do próprio sprite, ou seja, um objeto de uma classe FieldView
herdada Phaser.GameObjects.Sprite
.Porém, antes de fazer isso, devemos indicar explicitamente que o sprite é potencialmente interativo, ou seja, você geralmente precisa ouvir as informações do usuário. Você precisa fazer isso chamando o método setInteractive
sem parâmetros no próprio sprite, como fizemos no exemplo acima.Agora que nosso sprite se tornou interativo, voltemos à classe Board
no local em que novos objetos de modelo são criados Field
, ou seja, ao método _createFields
e registramos o retorno de chamada para eventos de entrada para a exibição:
Depois de estabelecermos que, clicando no sprite, queremos executar o método _onFieldClick
, precisamos implementá-lo. Mas removeremos a lógica de processar o clique da classe Board
. Acredita-se que é melhor processar o modelo dependendo da entrada e alterar seus dados em um controlador separado, cuja similaridade temos é a classe da cena do jogo GameScene
. Portanto, precisamos encaminhar o evento de clique ainda mais, da classe Board
para a própria cena. Então vamos fazer:
Aqui não estamos apenas lançando o evento click como estava, mas também especificando qual clique foi. Isso será útil no futuro, quando na aula de cena processaremos cada opção de maneira diferente. Obviamente, seria possível enviar o evento click como está, mas simplificaremos o código da cena, deixando parte da lógica do evento em si na classe Field
.Bem, agora vamos voltar à classe da cena do jogo GameScene
e adicionar um _create
código no final do método que rastreia eventos de um clique nas células:
4.2 Processamento do clique esquerdo
Prosseguimos para implementar o processamento de eventos de clique do mouse. E comece abrindo as células. As células devem ser abertas pressionando o botão esquerdo. E antes de começarmos a programar, vamos expressar as condições que devem ser atendidas:- ao clicar em uma célula fechada, ela deve ser aberta
- se houver uma mina em uma célula aberta - o jogo está perdido
- se não houver minas ou valores na célula aberta, min não estará nas células vizinhas; nesse caso, você precisará abrir todas as células vizinhas e continuar fazendo isso até que o valor apareça na célula aberta
- quando você clica em uma célula aberta, deve verificar se todas as bandeiras estão definidas corretamente e, se sim, terminar o jogo com uma vitória
E agora, para simplificar o entendimento da funcionalidade necessária, traduzimos a lógica acima em pseudo-código:
Agora temos uma compreensão do que precisa ser programado. Implementamos o método _onFieldClickLeft
:
E então, como sempre, finalizaremos as classes Field
e Board
implementaremos os métodos que chamamos no manipulador.Indicamos 3 estados possíveis da célula na enumeração States
, adicionamos um campo _state
e implementamos um getter para cada estado possível:
Agora que temos estados indicando se a célula está fechada ou não, podemos adicionar um método open
que alterará o estado:
Cada alteração no estado do modelo deve acionar um evento que relate isso. Portanto, introduzimos um método privado adicional _setState
no qual toda a lógica da mudança de estado será implementada. Este método será chamado em todos os métodos públicos do modelo, que devem mudar de estado.Adicione um sinalizador booleano _exploded
para indicar explicitamente exatamente o objeto Field que foi explodido:
Agora abra a classe Board
e implemente o método nela openClosestFields
. Este método é recursivo e sua tarefa será abrir todos os campos vizinhos vazios em relação à célula aceita no parâmetro.O algoritmo será o seguinte: :
E desta vez já temos todas as interfaces necessárias para a implementação completa deste método:
Adicione um getter completed
à classe Board
para indicar o posicionamento correto das bandeiras no quadro. Como podemos determinar se um quadro foi liberado com sucesso? O número de campos marcados corretamente deve ser igual ao número total de bombas no tabuleiro.
Este método filtra a matriz _fields
pelo getter completed
, o que deve indicar a validade da marca do campo. Se o comprimento da matriz filtrada (na qual somente os campos marcados corretamente caírem, pelos quais o getter completed
já é responsável pela classe Field
) for igual ao valor do campo _bombs
(ou seja, o número de bombas no tabuleiro), então retornaremos true
, em outras palavras, consideraremos o jogo ganho.Também não nos importamos com a oportunidade de abrir todo o quadro com uma chamada, o que temos que fazer no final do nível. Também adicionaremos esse recurso à classe Board
:
Resta adicionar um getter completed
à própria classe Field
. Nesse caso, o campo será considerado limpo com êxito? Se for extraído e sinalizado. Ambos os getters necessários já estão lá e podemos adicionar este método:
Para concluir o processamento do clique esquerdo do mouse, criaremos um método _onGameOver
no qual desativamos o rastreamento de eventos do tabuleiro e mostraremos ao jogador o tabuleiro inteiro. Mais tarde, nós também adicionar o código para a prestação de relatórios sobre o estado de conclusão do nível com base no parâmetro status
.
4.3 Exibição em campo
Antes de começar a processar o clique com o botão direito, aprenderemos como redesenhar as células recém-abertas.Anteriormente, Field
desenvolvemos um método _setState
que dispara um evento change
quando o estado do modelo é alterado. Usaremos isso e, na classe, FieldView
rastrearemos este evento:
Tornamos especificamente o método intermediário um _onStateChange
retorno de chamada do evento de alteração do modelo. No futuro, precisaremos verificar como o modelo foi alterado para entender se é necessário executar _render
.Para mostrar o sprite atual de uma célula em um novo estado, você precisa alterar seu quadro. Como carregamos o atlas como ativo, podemos chamar o método setFrame
para alterar o quadro atual para um novo.Para obter o quadro em uma linha, usamos astuciosamente o getter _frameName
, que agora precisa ser implementado. Primeiro, descrevemos todos os valores possíveis que um quadro de célula pode assumir.Temos uma descrição de todos os estados e já temos todos os métodos do modelo, graças aos quais esses estados podem ser obtidos. Vamos fazer uma pequena configuração no início do arquivo:
As chaves neste objeto serão os valores dos quadros e os valores dessas chaves são os retornos de chamada que retornam um resultado booleano. Com base nessa configuração, podemos desenvolver um método para obter o quadro desejado (ou seja, a chave da configuração):
Assim, por enumeração simples em um loop, examinamos todas as chaves do objeto de configuração e chamamos cada retorno de chamada por vez. A função que nos retorna primeiro true
indica que a chave key
na iteração atual é o quadro correto para o estado atual do modelo.Se nenhuma chave for adequada, para o estado padrão, consideraremos um campo aberto com um valor _value
, pois States
não definimos esse estado na configuração .Agora podemos testar completamente o clique esquerdo nos campos do quadro e verificar como as células se abrem e o que é exibido depois de abri-las.4.4 Processamento com o botão direito
Como no caso de criar o manipulador com o botão esquerdo, primeiro definimos claramente a funcionalidade esperada. Ao clicar com o botão direito do mouse, devemos marcar a célula selecionada com um sinalizador. Mas existem certas condições.- Apenas um campo fechado que não está marcado no momento pode ser sinalizado
- Se o campo estiver marcado, clique novamente com o botão direito do mouse para remover o sinalizador do campo
- Ao definir / remover um sinalizador, é necessário atualizar o número de sinalizadores disponíveis no nível e exibir o texto com o número atual
Traduzindo essas condições em pseudo-código, obtemos as seguintes linhas de comentários:
Agora podemos traduzir esse algoritmo em chamadas para os métodos de que precisamos, mesmo que eles ainda não tenham sido desenvolvidos:
Aqui também iniciamos um novo campo _flags
, que no início do nível do jogo é igual ao número de bombas no tabuleiro, pois no início do jogo nenhuma bandeira foi definida. Esse campo é forçado a ser atualizado a cada clique direito, pois nesse caso o sinalizador é adicionado ou removido do quadro. Adicione um Board
getter à classe countMarked
:
Definir e remover o sinalizador é uma alteração no estado do modelo Field
; portanto, implementamos esses métodos na classe correspondente de maneira semelhante ao método open
:
Deixe-me lembrá-lo de que ele _setState
acionará um evento change
que é rastreado na exibição e, portanto, o sprite será redesenhado automaticamente desta vez quando o modelo for alterado.Ao testar a funcionalidade desenvolvida, você certamente descobrirá que sempre que clicar no botão direito do mouse, um menu de contexto é aberto. Adicione o código que desativa esse comportamento ao construtor da cena do jogo:
4.5 Objeto GameSceneView
Para exibir a interface do usuário na cena do jogo, criaremos uma classe GameSceneView
e a colocaremos src/scripts/views/GameSceneView.ts
.Nesse caso, agiremos de maneira diferente da criação FieldView
e não faremos desta classe um pré-fabricado e um herdeiro GameObjects
.Nesse caso, precisamos gerar os seguintes elementos da exibição da cena:- texto no número de sinalizadores
- botão sair
- Mensagem de status de conclusão do jogo (vitória / derrota)
Vamos transformar cada elemento da interface do usuário em um campo separado na classe GameSceneView
.Vamos preparar um esboço. enum Styles { Color = '#008080', Font = 'Arial' } enum Texts { Flags = 'FLAGS: ', Exit = 'EXIT', Success = 'YOU WIN!', Failure = 'YOU LOOSE' }; export class GameSceneView { private _scene: Phaser.Scene = null; private _style: {font: string, fill: string}; constructor(scene: Phaser.Scene) { this._scene = scene; this._style = {font: `28px ${Styles.Font}`, fill: Styles.Color}; this._create(); } private _create(): void { } public render() { } }
Adicione texto com o número de sinalizadores.
Esse código colocará o texto que precisamos em uma posição recuada 50px dos lados superior e esquerdo e o definirá no estilo especificado. Além disso, o método setOrigin
define o ponto de articulação do texto para as coordenadas (0, 1). Isso significa que o texto ficará alinhado com sua borda esquerda.Adicione uma mensagem de status.
Colocamos o texto de status no centro da tela e o alinhamos com o meio da linha, chamando setOrigin
com o parâmetro 0.5 para a coordenada x. Além disso, por padrão, esse texto precisa ser oculto, pois somente o mostraremos após a conclusão do jogo.Crie um botão de saída, que em essência também é um objeto de texto.
Colocamos o botão no canto superior direito da tela e o usamos novamente setOrigin
para alinhar o texto dessa vez com a borda direita. Tornamos o botão interativo e adicionamos um retorno de chamada ao evento click, que envia o player para a cena inicial. Assim, damos ao jogador a oportunidade de sair do nível a qualquer momento.Resta desenvolver um método render
para atualizar corretamente todos os elementos da interface do usuário e adicionar chamadas a todos os métodos criados no _create
.
Dependendo da propriedade transmitida no parâmetro, atualizamos a interface do usuário, exibindo as alterações necessárias.Crie uma representação na cena do jogo na classe GameScene e escreva a chamada no método _render sempre que necessário, por significado:
5. Animações
Que tipo de fã criando um jogo, mesmo que simples como o nosso, se não houver animações ?! Além disso, desde que começamos a estudar o phaser, vamos nos familiarizar com os recursos mais básicos das animações e considerar a funcionalidade dos gêmeos. Gêmeos são implementados na própria estrutura e nenhuma biblioteca de terceiros é necessária.Adicione 2 animações ao jogo: encher o tabuleiro com células no início e virar a célula na abertura. Vamos começar com o primeiro deles.5.1 Animação de preenchimento de quadro
Garantimos que todas as células da placa voem no lugar a partir da borda superior esquerda da tela. Ao iniciar o nível do jogo, precisamos mudar todas as células para o canto superior esquerdo da tela e para cada célula iniciar a animação do movimento para as coordenadas correspondentes.Na classe, FiledView
adicione a _create
chamada ao final dos métodos _animateShow
:
B implementamos o novo método que precisamos. Nele, como concordamos acima, é necessário executar duas coisas:- mova a célula para trás do canto superior esquerdo para que não fique visível na tela
- inicie o movimento duplo com as coordenadas desejadas com o atraso correto
Como o canto superior esquerdo da tela possui coordenadas (0, 0), se definirmos a célula com as coordenadas iguais aos seus valores negativos de largura e altura, isso colocará a célula atrás do canto superior esquerdo e ocultará a tela. Assim, concluímos nossa primeira tarefa.O segundo objetivo a alcançar, chamando o método _moveTo
.
Para criar uma animação, usamos a propriedade scene tweens
. No método dele, add
passamos o objeto de configuração com as configurações:- A propriedade
targets
aqui deve conter como valor os objetos de jogo aos quais você deseja aplicar efeitos de animação. No nosso caso, este é um link this
para o objeto atual, pois é uma pré-fabricada do sprite. - O segundo e o terceiro parâmetros passam pelas coordenadas do destino.
- A propriedade
duration
é responsável pela duração da animação, no nosso caso - 600ms. - Parâmetros
ease
e easeParams
defina a função de atenuação. - No campo atraso, substituímos o valor do segundo argumento, gerado para cada célula individual, levando em consideração sua posição no quadro. Isso é feito para que as células não voem ao mesmo tempo. Em vez disso, cada célula aparecerá com um pequeno atraso em relação à anterior.
- Por fim,
onComplete
colocamos um retorno de chamada na propriedade , que será chamado no final da ação de interpolação.
É razoável envolver o gêmeo em uma promessa para que, no futuro, seja capaz de atracar maravilhosamente diferentes animações, para que, no retorno de chamada, façamos uma chamada de função resolve
indicando a execução bem-sucedida da animação.5.2 Animações de flip de célula
Será ótimo se, quando a célula foi aberta, o efeito de sua reversão foi reproduzido. Como podemos conseguir isso?A abertura de uma célula é atualmente realizada alterando o quadro quando o método é chamado _render
na exibição. Se verificarmos o estado do modelo neste método, veremos se a célula estava aberta. Se a célula estiver aberta, inicie a animação em vez de exibir instantaneamente um novo quadro de reversão.
Para obter o efeito desejado, utilizar transformação do sprite através da propriedade scale
. Se escalarmos o sprite ao longo do eixo x
para zero com o tempo , ele diminuirá, conectando os lados esquerdo e direito. E vice-versa, se você escalar o sprite ao longo do eixo x
de zero a sua largura total, o esticaremos para o tamanho máximo. Nós implementar esta lógica no método _animateFlip
.
Por analogia com o método, _moveTo
implementamos _scaleTo
:
Neste método, como parâmetro, pegamos o valor da escala, que usaremos para alterar o tamanho do sprite em ambas as direções e passá-lo como um segundo parâmetro para o objeto de configuração da animação. Todos os outros parâmetros de configuração já nos são familiares da animação anterior.Agora iniciaremos o projeto para teste e, após a depuração, consideraremos nosso jogo concluído e a tarefa de teste concluída! :)
Agradeço sinceramente a todos por terem chegado a este momento comigo!Conclusão
Colegas, ficarei muito satisfeito se o material apresentado no artigo for útil para você e você pode usar essas ou as abordagens descritas em seus próprios projetos. Você sempre pode recorrer a mim com qualquer pergunta, tanto neste artigo quanto na programação fasorial ou no gamedev em geral. Congratulo-me com a comunicação e ficarei feliz em fazer novos conhecidos e trocar experiências!E eu tenho uma pergunta para você agora. Desde que estou criando tutoriais em vídeo sobre desenvolvimento de jogos, naturalmente acumulei uma dúzia desses pequenos jogos. Cada jogo abre a estrutura à sua maneira. Por exemplo, neste jogo, abordamos o tema dos gêmeos, mas existem muitos outros recursos, como física, mapa de peças, coluna, etc.A esse respeito, a pergunta é: você gostou deste artigo e, em caso afirmativo, estaria interessado em continuar a ler artigos como este, mas sobre outros pequenos jogos clássicos? Se a resposta for afirmativa, traduzirei com prazer os materiais dos meus tutoriais em vídeo para o formato de texto e continuarei publicando novos manuais ao longo do tempo, mas para outros jogos. Eu trago a pesquisa correspondente.Obrigado a todos pela atenção! Terei o maior prazer em feedback e até breve!