Flappy Bird clone

overview

This Unity 3D project reimagines the iconic Flappy Bird gameplay with a custom-built clone focused on simplicity, smooth mechanics, and modular design. Players control a character navigating between moving obstacles by tapping to maintain altitude—simple in concept, challenging in execution. Featuring collision-based game over logic, score tracking, and responsive input handling, the project serves as a clean, extensible template for experimenting with casual game mechanics, procedural generation, or UI integration.

Year

2025

Genre

Endless runner

Platform

PC / mobile

fly!
C#
				using System.Collections;
using System.Collections.Generic;
using UnityEditor.Tilemaps;
using UnityEngine;
using UnityEngine.InputSystem;
public class Player : MonoBehaviour
{
    [Header("Player Settings")]
    [SerializeField] private float flyForce = 1.5f;
    [SerializeField] private float rotationSpeed = 10f;
    [SerializeField] private float autoFlyInterval = 0.5f;
    private Rigidbody2D rb2D;

    private bool canMove = false;
    private bool waitingForFirstInput = false;
    private bool firstInputReceived = false;
    private bool autoFlying = false;

    private Coroutine autoFlyRoutine;
    private PlayerInput playerInput;
    private void Awake()
    {
        playerInput = new PlayerInput();
        playerInput.Enable();

        SetUpActions();
    }
    public void StartAutoFly()
    {
        autoFlying = true;
        autoFlyRoutine = StartCoroutine(AutoFlyLoop());
        WaitForFirstInput();
    }
    public void StopAutoFly()
    {
        autoFlying = false;
        if (autoFlyRoutine != null)
            StopCoroutine(autoFlyRoutine);
    }
    private IEnumerator AutoFlyLoop()
    {
        while (autoFlying)
        {
            Fly();
            yield return new WaitForSeconds(autoFlyInterval);
        }
    }
    private void SetUpActions()
    {
        //playerInput.Player.fly.performed += OnFly;
        playerInput.Player.fly.performed += context =>
        {
            if (waitingForFirstInput && !firstInputReceived)
            {
                firstInputReceived = true;
                waitingForFirstInput = false;

                StopAutoFly(); // stop hovering
                GameManager.Instance.StartGamePlay(); // start actual game
            }

            Fly();
        };
    }
    private void OnFly(InputAction.CallbackContext context)
    {
        Fly();
    }
    private void Fly()
    {
        if (canMove)
        {
            rb2D.velocity = Vector2.up * flyForce;
        }
    }
    private void FixedUpdate()
    {
        if (canMove)
        {
            transform.rotation = Quaternion.Euler(0, 0, rb2D.velocity.y * rotationSpeed);
        }
    }
    private void OnDisable()
    {
        playerInput.Player.fly.performed -= OnFly;
    }
    public void WaitForFirstInput()
    {
        waitingForFirstInput = true;
        firstInputReceived = false;
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
        GameManager.Instance.GameOver();
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Score"))
        {
            GameManager.Instance.AddScore();
        }
    }
    public void SetMovement(bool value)
    {
        rb2D = gameObject.GetComponent<Rigidbody2D>();

        canMove = value;

        if (value)
        {
            rb2D.gravityScale = 0.56f;
        }
        else
        {
            rb2D.gravityScale = 0f;
        }
    }
}
			
