Space Shooter

0

Após Pong e Breakout, o Space Shooter é o próximo salto natural no aprendizado. Ele mantém tudo que você já sabe — física, grupos, destruição — e adiciona os sistemas que estão na base de praticamente todo jogo de ação moderno: disparo de projéteis, geração contínua de inimigos e combate.

🎮 Referências clássicas: este gênero foi definido por Space Invaders (1978), Galaga (1981) e Gradius (1985). Os sistemas criados nessa época — spawn de inimigos, projéteis, power-ups — continuam sendo a espinha dorsal de shooters, RPGs de ação e jogos multiplayer até hoje.

Por que Space Shooter depois do Breakout?

Cada jogo da série introduz novos conceitos sem descartar os anteriores. Veja a progressão:

ConceitoPongBreakoutSpace Shooter
Física de objetos
Grupos e destruição
Sistema de projéteis✔ novo
Spawn contínuo de inimigos✔ novo
Separação em sistemas✔ novo
Overlap vs Collider✔ novo

Objetivos do projeto

  • Nave do jogador com movimento horizontal
  • Disparo de lasers com a tecla ESPAÇO
  • Inimigos surgindo continuamente do topo da tela
  • Colisão entre tiros e inimigos com destruição de ambos
  • Sistema de pontuação acumulada
  • Game Over quando um inimigo atinge o jogador

Fluxo do jogo

Boot Menu SpaceScene Game Over

Estrutura de arquivos

src/
 ├── main.jsx
 ├── PhaserGame.jsx
 ├── scenes/
 │   ├── BootScene.js
 │   ├── MenuScene.js
 │   └── SpaceScene.js
 ├── objects/
 │   ├── Player.js
 │   ├── Laser.js
 │   └── Enemy.js
 └── systems/
     └── EnemySpawner.js    # nova pasta: sistemas de gameplay
Novidade arquitetural desta fase

A pasta systems/ é introduzida aqui. Enquanto objects/ guarda entidades do jogo (o que existe no mundo), systems/ guarda lógicas de gameplay independentes (o que acontece no mundo). Separar os dois torna o código muito mais fácil de expandir.


1
Player.js — a nave do jogador

A nave é um retângulo colorido com física arcade. Ela se move apenas no eixo X — o jogador não precisa de controle vertical neste estilo de shooter.

src/objects/Player.js
export default class Player {
  constructor(scene, x, y) {
    this.scene = scene

    // Nave: retângulo 40x40 na cor ciano
    this.sprite = scene.add.rectangle(x, y, 40, 40, 0x00ffcc)
    scene.physics.add.existing(this.sprite)

    // Não sai pelos lados da tela
    this.sprite.body.setCollideWorldBounds(true)

    this.speed = 320
  }

  moveLeft()  { this.sprite.body.setVelocityX(-this.speed) }
  moveRight() { this.sprite.body.setVelocityX(this.speed) }
  stop()      { this.sprite.body.setVelocityX(0) }

  destroy() {
    this.sprite.destroy()
  }
}
2
Laser.js — projétil do jogador novo

O laser é um objeto simples: nasce na posição da nave e sobe com velocidade Y negativa. Cada disparo cria uma nova instância dessa classe.

src/objects/Laser.js
export default class Laser {
  constructor(scene, x, y) {
    this.scene = scene

    // Projétil fino: 4x20 pixels na cor vermelha
    this.sprite = scene.add.rectangle(x, y, 4, 20, 0xff3366)
    scene.physics.add.existing(this.sprite)

    // Velocidade negativa = sobe na tela
    this.sprite.body.setVelocityY(-520)
  }
}
💡 Lasers que saem pelo topo da tela continuam existindo na memória se você não os destruir. Na SpaceScene vamos checar y < -20 no update() e chamar destroy() para evitar vazamento de memória.
3
Enemy.js — o inimigo novo

O inimigo nasce fora da tela (Y negativo) e desce com velocidade constante. Quando ultrapassa a base da tela, o jogo termina.

src/objects/Enemy.js
export default class Enemy {
  constructor(scene, x, y) {
    this.scene = scene

    // Inimigo: quadrado vermelho 36x36
    this.sprite = scene.add.rectangle(x, y, 36, 36, 0xff4444)
    scene.physics.add.existing(this.sprite)

    // Desce em direção ao jogador
    this.sprite.body.setVelocityY(130)
  }
}

Variação: inimigos com comportamento diferente

Você pode criar subtipos passando parâmetros ao construtor:

export default class Enemy {
  constructor(scene, x, y, type = 'normal') {
    this.scene = scene
    this.type  = type

    // Configurações por tipo
    const cfg = {
      normal: { size: 36, color: 0xff4444, speed: 130 },
      fast:   { size: 26, color: 0xff8800, speed: 260 },
      tank:   { size: 50, color: 0xaa0000, speed: 70  }
    }
    const { size, color, speed } = cfg[type] || cfg.normal

    this.sprite = scene.add.rectangle(x, y, size, size, color)
    scene.physics.add.existing(this.sprite)
    this.sprite.body.setVelocityY(speed)
  }
}
4
EnemySpawner.js — sistema de geração novo

O EnemySpawner é o primeiro sistema do projeto — uma classe que não representa um objeto visual, mas uma lógica de gameplay. Ele usa time.addEvent para criar inimigos em intervalos regulares.

src/systems/EnemySpawner.js
import Enemy from '../objects/Enemy'

export default class EnemySpawner {
  constructor(scene, interval = 1400) {
    this.scene = scene
    this.active = true

    // time.addEvent: repete o callback em loop
    this.event = scene.time.addEvent({
      delay: interval,           // ms entre cada spawn
      callback: this.spawn,
      callbackScope: this,
      loop: true
    })
  }

  spawn() {
    if (!this.active) return

    // Posição X aleatória, Y acima da tela
    const x = Phaser.Math.Between(30, this.scene.cameras.main.width - 30)
    const enemy = new Enemy(this.scene, x, -50)

    // Adiciona ao group da cena para rastreamento
    this.scene.enemies.add(enemy.sprite)
  }

  stop() {
    this.active = false
    this.event.remove()
  }
}
⚠️ Chame sempre spawner.stop() ao fim da partida. Se o timer continuar rodando após o Game Over, ele tentará criar inimigos em uma cena que não existe mais, causando erros.
5
SpaceScene.js — a cena principal

A SpaceScene orquestra todos os sistemas. O ponto mais importante é entender a diferença entre collider e overlap — o comportamento físico de cada um é diferente.

src/scenes/SpaceScene.js — create()
import Player       from '../objects/Player'
import Laser        from '../objects/Laser'
import EnemySpawner from '../systems/EnemySpawner'

export default class SpaceScene extends Phaser.Scene {
  constructor() { super('Game') }

  create() {
    const { width: W, height: H } = this.cameras.main

    // Fundo estrelado
    this.add.rectangle(W/2, H/2, W, H, 0x05050f)
    for (let i = 0; i < 80; i++) {
      const sx = Phaser.Math.Between(0, W)
      const sy = Phaser.Math.Between(0, H)
      const sz = Phaser.Math.Between(1, 3)
      this.add.circle(sx, sy, sz, 0xffffff, 0.6)
    }

    // Jogador
    this.player = new Player(this, W/2, H - 70)

    // Grupos de entidades
    this.lasers  = this.physics.add.group()
    this.enemies = this.physics.add.group()

    // Sistema de spawn
    this.spawner = new EnemySpawner(this, 1400)

    // Controles
    this.cursors  = this.input.keyboard.createCursorKeys()
    this.wasd     = this.input.keyboard.addKeys({ left: 'A', right: 'D' })
    this.spaceKey = this.input.keyboard.addKey('SPACE')

    // Cooldown de tiro (evita spam)
    this.lastShot = 0
    this.shotDelay = 220  // ms entre disparos

    // Pontuação
    this.score = 0
    this.scoreText = this.add.text(16, 16, 'Pontos: 0', {
      fontSize: '18px', fontFamily: 'monospace', color: '#ffffff'
    })

    // overlap: laser e inimigo se sobrepõem e são destruídos
    this.physics.add.overlap(
      this.lasers, this.enemies,
      this.onLaserHitEnemy, null, this
    )

    // overlap: inimigo atinge o jogador = game over
    this.physics.add.overlap(
      this.player.sprite, this.enemies,
      this.onPlayerHit, null, this
    )

    this.gameOver = false
  }
}
6
Callbacks, update() e Game Over

O update() roda 60 vezes por segundo e cuida do movimento, do disparo e da limpeza de objetos fora da tela. Os callbacks lidam com os dois tipos de colisão.

src/scenes/SpaceScene.js — update e callbacks
// Laser acertou um inimigo
onLaserHitEnemy(laser, enemy) {
  laser.destroy()
  enemy.destroy()

  this.score += 10
  this.scoreText.setText('Pontos: ' + this.score)
}

// Inimigo atingiu o jogador
onPlayerHit(player, enemy) {
  if (this.gameOver) return
  this.gameOver = true
  this.spawner.stop()   // para de gerar inimigos
  player.destroy()
  enemy.destroy()
  this.showGameOver()
}

