Todo tutorial de desenvolvimento de jogos começa da mesma maneira: retângulos coloridos deslizando por uma tela cinza. Isso é bom para aprender sintaxe, mas não ensina nada sobre como um motor de jogo realmente "respira". Eu queria sair do negócio dos retângulos. Eu queria construir algo que parecesse um jogo de verdade — um dungeon crawler com visão aérea, com movimentação, combate, um HUD e som.
Eu me comprometi com o Phaser v4 e impus a mim mesmo uma regra rígida. Zero assets externos. Sem arquivos de imagem, sem clipes de áudio e sem ferramentas de build como Webpack ou Vite. O projeto inteiro precisava viver dentro de um único arquivo HTML escrito com JavaScript puro. Essa restrição não era sobre minimalismo por si só. Era sobre remover cada desculpa e cada "caixa preta". Quando você não pode baixar um pacote de sprites para cobrir uma lacuna no seu conhecimento, você é forçado a aprender como o motor gerencia texturas, animações, áudio e estado nos bastidores.
Desenhando o Mundo através de Código
Em um projeto normal de Phaser, você chama this.load.image() dentro de uma função de preload e aponta o motor para um arquivo PNG. Sem essa opção, você recorre ao objeto Graphics. Você o instancia, desenha formas primitivas — retângulos para os azulejos do chão, traços mais grossos para as paredes, talvez um círculo para o marcador do jogador — e então chama generateTexture. Esse método captura o buffer de gráficos e o registra no gerenciador de texturas do Phaser sob uma chave que você escolher.
A partir desse momento, o motor trata esse bitmap gerado exatamente como um arquivo de imagem carregado. Você pode atribuí-lo a tilemaps, fatiá-lo em sprites ou aplicar um matiz (tint). Para o dungeon crawler, isso significou que eu poderia gerar proceduralmente uma grade de chão, carimbar segmentos de parede e iterar na paleta de cores sem nunca sair do meu editor de código. A lição prática é que uma textura é apenas um pedaço de dados bitmap residindo na memória. O Phaser não se importa se ela chegou via requisição HTTP ou por uma chamada Graphics escrita à mão.
Essa abordagem também faz você pensar deliberadamente sobre a ordem de desenho e o batching. Quando cada parede e azulejo de chão vem da mesma família de texturas geradas, você começa a prestar atenção em como o Phaser agrupa as chamadas de renderização. Você percebe a diferença entre uma camada de tilemap estática e um spritemap de objetos individuais porque você está decidindo manualmente qual merece existir como uma instância de textura.
Animando sem Spritesheets
Quadrados estáticos tornam-se monótonos rapidamente, mas o sistema de animação do Phaser espera uma spritesheet — geralmente um único PNG com quadros dispostos em uma grade. Eu repliquei essa tira usando um elemento HTML canvas fora da tela (offscreen). Para cada quadro de uma animação, eu limpava o canvas e desenhava uma nova pose: um simples balanço de espada, um ciclo de caminhada de dois passos ou um movimento de espera (idle) de um inimigo. Assim que a tira estava completa, eu a registrava no Phaser como uma spritesheet, definindo a largura e a altura do quadro para que o motor soubesse onde uma pose terminava e a próxima começava.
Fazer isso manualmente revela exatamente o que uma animação é por baixo da superfície: um conjunto de limites de quadros em uma textura compartilhada. O componente de animação do Phaser solicita um quadro inicial, um quadro final e uma taxa de quadros (frame rate). Ele então avança um ponteiro através desses cortes retangulares a cada ciclo (tick) do relógio do jogo. Você para de pensar em spritesheets como assets mágicos produzidos por artistas e começa a vê-las como matemática de coordenadas. Essa perspectiva é inestimável mais tarde, quando você estiver fatiando arte real ou depurando por que uma animação reproduz quadros fora de ordem.
Som do Nada
Arquivos de áudio foram banidos, então usei a Web Audio API diretamente. Algumas linhas de JavaScript podem criar um nó oscilador, configurá-lo para uma onda quadrada ou senoidal, passá-lo por um nó de ganho (gain node) e agendar um curto surto de som. Escrevi pequenos auxiliares para eventos comuns: um bipe de baixa frequência para passos, um chilreio ascendente para coletar itens (loot) e um tom áspero de onda quadrada para receber dano.
Esses sons sintetizados são placeholders por design, mas eles dão feedback mecânico ao jogo imediatamente. Você pode sentir se o tempo está correto antes de se comprometer com a gravação ou busca de áudio real. A verdadeira recompensa vem na arquitetura. Como você envolveu o gatilho de som em uma função simples, trocar o bipe sintetizado por um buffer de som carregado mais tarde exige apenas uma mudança de linha. O resto do jogo — o evento de colisão, o flash da UI, o incremento da pontuação — permanece intacto. Você prototipa a sensação primeiro e depois melhora a fidelidade.
Gerenciando Múltiplos Mundos
Um jogo real precisa de mais de uma tela, então dividi o projeto em cenas separadas do Phaser: Menu, Game, UI e Pause. A cena de UI roda em paralelo com a cena de Game, sendo lançada simultaneamente para que a barra de vida e o contador de pontuação vivam em seu próprio sandbox enquanto a exploração da masmorra acontece abaixo. Elas se comunicam estritamente através de eventos. Quando o jogador recebe dano, a cena de Game emite uma mudança. A cena de UI escuta e atualiza seus objetos de texto. A cena de Game não importa a UI, não chama seus métodos, nem sequer verifica se ela existe. Ela simplesmente envia dados para o vazio. Esse desacoplamento significa que você pode remover o HUD para testes, ou substituí-lo inteiramente, sem tocar no loop principal do jogo.
Para a pausa, usei uma cena de Pause empilhada que fica sobre a cena de Game. Crucialmente, chamar scene.pause() na cena de Game realmente congela o mundo físico e interrompe os timers. A cena de Game para de atualizar, mas a cena de Pause permanece ativa para renderizar um menu e aguardar um sinal de despausa. Se você sempre gerenciou estados de pausa apenas com uma flag booleana dentro de um loop de atualização gigante, isso parece a descoberta de um interruptor de luz. O motor oferece um ciclo de vida de pausa real, em vez de forçá-lo a salpicar seu código com proteções if (isPaused) return.
Lições Difíceis de Bugs Reais
Trabalhar tão próximo ao metal expôs dois hábitos que eu precisava mudar.
Primeiro, tentei usar um método no Phaser v4 que parecia público, mas não fazia parte da API documentada. Ele mudou entre as versões e quebrou meu build. Refatorei para usar getChildren(), que é um método público estável e documentado, e a instabilidade desapareceu. A lição é direta: se um método não está na documentação oficial, não construa seu jogo sobre ele. APIs internas são internas por um motivo. Atenha-se à área de superfície pública e seu projeto sobreviverá às atualizações do motor.
Segundo, aprendi a nunca confiar em eventos para tudo. Callbacks de eventos funcionam perfeitamente bem para interações de baixo risco, como pegar uma moeda ou abrir um baú. Mas para transições de estado críticas — especialmente o Game Over — adicionei uma verificação redundante dentro do loop de atualização principal. Eventos podem falhar se um listener for removido, uma cena pausar em um microssegundo estranho ou se uma condição de corrida surgir entre a emissão e o tratamento. Ao verificar a vida do jogador diretamente no loop de atualização e forçar o estado de game-over se ela chegar a zero, garanti que o jogo nunca ficasse preso em um estado de limbo caso um evento falhasse ao ser disparado. Os eventos ainda lidam com os efeitos secundários — tremor de tela, sinais sonoros, envio de pontuação — mas a lógica autoritativa reside onde o relógio do jogo reside.
Por Que Você Deve Tentar Isso
Se você está aprendendo desenvolvimento de jogos, imponha exatamente esta restrição ao seu próximo projeto: sem assets externos, apenas um arquivo HTML. Parece restritivo, mas remove todas as desculpas. Você não precisa configurar um bundler, lutar contra erros de CORS em arquivos de áudio locais ou passar uma tarde selecionando pacotes de assets gratuitos. Você escreve o código, atualiza o navegador e vê os resultados.
Mais importante ainda, você entenderá por que o motor se comporta da maneira que se comporta. Você saberá como uma textura entra na GPU porque você chamou generateTexture. Você saberá como os frames de animação são indexados porque você registrou os limites manualmente. Você saberá como o áudio chega aos alto-falantes porque você conectou o oscilador. Esse conhecimento se transfere diretamente para projetos maiores que utilizam assets externos, porque a mecânica subjacente nunca muda — o motor
