Como Fazer um Breakout na Godot: Tutorial Completo em GDScript

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


