Este guia constrói um jogo pequeno e verificável, em vez de apresentar apenas uma lista de recursos: você controla uma peça azul com as setas do teclado, coleta dez moedas e acompanha a pontuação. O exemplo usa formas desenhadas pelo próprio Phaser, então não depende de imagens ou arquivos de terceiros.

Usaremos Phaser 3.90.0, TypeScript 5.9.3 e Vite 8.3.5. O artigo inclui o código da cena, configuração do projeto, comandos de validação de tipos e compilação. As versões foram escolhidas para tornar o exemplo reproduzível; verifique os requisitos de Node.js do Vite antes de começar.

1. O que é Phaser?

Phaser é um framework gratuito e de código aberto voltado para o desenvolvimento de jogos 2D executados em navegadores. Ele oferece recursos para renderização gráfica, animações, entrada do teclado e do mouse, física, áudio, carregamento de imagens e gerenciamento de cenas.

Entre suas principais características estão:

  • Renderização utilizando WebGL ou Canvas.
  • Sistema de física para movimentação e colisões.
  • Suporte a teclado, mouse e controles por toque.
  • Gerenciamento de cenas, como menus, fases e telas de encerramento.
  • Carregamento de imagens, sprites, áudio e outros recursos.
  • Integração com JavaScript, TypeScript, Vite e outros ambientes web.

O Phaser é especialmente interessante para jogos educacionais, jogos casuais, quebra-cabeças, plataformas, aventuras 2D e experiências interativas.

2. Por que utilizar TypeScript?

TypeScript é uma linguagem baseada em JavaScript que adiciona recursos de tipagem estática. Isso permite identificar diversos erros durante o desenvolvimento, melhorar o preenchimento automático do editor e organizar melhor projetos que possuem muitas classes e funcionalidades.

Veja um exemplo simples:

let pontuacao: number = 0;

function adicionarPontos(valor: number): void {
    pontuacao += valor;
}

adicionarPontos(10);

console.log(pontuacao);

Nesse exemplo, a variável pontuacao armazena um número, enquanto a função adicionarPontos() recebe um argumento numérico. Se tentarmos passar um texto para essa função, o TypeScript poderá identificar o problema durante a verificação do código.

Em jogos maiores, essa organização ajuda a manter o projeto mais confiável e facilita a manutenção.

3. Preparando o ambiente de desenvolvimento

Para acompanhar este tutorial, você precisará de:

  • Node.js em uma versão compatível com o Vite utilizado.
  • npm, que normalmente acompanha o Node.js.
  • Visual Studio Code ou outro editor de código.
  • Um navegador moderno, como Google Chrome ou Microsoft Edge.

O Vite será responsável por iniciar o servidor de desenvolvimento e preparar os arquivos para publicação.

Criando o projeto

No terminal, crie um projeto inicial com o template TypeScript do Vite:

npm create vite@latest meu-jogo -- --template vanilla-ts

Entre na pasta criada:

cd meu-jogo

Instale as dependências:

npm install

Instale o Phaser e fixe as versões usadas neste exemplo. O Vite 8 requer uma versão de Node.js compatível com os requisitos publicados pelo próprio projeto.

npm install --save-exact phaser@3.90.0
npm install --save-dev --save-exact vite@8.3.5 typescript@5.9.3

Antes de continuar, confirme que o package.json tem comandos para desenvolvimento, verificação de tipos e compilação. Se necessário, deixe os scripts assim:

"scripts": {
  "dev": "vite",
  "typecheck": "tsc --noEmit",
  "build": "tsc --noEmit && vite build",
  "preview": "vite preview"
}

Agora o projeto possui a estrutura básica para trabalhar com TypeScript.

Organizando os arquivos

Para este exemplo, utilizaremos a seguinte organização:

meu-jogo/
├── public/
├── src/
│   ├── main.ts
│   └── style.css
├── index.html
├── package.json
└── tsconfig.json
                    

O arquivo main.ts conterá a lógica principal do jogo. O arquivo style.css será responsável pela apresentação da página, enquanto o index.html será o ponto de entrada da aplicação.

4. Entendendo a estrutura de um jogo Phaser

Antes de implementar o jogo, é importante conhecer três métodos fundamentais de uma cena Phaser:

preload() — utilizado para carregar imagens, áudios, sprites e outros recursos necessários.

create() — executado quando a cena é criada. É nele que normalmente configuramos personagens, textos, objetos e eventos.

update() — executado continuamente durante o jogo. É utilizado para atualizar movimentos, verificar condições e processar comportamentos que dependem do tempo.

Uma cena pode ser representada por uma classe que herda de Phaser.Scene.

class MinhaCena extends Phaser.Scene {
    constructor() {
        super("MinhaCena");
    }

    preload(): void {
        // Carregamento de recursos.
    }

