Jogo plataforma
Voltar para: Games com Phaser + Reactjs
O jogo de plataforma é o projeto mais completo da série. Ele integra todos os sistemas vistos até agora e adiciona os fundamentos que definem o gênero: gravidade, pulo condicionado ao chão, câmera que segue o jogador e um mundo maior que a tela.
🎮 Referências clássicas: o gênero foi definido por Super Mario Bros. (Nintendo, 1985) e Sonic the Hedgehog (Sega, 1991). Os sistemas que eles popularizaram — gravidade, pulo, coleta de itens, progressão de fase — continuam sendo a base de praticamente todo jogo de ação e aventura moderno.
A progressão da série
Cada projeto da série construiu sobre o anterior. O jogo de plataforma é o ponto onde tudo se encontra:
Objetivos do projeto
- Personagem com movimento lateral e pulo
- Gravidade real aplicada ao jogador e inimigos
- Plataformas estáticas como terreno colidível
- Inimigos que patrulham e mudam de direção
- Moedas coletáveis com sistema de pontuação
- Câmera que segue o jogador em um mundo amplo
Fluxo do jogo
Novos conceitos desta fase
| Conceito | O que é | Onde aparece |
|---|---|---|
| Gravidade global | Força que puxa todos os objetos para baixo | physics.world.gravity.y |
| Pulo condicionado | Só pode pular se estiver no chão | body.touching.down |
| Static Group | Plataformas fixas sem física dinâmica | physics.add.staticGroup() |
| Câmera com follow | Câmera segue o jogador no mundo | cameras.main.startFollow() |
| Mundo maior que a tela | Limites do mundo além da resolução | cameras.main.setBounds() |
| setAllowGravity(false) | Objeto não é afetado pela gravidade | Moedas flutuantes |
Estrutura de arquivos
src/ ├── main.jsx ├── PhaserGame.jsx ├── scenes/ │ ├── BootScene.js │ ├── MenuScene.js │ └── PlatformScene.js ├── objects/ │ ├── Player.js │ ├── Enemy.js │ └── Coin.js # novo objeto └── systems/ └── LevelBuilder.js # sistema de construção de fase
O LevelBuilder.js centraliza a criação das plataformas, moedas e inimigos. Em vez de espalhar create() com dezenas de linhas, você passa um array de dados de nível para o builder e ele monta a cena. Esse padrão é a base de editores de fase e sistemas de tilemap.
O jogador agora tem uma lógica nova e essencial: ele só pode pular se estiver tocando o chão. Isso é verificado via body.touching.down — uma propriedade que o Phaser atualiza automaticamente a cada frame.
export default class Player { constructor(scene, x, y) { this.scene = scene // physics.add.rectangle já cria com física // diferente de add.rectangle + physics.add.existing this.sprite = scene.physics.add.rectangle(x, y, 40, 60, 0x00ffcc) this.sprite.body.setCollideWorldBounds(true) this.speed = 220 this.jumpForce = -450 // negativo = sobe } moveLeft() { this.sprite.body.setVelocityX(-this.speed) } moveRight() { this.sprite.body.setVelocityX(this.speed) } stop() { this.sprite.body.setVelocityX(0) } jump() { // touching.down = true somente quando o sprite // está em contato com uma superfície abaixo dele if (this.sprite.body.touching.down) { this.sprite.body.setVelocityY(this.jumpForce) } } }
touching.down é diferente de blocked.down. O primeiro indica contato com outro objeto; o segundo indica que o objeto está bloqueado pela borda do mundo. Para plataformas, use sempre touching.down.O inimigo usa setBounce(1, 0) para inverter a direção horizontal ao bater nas bordas do mundo — criando uma patrulha automática sem nenhuma lógica adicional.
export default class Enemy { constructor(scene, x, y) { this.scene = scene this.sprite = scene.physics.add.rectangle(x, y, 40, 40, 0xff4444) // Velocidade inicial para a direita this.sprite.body.setVelocityX(100) // bounce(1, 0): reflete X ao bater, ignora Y this.sprite.body.setBounce(1, 0) // Fica dentro dos limites do mundo this.sprite.body.setCollideWorldBounds(true) } }
Variação: inimigo que patrulha entre dois pontos
Para um inimigo que vira ao atingir um ponto específico em vez de bater na parede:
update() { const x = this.sprite.x // Vira de direção entre x=200 e x=700 if (x > 700) this.sprite.body.setVelocityX(-100) else if (x < 200) this.sprite.body.setVelocityX(100) }
A moeda tem física para detectar overlap com o jogador, mas não é afetada pela gravidade. O método setAllowGravity(false) faz ela flutuar no ar.
export default class Coin { constructor(scene, x, y) { this.scene = scene // Círculo dourado de raio 12 this.sprite = scene.physics.add.circle(x, y, 12, 0xffd700) // Desativa a gravidade só para esta moeda // sem isso ela cairia ao chão imediatamente this.sprite.body.setAllowGravity(false) } }
setAllowGravity(false) é essencial para qualquer objeto flutuante: moedas, power-ups, plataformas móveis. Ele desativa a gravidade individualmente, sem afetar outros objetos da cena.O create() desta cena reúne todos os sistemas. Os três pontos mais importantes são: definir a gravidade global, criar plataformas com staticGroup e configurar a câmera para seguir o jogador em um mundo maior que a tela.
import Player from '../objects/Player' import Enemy from '../objects/Enemy' import Coin from '../objects/Coin' export default class PlatformScene extends Phaser.Scene { constructor() { super('Game') } create() { const { width: W, height: H } = this.cameras.main // ─── 1. Gravidade global ───────────────────────────── // Todos os objetos com física são puxados para baixo this.physics.world.gravity.y = 800 // ─── 2. Fundo e decoração ──────────────────────────── this.add.rectangle(1000, H/2, 2000, H, 0x87ceeb) // céu azul // ─── 3. Plataformas (staticGroup) ──────────────────── // staticGroup = grupo de corpos estáticos (não se movem) this.platforms = this.physics.add.staticGroup() // Chão principal: largura 2000, altura 32, na base this.platforms.create(1000, H - 16, null, null, 0x8B5E3C) .setDisplaySize(2000, 32).refreshBody() // Plataformas elevadas const plats = [ { x: 300, y: 420, w: 200 }, { x: 600, y: 320, w: 180 }, { x: 950, y: 420, w: 200 }, { x: 1200, y: 280, w: 160 }, { x: 1500, y: 380, w: 200 }, { x: 1750, y: 260, w: 160 }, ] plats.forEach(({ x, y, w }) => { this.platforms.create(x, y, null, null, 0x5C4033) .setDisplaySize(w, 24).refreshBody() }) // ─── 4. Jogador ────────────────────────────────────── this.player = new Player(this, 100, 400) this.physics.add.collider(this.player.sprite, this.platforms) // ─── 5. Inimigos ───────────────────────────────────── this.enemies = this.physics.add.group() const enemyPositions = [ { x: 600, y: 280 }, { x: 950, y: 380 }, { x: 1500, y: 340 } ] enemyPositions.forEach(({ x, y }) => { const e = new Enemy(this, x, y) this.enemies.add(e.sprite) }) this.physics.add.collider(this.enemies, this.platforms) // ─── 6. Moedas ─────────────────────────────────────── this.coins = this.physics.add.group() const coinPositions = [ { x: 300, y: 380 }, { x: 340, y: 380 }, { x: 600, y: 280 }, { x: 950, y: 380 }, { x: 1200, y: 240 }, { x: 1500, y: 340 }, { x: 1750, y: 220 }, { x: 1790, y: 220 } ] coinPositions.forEach(({ x, y }) => { const c = new Coin(this, x, y) this.coins.add(c.sprite) }) // ─── 7. Colisões e overlaps ────────────────────────── // Jogador coleta moeda this.physics.add.overlap( this.player.sprite, this.coins, this.onCoinCollect, null, this ) // Jogador toca inimigo = reinicia this.physics.add.collider( this.player.sprite, this.enemies, () => this.scene.restart() ) // ─── 8. Câmera ─────────────────────────────────────── // Segue o jogador this.cameras.main.startFollow(this.player.sprite) // Define os limites do mundo (2000x600) this.cameras.main.setBounds(0, 0, 2000, H) this.physics.world.setBounds(0, 0, 2000, H) // ─── 9. Pontuação (fixada na câmera) ───────────────── this.score = 0 this.scoreText = this.add.text(16, 16, 'Moedas: 0', { fontSize: '18px', fontFamily: 'monospace', color: '#ffffff' }).setScrollFactor(0) // fica fixo na tela, não segue o mundo // ─── 10. Controles ─────────────────────────────────── this.cursors = this.input.keyboard.createCursorKeys() this.wasd = this.input.keyboard.addKeys({ left: 'A', right: 'D', up: 'W' }) } }
O update() do plataformer é simples — toda a complexidade já está nos sistemas criados antes. O pulo combina JustDown para não voar ao segurar a tecla.
// Chamado ao sobrepor jogador e moeda onCoinCollect(player, coin) { coin.destroy() this.score++ this.scoreText.setText('Moedas: ' + this.score) // Verificar vitória (todas as moedas coletadas) if (this.coins.countActive() === 0) { this.showVictory() } } update() { // Movimento horizontal if (this.cursors.left.isDown || this.wasd.left.isDown) this.player.moveLeft() else if (this.cursors.right.isDown || this.wasd.right.isDown) this.player.moveRight() else this.player.stop() // Pulo: JustDown evita pulo contínuo ao segurar a tecla const jumpPressed = Phaser.Input.Keyboard.JustDown(this.cursors.up) || Phaser.Input.Keyboard.JustDown(this.wasd.up) if (jumpPressed) { this.player.jump() } } showVictory() { const { width: W, height: H } = this.cameras.main // setScrollFactor(0) = texto fixo na tela this.add.text(W/2, H/2, 'Fase completa!', { fontSize: '40px', fontFamily: 'monospace', color: '#ffd700' }).setOrigin(0.5).setScrollFactor(0) this.add.text(W/2, H/2 + 60, 'Moedas: ' + this.score + ' | ESPAÇO = jogar de novo', { fontSize: '14px', fontFamily: 'monospace', color: '#ffffff' }).setOrigin(0.5).setScrollFactor(0) this.input.keyboard .addKey('SPACE').on('down', () => this.scene.restart()) }
.setScrollFactor(0). Sem isso eles ficam presos ao mundo e saem de vista quando a câmera se move.Um erro comum é configurar apenas um dos dois bounds. Câmera e mundo são independentes e precisam ser definidos separadamente:
// Limita onde a CÂMERA pode ir // (evita mostrar o vazio além do cenário) this.cameras.main.setBounds(0, 0, 2000, 600) // Limita onde os OBJETOS com física podem ir // (evita que o jogador saia pelo lado do mundo) this.physics.world.setBounds(0, 0, 2000, 600)
| Método | Afeta | Sem ele |
|---|---|---|
cameras.main.setBounds() | O que a câmera mostra | Câmera mostra o vazio além do cenário |
physics.world.setBounds() | Onde os objetos podem ir | Jogador atravessa a borda do mundo |
cameras.main.startFollow() | O alvo que a câmera segue | Câmera fica parada no início da fase |
A integração com React é idêntica aos projetos anteriores. Apenas adicione a PlatformScene à lista de cenas.
import { useEffect, useRef } from 'react' import Phaser from 'phaser' import BootScene from './scenes/BootScene' import MenuScene from './scenes/MenuScene' import PlatformScene from './scenes/PlatformScene' export default function PhaserGame() { const divRef = useRef(null) useEffect(() => { const config = { type: Phaser.AUTO, width: 800, height: 600, parent: divRef.current, physics: { default: 'arcade', arcade: { debug: false } }, scene: [BootScene, MenuScene, PlatformScene] } const game = new Phaser.Game(config) return () => game.destroy(true) }, []) return <div ref={divRef} /> }
Conceitos fundamentais aprendidos
Define a força de gravidade aplicada a todos os objetos com física. Quanto maior o valor, mais rápida a queda.
Verdadeiro somente quando o sprite está em contato com uma superfície abaixo dele. Evita pulo duplo.
Grupo de corpos estáticos — não se movem nem são afetados por colisões. Ideal para o terreno da fase.
Desativa a gravidade individualmente para um objeto. Essencial para moedas, power-ups e plataformas móveis.
A câmera segue o jogador dentro dos limites do mundo, revelando o cenário conforme ele avança.
Faz o elemento ignorar o movimento da câmera. Obrigatório para placar, vidas e qualquer interface.
Desafios para evoluir o jogo
- Animação idle, walk, jump
- Sprite sheet com
anims.create - Virar o sprite pela direção
- Criar fase com Tiled editor
- Carregar JSON de tilemap
- Colisão por camada
- Pulo duplo
- Invencibilidade temporária
- Velocidade aumentada
- Patrulha entre dois pontos
- Perseguição ao detectar jogador
- Inimigo que pula
this.make.tilemap() — você deixa de criar plataformas no código e passa a desenhá-las em um editor dedicado.