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.

Ferramenta:Godot

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. Se voce ainda nao montou nenhum projeto 2D na engine, o guia base de como fazer um jogo 2D na Godot mostra o fluxo de cena, no e script que este tutorial ja da por sabido. E se voce ainda esta no comeco e quer comparar o raciocinio com uma ferramenta mais visual, vale ver ate onde da pra ir com blocos em como fazer um jogo 3D no Scratch.

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.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev vocΓͺ vai do zero ao primeiro jogo, com um desafio prΓ‘tico em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalΓ­cioGarantia de 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.

Fisica de verdade. Se o que te divertiu foi a bola quicando, o passo natural e um jogo de pinball na Godot, onde a bola vira RigidBody2D e gravidade, impulso e flippers entram em cena.

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.