C#
				using JetBrains.Annotations;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    #region Player Settings
    [Header("Player Settings")]
    [SerializeField] private Player playerScript;
    [SerializeField] private GameObject player;
    [SerializeField] private Transform playerStartPos;
    #endregion

    #region Spawner
    [Header("Spawner")]
    [SerializeField] private PipeSpawner spawner;
    #endregion

    #region Score
    [SerializeField] private int score;
    private bool canScore = false;
    public int Score => score;
    #endregion
    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            Instance = this;
        }

        SetUpPlayer();
        UIManager.Instance.ShowMainMenu();
    }
    #region Game State
    public void PlayGame()
    {
        UIManager.Instance.HideAllUI();
        StartCoroutine(StartIntroSequence());
    }
    private IEnumerator StartIntroSequence()
    {
        playerScript.SetMovement(true);
        yield return new WaitForSeconds(0.2f);
        playerScript.StartAutoFly();
    }
    public void ResetGame()
    {
        SetUpPlayer();
        spawner.ClearSpawner();
        UIManager.Instance.ShowMainMenu();
    }
    public void GameOver()
    {
        canScore = false;
        playerScript.SetMovement(false);
        spawner.StopSpawning();
        UIManager.Instance.ShowGameOverScreen();
        UIManager.Instance.ShowScoreGameOver();
    }
    public void StartGamePlay()
    {
        spawner.StartSpawning();
        canScore = true;
    }
    public void AddScore()
    {
        score += 1;
    }
    public int GetCurrentScore() { return score; }
    public bool GetCanScore() {  return canScore; }
    #endregion

    #region Setup
    private void SetUpPlayer()
    {
        playerScript.SetMovement(false);
        player.transform.SetPositionAndRotation(playerStartPos.position, playerStartPos.rotation);
    }
    public Transform PlayerStartPos() => playerStartPos;
    #endregion
}
			
C#
				using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Pipe : MonoBehaviour
{
    [Header("Speed Settings")]
    [SerializeField] private float moveSpeed = 0.65f;
    private bool canMove = true;

    private float lifeTime = 10f;
    private float timer = 0f;
    private PipeSpawner spawner;

    public void Init(PipeSpawner spawnerRef)
    {
        spawner = spawnerRef;
    }
    private void Update()
    {
        if (canMove)
        {
            transform.position += Vector3.left * moveSpeed * Time.deltaTime;

            timer += Time.deltaTime;

            // check if lifetime is expired
            if (timer >= lifeTime || IsOffScreen())
            {
                spawner.RemovePipe(this.gameObject);
                Destroy(gameObject);
            }
        }
    }
    private bool IsOffScreen()
    {
        Vector3 screenPos = Camera.main.WorldToViewportPoint(transform.position);
        return screenPos.x < -2;
    }
    public void StopMovement()
    {
        canMove = false;
    }
}
			
C#
				using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PipeMovement : MonoBehaviour
{
    [Header("Pipe Movement")]
    [SerializeField] private float pipeMovement = 5f;
    private bool canMove = true;
    private void Update()
    {
        transform.position += Vector3.left * pipeMovement * Time.deltaTime;
    }

    public void StopMovement()
    {
        canMove = false;
    }
}
			
C#
				using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using UnityEngine;

public class PipeSpawner : MonoBehaviour
{
    [SerializeField] private float maxTime = 1.5f;
    [SerializeField] private float heightRange = 0.45f;
    [SerializeField] private GameObject pipePrefab;
    [SerializeField] private List<GameObject> activePipes;

    private float timer;
    private bool canSpawn = false;
    private void Update()
    {
        if (timer > maxTime && canSpawn)
        {
            SpawnPipe();
            timer = 0;
        }

        timer += Time.deltaTime;
    }
    private void SpawnPipe()
    {
        Vector3 spawnPos = transform.position + new Vector3(0, Random.Range(-heightRange, heightRange));
        GameObject pipe = Instantiate(pipePrefab, spawnPos, Quaternion.identity);
        pipe.GetComponent<Pipe>().Init(this);
        activePipes.Add(pipe);
    }
    public void RemovePipe(GameObject pipe)
    {
        if (activePipes.Contains(pipe))
        {
            activePipes.Remove(pipe);
        }
    }
    public void StartSpawning()
    {
        canSpawn = true;
    }
    public void StopSpawning()
    {
        canSpawn = false;

        foreach (GameObject pipe in activePipes)
        {
            Pipe p = pipe.GetComponent<Pipe>();
            p.StopMovement();
        }
    }
    public void ClearSpawner()
    {
        foreach (GameObject activePipe in activePipes)
        {
            Destroy(activePipe);
        }

        activePipes.Clear(); // Clear the list after destruction
    }
}
			

Used assets