Voltar para o Blog
Quest Log

Como Fazer um Breakout na Godot: Tutorial Completo em GDScript

Bola quicando na raquete com fileiras de blocos coloridos no topo da tela, estilo Breakout, feito na Godot 4

Como fazer breakout na Godot 4 passo a passo: raquete, bola com bounce, grid de blocos por codigo, vidas, game over e dificuldade em GDScript tipado.

O Breakout e um dos melhores projetos pra quem quer praticar fisica arcade na Godot: uma raquete embaixo, uma bola quicando e um muro de blocos pra demolir. A mecanica parece simples, mas ela te obriga a resolver problemas de verdade: reflexao de vetor, colisao com resposta customizada, grid gerado por codigo, sinais, pontuacao e game over. Tudo isso em tres scripts curtos.

Neste tutorial voce monta um Breakout completo na Godot 4 com GDScript sempre tipado. A bola usa move_and_collide com bounce(), o angulo do rebote depende de onde ela bate na raquete, os blocos nascem de dois for aninhados e a dificuldade sobe conforme voce destroi blocos. Se voce ja fez o Flappy Bird na Godot, este aqui e o proximo degrau natural: mais fisica, mesma escala de projeto.

A estrutura da cena

O jogo inteiro cabe em uma cena principal e duas cenas menores instanciadas nela (a bola e o bloco). A arvore fica assim:

Main (Node2D)
├── Paredes (StaticBody2D)
│   ├── ColisaoEsquerda (CollisionShape2D)
│   ├── ColisaoDireita (CollisionShape2D)
│   └── ColisaoTopo (CollisionShape2D)
├── Raquete (CharacterBody2D)
│   ├── Sprite2D
│   └── CollisionShape2D
├── Bola (CharacterBody2D)
│   ├── Sprite2D
│   └── CollisionShape2D
├── Blocos (Node2D)
└── CanvasLayer
    ├── LabelPontos (Label)
    ├── LabelVidas (Label)
    └── LabelFim (Label)

As paredes sao um unico StaticBody2D com tres CollisionShape2D de retangulo: um colado na borda esquerda, um na direita e um no topo. Nao existe parede embaixo de proposito: e por ali que a bola escapa e voce perde vida. O no Blocos comeca vazio, ele e so um container que o codigo vai encher com o grid.

Antes de programar, crie as acoes de input. Em Project > Project Settings > Input Map, adicione mover_esquerda (seta esquerda e tecla A) e mover_direita (seta direita e tecla D).

A raquete: CharacterBody2D com movimento por input

A raquete e um CharacterBody2D com um Sprite2D (um retangulo de uns 140x24 pixels resolve) e um CollisionShape2D com RectangleShape2D do mesmo tamanho. O script so le o input, move no eixo X e trava nas bordas:

class_name Raquete
extends CharacterBody2D

@export var velocidade: float = 600.0
@export var largura: float = 140.0
@export var limite_esquerda: float = 90.0
@export var limite_direita: float = 1062.0

func _physics_process(delta: float) -> void:
    var direcao: float = Input.get_axis("mover_esquerda", "mover_direita")
    velocity = Vector2(direcao * velocidade, 0.0)
    move_and_collide(velocity * delta)
    position.x = clampf(position.x, limite_esquerda, limite_direita)

O Input.get_axis devolve um float entre -1.0 e 1.0 combinando as duas acoes, entao uma linha cobre esquerda, direita e parado. O clampf impede a raquete de sair da tela (os limites valem pra viewport padrao de 1152x648, ajuste se a sua for outra). O class_name Raquete importa: e ele que deixa a bola perguntar if alvo is Raquete daqui a pouco. A largura exportada tambem vai ser usada pela bola pra calcular o angulo do rebote.

A bola: velocidade constante, move_and_collide e bounce

Aqui mora o coracao do Breakout. A bola e um CharacterBody2D com CollisionShape2D circular (raio de uns 10 pixels). Nada de RigidBody2D: fisica realista faria a bola perder energia e ganhar efeitos que voce nao quer. No Breakout a bola tem velocidade constante e reflexo perfeito, e isso se faz na mao.