    create(): void {
        // Criação dos elementos do jogo.
    }

    update(): void {
        // Atualização contínua.
    }
}

Essa estrutura separa as responsabilidades e facilita a evolução do projeto.

5. Criando nosso primeiro jogo: Caça às Moedas

Vamos desenvolver um jogo simples com as seguintes regras:

  1. O personagem aparece na tela.
  2. O jogador utiliza as setas do teclado para movimentá-lo.
  3. Dez moedas ficam distribuídas pelo cenário.
  4. Cada moeda coletada aumenta a pontuação.
  5. Ao coletar todas as moedas, o jogo exibe uma mensagem de vitória.

Para facilitar o aprendizado, criaremos os elementos gráficos diretamente pelo código. Assim, não precisaremos baixar imagens ou configurar arquivos de sprites.

Captura do jogo Caça às Moedas: personagem azul, moedas amarelas e pontuação inicial de zero em dez.
Captura do protótipo executado a partir do código deste tutorial. O jogo usa as setas do teclado.

5.1 Código completo do jogo Caça às Moedas

Vamos ao código prático. Abra o arquivo src/main.ts, substitua seu conteúdo pelo código abaixo e salve.

import Phaser from "phaser";

type ArcadeOverlapObject =
    | Phaser.Types.Physics.Arcade.GameObjectWithBody
    | Phaser.Physics.Arcade.Body
    | Phaser.Physics.Arcade.StaticBody
    | Phaser.Tilemaps.Tile;

class JogoCena extends Phaser.Scene {
    private jogador!: Phaser.Physics.Arcade.Sprite;
    private moedas!: Phaser.Physics.Arcade.StaticGroup;
    private teclas!: Phaser.Types.Input.Keyboard.CursorKeys;
    private textoPontuacao!: Phaser.GameObjects.Text;
    private pontuacao: number = 0;
    private readonly totalMoedas: number = 10;
    private jogoFinalizado: boolean = false;

    constructor() {
        super("JogoCena");
    }

    create(): void {
        const largura = this.scale.width;
        const altura = this.scale.height;

        // Cor de fundo e título.
        this.cameras.main.setBackgroundColor("#14213d");

        this.add
            .text(largura / 2, 24, "CAÇA ÀS MOEDAS", {
                fontFamily: "Arial",
                fontSize: "28px",
                color: "#ffffff",
                fontStyle: "bold"
            })
            .setOrigin(0.5);

        this.add
            .text(
                largura / 2,
                58,
                "Use as setas para coletar todas as moedas!",
                {
                    fontFamily: "Arial",
                    fontSize: "15px",
                    color: "#dbeafe"
                }
            )
            .setOrigin(0.5);

        // Criando a textura do jogador.
        const graficoJogador = this.make.graphics({ x: 0, y: 0 });

        graficoJogador.fillStyle(0x38bdf8, 1);
        graficoJogador.fillCircle(16, 16, 15);
        graficoJogador.generateTexture(
            "texturaJogador",
            32,
            32
        );
        graficoJogador.destroy();

        // Criando a textura das moedas.
        const graficoMoeda = this.make.graphics({ x: 0, y: 0 });

        graficoMoeda.fillStyle(0xfacc15, 1);
        graficoMoeda.fillCircle(12, 12, 11);
        graficoMoeda.lineStyle(2, 0xfef3c7, 1);
        graficoMoeda.strokeCircle(12, 12, 8);
        graficoMoeda.generateTexture(
            "texturaMoeda",
            24,
            24
        );
        graficoMoeda.destroy();

        // Criando o jogador com física Arcade.
        this.jogador = this.physics.add.sprite(
            largura / 2,
            altura / 2,
            "texturaJogador"
        );

        this.jogador.setCollideWorldBounds(true);
        this.jogador.setBounce(0);

        // Configurando os controles do teclado.
        this.teclas = this.input.keyboard!.createCursorKeys();

        // Criando o grupo de moedas.
        this.moedas = this.physics.add.staticGroup();

        for (let i = 0; i < this.totalMoedas; i++) {
            let x = 40 + Math.random() * (largura - 80);
            let y = 110 + Math.random() * (altura - 150);

            // Evita colocar uma moeda muito perto do jogador.
            const distancia = Phaser.Math.Distance.Between(
                x,
                y,
                this.jogador.x,
                this.jogador.y
            );

            if (distancia < 65) {
                x = x < largura / 2
                    ? largura - 50
                    : 50;

                y = y < altura / 2
                    ? altura - 55
                    : 110;
            }

            this.moedas.create(x, y, "texturaMoeda");
        }

        // Exibindo a pontuação.
        this.textoPontuacao = this.add.text(
            16,
            altura - 34,
            `Moedas: ${this.pontuacao}/${this.totalMoedas}`,
            {
                fontFamily: "Arial",
                fontSize: "18px",
                color: "#ffffff"
            }
        );

        // Detectando a coleta das moedas.
        this.physics.add.overlap(
            this.jogador,
            this.moedas,
            this.coletarMoeda,
            undefined,
            this
        );
    }

