Como Fazer um Jogo de Pinball no Godot 4: Física Real, Flippers e Bumpers

Aprenda como fazer pinball no Godot 4: bola com RigidBody2D, flippers, bumpers e plunger com código GDScript tipado. Tutorial completo de física 2D.
Pinball é daqueles jogos que parecem simples até você tentar fazer um. A bola precisa ter peso, os flippers precisam responder na hora, os bumpers precisam cuspir a bola com força. Neste tutorial você vai aprender como fazer pinball no Godot 4 do zero: bola com física real, mesa com paredes inclinadas, flippers funcionais, bumpers que pontuam e um lançador com força proporcional. Tudo com GDScript tipado e APIs reais da Godot 4.
Por que pinball é um projeto excelente para estudar física 2D
A maioria dos tutoriais de física de jogos te faz empurrar caixas. Pinball te obriga a usar quase tudo que o motor de física da Godot oferece, e de forma nativa:
- Gravidade: a bola cai sozinha, e a inclinação da mesa é simulada só com a gravidade padrão puxando para baixo.
- Colisão e rebote: a bola quica em paredes, rampas e alvos com materiais físicos diferentes.
- Impulso: bumpers e plunger aplicam forças instantâneas na bola.
- Corpos cinemáticos e articulações: os flippers são corpos controlados por você interagindo com um corpo dinâmico.
Você não escreve uma linha de simulação. Você configura nós e deixa o motor trabalhar. É o mesmo raciocínio que expliquei no post sobre física de jogos no Godot: seu trabalho é escolher o corpo certo para cada papel, não reinventar física.
Como fazer pinball no Godot: a estrutura da cena
A cena principal fica assim:
Main (Node2D)
├── Mesa (StaticBody2D)
│ └── CollisionPolygon2D
├── Bola (RigidBody2D)
│ ├── CollisionShape2D (CircleShape2D)
│ └── Sprite2D
├── FlipperEsquerdo (AnimatableBody2D)
├── FlipperDireito (AnimatableBody2D)
├── Bumpers (Node2D)
├── Plunger (Node2D)
├── Dreno (Area2D)
└── HUD (CanvasLayer)
Cada peça tem um tipo de corpo com um papel claro. Mesa é estática, bola é dinâmica, flipper é controlado por código. Vamos montar uma de cada vez.
A bola: RigidBody2D com material físico
Crie um RigidBody2D chamado Bola com um CollisionShape2D filho usando CircleShape2D (raio entre 8 e 12 pixels funciona bem numa mesa de 1080 de altura).
No Inspector da bola:
- Gravity Scale: 1.3. Acima de 1 a queda fica mais rápida e a bola parece mais pesada. É o jeito barato de simular a inclinação da mesa.
- Physics Material Override: crie um
PhysicsMaterialnovo combounce = 0.5efriction = 0.1. Bounce dá o quique, friction baixo deixa a bola deslizar pelas curvas em vez de agarrar. - Continuous CD: mude para
Cast Ray. Sem isso, a bola em alta velocidade atravessa paredes finas (o famoso tunneling).
Em código, se preferir configurar tudo via script:
extends RigidBody2D
func _ready() -> void:
gravity_scale = 1.3
continuous_cd = RigidBody2D.CCD_MODE_CAST_RAY
var material: PhysicsMaterial = PhysicsMaterial.new()
material.bounce = 0.5
material.friction = 0.1
physics_material_override = material
Rode a cena. A bola cai e quica. Metade do jogo já existe sem uma linha de gameplay.
A mesa: StaticBody2D com colisão desenhada
A mesa é um StaticBody2D com um CollisionPolygon2D filho. Selecione o polígono e desenhe o contorno da mesa clicando ponto a ponto no editor: paredes laterais, o arco superior por onde a bola entra, e as paredes inclinadas em V acima dos flippers que afunilam a bola para o centro.
Duas dicas que evitam dor de cabeça:
- Paredes grossas: desenhe o polígono com espessura de pelo menos 16 pixels. Colisores finos são o principal motivo de bola atravessando parede, mesmo com CCD ligado.
- Inclinação nas laterais baixas: as paredes em diagonal acima dos flippers são o que dá ritmo ao pinball. Sem elas a bola cai reto no dreno e o jogador nem tem chance de rebater.
Dê à mesa um PhysicsMaterial próprio com bounce = 0.2, para as paredes devolverem um pouco de energia sem transformar a mesa num fliperama de borracha.
Flippers: o coração do tutorial
Aqui está a decisão de design mais importante do projeto. Existem dois caminhos:
Caminho 1, RigidBody2D + PinJoint2D: o flipper é um corpo dinâmico preso por uma junta a um StaticBody2D âncora. Você liga o motor da junta ou seta angular_velocity alta quando o jogador aperta a tecla, e limita o arco com angular_limit_lower e angular_limit_upper da junta. O resultado é orgânico: o flipper tem massa, verga com o impacto e transfere energia de forma realista. O custo é o ajuste: massa do flipper, força do motor e limites precisam de calibração fina, e um valor errado faz o flipper tremer ou atravessar a bola.
Caminho 2, AnimatableBody2D rotacionado via código: você controla o ângulo diretamente e o Godot, com sync_to_physics ativo, calcula a velocidade do corpo e empurra a bola de acordo. É determinístico: o flipper sempre sobe no mesmo tempo, sempre no mesmo arco. Menos "físico", muito mais previsível.
Para iniciante, recomendo o caminho 2. Você elimina uma classe inteira de bugs e o jogador não nota a diferença. Fica assim:
extends AnimatableBody2D
@export var angulo_repouso: float = 30.0
@export var angulo_ativo: float = -30.0
@export var velocidade_graus: float = 900.0
@export var acao: String = "flipper_esquerdo"
func _physics_process(delta: float) -> void:
var alvo: float = angulo_repouso
if Input.is_action_pressed(acao):
alvo = angulo_ativo
rotation_degrees = move_toward(rotation_degrees, alvo, velocidade_graus * delta)
Confirme que sync_to_physics está marcado no Inspector do AnimatableBody2D. É isso que faz o motor calcular a velocidade do flipper e aplicá-la na bola; sem essa flag o flipper vira uma parede teleportante que não transfere impulso direito. A velocidade_graus de 900 significa que o flipper cruza o arco de 60 graus em cerca de 66 milissegundos, rápido o suficiente para arremessar a bola mesa acima.
Duplique o nó para o flipper direito, inverta os ângulos (repouso em 150, ativo em 210, ou espelhe o sprite) e mapeie flipper_esquerdo e flipper_direito no Input Map, nas setas ou em Shift esquerdo e direito, como nos fliperamas.
Se depois você quiser experimentar o caminho 1, a estrutura é: StaticBody2D âncora, RigidBody2D flipper, PinJoint2D ligando os dois com angular_limit_enabled = true, e no aperto da tecla motor_enabled = true com motor_target_velocity negativa (sentido anti-horário para o flipper esquerdo).
Bumpers: impulso na direção contrária e pontos na tela
O bumper clássico é um círculo que detecta a bola e a rebate com violência. Use um StaticBody2D circular para o corpo (a bola precisa quicar nele fisicamente) e uma Area2D levemente maior por cima para detectar o contato e aplicar o impulso extra:
extends Area2D
@export var forca: float = 600.0
@export var pontos: int = 100
signal pontuou(valor: int)
func _ready() -> void:
body_entered.connect(_ao_tocar)
func _ao_tocar(corpo: Node2D) -> void:
if corpo is RigidBody2D:
var bola: RigidBody2D = corpo
var direcao: Vector2 = (bola.global_position - global_position).normalized()
bola.apply_central_impulse(direcao * forca)
pontuou.emit(pontos)
A direção do impulso é o vetor do centro do bumper até a bola, normalizado. Ou seja, a bola sempre sai na direção contrária ao ponto de impacto, exatamente como num pinball real. O sinal pontuou sobe para um script de placar no Main, que soma e atualiza o HUD.
A lógica de "detectou, aplicou impulso, pontuou" é prima direta da colisão de blocos que montei em como fazer um breakout no Godot. Se os sinais de Area2D ainda confundem você, aquele post destrincha o fluxo com calma.
O lançador: impulso proporcional ao tempo segurando
O plunger clássico carrega enquanto o jogador segura a tecla e dispara ao soltar. Uma Area2D na canaleta de lançamento detecta se a bola está na posição, e o script acumula carga:
extends Node2D
@export var bola: RigidBody2D
@export var forca_maxima: float = 1200.0
@export var tempo_carga_maxima: float = 1.5
var carga: float = 0.0
var bola_na_canaleta: bool = false
func _physics_process(delta: float) -> void:
if not bola_na_canaleta:
return
if Input.is_action_pressed("lancar"):
carga = minf(carga + delta / tempo_carga_maxima, 1.0)
elif Input.is_action_just_released("lancar"):
bola.apply_central_impulse(Vector2.UP * forca_maxima * carga)
carga = 0.0
carga vai de 0 a 1 em um segundo e meio, e o impulso final é a fração da força máxima. Segurou pouco, lançamento fraco. Segurou até o fim, a bola sobe voando. Mostre uma barrinha de carga ao lado do plunger; esse feedback visual transforma um botão em uma decisão.
Dreno, vidas e fim de jogo
Embaixo da mesa, entre os flippers e fora da tela, coloque uma Area2D chamada Dreno com um colisor retangular largo. Quando a bola entra, perde-se uma vida:
extends Area2D
@export var bola: RigidBody2D
@export var posicao_saque: Marker2D
var vidas: int = 3
signal fim_de_jogo
func _ready() -> void:
body_entered.connect(_ao_drenar)
func _ao_drenar(corpo: Node2D) -> void:
if corpo != bola:
return
vidas -= 1
if vidas <= 0:
fim_de_jogo.emit()
return
bola.linear_velocity = Vector2.ZERO
bola.angular_velocity = 0.0
bola.global_position = posicao_saque.global_position
Zerar linear_velocity e angular_velocity antes de reposicionar é obrigatório: se você só teleportar a bola, ela chega na canaleta com a velocidade antiga e sai voando sozinha. O Marker2D marca a posição de saque na canaleta do plunger.
Game feel: os 10% que fazem 90% da diferença
Com tudo acima você tem um pinball funcional. O que separa funcional de gostoso são detalhes baratos:
- Som no impacto: um
AudioStreamPlayer2Dem cada bumper e flipper, tocando no momento do contato. Varie opitch_scalealeatoriamente entre 0.9 e 1.1 para o som não enjoar. - Flash no bumper: ao pontuar, troque o
modulatedo sprite para branco puro e volte ao normal com umTweende 0.1 segundo. O bumper parece "acender" como nas mesas reais. - Screen shake leve: um tremor de 2 a 3 pixels por 0.1 segundo no impacto dos bumpers. Leve mesmo: pinball tem impacto o tempo todo, e shake exagerado enjoa rápido.
extends Camera2D
var intensidade: float = 0.0
func tremer(valor: float) -> void:
intensidade = valor
func _process(delta: float) -> void:
if intensidade > 0.0:
offset = Vector2(randf_range(-intensidade, intensidade), randf_range(-intensidade, intensidade))
intensidade = move_toward(intensidade, 0.0, 20.0 * delta)
else:
offset = Vector2.ZERO
Esses três itens são aplicações diretas do que mostro em game feel e juice para deixar o jogo gostoso. Se o seu pinball estiver funcionando mas parecendo "morto", o problema quase nunca é a física, é a falta de feedback.
Próximos passos
Você montou bola, mesa, flippers, bumpers, plunger e dreno. A partir daqui, o pinball vira um playground de ideias: alvos que caem e voltam (AnimatableBody2D de novo), rampas com camadas de colisão diferentes, multiball guardando as bolas num array, multiplicador de pontos por sequência de bumpers.
Duas lições ficam desse projeto. Primeira: no Godot 4, física boa é escolher o nó certo para cada papel, não escrever matemática. Segunda: valores importam mais que código. A diferença entre um pinball travado e um pinball delicioso está em meia dúzia de números, bounce, gravity_scale, velocidade do flipper, e a única forma de acertar é testar, ajustar e testar de novo. Abra o projeto e vá calibrar a sua mesa.
Perguntas frequentes
Devo usar RigidBody2D ou AnimatableBody2D para o flipper no Godot 4?
Para quem está começando, AnimatableBody2D rotacionado via código é mais previsível: você controla o ângulo diretamente e o motor de física calcula a velocidade que ele transfere para a bola. RigidBody2D com PinJoint2D dá um resultado mais orgânico, com peso e rebote, mas exige ajustar limites de ângulo e velocidade do motor. Comece com AnimatableBody2D e migre se sentir o flipper artificial.
Como deixar a bola do pinball mais viva no Godot?
O segredo é o PhysicsMaterial no RigidBody2D da bola. Suba o bounce para algo entre 0.4 e 0.7 para ela quicar com energia e baixe o friction para perto de 0.1 para ela deslizar pelas rampas. Combine isso com gravity_scale acima de 1 para a queda parecer rápida e pesada. Teste em incrementos pequenos, porque bounce alto demais faz a bola nunca assentar nos flippers.
Por que a bola atravessa as paredes quando está muito rápida?
Isso é tunneling: em um frame a bola está antes da parede e no frame seguinte já passou dela, então a colisão nunca é detectada. A solução no Godot 4 é ativar a detecção contínua de colisão no RigidBody2D, definindo continuous_cd como CCD_MODE_CAST_RAY. Também ajuda usar paredes com colisores mais grossos, de 16 pixels ou mais, em vez de linhas finas.
Vale mais a pena fazer pinball em 2D ou 3D?
Comece em 2D. Toda a física que o pinball precisa, como gravidade, rebote e impulso, funciona nativamente nos nós 2D do Godot, e você simula a inclinação da mesa apenas com a gravidade padrão puxando a bola para baixo. Em 3D você ganha visual, mas paga com câmera, iluminação e uma mesa inclinada de verdade. O 3D vale como segundo projeto, reaproveitando a lógica do 2D.
Pinball é um bom primeiro projeto de jogo?
É um ótimo segundo projeto. Ele usa quase só recursos nativos do motor de física, tem escopo fechado e resultado visível rápido, mas exige ajuste fino de valores que pode frustrar quem nunca abriu o Godot. Se você já fez um Pong ou um breakout, o pinball é o passo natural seguinte para dominar RigidBody2D de vez.