O truque e usar move_and_collide, que move o corpo e devolve um KinematicCollision2D se bateu em algo. Esse objeto entrega a normal da superficie, e Vector2.bounce() reflete a direcao em cima dessa normal, como luz num espelho:

class_name Bola
extends CharacterBody2D

signal bloco_atingido(pontos: int)
signal bola_perdida

@export var velocidade_inicial: float = 420.0
@export var velocidade_maxima: float = 820.0
@export var angulo_maximo: float = 60.0
@export var limite_inferior: float = 700.0

var velocidade_atual: float = 0.0
var direcao: Vector2 = Vector2.ZERO
var em_jogo: bool = false
var posicao_inicial: Vector2 = Vector2.ZERO

func _ready() -> void:
    posicao_inicial = global_position
    lancar()

func lancar() -> void:
    global_position = posicao_inicial
    velocidade_atual = velocidade_inicial
    var angulo: float = randf_range(-PI / 6.0, PI / 6.0)
    direcao = Vector2.UP.rotated(angulo)
    em_jogo = true

func acelerar(quanto: float) -> void:
    velocidade_atual = minf(velocidade_atual + quanto, velocidade_maxima)

func _physics_process(delta: float) -> void:
    if not em_jogo:
        return
    if global_position.y > limite_inferior:
        em_jogo = false
        bola_perdida.emit()
        return
    var colisao: KinematicCollision2D = move_and_collide(direcao * velocidade_atual * delta)
    if colisao == null:
        return
    var alvo: Object = colisao.get_collider()
    if alvo is Raquete:
        _quicar_na_raquete(alvo as Raquete)
    elif alvo is Bloco:
        direcao = direcao.bounce(colisao.get_normal()).normalized()
        var bloco: Bloco = alvo as Bloco
        bloco_atingido.emit(bloco.pontos)
        bloco.destruir()
    else:
        direcao = direcao.bounce(colisao.get_normal()).normalized()

func _quicar_na_raquete(raquete: Raquete) -> void:
    var deslocamento: float = (global_position.x - raquete.global_position.x) / (raquete.largura / 2.0)
    deslocamento = clampf(deslocamento, -1.0, 1.0)
    var angulo: float = deslocamento * deg_to_rad(angulo_maximo)
    direcao = Vector2.UP.rotated(angulo)

Repare em tres decisoes de design:

Velocidade separada de direcao. Em vez de um unico vetor de velocidade, guardo direcao (sempre normalizada) e velocidade_atual (um float). Assim a bola nunca acelera nem freia sozinha, e aumentar dificuldade vira so mexer num numero.

Paredes e blocos usam bounce puro. direcao.bounce(colisao.get_normal()) resolve qualquer superficie: parede lateral inverte o X, teto e bloco invertem o Y, quina reflete na diagonal. O .normalized() no final e paranoia saudavel contra erro acumulado de ponto flutuante.

A raquete nao usa bounce. Se usasse, a bola voltaria sempre no angulo espelhado e o jogador nao teria controle nenhum. Em vez disso, _quicar_na_raquete mede onde a bola bateu: no centro o deslocamento e 0 e ela sobe reta, na ponta o deslocamento chega a 1 e ela sai a 60 graus. E essa regra que transforma o Breakout num jogo de mira, nao de sorte.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

O bloco: StaticBody2D com queue_free

O bloco e a menor cena do projeto: um StaticBody2D com Sprite2D (retangulo de 96x32) e CollisionShape2D do mesmo tamanho. Salve como bloco.tscn. O script e minimo:

class_name Bloco
extends StaticBody2D

@export var pontos: int = 10

func destruir() -> void:
    queue_free()

O queue_free marca o no pra remocao no fim do frame, que e a forma segura de destruir algo durante o processamento de fisica. A pontuacao nao e somada aqui: o bloco so sabe quanto vale, e a bola emite o sinal bloco_atingido com esse valor. Quem soma e a cena principal, que e quem tem contexto de placar, vidas e fim de jogo. Cada script cuida so do seu pedaco.

