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; // Force applied when player flies
[SerializeField] private float rotationSpeed = 10f; // Rotation multiplier based on vertical speed
[SerializeField] private float autoFlyInterval = 0.5f; // Time between auto-fly inputs
private Rigidbody2D rb2D;
private bool canMove = false; // Controls if player can move (used after game start)
private bool waitingForFirstInput = false; // True when waiting for first player input
private bool firstInputReceived = false; // True after first manual input is detected
private bool autoFlying = false; // True when auto-flying is active
private Coroutine autoFlyRoutine; // Reference to auto-fly coroutine
private PlayerInput playerInput; // PlayerInput system instance
private void Awake()
{
playerInput = new PlayerInput(); // Create input system instance
playerInput.Enable(); // Enable input system
SetUpActions(); // Bind input events
}
// Start auto-flying and wait for the player to tap for manual control
public void StartAutoFly()
{
autoFlying = true;
autoFlyRoutine = StartCoroutine(AutoFlyLoop());
WaitForFirstInput();
}
// Stop auto-fly behavior if it's running
public void StopAutoFly()
{
autoFlying = false;
if (autoFlyRoutine != null)
StopCoroutine(autoFlyRoutine);
}
// Repeatedly applies fly input at fixed intervals until first tap
private IEnumerator AutoFlyLoop()
{
while (autoFlying)
{
Fly();
yield return new WaitForSeconds(autoFlyInterval);
}
}
// Set up player input actions
private void SetUpActions()
{
// This handles input for fly action
playerInput.Player.fly.performed += context =>
{
// If we’re still in auto-fly and waiting for first tap
if (waitingForFirstInput && !firstInputReceived)
{
firstInputReceived = true;
waitingForFirstInput = false;
StopAutoFly(); // Stop hovering
GameManager.Instance.StartGamePlay(); // Start actual game
}
Fly(); // Apply fly movement
};
}
// Handles flying logic (velocity upwards)
private void Fly()
{
if (canMove)
{
rb2D.velocity = Vector2.up * flyForce;
}
}
// Apply rotation based on vertical speed for visual feedback
private void FixedUpdate()
{
if (canMove)
{
transform.rotation = Quaternion.Euler(0, 0, rb2D.velocity.y * rotationSpeed);
}
}
// Unsubscribe input when object is disabled
private void OnDisable()
{
playerInput.Player.fly.performed -= OnFly;
}
// Called to start waiting for the first manual tap
public void WaitForFirstInput()
{
waitingForFirstInput = true;
firstInputReceived = false;
}
// Triggered when player hits anything (walls, pipes, etc.)
private void OnCollisionEnter2D(Collision2D collision)
{
GameManager.Instance.GameOver(); // End game
}
// Triggered when player enters score zone
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.CompareTag("Score"))
{
GameManager.Instance.AddScore(); // Increase score
}
}
// Enables or disables movement, and sets gravity accordingly
public void SetMovement(bool value)
{
rb2D = gameObject.GetComponent<Rigidbody2D>();
canMove = value;
if (value)
{
rb2D.gravityScale = 0.56f; // Enable gravity
}
else
{
rb2D.gravityScale = 0f; // Freeze player in place
}
}
// Old unused input hook (cleanable)
private void OnFly(InputAction.CallbackContext context)
{
Fly();
}
}
C#
using JetBrains.Annotations;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
// Singleton instance so other scripts can access GameManager easily
public static GameManager Instance { get; private set; }
[Header("Player Settings")]
[SerializeField] private Player playerScript; // Reference to the player script
[SerializeField] private GameObject player; // Player GameObject
[SerializeField] private Transform playerStartPos; // Player start position and rotation
[Header("Spawner")]
[SerializeField] private PipeSpawner spawner; // Pipe spawner reference
[Header("Score")]
[SerializeField] private int score; // Player's current score
private bool canScore = false; // Whether scoring is currently allowed
public int Score => score; // Public read-only property for score
private void Awake()
{
// Set up the singleton pattern
if (Instance != null && Instance != this)
{
Destroy(this);
}
else
{
Instance = this;
}
SetUpPlayer(); // Reset player to starting state
UIManager.Instance.ShowMainMenu(); // Show main menu at game launch
}
// Called from UI to start the game
public void PlayGame()
{
UIManager.Instance.HideAllUI(); // Hide menus and overlays
StartCoroutine(StartIntroSequence()); // Start flying intro
}
// Short intro before manual control
private IEnumerator StartIntroSequence()
{
playerScript.SetMovement(true); // Enable gravity/movement
yield return new WaitForSeconds(0.2f);
playerScript.StartAutoFly(); // Begin hovering automatically
}
// Reset all game elements and go back to main menu
public void ResetGame()
{
SetUpPlayer(); // Reposition player
spawner.ClearSpawner(); // Remove all pipes
UIManager.Instance.ShowMainMenu(); // Show main menu UI
}
// Called when player dies or hits obstacle
public void GameOver()
{
canScore = false; // Disable scoring
playerScript.SetMovement(false); // Freeze player
spawner.StopSpawning(); // Stop pipe spawning
UIManager.Instance.ShowGameOverScreen(); // Show "Game Over"
UIManager.Instance.ShowScoreGameOver(); // Display final score
}
// Called when player taps after intro to start real gameplay
public void StartGamePlay()
{
spawner.StartSpawning(); // Start spawning pipes
canScore = true; // Allow scoring
}
// Adds 1 to current score (called by Player when scoring trigger is hit)
public void AddScore()
{
score += 1;
}
// Returns current score
public int GetCurrentScore() { return score; }
// Returns whether the player is allowed to score
public bool GetCanScore() { return canScore; }
// Resets player position and disables movement
private void SetUpPlayer()
{
playerScript.SetMovement(false);
player.transform.SetPositionAndRotation(playerStartPos.position, playerStartPos.rotation);
}
// Public getter for player start position (if needed by other scripts)
public Transform PlayerStartPos() => playerStartPos;
}
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Pipe : MonoBehaviour
{
[Header("Speed Settings")]
[SerializeField] private float moveSpeed = 0.65f; // How fast the pipe moves to the left
private bool canMove = true; // Controls if the pipe should move
private float lifeTime = 10f; // Max time a pipe can exist before auto-destroy
private float timer = 0f; // Timer to track how long the pipe has been alive
private PipeSpawner spawner; // Reference to the spawner that created this pipe
// Called when pipe is spawned, gives it a reference back to the spawner
public void Init(PipeSpawner spawnerRef)
{
spawner = spawnerRef;
}
private void Update()
{
if (canMove)
{
// Move the pipe left based on speed and deltaTime
transform.position += Vector3.left * moveSpeed * Time.deltaTime;
timer += Time.deltaTime;
// Destroy pipe if lifetime is up or it has moved off-screen
if (timer >= lifeTime || IsOffScreen())
{
spawner.RemovePipe(this.gameObject); // Inform spawner to remove this pipe
Destroy(gameObject); // Destroy the pipe
}
}
}
// Returns true if the pipe is far off the left side of the screen
private bool IsOffScreen()
{
Vector3 screenPos = Camera.main.WorldToViewportPoint(transform.position);
return screenPos.x < -2; // -2 ensures it’s really far off-screen before removal
}
// Called externally to stop the pipe from moving (e.g. on game over)
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; // Speed at which the pipe moves left
private bool canMove = true; // Controls whether the pipe is currently moving
private void Update()
{
// Only move the pipe if movement is enabled
if (canMove)
{
transform.position += Vector3.left * pipeMovement * Time.deltaTime;
}
}
// Called externally (e.g. on game over) to stop the pipe from moving
public void StopMovement()
{
canMove = false;
}
}
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PipeSpawner : MonoBehaviour
{
[SerializeField] private float maxTime = 1.5f; // Time between each pipe spawn
[SerializeField] private float heightRange = 0.45f; // Random Y offset for pipe spawn position
[SerializeField] private GameObject pipePrefab; // Pipe prefab to spawn
[SerializeField] private List<GameObject> activePipes; // List of currently active pipes in the scene
private float timer; // Timer to track spawn intervals
private bool canSpawn = false; // Controls whether spawning is active
private void Update()
{
// If it's time to spawn a pipe and spawning is allowed
if (timer > maxTime && canSpawn)
{
SpawnPipe();
timer = 0;
}
timer += Time.deltaTime;
}
// Spawns a new pipe at a random vertical position
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); // Give the pipe a reference back to this spawner
activePipes.Add(pipe); // Add to the active list
}
// Called by a pipe when it's destroyed to remove it from the list
public void RemovePipe(GameObject pipe)
{
if (activePipes.Contains(pipe))
{
activePipes.Remove(pipe);
}
}
// Starts spawning pipes
public void StartSpawning()
{
canSpawn = true;
}
// Stops spawning and halts movement on all existing pipes
public void StopSpawning()
{
canSpawn = false;
foreach (GameObject pipe in activePipes)
{
Pipe p = pipe.GetComponent<Pipe>();
p.StopMovement(); // Stop pipe movement (e.g. on game over)
}
}
// Clears all active pipes and resets the list
public void ClearSpawner()
{
foreach (GameObject activePipe in activePipes)
{
Destroy(activePipe); // Destroy all pipe GameObjects
}
activePipes.Clear(); // Empty the list
}
}
Used assets
- Asset pack: Violet Themed UI
Publisher: The GUI Guy