A complete, step-by-step guide to building your first Unity game from scratch. No experience needed — just follow along.
By the end of this tutorial you'll have a fully working Flappy Bird clone: a bird that flaps when you press Space, pipes that scroll across the screen, a score counter, and a game-over screen with restart. Everything is done in Unity 2D with C# scripts.
Unity uses a launcher called Unity Hub to manage your editor versions and projects.
2022.3 or newer will work. The screenshots in this tutorial use 2022.3 LTS but the steps are the same across versions.
FlappyBird.Unity will open the editor. It takes a minute the first time. When it loads, you'll see:
#4EC0CA (R: 78, G: 192, B: 202).5. This controls how much of the world is visible.Ground (click on it in the Hierarchy and press F2).X: 0, Y: -4.5, Z: 0X: 20, Y: 1, Z: 1#DED895 (sandy yellow).Bird.X: -2, Y: 0, Z: 0X: 0.5, Y: 0.5, Z: 1#F2D831.1.5 (makes the bird fall faster and feel snappier).Press the Play button at the top of the editor to test. The bird should fall and hit the ground. Press Play again to stop.
Now let's make the bird flap when you press Space. This is your first C# script.
Scripts.BirdController (spelling and capitalization matter!).Replace everything in the file with this:
using UnityEngine;
public class BirdController : MonoBehaviour
{
// How hard the bird flaps upward
public float flapForce = 6f;
// Reference to the bird's physics body
private Rigidbody2D rb;
void Start()
{
// Get the Rigidbody2D component attached to this object
rb = GetComponent<Rigidbody2D>();
}
void Update()
{
// When the player presses Space (or clicks), flap!
if (Input.GetKeyDown(KeyCode.Space) || Input.GetMouseButtonDown(0))
{
Flap();
}
}
void Flap()
{
// Reset the bird's vertical speed, then push it up
rb.linearVelocity = Vector2.zero;
rb.AddForce(Vector2.up * flapForce, ForceMode2D.Impulse);
}
}
Ctrl+S / Cmd+S) and switch back to Unity.BirdController script from the Project panel onto the Inspector (or click Add Component and search for it).Press Play and hit Space. The bird should flap upward each time you press it!
Start() runs once when the game begins — we use it to grab the Rigidbody.
Update() runs every single frame — we check for Space here.
Flap() resets the velocity (so flaps feel consistent) and pushes the bird up.
To make the bird tilt up when flapping and tilt down when falling, update the script — add this inside Update(), after the if statement:
// Tilt the bird based on its vertical speed
float tilt = Mathf.Clamp(rb.linearVelocity.y * 5f, -90f, 30f);
transform.rotation = Quaternion.Euler(0, 0, tilt);
The pipes are pairs of tall rectangles with a gap in the middle. We'll build one pair, then turn it into a prefab so we can spawn copies.
Pipe.X: 5, Y: 0, Z: 0.TopPipe.X: 0, Y: 5, Z: 0X: 1, Y: 8, Z: 1#73BF2E.BottomPipe.X: 0, Y: -5, Z: 0X: 1, Y: 8, Z: 1#73BF2E.Create a new C# script in your Scripts folder called PipeMove:
using UnityEngine;
public class PipeMove : MonoBehaviour
{
// How fast the pipes scroll to the left
public float speed = 3f;
// How far off-screen before we delete this pipe
public float destroyX = -10f;
void Update()
{
// Move the pipe to the left every frame
transform.position += Vector3.left * speed * Time.deltaTime;
// Destroy the pipe once it's off-screen
if (transform.position.x < destroyX)
{
Destroy(gameObject);
}
}
}
PipeMove to the Pipe parent object (not the individual top/bottom pipes).Prefabs.We need something to keep creating new pipe pairs at regular intervals with random heights.
PipeSpawner.using UnityEngine;
public class PipeSpawner : MonoBehaviour
{
// Drag your Pipe prefab here in the Inspector
public GameObject pipePrefab;
// Where pipes spawn (off-screen right)
public float spawnX = 8f;
// How far up or down the pipes can shift
public float heightRange = 1.5f;
// Seconds between each new pipe pair
public float spawnInterval = 2f;
private float timer;
void Update()
{
timer += Time.deltaTime;
if (timer >= spawnInterval)
{
SpawnPipe();
timer = 0f;
}
}
void SpawnPipe()
{
// Pick a random height offset
float randomY = Random.Range(-heightRange, heightRange);
// Create a new pipe pair at the spawn position
Vector3 spawnPos = new Vector3(spawnX, randomY, 0);
Instantiate(pipePrefab, spawnPos, Quaternion.identity);
}
}
PipeSpawner script to the PipeSpawner object.Press Play. Pipes should now appear from the right side and scroll left. The bird should be able to flap through the gaps!
The score goes up by 1 each time the bird passes through a pipe gap. We do this with an invisible trigger collider between the pipes.
ScoreZone.X: 0, Y: 0, Z: 0.X: 0.5, Y: 3 (covers the gap).ScoreZone and save.ScoreZone.ScoreText.0.72.X: 0, Y: -80.This script tracks the score and controls game state:
using UnityEngine;
using TMPro;
public class GameManager : MonoBehaviour
{
// A static reference so other scripts can access this easily
public static GameManager Instance;
public TextMeshProUGUI scoreText;
private int score;
void Awake()
{
// Set up the singleton so we can call GameManager.Instance
Instance = this;
}
void Start()
{
score = 0;
UpdateScoreDisplay();
}
public void AddScore()
{
score++;
UpdateScoreDisplay();
}
void UpdateScoreDisplay()
{
scoreText.text = score.ToString();
}
public int GetScore()
{
return score;
}
}
GameManager.GameManager script to it.Add this method to your BirdController.cs script, right below the Flap() method:
void OnTriggerEnter2D(Collider2D other)
{
// When the bird passes through the score zone
if (other.CompareTag("ScoreZone"))
{
GameManager.Instance.AddScore();
}
}
Press Play. Fly through pipes and the score should count up!
When the bird hits a pipe or the ground, the game should stop and show a game-over screen.
GameOverPanel.RGBA(0, 0, 0, 180).Game Over, font size 48, white, centered.RestartButton.Play Again.Replace your GameManager.cs with this updated version:
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;
public class GameManager : MonoBehaviour
{
public static GameManager Instance;
public TextMeshProUGUI scoreText;
public GameObject gameOverPanel;
private int score;
private bool isGameOver;
void Awake()
{
Instance = this;
}
void Start()
{
score = 0;
isGameOver = false;
UpdateScoreDisplay();
// Make sure the game over panel is hidden
gameOverPanel.SetActive(false);
}
public void AddScore()
{
if (isGameOver) return;
score++;
UpdateScoreDisplay();
}
void UpdateScoreDisplay()
{
scoreText.text = score.ToString();
}
public void GameOver()
{
if (isGameOver) return;
isGameOver = true;
// Show the game over panel
gameOverPanel.SetActive(true);
// Freeze the game
Time.timeScale = 0f;
}
public void RestartGame()
{
// Unfreeze time
Time.timeScale = 1f;
// Reload the current scene
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
public bool IsGameOver()
{
return isGameOver;
}
public int GetScore()
{
return score;
}
}
Add this method to BirdController.cs:
void OnCollisionEnter2D(Collision2D collision)
{
// Bird hit something solid (pipe or ground) = game over
GameManager.Instance.GameOver();
}
Update the Update() method in BirdController.cs to check for game over:
void Update()
{
// Don't allow input if game is over
if (GameManager.Instance != null && GameManager.Instance.IsGameOver())
return;
if (Input.GetKeyDown(KeyCode.Space) || Input.GetMouseButtonDown(0))
{
Flap();
}
// Tilt the bird based on its vertical speed
float tilt = Mathf.Clamp(rb.linearVelocity.y * 5f, -90f, 30f);
transform.rotation = Quaternion.Euler(0, 0, tilt);
}
Update the Update() method in PipeSpawner.cs:
void Update()
{
// Don't spawn if the game is over
if (GameManager.Instance != null && GameManager.Instance.IsGameOver())
return;
timer += Time.deltaTime;
if (timer >= spawnInterval)
{
SpawnPipe();
timer = 0f;
}
}
Press Play. The full game loop should work: flap, dodge pipes, score points, die, see game over, and restart!
The game works. Now let's make it feel better and build a real executable.
Stop the bird from flying off the top of the screen. Add this to the end of Flap() in BirdController:
void LateUpdate()
{
// Clamp the bird's position so it can't fly above the screen
Vector3 pos = transform.position;
pos.y = Mathf.Clamp(pos.y, -4.5f, 4.5f);
transform.position = pos;
}
Then add this to BirdController.cs:
// Add these fields at the top of the class
public AudioClip flapSound;
public AudioClip hitSound;
private AudioSource audioSource;
// Add this line inside Start()
// audioSource = GetComponent<AudioSource>();
// Play flap sound — add inside Flap()
// audioSource.PlayOneShot(flapSound);
// Play hit sound — add inside OnCollisionEnter2D()
// audioSource.PlayOneShot(hitSound);
Then drag your audio clips into the Inspector fields.
A simple approach: freeze the game at the start and wait for the first tap.
In GameManager.cs, add a bool gameStarted = false; field. Set Time.timeScale = 0f; in Start(). Then in a new method called StartGame(), set Time.timeScale = 1f;. Call it from BirdController on the first flap.
Here are the final, complete versions of every script in the project.
using UnityEngine;
public class BirdController : MonoBehaviour
{
public float flapForce = 6f;
public AudioClip flapSound;
public AudioClip hitSound;
private Rigidbody2D rb;
private AudioSource audioSource;
void Start()
{
rb = GetComponent<Rigidbody2D>();
audioSource = GetComponent<AudioSource>();
}
void Update()
{
// Don't allow input if game is over
if (GameManager.Instance != null && GameManager.Instance.IsGameOver())
return;
if (Input.GetKeyDown(KeyCode.Space) || Input.GetMouseButtonDown(0))
{
Flap();
}
// Tilt the bird based on its vertical speed
float tilt = Mathf.Clamp(rb.linearVelocity.y * 5f, -90f, 30f);
transform.rotation = Quaternion.Euler(0, 0, tilt);
}
void LateUpdate()
{
// Clamp the bird so it can't fly off screen
Vector3 pos = transform.position;
pos.y = Mathf.Clamp(pos.y, -4.5f, 4.5f);
transform.position = pos;
}
void Flap()
{
rb.linearVelocity = Vector2.zero;
rb.AddForce(Vector2.up * flapForce, ForceMode2D.Impulse);
if (audioSource != null && flapSound != null)
audioSource.PlayOneShot(flapSound);
}
void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("ScoreZone"))
{
GameManager.Instance.AddScore();
}
}
void OnCollisionEnter2D(Collision2D collision)
{
if (audioSource != null && hitSound != null)
audioSource.PlayOneShot(hitSound);
GameManager.Instance.GameOver();
}
}
using UnityEngine;
public class PipeMove : MonoBehaviour
{
public float speed = 3f;
public float destroyX = -10f;
void Update()
{
transform.position += Vector3.left * speed * Time.deltaTime;
if (transform.position.x < destroyX)
{
Destroy(gameObject);
}
}
}
using UnityEngine;
public class PipeSpawner : MonoBehaviour
{
public GameObject pipePrefab;
public float spawnX = 8f;
public float heightRange = 1.5f;
public float spawnInterval = 2f;
private float timer;
void Update()
{
if (GameManager.Instance != null && GameManager.Instance.IsGameOver())
return;
timer += Time.deltaTime;
if (timer >= spawnInterval)
{
SpawnPipe();
timer = 0f;
}
}
void SpawnPipe()
{
float randomY = Random.Range(-heightRange, heightRange);
Vector3 spawnPos = new Vector3(spawnX, randomY, 0);
Instantiate(pipePrefab, spawnPos, Quaternion.identity);
}
}
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;
public class GameManager : MonoBehaviour
{
public static GameManager Instance;
public TextMeshProUGUI scoreText;
public GameObject gameOverPanel;
private int score;
private bool isGameOver;
void Awake()
{
Instance = this;
}
void Start()
{
score = 0;
isGameOver = false;
UpdateScoreDisplay();
gameOverPanel.SetActive(false);
}
public void AddScore()
{
if (isGameOver) return;
score++;
UpdateScoreDisplay();
}
void UpdateScoreDisplay()
{
scoreText.text = score.ToString();
}
public void GameOver()
{
if (isGameOver) return;
isGameOver = true;
gameOverPanel.SetActive(true);
Time.timeScale = 0f;
}
public void RestartGame()
{
Time.timeScale = 1f;
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
public bool IsGameOver()
{
return isGameOver;
}
public int GetScore()
{
return score;
}
}
Seriously — that's a real game with physics, scoring, and UI. Take what you learned and try adding new features: parallax backgrounds, difficulty scaling, high scores, or new obstacles.
Try a Free Hackingtons Class