O grid de blocos gerado por codigo

Posicionar 50 blocos na mao no editor e tortura. Dois for aninhados fazem isso em dez linhas, e mudar o formato da fase vira trocar numeros no Inspector. Este e o script da cena principal, o Main:

extends Node2D

const CENA_BLOCO: PackedScene = preload("res://bloco.tscn")

@export var linhas: int = 5
@export var colunas: int = 10
@export var largura_bloco: float = 96.0
@export var altura_bloco: float = 32.0
@export var margem: float = 8.0
@export var origem: Vector2 = Vector2(88.0, 96.0)
@export var vidas_iniciais: int = 3
@export var blocos_por_aceleracao: int = 8
@export var aceleracao: float = 45.0

var pontuacao: int = 0
var vidas: int = 3
var blocos_destruidos: int = 0
var total_blocos: int = 0
var fim: bool = false

@onready var bola: Bola = $Bola
@onready var blocos: Node2D = $Blocos
@onready var label_pontos: Label = $CanvasLayer/LabelPontos
@onready var label_vidas: Label = $CanvasLayer/LabelVidas
@onready var label_fim: Label = $CanvasLayer/LabelFim

func _ready() -> void:
    vidas = vidas_iniciais
    label_fim.visible = false
    bola.bloco_atingido.connect(_ao_atingir_bloco)
    bola.bola_perdida.connect(_ao_perder_bola)
    _gerar_grid()
    _atualizar_hud()

func _gerar_grid() -> void:
    for linha: int in range(linhas):
        for coluna: int in range(colunas):
            var bloco: Bloco = CENA_BLOCO.instantiate() as Bloco
            var x: float = origem.x + coluna * (largura_bloco + margem)
            var y: float = origem.y + linha * (altura_bloco + margem)
            bloco.position = Vector2(x, y)
            bloco.pontos = (linhas - linha) * 10
            blocos.add_child(bloco)
    total_blocos = linhas * colunas

No _ready, os sinais da bola sao conectados passando a funcao direto: em Godot 4, _ao_atingir_bloco sem parenteses e um Callable, e connect recebe ele como argumento. Sem strings, com autocomplete e com erro em tempo de editor se voce digitar errado.

No _gerar_grid, a posicao de cada bloco sai da formula origem + indice * (tamanho + margem). A linha bloco.pontos = (linhas - linha) * 10 da o tempero classico: as fileiras de cima valem mais, entao a primeira linha vale 50 e a de baixo vale 10, premiando quem abre tunel ate o topo.

Pontuacao, vidas, game over e dificuldade

Ainda no Main, as funcoes conectadas aos sinais fecham o loop do jogo:

func _ao_atingir_bloco(pontos: int) -> void:
    pontuacao += pontos
    blocos_destruidos += 1
    if blocos_destruidos % blocos_por_aceleracao == 0:
        bola.acelerar(aceleracao)
    if blocos_destruidos >= total_blocos:
        _fim_de_jogo("Voce venceu!")
    _atualizar_hud()

func _ao_perder_bola() -> void:
    vidas -= 1
    _atualizar_hud()
    if vidas <= 0:
        _fim_de_jogo("Game over.")
        return
    await get_tree().create_timer(1.0).timeout
    bola.lancar()

func _fim_de_jogo(mensagem: String) -> void:
    fim = true
    bola.em_jogo = false
    label_fim.text = "%s Pontos: %d" % [mensagem, pontuacao]
    label_fim.visible = true

func _atualizar_hud() -> void:
    label_pontos.text = "Pontos: %d" % pontuacao
    label_vidas.text = "Vidas: %d" % vidas

func _unhandled_input(event: InputEvent) -> void:
    if fim and event.is_action_pressed("ui_accept"):
        get_tree().reload_current_scene()

A dificuldade progressiva esta em _ao_atingir_bloco: a cada blocos_por_aceleracao blocos destruidos (8 por padrao), a bola chama acelerar e ganha 45 pixels por segundo, com teto em velocidade_maxima pra nao virar injogavel. Com os valores padrao, a bola comeca em 420 e termina a fase perto de 690, e voce sente a partida apertar sem nenhum sistema de fases.

