Como Fazer um Flappy Bird na Godot 4 (Tutorial Completo)

Como fazer um Flappy Bird na Godot 4 com GDScript tipado: passaro com gravidade e pulo, canos com gap aleatorio, colisao, game over e pontuacao.
Como Fazer um Flappy Bird na Godot 4 (Tutorial Completo)
Fazer um Flappy Bird na Godot e praticamente um rito de passagem pra quem esta comecando. O jogo tem uma mecanica so: o passaro cai o tempo todo por gravidade e sobe um pouco cada vez que voce aperta. No meio disso, canos vem da direita com um buraco pra voce atravessar, e encostar em qualquer coisa reinicia tudo. Escopo pequeno, viciante na hora, e o tipo de projeto que voce termina de verdade em vez de largar pela metade.
Neste tutorial voce monta um Flappy Bird completo com GDScript tipado de Godot 4: o passaro com gravidade manual e pulo, os canos se movendo com gap em altura aleatoria, o spawn com Timer, a colisao que leva ao game over e a pontuacao por cano ultrapassado. Todo o codigo aqui compila e roda de verdade, nada de pseudocodigo.
A ideia central do Flappy Bird na Godot
Antes de abrir o editor, entenda o truque do genero: o passaro nao anda pra frente. Ele fica parado no eixo X, ali na esquerda da tela, so subindo e descendo. Quem se move sao os canos, vindo da direita pra esquerda. A ilusao de voo pra frente vem dos canos passando por voce, exatamente como no endless runner classico.
Isso simplifica tudo. Voce nao precisa mover a camera, nem controlar ate onde o cenario vai. A camera fica fixa, o passaro so lida com gravidade e pulo, e os canos nascem fora da tela, deslizam pra esquerda e se autodestroem quando saem do outro lado.
A cena principal fica mais ou menos assim:
Main (Node2D)
├── Passaro (CharacterBody2D)
│ ├── Sprite2D
│ └── CollisionShape2D
├── SpawnTimer (Timer)
├── SpawnPoint (Marker2D)
└── CanvasLayer
└── LabelPontos (Label)
E cada par de canos vira uma cena separada, que voce instancia a cada timeout do Timer:
Canos (Node2D)
├── CanoCima (Area2D)
│ ├── Sprite2D
│ └── CollisionShape2D
├── CanoBaixo (Area2D)
│ ├── Sprite2D
│ └── CollisionShape2D
└── AreaPonto (Area2D)
└── CollisionShape2D
Com essa estrutura na cabeca, cada peca vira um pedaco pequeno de codigo. Vamos por partes.
O passaro: gravidade manual e pulo com CharacterBody2D
O passaro e um CharacterBody2D com um Sprite2D e um CollisionShape2D. Escolhi CharacterBody2D em vez de RigidBody2D porque quero controlar a gravidade na mao, direto na velocity.y. Isso deixa o pulo previsivel e o game feel na sua mao, que e o que mais importa aqui.
Primeiro, configure o input. No menu Project > Project Settings > Input Map, crie uma acao chamada pular e adicione a ela a tecla espaco (e, se quiser jogar no celular, um evento de toque na tela). Assim o mesmo Input.is_action_just_pressed("pular") responde ao teclado e ao dedo.
extends CharacterBody2D
@export var gravidade: float = 1400.0
@export var forca_pulo: float = -450.0
var vivo: bool = true
func _physics_process(delta: float) -> void:
if not vivo:
return
# Gravidade puxa pra baixo sempre, somando na velocidade a cada frame.
velocity.y += gravidade * delta
# Ao apertar, zera a queda e joga o passaro pra cima de uma vez.
if Input.is_action_just_pressed("pular"):
velocity.y = forca_pulo
# O passaro NAO anda no X: quem se move sao os canos.
velocity.x = 0.0
move_and_slide()
Repare em tres coisas. A velocity.y += gravidade * delta e a gravidade manual: como o CharacterBody2D nao cai sozinho, voce mesmo soma a aceleracao a cada frame. O forca_pulo e negativo porque no Godot 2D o eixo Y cresce pra baixo, entao subir e ir pro negativo. E, ao pular, eu faco velocity.y = forca_pulo (atribuindo, nao somando), pra que cada toque de um impulso limpo pra cima, sem acumular com a queda anterior. Esse detalhe e o que da aquela resposta seca e imediata do Flappy Bird.
Se depois voce quiser evoluir o controle do personagem pra algo de plataforma, o proximo passo natural e o pulo com double jump e wall jump na Godot, que parte exatamente dessa mesma ideia de gravidade na velocity.y.
Os canos: movimento pra esquerda e autodestruicao
Cada par de canos e uma cena Node2D com dois Area2D (cano de cima e cano de baixo) e uma terceira Area2D fina no meio, que vai marcar o ponto. Uso Area2D em vez de corpo fisico porque so quero detectar o toque com o passaro, nao empurrar nada.
O script dos canos cuida de mover o conjunto pra esquerda e de se destruir ao sair da tela:
extends Node2D
@export var velocidade: float = 200.0
func _process(delta: float) -> void:
# O par inteiro desliza pra esquerda na mesma velocidade.
position.x -= velocidade * delta
# Saiu pela esquerda? Libera da memoria pra nao acumular nos.
if position.x < -100.0:
queue_free()
Como os dois canos e a area de ponto sao filhos deste Node2D, mover o pai move tudo junto, mantendo o tamanho do gap sempre igual. E o queue_free() ao sair da tela e o que garante que o jogo rode por horas sem encher a memoria: pra cada cano que nasce, um cano morre.
Spawn dos canos com Timer e gap em altura aleatoria
Aqui mora a alma do jogo. Os canos nascem fora da tela na direita, em intervalos regulares, com o buraco em uma altura sorteada a cada vez. Coloque um Timer na cena principal (marque Autostart ou dispare no _ready) e um Marker2D (o SpawnPoint) posicionado fora da tela, na direita.
A cada timeout do Timer voce instancia um par de canos na posicao do marker e sorteia a altura do gap:
extends Node2D
@export var cena_canos: PackedScene
@onready var spawn_timer: Timer = $SpawnTimer
@onready var spawn_point: Marker2D = $SpawnPoint
func _ready() -> void:
spawn_timer.timeout.connect(_on_spawn_timer_timeout)
spawn_timer.start()
func _on_spawn_timer_timeout() -> void:
var canos: Node2D = cena_canos.instantiate()
canos.position.x = spawn_point.position.x
# Sorteia a altura do gap dentro de uma faixa segura da tela.
canos.position.y = randf_range(250.0, 450.0)
add_child(canos)
O cena_canos e um @export var ... : PackedScene: arraste o arquivo .tscn dos canos pra esse campo no Inspector. O randf_range(250.0, 450.0) sorteia o Y do par inteiro dentro de uma faixa que nunca cola o gap no topo nem no chao, deixando o buraco sempre alcancavel. Ajuste esses dois numeros conforme o tamanho da sua tela e da arte dos canos.
O intervalo entre canos e o wait_time do Timer no Inspector. Comece com algo em torno de 1.5 segundos e ajuste testando. Se quiser entender a fundo como controlar ritmo e intervalos, incluindo pausar e reiniciar contadores, vale ler sobre Timer e cooldown na Godot, que e a mesma ferramenta que sustenta o spawn aqui.
Colisao e game over
Falta fechar o ciclo: encostou, acabou. Como cada cano e uma Area2D e o passaro e um CharacterBody2D (ou seja, um corpo), basta a area disparar o sinal body_entered quando o passaro entra nela. Eu conecto esse sinal dentro da cena dos canos e aviso a cena principal por um sinal proprio.
No script dos canos, conecte os dois canos a um mesmo tratador:
extends Node2D
signal passaro_bateu
@onready var cano_cima: Area2D = $CanoCima
@onready var cano_baixo: Area2D = $CanoBaixo
func _ready() -> void:
cano_cima.body_entered.connect(_on_cano_body_entered)
cano_baixo.body_entered.connect(_on_cano_body_entered)
func _on_cano_body_entered(corpo: Node2D) -> void:
# O unico corpo que entra aqui e o passaro.
if corpo.is_in_group("passaro"):
passaro_bateu.emit()
Pra isso funcionar, adicione o passaro ao grupo passaro (no Inspector, aba Node > Groups). Assim o is_in_group("passaro") confirma que foi mesmo o jogador que encostou, e nao outra coisa qualquer. O sinal passaro_bateu sobe pra cena principal, que decide o que fazer:
@onready var passaro: CharacterBody2D = $Passaro
var jogo_ativo: bool = true
func _game_over() -> void:
jogo_ativo = false
passaro.vivo = false
spawn_timer.stop()
# Aqui voce mostraria a tela de game over, tocaria um som, etc.
await get_tree().create_timer(1.0).timeout
get_tree().reload_current_scene()
Ao instanciar cada par de canos no _on_spawn_timer_timeout, conecte o sinal deles a esse metodo com canos.passaro_bateu.connect(_game_over). O passaro.vivo = false congela o passaro (lembra do if not vivo: return la no _physics_process), o Timer para de spawnar e, depois de um segundo, reload_current_scene() reinicia o jogo de forma crua. Numa versao caprichada voce trocaria isso por uma tela de game over com botao de tentar de novo.
Pontuacao: um ponto por cano ultrapassado
O ponto no Flappy Bird nao vem de bater, vem de passar pelo gap. O truque e aquela terceira Area2D fina e invisivel que colocamos no meio do buraco, entre os dois canos. Quando o passaro atravessa essa area, marcamos um ponto.
Na cena dos canos, conecte a area de ponto:
signal ponto_marcado
@onready var area_ponto: Area2D = $AreaPonto
func _ready() -> void:
cano_cima.body_entered.connect(_on_cano_body_entered)
cano_baixo.body_entered.connect(_on_cano_body_entered)
area_ponto.body_entered.connect(_on_area_ponto_body_entered)
func _on_area_ponto_body_entered(corpo: Node2D) -> void:
if corpo.is_in_group("passaro"):
ponto_marcado.emit()
# Evita marcar o mesmo cano duas vezes.
area_ponto.set_deferred("monitoring", false)
O set_deferred("monitoring", false) desliga a area logo apos o primeiro toque, garantindo exatamente um ponto por cano. Na cena principal, conecte o sinal ponto_marcado (junto com o passaro_bateu, no mesmo instante em que instancia os canos) e atualize o placar:
@onready var label_pontos: Label = $CanvasLayer/LabelPontos
var pontos: int = 0
func _on_ponto_marcado() -> void:
pontos += 1
label_pontos.text = str(pontos)
Juntando o spawn com as conexoes de sinal, o _on_spawn_timer_timeout fica assim:
func _on_spawn_timer_timeout() -> void:
var canos: Node2D = cena_canos.instantiate()
canos.position.x = spawn_point.position.x
canos.position.y = randf_range(250.0, 450.0)
canos.passaro_bateu.connect(_game_over)
canos.ponto_marcado.connect(_on_ponto_marcado)
add_child(canos)
Agora cada par de canos que nasce ja avisa a cena principal tanto quando mata o passaro quanto quando e ultrapassado. Simples e fechado.
Polimento: o que separa o cru do gostoso
Com o nucleo de pe, uns retoques baratos mudam muito a sensacao do jogo:
- Incline o passaro conforme a
velocity.y:passaro.rotation = lerp_angle(passaro.rotation, velocity.y * 0.001, 0.1)faz o bico apontar pra cima ao subir e pra baixo ao cair. - Toque um som curto de "flap" a cada pulo e um de batida no game over. Audio barato e o que mais aumenta a percepcao de qualidade.
- Guarde o recorde num arquivo com
FileAccesse mostre na tela de game over, pra dar aquele "mais uma vez". - Adicione um pequeno delay ou uma tela de start antes do primeiro cano, pra o jogador nao morrer no susto.
Nenhum desses e obrigatorio pro jogo funcionar, mas cada um empurra o projeto de "prototipo" pra "joguinho de verdade". Faca a versao crua primeiro, jogue ela, e so entao adicione um polimento de cada vez.
Proximos passos
Junte as pecas e voce tem um jogo completo: passaro caindo por gravidade e pulando ao toque, canos nascendo com gap aleatorio, colisao levando ao game over e pontuacao por cano ultrapassado. Parece pouco codigo, e e mesmo, mas repare no que voce acabou de praticar: gravidade manual, input, Timer, instantiate, grupos, sinais, colisao por Area2D e placar. Essas mesmas pecas aparecem em quase todo jogo 2D que voce for fazer depois.
E por isso que o Flappy Bird e o primeiro jogo que eu recomendo: ele e a base. Um plataforma troca o cano por inimigo e adiciona chao; um shooter troca o pulo por tiro; um roguelike empilha essas mesmas mecanicas em camadas. Dominar esse nucleo pequeno e o que te deixa pronto pra escopos maiores sem se perder.
Se voce sentiu que ja entendeu o suficiente pra querer construir uma base solida de Godot em vez de catar tutorial solto, o jeito mais rapido de sair do zero pra um jogo publicado e seguir uma trilha pensada pra isso. Veja o melhor curso de Godot pra encurtar o caminho.
O segredo do Flappy Bird e que ele e pequeno o bastante pra voce terminar hoje e viciante o bastante pra voce querer melhorar amanha. Faca a versao crua agora, jogue ela, e ajuste os numeros da gravidade e da forca do pulo ate ficar gostoso. Esse vai e volta e o trabalho de quem faz jogo de verdade.
Perguntas frequentes
Flappy Bird e um bom primeiro jogo pra fazer na Godot?
E um dos melhores. A mecanica central e uma so (apertar pra subir, soltar pra cair), o escopo cabe em tres cenas e mesmo assim voce pratica gravidade, input, Timer, instantiate, colisao por sinal e pontuacao. Como da pra terminar em uma tarde, e um projeto que voce realmente conclui em vez de abandonar no meio.
Devo usar CharacterBody2D ou RigidBody2D pro passaro?
Pra iniciante, CharacterBody2D e mais previsivel: voce controla a gravidade na mao, na velocity.y, e sabe exatamente o que acontece a cada frame. O RigidBody2D simula fisica de verdade, o que e mais realista, mas tira de voce o controle fino do pulo e complica o game feel. Este tutorial usa CharacterBody2D justamente por isso.
Como faco o gap entre os canos ficar em uma altura aleatoria?
Voce sorteia um numero com randf_range dentro de um intervalo seguro (nem colado no topo, nem colado no chao) e usa esse valor pra posicionar o par de canos no eixo Y. Como os dois canos e o gap ficam num unico no pai, mover esse pai sobe e desce o conjunto inteiro de uma vez, mantendo o tamanho do buraco constante.
Por que uso Area2D nos canos em vez de um corpo fisico?
Porque eu so quero detectar quando o passaro encosta, nao empurrar nem colidir de verdade. A Area2D dispara o sinal body_entered quando um corpo (o CharacterBody2D do passaro) entra nela, e isso e tudo que preciso pra chamar o game over. Corpo fisico serviria pra bloquear ou empurrar, o que aqui atrapalharia.
Como conto a pontuacao no Flappy Bird?
O ponto vem de passar pelo gap, nao de bater no cano. O truque e colocar uma Area2D fina e invisivel no meio do buraco, entre os dois canos. Quando o passaro atravessa essa area, o sinal body_entered soma um ponto. Assim voce marca exatamente uma vez por cano ultrapassado.
O jogo trava ou fica lento se eu jogar por muito tempo?
Nao, desde que os canos se autodestruam ao sair da tela. Cada par de canos chama queue_free quando passa da borda esquerda, liberando a memoria. Como voce cria com instantiate e remove com queue_free na mesma cadencia, a quantidade de nos na cena fica estavel e o jogo roda liso indefinidamente.