update(time) {
  if (this.gameOver) return

  // Movimento
  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()

  // Disparo com cooldown
  if (this.spaceKey.isDown && time > this.lastShot + this.shotDelay) {
    this.lastShot = time
    const laser = new Laser(
      this,
      this.player.sprite.x,
      this.player.sprite.y - 28
    )
    this.lasers.add(laser.sprite)
  }

  // Limpar lasers que saíram pelo topo
  this.lasers.getChildren().forEach(l => {
    if (l.y < -20) l.destroy()
  })

  // Limpar inimigos que passaram pela base
  const H = this.cameras.main.height
  this.enemies.getChildren().forEach(e => {
    if (e.y > H + 20) e.destroy()
  })
}

showGameOver() {
  const { width: W, height: H } = this.cameras.main

  this.add.text(W/2, H/2 - 30, 'Game Over', {
    fontSize: '44px', fontFamily: 'monospace', color: '#ff4444'
  }).setOrigin(0.5)

  this.add.text(W/2, H/2 + 30,
    'Pontos: ' + this.score, {
    fontSize: '22px', fontFamily: 'monospace', color: '#ffffff'
  }).setOrigin(0.5)

  this.add.text(W/2, H/2 + 80,
    'ESPAÇO = jogar de novo   |   ESC = menu', {
    fontSize: '13px', fontFamily: 'monospace', color: '#aaaaaa'
  }).setOrigin(0.5)

  this.input.keyboard
    .addKey('SPACE').on('down', () => this.scene.restart())
  this.input.keyboard
    .addKey('ESC').on('down', () => this.scene.start('Menu'))
}
💡 O cooldown de tiro (time > lastShot + shotDelay) é um padrão essencial. Sem ele, manter o ESPAÇO pressionado dispara centenas de lasers por segundo, travando o jogo.
7
Collider vs Overlap — qual usar?

Essa é uma dúvida muito comum. O Phaser oferece dois tipos de detecção de contato entre objetos, com comportamentos completamente diferentes:

collideroverlap
Reação físicaOs objetos se repelemOs objetos se ignoram fisicamente
Uso idealBola x parede, bola x raqueteLaser x inimigo, jogador x power-up
Exemplo práticoPong, BreakoutSpace Shooter, RPG
// collider: empurra os objetos ao colidir
this.physics.add.collider(bola, raquete)

// overlap: detecta o contato sem reação física
this.physics.add.overlap(laser, inimigo, callback)
⚠️ Use overlap para projéteis e power-ups. Se usar collider, o laser vai empurrar o inimigo ao invés de destruí-lo, criando um comportamento estranho.
8
Registrando no PhaserGame.jsx

A integração com React continua idêntica aos projetos anteriores. Adicione a SpaceScene à lista de cenas e o Phaser cuida do resto.

src/PhaserGame.jsx
import { useEffect, useRef } from 'react'
import Phaser from 'phaser'
import BootScene  from './scenes/BootScene'
import MenuScene  from './scenes/MenuScene'
import SpaceScene from './scenes/SpaceScene'

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, SpaceScene]
    }

    const game = new Phaser.Game(config)
    return () => game.destroy(true)
  }, [])

  return <div ref={divRef} />
}

Conceitos fundamentais aprendidos

physics.add.overlap()
detecção sem física

Detecta contato entre objetos sem reação física. Ideal para projéteis, power-ups e coleta de itens.

time.addEvent(loop)
spawn contínuo

Executa um callback em intervalo fixo. Base de qualquer sistema de spawn ou progressão de dificuldade.

Cooldown de tiro
time > lastShot + delay

Padrão para limitar a cadência de disparo. Sem ele, uma tecla pressionada dispara centenas de projéteis.

Limpeza de objetos
getChildren().forEach

Remove objetos fora da tela para não acumular sprites invisíveis na memória.

Pasta systems/
separação de lógica

Separa lógicas de gameplay (o que acontece) dos objetos do jogo (o que existe). Código mais organizado.

spawner.stop()
limpeza de timers

Para os timers ativos ao fim da partida. Timers esquecidos causam erros e comportamentos inesperados.


Desafios para evoluir o jogo

👹 Tipos de inimigos
  • Inimigos rápidos (fast)
  • Tanques com mais vida
  • Inimigos que disparam de volta
🔫 Sistema de armas
  • Tiro duplo lateral
  • Tiro espalhado em 3 direções
  • Laser contínuo (hold)
⚡ Power-ups
  • Escudo temporário
  • Velocidade de tiro dobrada
  • Bomba que limpa a tela
🎪 Progressão
  • Velocidade aumenta por nível
  • Spawn mais frequente
  • Chefe a cada 500 pontos
🚀 Dica final: para adicionar efeitos de explosão ao destruir inimigos, explore this.add.particles() do Phaser 3 combinado com emitter.explode(). Com poucas linhas você tem um efeito visual que transforma completamente a sensação do jogo.