Como Fazer Flappy Bird na Unity (C# Completo)
Fazer Flappy Bird na Unity é o melhor primeiro projeto completo que existe: uma mecânica (pular), um obstáculo (cano), uma regra (encostou, perdeu) e um placar. Neste tutorial de unity flappy bird você monta o jogo inteiro em C# do Unity 6.3 LTS: pássaro com pulo por linearVelocity e rotação, canos gerados e reaproveitados, pontuação por trigger, recorde, game over, reinício e build Web para publicar no itch.io.
O objetivo não é aprender Flappy Bird. É sair do dia com um jogo terminado e publicado, o que ensina mais do que dez mecânicas soltas que nunca viram jogo.
O que vamos construir
O escopo fechado, para não crescer no meio do caminho:
- Tela inicial parada: o pássaro flutua e espera o primeiro toque.
- Toque, clique ou espaço faz o pássaro subir. A gravidade puxa para baixo.
- Pares de canos entram pela direita com a abertura em altura aleatória.
- Passar pela abertura soma 1 ponto.
- Encostar em cano, no chão ou no teto termina a partida.
- Tela de game over com pontuação e recorde, e clique para reiniciar.
Cinco scripts: Bird, PipePair, PipeSpawner, ScoreZone e GameManager.
Projeto 2D e sprites
Crie o projeto pelo Unity Hub com o template Universal 2D. Ele já vem com câmera ortográfica e o Input System ativo.
Sprites mínimos (desenhe em qualquer editor de pixel art, ou use quadrados coloridos para começar):
- Pássaro: 17x12 pixels.
- Cano: 26x160 pixels, verde, com a borda da boca mais larga.
- Chão: uma faixa de 168x56 pixels que repete.
Na importação, para pixel art: Filter Mode em Point (no filter), Compression em None e Pixels Per Unit igual para todos (16 é um bom valor). Na câmera, Size 5 dá uma tela de 10 unidades de altura, que é a medida usada nos números abaixo.
Pássaro: pulo e rotação
No pássaro, adicione Rigidbody2D (Gravity Scale 2,5, Collision Detection Continuous, Interpolate Interpolate) e um CircleCollider2D um pouco menor que o sprite: hitbox generosa é o que faz o jogo parecer justo. Crie Bird.cs:
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody2D))]
public class Bird : MonoBehaviour
{
[SerializeField] private GameManager gameManager;
[SerializeField] private float flapVelocity = 7f;
[SerializeField] private float gravityScale = 2.5f;
[SerializeField] private float maxUpAngle = 30f;
[SerializeField] private float maxDownAngle = -80f;
[SerializeField] private float rotationSpeed = 8f;
private Rigidbody2D rb;
private InputAction jumpAction;
private bool isDead;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
jumpAction = InputSystem.actions.FindAction("Jump");
rb.gravityScale = 0f;
}
private void Update()
{
if (isDead || !FlapPressed())
{
return;
}
if (gameManager.State == GameState.Ready)
{
gameManager.StartGame();
rb.gravityScale = gravityScale;
}
rb.linearVelocity = new Vector2(0f, flapVelocity);
}
private void FixedUpdate()
{
if (gameManager.State != GameState.Playing)
{
return;
}
float t = Mathf.InverseLerp(-flapVelocity * 1.5f, flapVelocity, rb.linearVelocity.y);
float targetAngle = Mathf.Lerp(maxDownAngle, maxUpAngle, t);
float angle = Mathf.LerpAngle(rb.rotation, targetAngle, rotationSpeed * Time.fixedDeltaTime);
rb.MoveRotation(angle);
}
private bool FlapPressed()
{
bool jump = jumpAction != null && jumpAction.WasPressedThisFrame();
bool click = Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame;
bool touch = Touchscreen.current != null && Touchscreen.current.primaryTouch.press.wasPressedThisFrame;
return jump || click || touch;
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (isDead)
{
return;
}
isDead = true;
rb.angularVelocity = 0f;
rb.freezeRotation = true;
gameManager.GameOver();
}
}
A decisão que define a sensação do jogo: o pulo define a velocidade vertical (rb.linearVelocity = new Vector2(0f, flapVelocity)) em vez de somar força. Com AddForce, um toque enquanto o pássaro despenca mal o freia, e um toque enquanto sobe o lança longe. Definindo a velocidade, todo toque dá o mesmo impulso, e o jogador aprende o ritmo. No Unity 6, a propriedade é linearVelocity: o velocity dos tutoriais antigos está obsoleto.
A rotação mapeia a velocidade vertical para um ângulo: subindo, bico para cima até 30 graus; caindo rápido, bico para baixo até -80. MoveRotation no FixedUpdate gira pelo próprio Rigidbody2D, sem brigar com a física.
O input junta três fontes: a ação Jump do asset de ações padrão do Unity 6 (espaço no teclado e botão do controle), o clique do mouse e o toque na tela. Assim o mesmo build funciona no PC e no celular pelo navegador.
Canos: spawner e movimento
Monte o prefab PipePair: um GameObject vazio com Rigidbody2D em Body Type Kinematic, dois filhos com o sprite do cano e BoxCollider2D (o de cima girado 180 graus em Z), separados por uma abertura de 2,8 unidades, e um terceiro filho ScoreZone com BoxCollider2D Is Trigger ocupando a abertura. Crie PipePair.cs:
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public class PipePair : MonoBehaviour
{
[SerializeField] private ScoreZone scoreZone;
private Rigidbody2D rb;
private float despawnX;
private PipeSpawner owner;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
}
public void Launch(PipeSpawner spawner, GameManager gameManager, Vector2 position, float speed, float limitX)
{
owner = spawner;
despawnX = limitX;
transform.position = position;
rb.position = position;
rb.linearVelocity = Vector2.left * speed;
scoreZone.Setup(gameManager);
}
public void Stop()
{
rb.linearVelocity = Vector2.zero;
}
private void Update()
{
if (transform.position.x < despawnX)
{
owner.Recycle(this);
}
}
}
Corpo Kinematic se move pela velocidade que você define e não sofre gravidade nem empurrão, que é exatamente um cano. Mover collider sem Rigidbody2D (só pelo transform) funciona, mas obriga a física a recalcular collider estático a cada frame. Agora PipeSpawner.cs, que cria os canos e os reaproveita numa fila:
using System.Collections.Generic;
using UnityEngine;
public class PipeSpawner : MonoBehaviour
{
[SerializeField] private PipePair pipePrefab;
[SerializeField] private GameManager gameManager;
[SerializeField] private float interval = 1.4f;
[SerializeField] private float speed = 2.5f;
[SerializeField] private float spawnX = 4f;
[SerializeField] private float despawnX = -4f;
[SerializeField] private float minY = -1.5f;
[SerializeField] private float maxY = 2.5f;
private readonly Queue<PipePair> pool = new Queue<PipePair>();
private readonly List<PipePair> active = new List<PipePair>();
private float timer;
private void Update()
{
if (gameManager.State != GameState.Playing)
{
return;
}
timer -= Time.deltaTime;
if (timer <= 0f)
{
timer = interval;
Spawn();
}
}
private void Spawn()
{
PipePair pipe = pool.Count > 0 ? pool.Dequeue() : Instantiate(pipePrefab, transform);
pipe.gameObject.SetActive(true);
Vector2 position = new Vector2(spawnX, Random.Range(minY, maxY));
pipe.Launch(this, gameManager, position, speed, despawnX);
active.Add(pipe);
}
public void Recycle(PipePair pipe)
{
pipe.gameObject.SetActive(false);
active.Remove(pipe);
pool.Enqueue(pipe);
}
public void StopAll()
{
foreach (PipePair pipe in active)
{
pipe.Stop();
}
}
}
Com intervalo de 1,4 s e velocidade de 2,5, nunca há mais que 4 ou 5 pares na tela, então a fila nunca passa disso. O jogo inteiro roda com meia dúzia de canos reciclados. A faixa de altura (minY a maxY) controla a dificuldade: faixa larga dá saltos de altura bruscos entre canos.
Pontuação e recorde
O trigger da abertura soma ponto. Crie ScoreZone.cs:
using UnityEngine;
public class ScoreZone : MonoBehaviour
{
private GameManager gameManager;
public void Setup(GameManager manager)
{
gameManager = manager;
}
private void OnTriggerExit2D(Collider2D other)
{
if (other.GetComponent<Bird>() != null)
{
gameManager.AddPoint();
}
}
}
OnTriggerExit2D em vez de Enter: o ponto conta quando o pássaro sai da abertura, ou seja, quando ele passou de fato. Contar na entrada dá ponto para quem morre batendo no meio do cano.
O cano é prefab e não pode guardar referência ao GameManager da cena no Inspector, por isso o spawner passa a referência no Launch. Isso é injeção de dependência na forma mais simples, e evita Find em cada cano.
Game over e reiniciar
Crie GameManager.cs:
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
public enum GameState { Ready, Playing, GameOver }
public class GameManager : MonoBehaviour
{
private const string BestKey = "FlappyBest";
[SerializeField] private PipeSpawner spawner;
[SerializeField] private TMP_Text scoreText;
[SerializeField] private GameObject readyPanel;
[SerializeField] private GameObject gameOverPanel;
[SerializeField] private TMP_Text finalText;
[SerializeField] private float restartDelay = 0.5f;
private int score;
private float gameOverTime;
public GameState State { get; private set; } = GameState.Ready;
private void Start()
{
readyPanel.SetActive(true);
gameOverPanel.SetActive(false);
scoreText.SetText("{0}", score);
}
private void Update()
{
if (State != GameState.GameOver || Time.time < gameOverTime + restartDelay)
{
return;
}
bool pressed = (Keyboard.current != null && Keyboard.current.spaceKey.wasPressedThisFrame)
|| (Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame)
|| (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.wasPressedThisFrame);
if (pressed)
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
}
public void StartGame()
{
State = GameState.Playing;
readyPanel.SetActive(false);
}
public void AddPoint()
{
if (State != GameState.Playing)
{
return;
}
score++;
scoreText.SetText("{0}", score);
}
public void GameOver()
{
if (State == GameState.GameOver)
{
return;
}
State = GameState.GameOver;
gameOverTime = Time.time;
spawner.StopAll();
int best = Mathf.Max(score, PlayerPrefs.GetInt(BestKey, 0));
PlayerPrefs.SetInt(BestKey, best);
PlayerPrefs.Save();
finalText.SetText("Pontos: {0}\nRecorde: {1}", score, best);
gameOverPanel.SetActive(true);
}
}
O restartDelay de meio segundo existe porque o jogador está martelando o botão quando morre. Sem ele, o toque seguinte à morte reinicia na hora e ele nem vê a pontuação. Reiniciar é recarregar a cena com SceneManager.LoadScene: num jogo deste tamanho, é a forma mais simples e sem estado sobrando.
Coloque um collider no chão e outro invisível no teto (BoxCollider2D largo acima da tela), para o pássaro não escapar por cima dos canos.
Repare que, mesmo num jogo de cinco scripts, a maior parte do trabalho foi ligar as peças: quem avisa quem, quem guarda a referência, quem para o quê no game over. Num jogo maior, essa costura é onde o projeto solo trava, e é exatamente o que um curso de Unity com projetos completos treina, um jogo terminado de cada vez.
Flappy Bird na Unity: polimento e build
Polimento que cabe em uma hora e muda o jogo:
- Som: pulo, ponto e batida. Três
AudioClipe umAudioSourcecomPlayOneShot. - Chão rolando: o sprite do chão com Draw Mode
Tilede um script que desloca a posição em loop, na mesma velocidade dos canos. - Flash branco e tremida de câmera na morte: uma Image branca que pisca 0,1 s e um deslocamento curto da câmera. É o tipo de detalhe que o post de game feel e juice explica.
Para a build Web:
- File > Build Profiles, adicione a cena em Scene List e selecione a plataforma Web (instale o módulo pelo Unity Hub se ele não aparecer).
- Em Player Settings > Resolution and Presentation, defina uma resolução retrato, como 450x800.
- Em Publishing Settings, ative Decompression Fallback. Sem isso, a build compactada pode travar no carregamento em servidores que não enviam os cabeçalhos de compressão esperados.
- Build, compacte a pasta gerada em
.zipe envie no itch.io como projeto HTML, marcando a opção de rodar no navegador.
O passo a passo detalhado da publicação, com as configurações da página do itch.io, está em publicar jogo WebGL da Unity no itch.io.
Ajustar a sensação: os números que importam
Flappy Bird é um jogo de quatro números, e mexer neles muda tudo. Antes de publicar, jogue 10 partidas mudando um de cada vez:
- Gravity Scale (2,5) e flapVelocity (7) andam juntos. A razão entre eles define o "peso" do pássaro. Gravidade alta com pulo alto deixa o jogo nervoso e preciso. Os dois baixos deixam o jogo flutuante e fácil de controlar, mas lento.
- Abertura entre canos (2,8 unidades): com o pássaro de 12 pixels (0,75 unidade com PPU 16), a abertura deixa folga para um pulo errado. Abaixo de 2,4, o jogo fica cruel para quem está começando.
- Intervalo (1,4 s) e velocidade (2,5): definem a distância horizontal entre canos (3,5 unidades). Distância curta não dá tempo de corrigir a altura entre um cano e outro.
- Faixa de altura (
minYamaxY): limite a diferença entre canos seguidos. Uma abertura colada no chão logo depois de uma colada no teto é impossível na velocidade normal.
Anote os valores que funcionaram. Esse caderno de números é o que vira o "nível de dificuldade" quando você adicionar modos de jogo.
Erros comuns e próximos passos
O pássaro não pula. A ação Jump não existe porque o projeto não usa o asset de ações padrão. Abra Edit > Project Settings > Input System Package e confira se há um asset de ações do projeto atribuído.
O pássaro gira sem parar depois de bater. A batida no cano aplica torque. Na morte, zere rb.angularVelocity e ligue rb.freezeRotation, como o OnCollisionEnter2D do Bird faz. Não marque Freeze Rotation Z no Inspector desde o começo: a inclinação do bico durante o voo depende da rotação livre.
O ponto conta duas vezes. O pássaro tem dois colliders. Use um só.
Os canos param de aparecer depois de reiniciar. O PipeSpawner ficou com estado de outra cena porque foi marcado como DontDestroyOnLoad. Neste jogo, nada precisa sobreviver ao reinício.
Próximos passos: medalhas por pontuação, dificuldade que aumenta a velocidade dos canos a cada 10 pontos, e um dia e noite trocando o fundo. Para comparar a mesma estrutura em outra engine, o Flappy Bird no Godot monta o mesmo jogo com nós e sinais. Fazer o mesmo jogo nas duas engines é um exercício ótimo para separar o que é conceito do que é sintaxe.
Perguntas frequentes
Quanto tempo leva para fazer Flappy Bird na Unity?
Com este passo a passo, a versão jogável sai em uma tarde, e com polimento e build para a web cabe num dia. É um projeto pequeno de propósito: o objetivo é terminar e publicar, não fazer o jogo perfeito.
Como fazer o pássaro pular no Flappy Bird na Unity?
Em vez de AddForce, defina a velocidade vertical direto: rb.linearVelocity = new Vector2(0f, flapVelocity). Assim todo pulo tem a mesma altura, não importa se o pássaro estava caindo rápido ou subindo.
Como reiniciar o jogo na Unity?
Recarregue a cena ativa com SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex), do namespace UnityEngine.SceneManagement. A cena precisa estar na lista de cenas do Build Profile.
Dá para publicar o Flappy Bird da Unity no itch.io?
Sim. Gere uma build para a plataforma Web, compacte a pasta gerada em .zip e envie no itch.io marcando a opção de jogar no navegador. Se a página ficar travada no carregamento, ative a Decompression Fallback nas configurações de publicação.



