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 pngejsonatlas usando orequire:
 
   
 
- carregue-os no método de preload-preloadda 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 xeyna tela
- a chave da string para a qual o atlas está disponível, que carregamos no método de preload-preloadda 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 
_createComo 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, FieldViewadicione o _createseguinte 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 FieldViewherdada 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 setInteractivesem parâmetros no próprio sprite, como fizemos no exemplo acima.Agora que nosso sprite se tornou interativo, voltemos à classe Boardno local em que novos objetos de modelo são criados Field, ou seja, ao método _createFieldse 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 Boardpara 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 GameScenee adicionar um _createcó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 Fielde Boardimplementaremos os métodos que chamamos no manipulador.Indicamos 3 estados possíveis da célula na enumeração States, adicionamos um campo _statee 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 openque alterará o estado: 
Cada alteração no estado do modelo deve acionar um evento que relate isso. Portanto, introduzimos um método privado adicional _setStateno 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 _explodedpara indicar explicitamente exatamente o objeto Field que foi explodido: 
Agora abra a classe Boarde 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 Boardpara 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 _fieldspelo 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 completedjá é 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 _onGameOverno 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, Fielddesenvolvemos um método _setStateque dispara um evento changequando o estado do modelo é alterado. Usaremos isso e, na classe, FieldViewrastrearemos este evento: 
Tornamos especificamente o método intermediário um _onStateChangeretorno 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 setFramepara 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 trueindica que a chave keyna 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 Statesnã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 Boardgetter à 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 _setStateacionará um evento changeque é 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 GameSceneViewe a colocaremos src/scripts/views/GameSceneView.ts.Nesse caso, agiremos de maneira diferente da criação FieldViewe 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 classeGameSceneView.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 setOrigindefine 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 setOrigincom 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 setOriginpara 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 renderpara 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,
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, FiledViewadicione a _createchamada 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, addpassamos o objeto de configuração com as configurações:- A propriedade targetsaqui deve conter como valor os objetos de jogo aos quais você deseja aplicar efeitos de animação. No nosso caso, este é um linkthispara 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 easeeeaseParamsdefina 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, onCompletecolocamos 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çãoresolveindicando 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
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 _renderna 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 xpara zero com o tempo , ele diminuirá, conectando os lados esquerdo e direito. E vice-versa, se você escalar o sprite ao longo do eixo xde 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, _moveToimplementamos _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!