Jogo plataforma

0

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:

Pong
física · colisão · input
Breakout
grupos · destruição
Space Shooter
spawn · projéteis · overlap
Plataforma
gravidade · câmera · mundo

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

Boot Menu PlatformScene Coletar moedas / Game Over

Novos conceitos desta fase

ConceitoO que éOnde aparece
Gravidade globalForça que puxa todos os objetos para baixophysics.world.gravity.y
Pulo condicionadoSó pode pular se estiver no chãobody.touching.down
Static GroupPlataformas fixas sem física dinâmicaphysics.add.staticGroup()
Câmera com followCâmera segue o jogador no mundocameras.main.startFollow()
Mundo maior que a telaLimites do mundo além da resoluçãocameras.main.setBounds()
setAllowGravity(false)Objeto não é afetado pela gravidadeMoedas 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
Novidade arquitetural desta 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.


1
Player.js — gravidade e pulo condicionado novo

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.

src/objects/Player.js
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.
2
Enemy.js — inimigo que patrulha

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.

src/objects/Enemy.js
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)
}
3
Coin.js — item coletável sem gravidade novo

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.

src/objects/Coin.js
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.
4
PlatformScene — gravidade, plataformas e câmera novo

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.

src/scenes/PlatformScene.js — create()
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'
    })
  }
}
5
Callbacks e update()

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.

src/scenes/PlatformScene.js — callbacks e update
// 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())
}
⚠️ Textos de HUD (placar, vidas, cronômetro) devem sempre ter .setScrollFactor(0). Sem isso eles ficam presos ao mundo e saem de vista quando a câmera se move.
6
Câmera e mundo — os dois setBounds novo

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étodoAfetaSem ele
cameras.main.setBounds()O que a câmera mostraCâmera mostra o vazio além do cenário
physics.world.setBounds()Onde os objetos podem irJogador atravessa a borda do mundo
cameras.main.startFollow()O alvo que a câmera segueCâmera fica parada no início da fase
7
Registrando no PhaserGame.jsx

A integração com React é idêntica aos projetos anteriores. Apenas adicione a PlatformScene à lista de cenas.

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

world.gravity.y
gravidade global

Define a força de gravidade aplicada a todos os objetos com física. Quanto maior o valor, mais rápida a queda.

body.touching.down
pulo condicionado

Verdadeiro somente quando o sprite está em contato com uma superfície abaixo dele. Evita pulo duplo.

staticGroup
plataformas fixas

Grupo de corpos estáticos — não se movem nem são afetados por colisões. Ideal para o terreno da fase.

setAllowGravity(false)
objetos flutuantes

Desativa a gravidade individualmente para um objeto. Essencial para moedas, power-ups e plataformas móveis.

startFollow + setBounds
câmera dinâmica

A câmera segue o jogador dentro dos limites do mundo, revelando o cenário conforme ele avança.

setScrollFactor(0)
HUD fixo na tela

Faz o elemento ignorar o movimento da câmera. Obrigatório para placar, vidas e qualquer interface.


Desafios para evoluir o jogo

🎨 Sprites animados
  • Animação idle, walk, jump
  • Sprite sheet com anims.create
  • Virar o sprite pela direção
🡴 Tilemap
  • Criar fase com Tiled editor
  • Carregar JSON de tilemap
  • Colisão por camada
🏄 Power-ups
  • Pulo duplo
  • Invencibilidade temporária
  • Velocidade aumentada
⚡ Inimigos avançados
  • Patrulha entre dois pontos
  • Perseguição ao detectar jogador
  • Inimigo que pula
🚀 Próximo passo sugerido: experimente o Tiled Map Editor (gratuito) para criar fases visualmente e exportar para JSON. O Phaser lê esses arquivos com this.make.tilemap() — você deixa de criar plataformas no código e passa a desenhá-las em um editor dedicado.