Space Shooter
Voltar para: Games com Phaser + Reactjs
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:
| Conceito | Pong | Breakout | Space 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
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
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.
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.
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() } }
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.
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) } }
y < -20 no update() e chamar destroy() para evitar vazamento de memória.O inimigo nasce fora da tela (Y negativo) e desce com velocidade constante. Quando ultrapassa a base da tela, o jogo termina.
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) } }
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.
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() } }
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.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.
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 } }
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.
// 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')) }
time > lastShot + shotDelay) é um padrão essencial. Sem ele, manter o ESPAÇO pressionado dispara centenas de lasers por segundo, travando o jogo.Essa é uma dúvida muito comum. O Phaser oferece dois tipos de detecção de contato entre objetos, com comportamentos completamente diferentes:
| collider | overlap | |
|---|---|---|
| Reação física | Os objetos se repelem | Os objetos se ignoram fisicamente |
| Uso ideal | Bola x parede, bola x raquete | Laser x inimigo, jogador x power-up |
| Exemplo prático | Pong, Breakout | Space 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)
collider, o laser vai empurrar o inimigo ao invés de destruí-lo, criando um comportamento estranho.A integração com React continua idêntica aos projetos anteriores. Adicione a SpaceScene à lista de cenas e o Phaser cuida do resto.
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
Detecta contato entre objetos sem reação física. Ideal para projéteis, power-ups e coleta de itens.
Executa um callback em intervalo fixo. Base de qualquer sistema de spawn ou progressão de dificuldade.
Padrão para limitar a cadência de disparo. Sem ele, uma tecla pressionada dispara centenas de projéteis.
Remove objetos fora da tela para não acumular sprites invisíveis na memória.
Separa lógicas de gameplay (o que acontece) dos objetos do jogo (o que existe). Código mais organizado.
Para os timers ativos ao fim da partida. Timers esquecidos causam erros e comportamentos inesperados.
Desafios para evoluir o jogo
- Inimigos rápidos (fast)
- Tanques com mais vida
- Inimigos que disparam de volta
- Tiro duplo lateral
- Tiro espalhado em 3 direções
- Laser contínuo (hold)
- Escudo temporário
- Velocidade de tiro dobrada
- Bomba que limpa a tela
- Velocidade aumenta por nível
- Spawn mais frequente
- Chefe a cada 500 pontos
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.