    update(): void {
        if (this.jogoFinalizado) {
            return;
        }

        const velocidade = 220;
        const corpo = this.jogador.body as
            Phaser.Physics.Arcade.Body;

        corpo.setVelocity(0, 0);

        if (this.teclas.left.isDown) {
            corpo.setVelocityX(-velocidade);
        } else if (this.teclas.right.isDown) {
            corpo.setVelocityX(velocidade);
        }

        if (this.teclas.up.isDown) {
            corpo.setVelocityY(-velocidade);
        } else if (this.teclas.down.isDown) {
            corpo.setVelocityY(velocidade);
        }

        // Evita que o movimento diagonal seja mais rápido.
        corpo.velocity.normalize().scale(velocidade);
    }

    private coletarMoeda(
        _objetoJogador: ArcadeOverlapObject,
        objetoMoeda: ArcadeOverlapObject
    ): void {
        if (!(objetoMoeda instanceof Phaser.Physics.Arcade.Image)) {
            return;
        }

        objetoMoeda.disableBody(true, true);

        this.pontuacao++;

        this.textoPontuacao.setText(
            `Moedas: ${this.pontuacao}/${this.totalMoedas}`
        );

        if (this.pontuacao >= this.totalMoedas) {
            this.finalizarJogo();
        }
    }

    private finalizarJogo(): void {
        this.jogoFinalizado = true;

        this.jogador.setVelocity(0, 0);

        const largura = this.scale.width;
        const altura = this.scale.height;

        this.add
            .rectangle(
                largura / 2,
                altura / 2,
                largura - 40,
                100,
                0x000000,
                0.8
            );

        this.add
            .text(
                largura / 2,
                altura / 2,
                "PARABÉNS! VOCÊ VENCEU!",
                {
                    fontFamily: "Arial",
                    fontSize: "24px",
                    color: "#facc15",
                    fontStyle: "bold",
                    align: "center"
                }
            )
            .setOrigin(0.5);
    }
}

const configuracao: Phaser.Types.Core.GameConfig = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: "app",
    backgroundColor: "#14213d",
    physics: {
        default: "arcade",
        arcade: {
            debug: false
        }
    },
    scene: [JogoCena],
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH
    }
};

new Phaser.Game(configuracao);

5.2 Preparando a página HTML

Abra o arquivo index.html na raiz do projeto e verifique se ele contém a estrutura abaixo:

<!doctype html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Jogo de coleta de moedas criado com Phaser e TypeScript." />
    <title>Caça às Moedas | Phaser e TypeScript</title>
</head>
<body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
</body>
</html>

O elemento app é o contêiner no qual o Phaser inserirá a área de renderização do jogo.

5.3 Ajustando o CSS

Abra src/style.css e substitua seu conteúdo por:

* {
    box-sizing: border-box;
}

html,
body {
    margin: 0;
    min-width: 320px;
    min-height: 100%;
    background: #0b1120;
    color: #ffffff;
    font-family: Arial, sans-serif;
}

body {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
}

#app {
    width: 100%;
    max-width: 800px;
}

canvas {
    display: block;
    max-width: 100%;
    height: auto;
    border-radius: 8px;
}

Depois, confira se o arquivo src/main.ts importa o CSS. Adicione esta linha no início do arquivo, caso ela ainda não exista:

import "./style.css";

5.4 Executando o jogo

Com os arquivos salvos, abra o terminal na pasta do projeto e execute:

npm run dev

O Vite exibirá um endereço local, normalmente semelhante a:

http://localhost:5173/

Abra o endereço no navegador. O jogo deverá aparecer com um personagem azul, dez moedas amarelas e o contador de pontuação.

Utilize as setas do teclado para movimentar o personagem. Ao tocar nas moedas, elas desaparecerão e a pontuação aumentará. Depois de coletar as dez moedas, será exibida a mensagem de vitória.

Para interromper o servidor, utilize Ctrl + C no terminal.

5.5 Gerando a versão para publicação

Quando o jogo estiver funcionando corretamente, execute:

npm run build

O script indicado executa primeiro tsc --noEmit, que verifica os tipos sem gerar arquivos JavaScript, e depois vite build, que prepara a versão de produção na pasta dist.

Antes de publicar, confira também:

  • Se todos os recursos utilizados estão disponíveis.
  • Se os caminhos dos arquivos funcionam no endereço de destino.
  • Se o jogo funciona em navegadores móveis.
  • Se o console do navegador não apresenta erros.
  • Se a aplicação funciona em um caminho de publicação diferente da raiz do domínio, quando aplicável.

