Voltar para o Blog
Quest Log

Como Fazer Flappy Bird na Unity (C# Completo)

Passarinho em pixel art voando entre canos verdes num céu azul com o placar no topo da tela

Como fazer Flappy Bird na Unity em C#: pulo com linearVelocity, canos reaproveitados, pontuação, recorde, game over e build Web para publicar no itch.io.

Ferramenta:UnityC#

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.

Curso de Unity
Quer fazer seu primeiro jogo na Unity?

No curso de Unity 6 da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Unity
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

Flappy Bird na Unity: polimento e build

Polimento que cabe em uma hora e muda o jogo:

  • Som: pulo, ponto e batida. Três AudioClip e um AudioSource com PlayOneShot.
  • Chão rolando: o sprite do chão com Draw Mode Tiled e 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:

  1. 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).
  2. Em Player Settings > Resolution and Presentation, defina uma resolução retrato, como 450x800.
  3. 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.
  4. Build, compacte a pasta gerada em .zip e 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 (minY a maxY): 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.