Um jogo 2D top-down criado para ensinar Phaser 3 a alunos do primeiro período de faculdade.
Todos os conceitos são demonstrados por código real e executável — sem frameworks extras, sem bundlers.
- Visão geral do projeto
- Como rodar localmente
- Estrutura de arquivos
- Conceitos ensinados — ordem sugerida para live coding
- config.js — Configurações globais
- main.js — Ponto de entrada
- Ciclo de vida de uma Scene
- BootScene e PreloadScene — carregando assets
- assets.json — manifesto de assets
- Phaser.Physics.Arcade.Sprite — entidades com física
- Player — entrada do teclado e interação
- NPC e dialogs
- Enemy — patrulha simples
- Tiled Map Editor — mapas no Phaser
- BaseGameScene — herança para evitar repetição
- Transições de cena com fade
- UIScene — cena paralela para HUD e diálogos
- Comunicação entre scenes — event bus
- Substituindo os placeholders por assets reais
- Publicando no GitHub Pages
- Roteiro de live coding (passo a passo)
Phaser Quest é um RPG top-down minimalista com dois mapas: uma cidade e uma dungeon.
| Funcionalidade | Como é demonstrada |
|---|---|
| Configurações globais | src/config.js — uma única fonte da verdade |
| Carregamento de assets via JSON | assets/assets.json + PreloadScene |
| Separação de arquivos por funcionalidade | scenes/, entities/, ui/, utils/ |
| Entidades como classes | Player, NPC, Enemy estendem Phaser.Physics.Arcade.Sprite |
| Herança para reaproveitamento | BaseGameScene ← GameScene / DungeonScene |
| Mapas Tiled com colisão | Camada Walls + setCollisionByExclusion([-1]) |
| Spawns via camada de objetos do Tiled | map.getObjectLayer('Objects').objects |
| Transição de cenas | cameras.main.fadeOut + scene.start() |
| Scene paralela (HUD/dialog) | scene.launch(SCENES.UI) |
| Comunicação entre scenes | this.game.events.emit/on |
| Física arcade | Colisores (add.collider) e overlaps (add.overlap) |
- Node.js (para gerar os mapas e servir arquivos)
- VS Code com a extensão Live Server (recomendado)
# 1. Clone ou copie o projeto
cd aulao-de-phaser-1
# 2. (Re)gere os mapas placeholder se necessário
node tools/generate-maps.js
# 3. Sirva os arquivos com qualquer servidor HTTP local
# Opção A — VS Code: clique direito em index.html → "Open with Live Server"
# Opção B — Node.js:
npx serve .
# Opção C — Python:
python -m http.server 8080Por que precisa de um servidor?
Módulos ES (type="module") efetch()são bloqueados pelo navegador quando
abertos viafile://. Um servidor HTTP resolve isso.
aulao-de-phaser-1/
│
├── index.html ← Único HTML; carrega Phaser via CDN
│
├── assets/
│ ├── assets.json ← Manifesto de assets (LEIA AQUI)
│ ├── maps/
│ │ ├── town.json ← Mapa Tiled da cidade
│ │ └── dungeon.json ← Mapa Tiled da dungeon
│ ├── tilesets/ ← tileset.png (forneça o seu)
│ ├── sprites/ ← spritesheets do player e NPCs
│ └── audio/ ← músicas e efeitos sonoros
│
├── src/
│ ├── main.js ← new Phaser.Game(config)
│ ├── config.js ← TODAS as constantes do jogo
│ │
│ ├── scenes/
│ │ ├── BootScene.js ← Carrega assets.json
│ │ ├── PreloadScene.js ← Carrega todos os assets + placeholders
│ │ ├── MenuScene.js ← Tela de título
│ │ ├── BaseGameScene.js ← Lógica compartilhada entre cenas de jogo
│ │ ├── GameScene.js ← Cidade (herda BaseGameScene)
│ │ ├── DungeonScene.js ← Dungeon (herda BaseGameScene + adiciona inimigos)
│ │ └── UIScene.js ← HUD e dialogs (cena paralela)
│ │
│ ├── entities/
│ │ ├── Player.js ← Entrada, movimento, interação
│ │ ├── NPC.js ← Personagem com diálogo
│ │ └── Enemy.js ← Inimigo com patrulha
│ │
│ └── ui/
│ └── DialogBox.js ← Caixa de diálogo paginada
│
├── tools/
│ └── generate-maps.js ← Script Node.js que gera os JSON do Tiled
│
└── docs/
└── README.md ← Este arquivo
1. Phaser.Game + config → main.js
2. Scenes e ciclo de vida → BootScene, PreloadScene
3. Configurações globais → config.js
4. Manifesto de assets (JSON) → assets.json + PreloadScene
5. Texturas placeholder → PreloadScene._generatePlaceholderTextures()
6. Tela de menu → MenuScene
7. Classes de entidade → Player.js
8. Física Arcade → Player + GameScene
9. Tiled: tilemaps e colisão → BaseGameScene._buildMap()
10. Tiled: camada de objetos → BaseGameScene._spawnFromObjectLayer()
11. NPCs e diálogos → NPC.js + UIScene
12. Herança de Scene → BaseGameScene → GameScene/DungeonScene
13. Inimigos e patrulha → Enemy.js + DungeonScene
14. Transição com fade → BaseGameScene._onPortalEnter()
15. Cena paralela (UIScene) → scene.launch() em MenuScene
16. Event bus entre scenes → game.events.emit/on
Princípio: nunca coloque "números mágicos" espalhados pelo código.
Centralizar tudo em config.js facilita ajustes e evita bugs difíceis de achar.
// src/config.js
export const GAME_WIDTH = 800;
export const GAME_HEIGHT = 600;
export const TILE_SIZE = 32;
export const PLAYER_SPEED = 160;
export const SCENES = {
BOOT: "Boot",
PRELOAD: "Preload",
MENU: "Menu",
GAME: "GameScene",
DUNGEON: "DungeonScene",
UI: "UI",
};Dica de live coding: mostre o que acontece quando a velocidade do player
está hard-coded em 5 arquivos diferentes vs. centralizada emconfig.js.
// src/main.js
import { BootScene } from "./scenes/BootScene.js";
// ... outros imports
const config = {
type: Phaser.AUTO, // WebGL se disponível, senão Canvas
width: GAME_WIDTH,
height: GAME_HEIGHT,
physics: {
default: "arcade",
arcade: { debug: false },
},
scene: [BootScene, PreloadScene, MenuScene, GameScene, DungeonScene, UIScene],
};
new Phaser.Game(config);Pontos a destacar:
| Propriedade | O que ensina |
|---|---|
type: Phaser.AUTO |
Phaser detecta automaticamente o melhor renderer |
physics.arcade |
Física simples AABB — ideal para top-down 2D |
scene: [...] |
A primeira scene da lista é iniciada automaticamente |
Phaser.Scene
│
├── init(data) → chamado ao iniciar; recebe dados de scene.start(key, data)
├── preload() → carregamento de assets (load.*)
├── create() → montagem inicial (sprites, groups, colliders...)
└── update(t, dt) → loop de jogo (~60x por segundo)
Cada método tem uma responsabilidade clara — misturá-los cria código confuso.
| Scene | Responsabilidade |
|---|---|
| BootScene | Carrega APENAS o manifesto assets.json (arquivo minúsculo) |
| PreloadScene | Lê o manifesto e carrega TODOS os assets; mostra barra de progresso |
Separar assim garante que a tela nunca fique em branco e que o código de loading seja reutilizável.
// src/scenes/PreloadScene.js
preload() {
const bar = this.add.rectangle(400, 300, 0, 20, 0x3399ff).setOrigin(0, 0.5);
this.load.on('progress', (value) => {
bar.width = 320 * value; // 0% → 0px, 100% → 320px
});
// Carrega os assets do manifesto...
}{
"images": [],
"spritesheets": [],
"tilemaps": [
{ "key": "town", "path": "assets/maps/town.json" },
{ "key": "dungeon", "path": "assets/maps/dungeon.json" }
],
"tilesets": [{ "key": "tileset", "path": "assets/tilesets/tileset.png" }],
"audio": []
}Por que JSON em vez de código?
- Designers e artistas podem editar sem tocar em código JavaScript.
- Fácil de versionar e comparar mudanças com
git diff. PreloadScenelê o manifesto e carrega tudo dinamicamente:
manifest.tilesets.forEach(({ key, path }) => {
this.load.image(key, path);
}); Phaser.GameObjects.GameObject
│
Phaser.GameObjects.Sprite
│
Phaser.Physics.Arcade.Sprite ←── Player, Enemy
export class Player extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y) {
super(scene, x, y, "player"); // chave da textura
scene.add.existing(this); // adiciona ao display list da scene
scene.physics.add.existing(this); // cria um physics body para ele
this.setCollideWorldBounds(true); // não sai do mapa
}
}Hierarquia de classes é uma boa prática porque:
- Cada entidade encapsula sua própria lógica
- A scene não precisa saber como o player se move — só chama
player.update() - Fácil de trocar implementações sem quebrar nada
// Criação dos controles
this._cursors = scene.input.keyboard.createCursorKeys(); // ↑↓←→
this._wasd = scene.input.keyboard.addKeys({ up: 'W', down: 'S', left: 'A', right: 'D' });
// Movimento no update()
update() {
let vx = 0, vy = 0;
if (this._cursors.left.isDown || this._wasd.left.isDown) vx = -PLAYER_SPEED;
if (this._cursors.right.isDown || this._wasd.right.isDown) vx = PLAYER_SPEED;
// ...
this.setVelocity(vx, vy);
}Diagonal mais lenta (normalização):
if (vx !== 0 && vy !== 0) {
vx *= Math.SQRT1_2; // ≈ 0.707
vy *= Math.SQRT1_2;
}Tecla de interação com JustDown:
// JustDown → true apenas no PRIMEIRO frame em que a tecla é pressionada
if (Phaser.Input.Keyboard.JustDown(this._interactKey)) {
this.emit("interact", closestObject);
}No Tiled Map Editor, crie um objeto na camada Objects com:
| Campo | Valor |
|---|---|
| Type | npc |
| Name | Professor Phaser |
Propriedade dialog (string) |
Página 1|Página 2|Página 3 |
O separador | divide a string em páginas:
// src/entities/NPC.js
this.dialogPages = dialogRaw.split("|").map((s) => s.trim());Player pressiona E
│
▼
Player.emit('interact', npc)
│
▼
GameScene._onPlayerInteract(npc)
│
▼
game.events.emit('dialog:open', { speaker, pages })
│
▼
UIScene ouve o evento → DialogBox.open(data)
│
▼
Player.blocked = true (não pode se mover)
│
▼
E / Espaço avança páginas → na última: DialogBox.hide()
│
▼
game.events.emit('dialog:close')
│
▼
Player.blocked = false
export class Enemy extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y, range = 4) {
super(scene, x, y, "enemy");
// ...
this._leftBound = x - range * TILE_SIZE;
this._rightBound = x + range * TILE_SIZE;
this.setVelocityX(ENEMY_SPEED);
}
update() {
if (this.x >= this._rightBound) this.setVelocityX(-ENEMY_SPEED);
if (this.x <= this._leftBound) this.setVelocityX(ENEMY_SPEED);
}
}No Tiled, o objeto inimigo tem a propriedade range (int) que define quantos
tiles ele patrulha para cada lado. Isso evita hard-code de posições no código.
-
Novo mapa:
Arquivo → Novo mapa- Orientação: Ortogonal
- Tamanho do tile: 32×32
- Tamanho do mapa: quantos tiles quiser
-
Tileset:
Mapa → Novo Tileset → imagem tileset.png -
Camadas necessárias:
Nome Tipo Para quê GroundTile Layer Piso — sem colisão WallsTile Layer Paredes — colisão automática ObjectsObject Layer Spawns de player, NPCs, portais, inimigos -
Exportar:
Arquivo → Exportar como → JSON (*.json)
Salve emassets/maps/nome-do-mapa.json.
// PreloadScene.preload()
this.load.tilemapTiledJSON("town", "assets/maps/town.json");
this.load.image("tileset", "assets/tilesets/tileset.png");
// GameScene.create()
const map = this.make.tilemap({ key: "town" });
const tileset = map.addTilesetImage("tileset", "tileset"); // (nomeTiled, chavePhaer)
const ground = map.createLayer("Ground", tileset, 0, 0);
const walls = map.createLayer("Walls", tileset, 0, 0);
// Qualquer tile não-vazio na camada Walls é sólido
walls.setCollisionByExclusion([-1]);
// Conectar o player à colisão
this.physics.add.collider(this.player, walls);const objectLayer = map.getObjectLayer("Objects");
objectLayer.objects.forEach((obj) => {
if (obj.type === "npc") {
const dialogRaw = obj.properties.find((p) => p.name === "dialog").value;
new NPC(this, obj.x, obj.y, obj.name, dialogRaw);
}
});Posição Y no Tiled: a coordenada Y de um objeto Tiled aponta para a borda
inferior do tile. Somamos- TILE_SIZE / 2para centrar o sprite.
GameScene e DungeonScene têm o mesmo fluxo de criação:
mapa → player → NPCs → portais → colisores → câmera.
Em vez de copiar esse código, ambas herdam de BaseGameScene:
BaseGameScene (lógica compartilhada)
├── GameScene (cidade; get mapKey() { return 'town' })
└── DungeonScene (dungeon; get mapKey() { return 'dungeon' }
+ sobrescreve _spawnEnemy())
// Subclasse só precisa declarar o que muda
export class GameScene extends BaseGameScene {
get mapKey() {
return "town";
}
get tilesetKey() {
return "tileset";
}
get bgColor() {
return "#2d5a1b";
}
create() {
super.create();
this.cameras.main.fadeIn(500, 0, 0, 0);
}
}// Entrou em um portal → fade para preto → muda de scene
_onPortalEnter(portal) {
if (this._transitioning) return;
this._transitioning = true;
this.cameras.main.fadeOut(500, 0, 0, 0);
this.cameras.main.once('camerafadeoutcomplete', () => {
this.scene.start(portal.targetScene); // inicia a próxima scene
});
}A nova scene faz o fade de entrada:
create() {
super.create();
this.cameras.main.fadeIn(500, 0, 0, 0);
}MenuScene._startGame()
│
├── this.scene.launch(SCENES.UI) ← inicia UIScene sem parar a MenuScene
└── this.scene.start(SCENES.GAME) ← troca para GameScene
scene.launch() vs scene.start():
| Método | O que faz |
|---|---|
scene.start(key) |
Para a scene atual e inicia outra |
scene.launch(key) |
Inicia outra scene EM PARALELO (ambas rodam) |
A UIScene fica viva durante toda a sessão de jogo — gerencia a DialogBox
e responde a eventos do bus global.
Scenes não devem ter referências diretas umas às outras. Usamos o
bus de eventos do jogo (this.game.events) para comunicação desacoplada:
// GameScene emite quando o player inicia um diálogo
this.game.events.emit("dialog:open", { speaker: "NPC", pages: ["..."] });
// UIScene escuta
this.game.events.on(
"dialog:open",
(data) => {
this._dialog.open(data);
},
this,
);Vantagem: GameScene não precisa saber que UIScene existe.
Se você remover o HUD, o jogo continua funcionando.
O jogo vem com gráficos gerados por código (retângulos coloridos).
Quando você tiver os assets reais, siga estes passos:
Coloque seus assets nas pastas:
assets/
tilesets/tileset.png
sprites/player.png
sprites/npc.png
sprites/enemy.png
audio/bgm.ogg
// assets/assets.json
{
"spritesheets": [
{
"key": "player",
"path": "assets/sprites/player.png",
"frameWidth": 32,
"frameHeight": 32
},
{
"key": "npc",
"path": "assets/sprites/npc.png",
"frameWidth": 32,
"frameHeight": 32
},
{
"key": "enemy",
"path": "assets/sprites/enemy.png",
"frameWidth": 32,
"frameHeight": 32
}
],
"tilesets": [{ "key": "tileset", "path": "assets/tilesets/tileset.png" }],
"audio": [{ "key": "bgm", "path": "assets/audio/bgm.ogg" }]
}No PreloadScene.create(), adicione:
this.anims.create({
key: "player-walk-down",
frames: this.anims.generateFrameNumbers("player", { start: 0, end: 3 }),
frameRate: 8,
repeat: -1,
});// src/entities/Player.js — no método _handleMovement()
if (vy < 0) this.play("player-walk-up", true);
if (vy > 0) this.play("player-walk-down", true);
if (vx < 0) this.play("player-walk-left", true);
if (vx > 0) this.play("player-walk-right", true);
if (vx === 0 && vy === 0) this.stop();Em PreloadScene._generatePlaceholderTextures(), remova os blocos
que geram texturas para as chaves que você agora carrega do disco.
-
Inicialize um repositório Git:
git init git add . git commit -m "Initial commit"
-
Crie um repositório no GitHub e faça push:
git remote add origin https://github.com/SEU-USUARIO/aulao-de-phaser-1.git git push -u origin main
-
Ative o GitHub Pages:
Settings → Pages → Source: Deploy from a branch → Branch: main / (root) -
Acesse em:
https://SEU-USUARIO.github.io/aulao-de-phaser-1/
Não é necessário nenhum processo de build — o projeto funciona diretamente
como arquivos estáticos porque usa Phaser via CDN e módulos ES nativos.
Este roteiro está ordenado do mais simples ao mais complexo.
Cada etapa deve ter um resultado visível antes de avançar.
Criar: index.html (CDN do Phaser + <script type="module">)
src/main.js (new Phaser.Game com scene vazia)
src/config.js
Resultado esperado: janela preta com a cor de fundo do jogo.
Criar: src/scenes/BootScene.js
src/scenes/PreloadScene.js
assets/assets.json (vazio por enquanto)
Demonstre a barra de progresso e a geração de textures placeholder.
Resultado esperado: barra de loading que chega a 100%.
Criar: src/scenes/MenuScene.js
Mostre: textos, botão interativo, tweens, cameras.main.fadeOut.
Resultado esperado: tela de título com botão que responde ao mouse/teclado.
Criar: src/entities/Player.js
src/scenes/BaseGameScene.js (só _buildMap por enquanto)
src/scenes/GameScene.js
Mostre: Phaser.Physics.Arcade.Sprite, setVelocity, setCollideWorldBounds,
createCursorKeys, mapa Tiled carregado.
Resultado esperado: player azul se movendo pelo mapa sem atravessar paredes.
Criar: src/entities/NPC.js
Editar: src/scenes/BaseGameScene.js (_spawnFromObjectLayer)
Mostre: map.getObjectLayer, leitura de propriedades customizadas do Tiled,
indicador "!" com tween.
Resultado esperado: NPCs verdes no mapa com indicador piscando.
Criar: src/ui/DialogBox.js
src/scenes/UIScene.js
Editar: src/scenes/MenuScene.js (scene.launch(UI))
src/scenes/BaseGameScene.js (event bus + player.blocked)
Mostre: scene.launch, game.events.emit/on, Container, JustDown.
Resultado esperado: diálogo aparece ao pressionar E perto de um NPC.
Criar: src/scenes/DungeonScene.js
src/entities/Enemy.js
Editar: tools/generate-maps.js (portal no mapa)
Mostre: cameras.main.fadeOut, portal overlap, herança de scene, inimigos.
Resultado esperado: pressionar E no portal faz fade e abre o dungeon com inimigos patrulhando.
Siga a seção 19 — Substituindo os placeholders.
Bom live coding! 🎮