Voltar para o Blog
Quest Log

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

Passaro 2D voando entre canos verdes com um gap no meio, estilo Flappy Bird, feito na Godot

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.

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

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