Quando a bola cai, _ao_perder_bola desconta a vida, espera um segundo com create_timer e await, e relanca do ponto inicial. Zerou as vidas, ou destruiu tudo, o LabelFim aparece e Enter reinicia a cena inteira com reload_current_scene, que reconstroi o grid do zero.

Aperte F5 e jogue. Se a bola atravessar algum bloco em velocidade alta, diminua velocidade_maxima ou aumente o raio da colisao: com os valores deste tutorial isso nao acontece, porque 820 pixels por segundo a 60 fps move menos de 14 pixels por frame, bem menos que a espessura de um bloco.

Proximos passos

Seu Breakout esta de pe: raquete com controle fino, bola com fisica arcade honesta, grid por codigo, placar, vidas e dificuldade crescente. Daqui, tres upgrades classicos:

Power-ups. E o que separa o Breakout do Arkanoid. Sorteie uma chance em destruir() de soltar uma capsula que cai, e ao pegar na raquete alargue ela, solte uma segunda bola ou desacelere o jogo por alguns segundos.

Sons e game feel. Um AudioStreamPlayer pra cada evento (rebote, bloco, vida perdida) ja muda tudo. E quando um bloco explodir, um tremor de camera vende o impacto: o tutorial de screen shake na Godot mostra como fazer isso com Camera2D em poucos minutos.

Outro classico de grid. A logica de gerar e destruir celulas por codigo que voce usou aqui e a mesma base do Tetris na Godot, que leva a ideia de grid muito mais longe, com rotacao de pecas e limpeza de linhas.

Bom jogo, e bom codigo.

Perguntas frequentes

Como a bola quica nas paredes e nos blocos na Godot?

Com o metodo bounce do Vector2. Voce move a bola com move_and_collide, que devolve um KinematicCollision2D quando encosta em algo. Esse objeto traz a normal da superficie atingida, e direcao.bounce(normal) devolve o vetor refletido, como um espelho. E uma linha de codigo e funciona igual pra parede, teto e bloco.

Devo usar CharacterBody2D ou RigidBody2D pra bola do Breakout?

CharacterBody2D. O Breakout precisa de fisica arcade: velocidade constante, reflexo perfeito e angulo controlado na raquete. O RigidBody2D simula fisica realista, entao a bola perderia energia, ganharia spin e ficaria imprevisivel. Com CharacterBody2D e move_and_collide voce decide exatamente o que acontece em cada colisao.

Como gero o grid de blocos por codigo em vez de posicionar um por um?

Com dois for aninhados, um pra linha e outro pra coluna. A cada volta voce chama instantiate na cena do bloco, calcula a posicao multiplicando o indice pelo tamanho do bloco mais uma margem, e adiciona como filho de um no container. Mudar o numero de linhas e colunas vira so trocar dois numeros exportados no Inspector.

Quanto tempo leva pra fazer um Breakout na Godot?

Entre duas e quatro horas se voce esta comecando, menos de uma se ja conhece o editor. Sao tres cenas pequenas (raquete, bola, bloco) mais a cena principal, e nenhum script passa de 60 linhas. E um projeto de fim de tarde, o que e otimo: voce termina, joga e ainda sobra energia pra polir.

Preciso saber programar pra seguir este tutorial?

O basico ajuda muito: variaveis, funcoes e if. Todo o codigo esta pronto e comentado no texto, entao da pra seguir copiando e testando, mas voce aproveita mais se entender o que cada linha faz. Se nunca programou, faca o tutorial com calma, rode o jogo apos cada secao e mexa nos valores exportados pra ver o efeito.

Breakout e Arkanoid sao a mesma coisa?

Quase. Breakout e o original da Atari, de 1976: raquete, bola e blocos. Arkanoid e a versao da Taito, de 1986, que adicionou power-ups caindo dos blocos, inimigos e fases desenhadas. O nucleo de codigo e identico, entao este tutorial serve de base pros dois: fazendo o Breakout, o Arkanoid e uma extensao natural.