Breakout
Voltar para: Games com Phaser + Reactjs
Após dominar o Pong, o próximo passo natural é o Breakout — um jogo que mantém a física da bola e da raquete, mas adiciona um novo desafio: destruir blocos. Neste tutorial você aprende a gerenciar grupos de objetos, colisões múltiplas e destruição dinâmica com React + Phaser 3.
📚 Curiosidade histórica: O Breakout foi desenvolvido pela Atari em 1976. Um dos engenheiros envolvidos foi Steve Wozniak, com participação de Steve Jobs — ainda no início da história que levaria à fundação da Apple.
Por que Breakout depois do Pong?
O Breakout é didaticamente perfeito como segundo projeto porque reutiliza o que você já sabe (bola, raquete, física arcade) e adiciona conceitos novos e essenciais para qualquer jogo moderno:
| Conceito | Pong | Breakout |
|---|---|---|
| Raquete + bola com física | ✔ | ✔ |
| Física de rebatida (bounce) | ✔ | ✔ |
| Grupos de objetos (group) | — | ✔ novo |
| Destruição dinâmica de sprites | — | ✔ novo |
| Colisões com múltiplos objetos | — | ✔ novo |
| Progressão e condição de vitória | — | ✔ novo |
Objetivos do projeto
- Paddle controlada pelo jogador (esquerda e direita)
- Bola com física e bounce nas paredes
- Grade de blocos coloridos e destrutíveis
- Sistema de pontuação por bloco quebrado
- Reinício automático da bola ao sair pela base
- Condição de vitória ao limpar todos os blocos
Fluxo do jogo
Estrutura de arquivos
src/
├── main.jsx
├── PhaserGame.jsx
├── scenes/
│ ├── BootScene.js
│ ├── MenuScene.js
│ └── BreakoutScene.js
└── objects/
├── Paddle.js
├── Ball.js
└── Brick.js # novo objeto desta fase!
A raquete do Breakout é praticamente idêntica à do Pong. A diferença está no eixo de movimento: agora usamos setVelocityX no lugar de setVelocityY, pois a raquete se move da esquerda para a direita.
export default class Paddle { constructor(scene, x, y) { this.scene = scene // Raquete mais larga (120x20) e horizontal this.sprite = scene.add.rectangle(x, y, 120, 20, 0xffffff) scene.physics.add.existing(this.sprite) this.sprite.body.setImmovable(true) this.sprite.body.setCollideWorldBounds(true) } // Diferença do Pong: movimento no eixo X moveLeft() { this.sprite.body.setVelocityX(-420) } moveRight() { this.sprite.body.setVelocityX(420) } stop() { this.sprite.body.setVelocityX(0) } }
A bola funciona igual à do Pong. O detalhe importante está no launch(): a velocidade Y precisa ser negativa para que ela suba em direção aos blocos — no Phaser, o eixo Y cresce para baixo.
export default class Ball { constructor(scene, x, y) { this.scene = scene this.sprite = scene.add.circle(x, y, 10, 0xffffff) scene.physics.add.existing(this.sprite) this.sprite.body.setBounce(1, 1) this.sprite.body.setCollideWorldBounds(true) } launch() { // X aleatório para variar a direção inicial // Y negativo = a bola sobe em direção aos blocos const vx = Phaser.Math.Between(-180, 180) this.sprite.body.setVelocity(vx, -320) } reset(x, y) { this.sprite.setPosition(x, y) this.sprite.body.setVelocity(0, 0) } }
O Brick é simples: um retângulo com física imóvel. A grande novidade em relação ao Pong é que ele pode ser removido da cena em tempo real com destroy().
export default class Brick { constructor(scene, x, y, color = 0xff4444) { this.scene = scene // Bloco de 64x28 pixels com cor configurável this.sprite = scene.add.rectangle(x, y, 64, 28, color) scene.physics.add.existing(this.sprite) // Imóvel: não é empurrado pela bola ao colidir this.sprite.body.setImmovable(true) } destroy() { // Remove o sprite da cena e da física this.sprite.destroy() } }
Variação: blocos com múltiplos hits
Quer blocos mais resistentes que precisam de 2 ou 3 golpes para quebrar? Adicione um sistema de hp:
export default class Brick { constructor(scene, x, y, hp = 1) { this.hp = hp this.scene = scene // Cor indica a resistência do bloco const colors = { 1: 0xff4444, 2: 0xff8800, 3: 0xaaaaff } this.sprite = scene.add.rectangle( x, y, 64, 28, colors[hp] || 0xff4444 ) scene.physics.add.existing(this.sprite) this.sprite.body.setImmovable(true) } hit() { this.hp-- if (this.hp <= 0) { this.sprite.destroy() return true // bloco destruído } // Muda para amarelo ao tomar dano this.sprite.setFillStyle(0xffcc00) return false // ainda vivo } }
O coração do Breakout é o physics group: uma coleção de sprites que o Phaser monitora em conjunto. O for duplo (linhas × colunas) gera a grade inteira automaticamente com uma paleta de cores por linha.
import Paddle from '../objects/Paddle' import Ball from '../objects/Ball' import Brick from '../objects/Brick' export default class BreakoutScene extends Phaser.Scene { constructor() { super('Game') } create() { const { width: W, height: H } = this.cameras.main // Fundo this.add.rectangle(W/2, H/2, W, H, 0x1a1a2e) // Raquete e bola this.paddle = new Paddle(this, W/2, H - 50) this.ball = new Ball(this, W/2, H - 80) // ─── Grade de blocos ───────────────────────────────── this.bricks = this.physics.add.group() const cols = 10 const rows = 5 const bW = 68, bH = 28 const startX = 44, startY = 80 // Uma cor por linha const rowColors = [ 0xee4444, 0xff8800, 0xffcc00, 0x44cc44, 0x4488ff ] for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { const x = startX + col * (bW + 6) const y = startY + row * (bH + 6) const brick = new Brick(this, x, y, rowColors[row]) // Adiciona ao group para o Phaser monitorar this.bricks.add(brick.sprite) } } // ───────────────────────────────────────────────────── // Colisão bola ↔ raquete this.physics.add.collider( this.ball.sprite, this.paddle.sprite, this.onHitPaddle, null, this ) // Colisão bola ↔ qualquer bloco do group this.physics.add.collider( this.ball.sprite, this.bricks, this.onHitBrick, null, this ) // Controles: setas + WASD this.cursors = this.input.keyboard.createCursorKeys() this.wasd = this.input.keyboard.addKeys({ left: 'A', right: 'D' }) // Placar this.score = 0 this.scoreText = this.add.text(16, H - 32, 'Pontos: 0', { fontSize: '18px', fontFamily: 'monospace', color: '#ffffff' }) // Lança bola após 1s para o jogador se preparar this.time.delayedCall(1000, () => this.ball.launch()) } }
Os callbacks são o núcleo da lógica do Breakout. O onHitBrick destrói o bloco e atualiza o placar; o onHitPaddle ajusta o ângulo da bola conforme onde ela acerta a raquete — isso dá ao jogador controle sobre a direção.
// Chamado quando a bola toca a raquete onHitPaddle(ball, paddle) { // Quanto mais na borda, mais inclinado o rebate const diff = ball.x - paddle.x const angle = diff * 3 const speed = Math.min(ball.body.speed * 1.04, 520) ball.body.setVelocity(angle, -speed) } // Chamado quando a bola toca qualquer bloco onHitBrick(ball, brickSprite) { brickSprite.destroy() // remove o bloco this.score += 10 this.scoreText.setText('Pontos: ' + this.score) // countActive() conta sprites ainda vivos no group if (this.bricks.countActive() === 0) { this.showVictory() } } update() { // Controle da raquete: setas ou WASD if (this.cursors.left.isDown || this.wasd.left.isDown) this.paddle.moveLeft() else if (this.cursors.right.isDown || this.wasd.right.isDown) this.paddle.moveRight() else this.paddle.stop() // Bola saiu pela base: reinicia acima da raquete const H = this.cameras.main.height if (this.ball.sprite.y > H + 20) { this.ball.reset(this.paddle.sprite.x, H - 80) this.time.delayedCall(1000, () => this.ball.launch()) } } showVictory() { const { width: W, height: H } = this.cameras.main this.ball.sprite.body.setVelocity(0, 0) this.add.text(W/2, H/2, 'Você venceu!', { fontSize: '42px', fontFamily: 'monospace', color: '#66ff88' }).setOrigin(0.5) this.add.text(W/2, H/2 + 60, 'Pontos: ' + this.score + ' | ESPAÇO = jogar de novo', { fontSize: '14px', fontFamily: 'monospace', color: '#aaaaaa' }).setOrigin(0.5) this.input.keyboard .addKey('SPACE').on('down', () => this.scene.restart()) }
this.bricks.countActive() para verificar se todos os blocos foram destruídos. Evite manter um contador manual — ele pode sair de sincronia com o estado real do group.A integração com React é exatamente igual à do Pong. Basta substituir (ou adicionar) a BreakoutScene na lista de cenas do config.
import { useEffect, useRef } from 'react' import Phaser from 'phaser' import BootScene from './scenes/BootScene' import MenuScene from './scenes/MenuScene' import BreakoutScene from './scenes/BreakoutScene' export default function PhaserGame() { const divRef = useRef(null) useEffect(() => { const config = { type: Phaser.AUTO, width: 700, height: 560, parent: divRef.current, physics: { default: 'arcade', arcade: { debug: false } }, scene: [BootScene, MenuScene, BreakoutScene] } const game = new Phaser.Game(config) return () => game.destroy(true) // cleanup ao desmontar }, []) return <div ref={divRef} /> }
Conceitos fundamentais aprendidos
Agrupa múltiplos sprites para colisões coletivas. O Phaser monitora o estado de cada um automaticamente.
Remove o objeto da cena e da física ao mesmo tempo. O group é atualizado sem intervenção manual.
Função executada a cada colisão. Recebe os dois objetos como parâmetros para você agir sobre eles.
Padrão clássico para criar grades. O mesmo princípio vale para mapas de tiles e ondas de inimigos.
Conta sprites ainda vivos no group. Retorna 0 quando todos os blocos foram destruídos.
Executa uma função após N ms. Ideal para dar uma pausa dramática antes de relançar a bola.
Desafios para evoluir o jogo
- Blocos com 2 ou 3 hits
- Blocos indestrutíveis
- Cor muda ao tomar dano
- Raquete maior por 10s
- Bola em câmera lenta
- 3 bolas simultâneas
- Partículas ao quebrar bloco
- Flash de cor no impacto
- Efeitos sonoros de rebatida
- Layouts diferentes por nível
- Velocidade cresce por fase
- Pontuação multiplica por nível
this.add.particles() do Phaser 3. Com 5 a 6 linhas de código você obtém uma explosão colorida que transforma completamente o visual do jogo.