Em hospedagens tradicionais, os arquivos gerados podem ser enviados ao servidor web. Entretanto, é importante respeitar a configuração de caminhos do Vite, principalmente quando o jogo ficará dentro de uma subpasta de um site.

6. Problemas comuns durante o desenvolvimento

O jogo não aparece na tela

Confira o console do navegador, verifique se o arquivo src/main.ts foi carregado e confirme se o elemento app existe no HTML. Erros de caminho no script também podem impedir que o Vite carregue a cena.

O TypeScript apresenta erros

Execute npm run typecheck para localizar a linha indicada pelo compilador. Confira os tipos dos callbacks do Arcade Physics e a versão de Phaser instalada; não remova a verificação de tipos para ocultar um erro.

O personagem não coleta as moedas

Confirme que o jogo usa a física Arcade, que as moedas pertencem ao grupo estático e que a chamada a this.physics.add.overlap() está dentro da cena ativa.

O jogo fica lento em dispositivos móveis

Este exemplo usa controles de teclado e foi pensado para ser acompanhado em um computador. Para suportar toque, implemente controles na tela e teste em aparelhos reais; não presuma que a escala visual do canvas, por si só, torna o jogo jogável por toque.

7. Como evoluir o exemplo

O exemplo apresentado é simples, mas pode ser expandido de diversas maneiras.

Adicionar imagens e animações

O Phaser permite carregar spritesheets e criar animações para o personagem. Em vez de utilizar um círculo, você pode criar um personagem com movimentos de caminhada, corrida e comemoração.

Criar obstáculos

É possível adicionar paredes, inimigos e outros objetos usando os recursos de física. Assim, o jogador precisará desviar dos obstáculos para alcançar as moedas.

Implementar níveis

Você pode criar várias cenas, cada uma com um mapa e uma dificuldade diferente. Ao terminar uma fase, o jogo pode carregar a próxima.

Adicionar efeitos sonoros

O Phaser oferece suporte a áudio. Sons de coleta, vitória e colisão ajudam a tornar a experiência mais envolvente.

Criar controles para celulares

Além das setas do teclado, é possível implementar botões virtuais, joystick na tela ou controles por toque, tornando o jogo mais adequado para smartphones e tablets.

Salvar a pontuação

O localStorage do navegador pode ser utilizado para manter um recorde local entre sessões. Para placares compartilhados entre jogadores, pode ser necessário um servidor com banco de dados e mecanismos de validação.

8. Organização para projetos maiores

À medida que o jogo crescer, evite concentrar toda a lógica em uma única classe. Uma organização possível é:

src/
├── scenes/
│   ├── MenuScene.ts
│   ├── GameScene.ts
│   └── GameOverScene.ts
├── objects/
│   ├── Player.ts
│   └── Coin.ts
├── systems/
│   └── ScoreSystem.ts
├── main.ts
└── style.css

Essa estrutura é apenas uma sugestão: projetos pequenos podem utilizar menos arquivos, enquanto jogos maiores podem exigir sistemas adicionais.

Separar cenas, personagens e regras de negócio facilita a leitura, a manutenção e a implementação de novos recursos. Também é recomendável manter nomes consistentes, utilizar os tipos do TypeScript e testar o jogo depois de cada mudança importante.

Conclusão

Desenvolver jogos com Phaser e TypeScript é uma forma prática de combinar programação web com conceitos tradicionais de desenvolvimento de jogos.

Neste tutorial, criamos um jogo completo de coleta de moedas, configuramos o ambiente com Vite, implementamos controles por teclado, utilizamos a física Arcade e preparamos o projeto para publicação.

O próximo passo é experimentar novas mecânicas e transformar o protótipo em um jogo mais elaborado, com personagens animados, níveis, obstáculos, efeitos sonoros e interface adaptada a diferentes dispositivos.

No Estúdio Multimídia, acreditamos que aprender programação fica mais interessante quando os conceitos são aplicados em projetos reais. Por isso, continuaremos explorando técnicas, ferramentas e soluções para o desenvolvimento de jogos HTML5 e aplicações interativas.

Gostou deste tutorial? Continue acompanhando o blog do Estúdio Multimídia para conhecer novos conteúdos sobre Phaser, TypeScript, JavaScript e desenvolvimento de jogos.

Referências técnicas

Ambiente usado na validação local deste exemplo: Node.js 24.11.0, Phaser 3.90.0, Vite 8.3.5 e TypeScript 5.9.3. As versões e os comandos podem mudar; consulte as documentações oficiais antes de iniciar um projeto novo.

Leia também

Quer um jogo profissional para o seu projeto?

O Estúdio Multimídia desenvolve games em HTML5 sob medida para web, aplicativos e campanhas, do conceito à publicação.

Fale com a nossa equipe