Breakout

0

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:

ConceitoPongBreakout
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

Boot Menu BreakoutScene Vitória / Reinício

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!

1
Paddle.js — raquete horizontal

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.

src/objects/Paddle.js
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) }
}
2
Ball.js — lançamento em direção aos blocos

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.

src/objects/Ball.js
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)
  }
}
💡 No Phaser 3 o eixo Y começa no topo da tela (Y=0) e cresce para baixo. Por isso, velocidade Y negativa move a bola para cima, em direção aos blocos.
3
Brick.js — o bloco destrutível novo

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().

src/objects/Brick.js
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
  }
}
4
BreakoutScene — criando a grade de blocos novo

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.

src/scenes/BreakoutScene.js — create()
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())
  }
}
5
Callbacks de colisão e update()

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.

src/scenes/BreakoutScene.js — callbacks e update
// 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())
}
⚠️ Use sempre 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.
6
Registrando no PhaserGame.jsx

A integração com React é exatamente igual à do Pong. Basta substituir (ou adicionar) a BreakoutScene na lista de cenas do config.

src/PhaserGame.jsx
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

physics.add.group()
grupo de objetos

Agrupa múltiplos sprites para colisões coletivas. O Phaser monitora o estado de cada um automaticamente.

sprite.destroy()
destruição dinâmica

Remove o objeto da cena e da física ao mesmo tempo. O group é atualizado sem intervenção manual.

collider callback
3º parâmetro

Função executada a cada colisão. Recebe os dois objetos como parâmetros para você agir sobre eles.

loop duplo for
grade de objetos

Padrão clássico para criar grades. O mesmo princípio vale para mapas de tiles e ondas de inimigos.

countActive()
verificar vitória

Conta sprites ainda vivos no group. Retorna 0 quando todos os blocos foram destruídos.

time.delayedCall()
atraso de ação

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

🡹 Tipos de blocos
  • Blocos com 2 ou 3 hits
  • Blocos indestrutíveis
  • Cor muda ao tomar dano
⚡ Power-ups
  • Raquete maior por 10s
  • Bola em câmera lenta
  • 3 bolas simultâneas
🎨 Efeitos visuais
  • Partículas ao quebrar bloco
  • Flash de cor no impacto
  • Efeitos sonoros de rebatida
🎮 Sistema de fases
  • Layouts diferentes por nível
  • Velocidade cresce por fase
  • Pontuação multiplica por nível
🚀 Dica final: para criar partículas ao quebrar blocos, explore o 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.