Voltar para o Blog
Quest Log

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

Mesa de pinball 2D feita no Godot 4 com flippers, bumpers e bola em movimento

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:

  1. 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.
  2. Physics Material Override: crie um PhysicsMaterial novo com bounce = 0.5 e friction = 0.1. Bounce dá o quique, friction baixo deixa a bola deslizar pelas curvas em vez de agarrar.
  3. 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).

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

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 AudioStreamPlayer2D em cada bumper e flipper, tocando no momento do contato. Varie o pitch_scale aleatoriamente entre 0.9 e 1.1 para o som não enjoar.
  • Flash no bumper: ao pontuar, troque o modulate do sprite para branco puro e volte ao normal com um Tween de 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.