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
- Asset pack: Violet Themed UI
Publisher: The GUI Guy