Welcome! 👋
This is a step-by-step tutorial designed especially for beginners who want to make their first Unity game. Whether you're new to game development or just getting started with Unity, this guide will walk you through the process in a simple and clear way.
We’ll be using Unity and Visual Studio 2022, and all code changes will be documented right here as commits are made.
The Tutorial is been inspired from Game Maker's Toolkit's video, Click here to watch.
- Download and Install Unity for your device from here.
- Also Download and Install Visual Studio 2022 IDE from Here.
- Make sure you have Installed Unity Hub as well as Unity Editor (Latest Version).
- Github(For Version Control)
- Open Unity Hub > New Project.
- Navigate to "Core" > Universal 2D.
- Name your project.
- Click "Create".
When you open Unity, the layout might seem overwhelming at first — but don't worry, it's easy once you know what each panel does. Here's a quick breakdown:
- This is your "workspace" — where you design and build your game.
- You can drag, move, rotate, and scale GameObjects here.
- Think of it like your level editor.
- This shows what the player will see when they play the game.
- Hit the
▶️ (Play) button at the top to preview the game in this window.
- Lists all GameObjects in your current Scene.
- This is where you’ll find things like the Camera, Lights, and your Player object.
- You can drag things in and out to organize objects (like putting enemies inside a folder called "Enemies").
- When you select any GameObject, its properties show up here.
- You can change the position, size, color, add scripts, colliders, and more from this panel.
- This is your file manager inside Unity.
- Everything you import or create (scripts, images, audio, etc.) lives here in folders.
- Displays messages like errors, warnings, and print/debug logs from your scripts.
- Super useful for finding issues in your game.
🔄 Tip: You can rearrange or reset the layout anytime:
Go to Window > Layouts > Default
if something gets messed up.
Now that you're familiar with the Unity interface, let's start adding things to your game!
To bring in images, sounds, or other files into Unity:
- Find the Assets tab in the Project window.
- Simply drag and drop your image (like a character sprite) into the Assets folder.
- Unity will automatically import it and make it available to use in your scene.
📝 Tip: You can also organize your assets by creating folders like
Images
,Audio
,Prefabs
, andScripts
.
- Go to the Hierarchy window.
- Right-click >
Create Empty
. - A new empty GameObject will appear in your Scene. Let's rename it to
Barb
:- Click the GameObject, press
F2
(or right-click > Rename). - Type
Barb
and hit Enter.
- Click the GameObject, press
- With
Barb
selected, go to the Inspector tab. - Find the Transform component.
- Set the Position values (X, Y, Z) to
0
, so it sits in the center of the canvas.
Position: X = 0 | Y = 0 | Z = 0
Right now, Barb
is just an empty GameObject — it doesn’t show up in the Scene because it has no visual component. Let’s fix that by adding a Sprite Renderer and assigning an image (sprite) to it.
- Select the
Barb
GameObject in the Hierarchy. - In the Inspector, click the Add Component button at the bottom.
- Search for and select
Sprite Renderer
.
This tells Unity: “Hey, I want this object to display an image.”
- In your Project > Assets folder, locate the sprite image you imported earlier (for example:
barb.png
). - Drag and drop the sprite into the Sprite field of the Sprite Renderer component (in the Inspector).
Once you do this, your Barb
object will now be visible in the Scene!
✅ If the image doesn't appear, make sure the Sprite’s Texture Type (in the Import Settings) is set to
Sprite (2D and UI)
.
If the Sprite is not Visible, try changing the Position Value for Z of either Main Camera or Barb.
Now Barb
has a visual — you're one step closer to bringing your character to life! 🎉
Now that we know how to add visual components, let’s make our GameObject interactive — like giving it gravity and letting it detect collisions. We’ll also attach a script so we can start coding behavior.
- In the Hierarchy, select your
Bird
GameObject (make sure you’ve created and named it). - In the Inspector, click Add Component.
- Search for and add Rigidbody 2D – this gives your GameObject physics like gravity and movement.
- Again, click Add Component, search for and add Circle Collider 2D – this lets the Bird detect collisions using a circular hitbox.
🧠 Why these?
Rigidbody 2D
: Makes the Bird fall or respond to forces.Circle Collider 2D
: Lets it bump into things or trigger events when it hits other objects.
- With the Bird still selected, click Add Component.
- Search for New Script, name it something like
BirdController
, and click Create and Add. - In the Inspector, double-click the script to open it in Visual Studio 2022 IDE.
Once the script opens, you’ll see something like this:
using UnityEngine;
public class BirdController : MonoBehaviour
{
void Start()
{
// This runs once when the game starts
}
void Update()
{
// This runs every frame (continuously)
}
}
Let’s start writing actual logic inside our script to change properties of our Bird GameObject!
In Unity, gameObject
refers to the object this script is attached to — in our case, the Bird.
Let’s change the name of our Bird from Bird
to something like FlappyBird
using the Start() function.
Inside BirdController.cs
, modify the Start()
method like this:
void Start()
{
gameObject.name = "FlappyBird";
}
The Inspector view shows all the components attached to a GameObject — like Transform, Rigidbody2D, Collider, Sprite Renderer, and any scripts.
Right now, your script can only directly access:
-
gameObject
-
transform (for position, rotation, scale)
But what if we want to control other components like:
-
Rigidbody2D (for movement, gravity)
-
Collider2D (for detecting collisions)
-
SpriteRenderer (for changing visuals)
To do this, we need to create references to those components using code.
Now let’s make our bird move only when we press the spacebar — just like in Flappy Bird. We'll be using Unity’s Rigidbody2D component and writing code inside the Update()
method.
Before we can control the bird’s physics, we need to create a reference to its Rigidbody2D component.
At the top of your script (but inside the class), add this line:
public float flapStrength;
Now inside the Update()
method, we’ll apply a velocity to the Rigidbody2D when the spacebar is pressed:
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
myRigidbody.linearvelocity = Vector2.up * flapStrength;
}
}
-
Input.GetKeyDown(KeyCode.Space)
detects when you press the spacebar. -
Vector2.up * flapStrength
applies force in the upward direction. -
.velocity
sets the linear velocity, which moves the bird.
⚠️ Right now, each spacebar press will make the bird flap upward. Without pressing, gravity (from Rigidbody2D) pulls the bird down.
🔧 Set flapStrength in Unity
Since flapStrength
is marked public
, it appears in the Unity Inspector.
- Click on the Bird GameObject.
- In the Inspector, you’ll see your BirdController script.
- Set Flap Strength to something like 5 or 10 — play around with it to get the right feel.
Let’s add the classic Flappy Bird pipes as obstacles. We'll set up a parent GameObject with two child objects: Top and Bottom pipes. Each child will have its own sprite and collider.
- In the Hierarchy, right-click >
Create Empty
. - Rename the new GameObject to
Pipe
.
This will act as the container for both the top and bottom pipes.
- Right-click on
Pipe
>Create Empty
again — this makes a child object. - Rename this child to
Top
. - With
Top
selected, click Add Component:- Add a Sprite Renderer – this is how we display the image.
- Add a Box Collider 2D – this lets the bird detect and react to the pipe.
- In the Sprite Renderer, drag and drop your pipe sprite image into the Sprite field.
✅ No need to add
Rigidbody 2D
since the pipes are stationary — they don’t need physics like gravity.
- Right-click the
Top
object > Duplicate (or pressCtrl + D
). - Rename the duplicated object to
Bottom
. - Select the
Bottom
pipe and go to the Inspector. - Change the Scale Y value to
-1
— this flips the pipe upside down so it faces downward.
🎨 Both pipes will now share the same sprite, but flipped to face each other.
In Flappy Bird, it’s not the bird that flies forward — instead, the world (pipes) scrolls towards the bird. We can simulate this by moving our Pipe
GameObjects to the left constantly.
The bird stays still on the X-axis and only moves up/down. To create the illusion of movement, we’ll scroll the obstacles from right to left.
- In your Scripts folder, create a new script called
PipeSpawner
. - Attach this script to the Pipe parent GameObject.
- Double-click to open it in Visual Studio 2022 IDE.
Inside the script, write this:
using UnityEngine;
public class PipeSpawner : MonoBehaviour
{
public float moveSpeed = 5f;
void Update()
{
transform.position += Vector3.left * moveSpeed * Time.deltaTime;
}
}
🔍 What’s Happening Here?
-
transform.position += Vector3.left * moveSpeed;
: Moves the object left by moveSpeed every frame. -
Problem: This would move faster on high-end devices because Update() runs more often on them.
-
✅ Solution: Multiply by
Time.deltaTime
, which is the time that has passed since the last frame.
🎯 Time.deltaTime makes the movement frame-rate independent — ensuring all devices scroll at the same speed.
Let’s fix that so our game behaves the same on every device.
Time.deltaTime
is the amount of time (in seconds) that passed since the last frame.
So if your game runs at:
- 60 FPS ➜
deltaTime
≈ 0.016 seconds - 30 FPS ➜
deltaTime
≈ 0.033 seconds
By multiplying your movement by deltaTime
, you make it time-based instead of frame-based — much smoother and consistent.
Open your PipeSpawner
script and update the Update()
method like this:
void Update()
{
transform.position += Vector3.left * moveSpeed * Time.deltaTime;
}
✅ This ensures your pipes move at
moveSpeed
units per second, regardless of how fastUpdate()
is called.
Before we can spawn multiple pipes over time, we need to understand Prefabs — one of Unity’s most powerful tools!
A Prefab is like a blueprint of a GameObject. Once something is turned into a prefab, Unity can easily clone (instantiate) it anytime during gameplay.
Think of it like a cookie cutter — the prefab is the cutter, and each clone is a cookie 🍪.
- In the Hierarchy, find your complete
Pipe
GameObject (the one withTop
andBottom
as children). - Drag and drop this
Pipe
object into the Assets panel (at the bottom of the screen). - You’ll now see a blue version of your object — that’s your Prefab!
🎯 Now Unity can use this blueprint to spawn new pipes during gameplay.
- Create an Empty GameObject in the Hierarchy.
- Rename it to
PipeSpawner
. - Create a new C# script and name it
PipeSpawner.cs
. - Attach the script to the
PipeSpawner
GameObject.
Inside the script, we need a way to reference the prefab so Unity knows what to clone:
public class PipeSpawner : MonoBehaviour
{
public GameObject pipe; // Drag the prefab here in the Editor
}
🔗 Final Step: Linking the Prefab to the Script Unity doesn’t automatically know which GameObject you mean — so you must manually assign the reference:
- Click on the PipeSpawner GameObject in the Hierarchy.
- Look at the Inspector — you’ll see a slot labeled Pipe under the PipeSpawner script.
- Drag your Pipe Prefab from the Assets folder into this slot.
Let’s now bring everything together and build the complete Pipe Spawner logic step-by-step.
We'll go from a basic version to a more efficient and clean version.
In the PipeSpawner.cs
script attached to the PipeSpawner
GameObject, start with the raw logic:
using UnityEngine;
public class PipeSpawner : MonoBehaviour
{
public GameObject pipe; // Link your prefab in the Inspector
void Update()
{
Instantiate(pipe, transform.position, transform.rotation);
}
}
🔁 Warning: When you press Play, you’ll see a flood of pipes! That’s because Update() runs every frame — and we’re instantiating a pipe each time.
To slow down the spawning, we’ll add two new variables:
-
spawnRate
: How often a pipe should spawn (in seconds). -
timer
: Keeps track of time since the last spawn.
Update your script like this:
using UnityEngine;
public class PipeSpawner : MonoBehaviour
{
public GameObject pipe;
public float spawnRate = 5f;
private float timer = 0f;
void Update()
{
if (timer < spawnRate)
{
timer += Time.deltaTime;
}
else
{
Instantiate(pipe, transform.position, transform.rotation);
timer = 0f;
}
}
}
🕒 Now, pipes will spawn every
spawnRate
seconds — much more controlled!
To avoid repeating code and keep things clean, let’s move the spawning logic into a reusable function:
using UnityEngine;
public class PipeSpawner : MonoBehaviour
{
public GameObject pipe;
public float spawnRate = 5f;
private float timer = 0f;
void Start()
{
Spawn(); // Spawn one pipe immediately when the game starts
}
void Update()
{
if (timer < spawnRate)
{
timer += Time.deltaTime;
}
else
{
Spawn();
timer = 0f;
}
}
void Spawn()
{
Instantiate(pipe, transform.position, transform.rotation);
}
}
🎉 This structure is modular, readable, and efficient — your first big step into writing maintainable game code!
Right now, all the pipes spawn at the exact same height — making the game super predictable and… boring 😴.
Let’s spice things up by adding random vertical offsets to each spawned pipe!
We’ll give each pipe a random vertical position when it spawns — but keep it within a reasonable range so the game stays playable.
Open your PipeSpawner.cs
script and add this new variable at the top:
public float heightOffset = 10f;
This is how far up and down we allow the pipes to randomly shift.
Update your function like this:
void Spawn()
{
float lowestPoint = transform.position.y - heightOffset;
float highestPoint = transform.position.y + heightOffset;
Vector3 spawnPosition = new Vector3(
transform.position.x,
Random.Range(lowestPoint, highestPoint),
0f // Because we're in 2D
);
Instantiate(pipe, spawnPosition, transform.rotation);
}
Even though we’re making a 2D game, Unity still expects positions to be in Vector3 format because its engine is 3D under the hood. We just leave the Z-axis at 0
.
Now every time a pipe spawns, it appears at a different Y position — making your game much more engaging and challenging!
🎉 Try adjusting the
heightOffset
in the Inspector to make the pipes easier or harder to dodge.
Your game now spawns pipes beautifully, but there’s a hidden problem…
😨 Pipes that go off-screen to the left are still in memory, taking up resources!
If you leave them piling up, your game will eventually lag or even crash.
Let’s fix that by automatically deleting pipes once they pass the player.
We want to destroy the pipe once it moves too far left — outside the camera's view.
This is the same script where you handled moveSpeed
. It should be attached to the parent pipe prefab.
At the top of your script, add:
public float deadZone = -10f; // Adjust this based on your camera view
This is the X-position beyond which a pipe is considered "dead."
Now update your Update()
function like this:
void Update()
{
transform.position += Vector3.left * moveSpeed * Time.deltaTime;
if (transform.position.x < deadZone)
{
Debug.Log("Pipe Deleted"); // Optional: for debugging
Destroy(gameObject);
}
}
####✅ Done! Now You're Saving Memory Every pipe that leaves the screen on the left will be automatically destroyed, keeping your game clean and efficient.
🧠 Pro Tip: You can also use Unity's
OnBecameInvisible()
function for more advanced optimization later!
It's finally time to display the score on the screen! For this, we’ll use Unity’s UI System.
To draw anything like text or buttons on the screen, you need a special GameObject called a Canvas.
- In the Hierarchy panel, right-click →
UI
→Canvas
. - This creates a Canvas GameObject (along with an EventSystem).
🧠 Unity automatically creates an EventSystem with it — that’s normal.
- Right-click on the Canvas →
UI
→Text - Legacy
. - This will create a child GameObject of Canvas called
Text
.
💬 This is where we’ll show the player’s score.
- Select the Canvas in Hierarchy.
- In the Inspector, under the Canvas Scaler component:
- Change
UI Scale Mode
to: Scale With Screen Size - Set
Reference Resolution
to: 1920 x 1080
- Change
🖥️ This ensures your UI looks good on all devices and resolutions.
- Select the Text GameObject.
- To make the text visible and clean:
- ✅ Tick the Best Fit checkbox (auto adjusts font size).
- 🎨 Change Font, Color, and Style as you like.
- 📐 Adjust Width & Height using Rect Transform, not scale!
- 💥 Do not scale the object — it will blur the text.
📍 You might need to zoom out a lot in the Scene view to see your Canvas and text — that’s totally normal.
✅ Done! You now have a text element ready to display score.
🧠 Up Next: In the next step, we’ll write a script that updates this text in real-time as the player scores points!
Now that we’ve added a score text on the screen, let’s make it actually change when the player scores!
We’ll create a Logic Manager script that:
- Handles the score system 🎯
- Updates the text on screen 📺
- In the Hierarchy, right-click →
Create Empty
→ rename it to LogicManager. - This GameObject will handle all game logic behind the scenes.
- Select LogicManager → click Add Component → create new script
LogicManager
. - Open the script.
At the very top of your script, add:
using UnityEngine.UI;
This allows the script to interact with UI elements like Text.
Inside your script, add:
public int score;
public Text scoretext;
We’ll update the score
and display it using scoreText
.
Now add this method:
public void addscore(){
score += 1;
scoretext.Text = score.Tostring();
}
This function:
- Increases the score by 1
- Updates the text to match the new score
- Go to Inspector of LogicManager in Unity.
- You’ll see an empty field for
Score Text
. - Drag the
Text
element from the Canvas and drop it into that slot.
If the bird is not colliding with pipes...
- You probably need to increase the collider size.
- Select your Pipe prefab in the Hierarchy.
- Expand it to reveal Top and Bottom pipes.
- Select each one and go to the Box Collider 2D component.
- Adjust the X and Y size values to match the visible size of your pipe sprite.
We need a trigger to call the AddScore()
function every time the bird passes between pipes.
We'll add a new invisible trigger zone to the pipe prefab to detect if the bird successfully passes through — and update the score accordingly.
- In your Pipe prefab, right-click on it →
Create Empty
→ rename it toMiddle
. - Select
Middle
→ Add Component →Box Collider 2D
. - In the Box Collider settings, tick the checkbox for "Is Trigger" ✅
- Add a new script to
Middle
and name itMiddleScript
. - Open the script.
We want this script to call AddScore()
from LogicManager
, but Pipe
is not a GameObject in the scene — it’s spawned at runtime by the PipeSpawner
.
So we can’t drag and drop a reference manually.
- Select
LogicManager
in the Hierarchy. - In the Inspector → click the "Tag" dropdown → choose
Add Tag
. - Click
+
, name it logic, and save. - Now, go back to
LogicManager
GameObject → assign the new logic tag to it.
In MiddleScript
, declare:
public LogicScript logic;
Then in the Start()
method, write:
logic = GameObject.FindGameObjectWithTag("logic").GetComponent<LogicScript>();
This is the same as drag-and-drop, except it happens during runtime!
To ensure only the bird adds score (and not other objects), we’ll use layers.
- Select your Bird GameObject.
- In the Inspector, click Layer → Add Layer → create a new one called Bird.
- Assign the Bird GameObject to the Bird layer (should be index 3 if it’s the first new layer).
Now, in MiddleScript
, add:
void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.layer == 3)
{
logic.AddScore();
}
}
✅ Done! Now when the bird passes through the gap:
- The trigger activates
- LogicManager updates the score 🎉
When the bird hits a pipe, we want to display a Game Over screen and allow the player to restart the game with a button click.
- Inside the Canvas GameObject in your Hierarchy:
- Right-click →
Create Empty
→ rename it to Game Over Screen. - Right-click on Game Over Screen →
UI > Text (Legacy)
→ rename it to Game Over Text. - Right-click again on Game Over Screen →
UI > Button
→ rename it to Restart Button.
- Right-click →
✅ You should now have:
- A parent
Game Over Screen
- A child text element to say "Game Over"
- A button to restart the game
- Edit the text, width/height, font size, etc. to make it look clean.
- Tip: Check "Best Fit" in the text component for dynamic resizing.
- Adjust position of elements using the Rect Transform.
- You can initially disable the whole Game Over Screen by unchecking the box next to its name in the Inspector (we'll enable it later).
- Go to your
LogicScript
. - At the top, add:
using UnityEngine.SceneManagement;
- Then, below your
AddScore()
function, add:
public void RestartGame()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
This will reload the current scene, effectively restarting the game.
- Select the Restart Button in the Hierarchy.
- In the Inspector, scroll to the Button (Script) section.
- Under OnClick():
- Click the "+" icon to add a new action.
- Drag and drop the LogicManager GameObject into the object field.
- From the dropdown, choose: LogicScript > RestartGame().
Now clicking the button will restart the game!
Now it’s time to show the Game Over screen when the bird hits a pipe or any obstacle.
- Select the Game Over Screen GameObject in the Hierarchy.
- In the Inspector, uncheck the box beside the name to disable it by default.
- ✅ This means it won't be visible until we turn it on through code.
- In your
LogicScript
, declare a reference to the Game Over UI:
public GameObject GameOverScreen;
- Then create a new function below
AddScore()
:
public void GameOver()
{
GameOverScreen.SetActive(true);
}
- Don’t forget to drag and drop the Game Over Screen GameObject into the LogicManager script’s
GameOverScreen
field in the Unity Editor.
We’ll use the same logic reference code that we used in the MiddleScript
.
In BirdScript
, declare:
private LogicScript logic;
private bool birdIsAlive = true;
- Inside the
Start()
method:
logic = GameObject.FindGameObjectWithTag("logic").GetComponent<LogicScript>();
- Beloew
Update()
, add:
void OnCollisionEnter2D(Collision2D collision)
{
logic.GameOver();
birdIsAlive = false;
}
Inside the Update()
function, modify the space key press logic:
if (Input.GetKeyDown(KeyCode.Space) && birdIsAlive)
{
myRigidbody.velocity = Vector2.up * flapStrength;
}
Now the bird will only flap if it's still alive, and once it collides, the Game Over screen appears and flapping stops.
You made it this far — that’s seriously awesome! 💪 You’ve built your first Unity game from scratch — complete with physics, collisions, a scoring system, UI elements, and a Game Over screen. Before we wrap up, let’s build the game so you can share it with friends or run it as a standalone app.
-
Save your scene and make sure everything works as expected.
- Go to
File > Save Scenes
or pressCtrl + S
.
- Go to
-
Open the Build Settings:
- Click
File > Build Settings
.
- Click
-
Add your current scene:
- Click on the "Add Open Scenes" button so Unity knows what to build.
-
Choose your platform:
- For PC: Select Windows.
- For Mac: Select macOS.
- For Web: Select WebGL.
- For Android: Select Android (requires setup).
-
Set player settings (optional):
- You can change the game name, icon, resolution, etc. by clicking "Player Settings".
-
Click on "Build":
- Choose a folder where the game will be saved.
- Unity will now export your game — just run the
.exe
(or appropriate file) to play!
👏 You just built your first Unity game! That’s a huge achievement, especially if you’re new to game development.
By finishing this guide, you’ve learned:
- Unity’s UI and layout
- GameObject creation & manipulation
- Using Rigidbody2D and Colliders for physics
- Writing C# scripts for game logic
- Spawning and destroying objects dynamically
- Creating interactive UI with buttons
- Building and exporting your game
You’ve got all the core tools and concepts to start building bigger and cooler games from here.
🚀 Keep experimenting. Keep building. And most importantly — have fun doing it!