1
Player Movement
PlayerMovement.cs
Scene Setup
- Create a Plane at (0, 0, 0).
- Create a Cube at (0, 0.5, 0).
- Add a Rigidbody component to the Cube (Add Component → Physics → Rigidbody).
- Create a new Material, pick a colour, and drag it onto the Cube.
- Create PlayerMovement.cs and attach it to the Cube.
Script
PlayerMovement.cs
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
[SerializeField] private float speed = 5f;
private Rigidbody rb;
void Awake()
{
rb = GetComponent<Rigidbody>();
}
void FixedUpdate()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(h, 0f, v);
rb.velocity = movement * speed;
}
}
Common mistakes
Using Update() instead of FixedUpdate() for Rigidbody movement — causes jittery physics. Forgetting to add the Rigidbody component before running — causes NullReferenceException on rb.velocity.
2
Collectible Pickup
PlayerCollector.cs
Scene Setup
- Create 4–5 Sphere objects. Scale to (0.5, 0.5, 0.5). Apply a yellow Material.
- On each Sphere's Sphere Collider, tick Is Trigger.
- Go to Edit → Project Settings → Tags and Layers. Add a new tag: Collectible.
- Select all Spheres and set their Tag to Collectible.
- Create PlayerCollector.cs and attach it to the player Cube (not the spheres).
Script
PlayerCollector.cs
using UnityEngine;
public class PlayerCollector : MonoBehaviour
{
private int collectCount = 0;
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Collectible"))
{
collectCount++;
Debug.Log("Collected: " + collectCount);
Destroy(other.gameObject);
}
}
}
Common mistakes
Writing Destroy(other) instead of Destroy(other.gameObject) — only removes the Collider component, leaving a visible sphere with no collider. Forgetting to tick Is Trigger — fires OnCollisionEnter instead, and the player bounces off. Forgetting to register the tag in Tag Manager — CompareTag throws an exception at runtime.
3
Enemy Spawner
EnemySpawner.cs
Scene Setup
- Create a Capsule. Apply a red Material.
- Drag the Capsule from Hierarchy into the Project window (Assets) to create a Prefab.
- Delete the Capsule from the scene.
- Create an empty GameObject named Spawner at (0, 0, 0).
- Create EnemySpawner.cs and attach it to Spawner.
- Drag the enemy Prefab from the Project window into the enemyPrefab slot in the Inspector.
Script
EnemySpawner.cs
using System.Collections;
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private GameObject enemyPrefab;
[SerializeField] private float spawnInterval = 2f;
[SerializeField] private float spawnRange = 5f;
void Start()
{
StartCoroutine(SpawnLoop());
}
IEnumerator SpawnLoop()
{
while (true)
{
float x = Random.Range(-spawnRange, spawnRange);
float z = Random.Range(-spawnRange, spawnRange);
Vector3 pos = new Vector3(x, 1f, z);
Instantiate(enemyPrefab, pos, Quaternion.identity);
yield return new WaitForSeconds(spawnInterval);
}
}
}
How the coroutine works
StartCoroutine() begins running SpawnLoop(). The while(true) loop spawns one enemy, then yield return new WaitForSeconds(2f) pauses only this coroutine for 2 seconds — the rest of the game keeps running. After 2 seconds, the coroutine resumes from where it paused and loops again.
Common mistakes
Forgetting using System.Collections; at the top — IEnumerator won't resolve. Forgetting to drag the Prefab into the Inspector slot — enemyPrefab is null and Instantiate throws an error. Using Y = 0 for the spawn position — the capsule spawns halfway through the ground (its pivot is at the centre, so Y should be 1 for a default capsule).
4
UI Score Display
ScoreManager.cs
Scene Setup
- Go to GameObject → UI → Text - TextMeshPro. (If prompted, click "Import TMP Essentials".)
- Unity auto-creates a Canvas and EventSystem.
- Select the Text object. In the RectTransform, click the anchor presets square (hold Alt) and choose top-left.
- Set Pos X to 20, Pos Y to -20. Set Width to 300, Height to 50.
- In the TextMeshPro component, set the text to Score: 0 and font size to 36.
- Create an empty GameObject named GameManager.
- Create ScoreManager.cs and attach it to GameManager.
- Drag the TextMeshPro object from the Hierarchy into the scoreText slot in the Inspector.
Script
ScoreManager.cs
using TMPro;
using UnityEngine;
public class ScoreManager : MonoBehaviour
{
[SerializeField] private TMP_Text scoreText;
private int score = 0;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
score++;
scoreText.text = "Score: " + score;
}
}
}
Common mistakes
Using GetKey instead of GetKeyDown — the score rockets up while Space is held because GetKey returns true every frame. Using Text (legacy) instead of TMP_Text — requires using UnityEngine.UI; instead of using TMPro; and is the older UI component. Forgetting to drag the text object into the Inspector — NullReferenceException on first Space press.
5
Countdown Timer + Game Over
GameTimer.cs
Scene Setup
- Create a UI → Text - TextMeshPro for the timer. Anchor to top-right. Set Pos X to -20, Pos Y to -20. Font size 36. Default text: 30.
- Create another UI → Text - TextMeshPro for the game over message. Anchor to centre. Font size 64. Set text to Time's Up!. Set alignment to centre.
- Disable the Game Over text object by unchecking its checkbox in the Inspector.
- Create GameTimer.cs and attach it to the GameManager object (or any object).
- Drag the timer text into the timerText slot and the Game Over GameObject into the gameOverUI slot in the Inspector.
Script
GameTimer.cs
using TMPro;
using UnityEngine;
public class GameTimer : MonoBehaviour
{
[SerializeField] private TMP_Text timerText;
[SerializeField] private GameObject gameOverUI;
[SerializeField] private float startTime = 30f;
private float timeRemaining;
private bool isGameOver = false;
void Start()
{
timeRemaining = startTime;
gameOverUI.SetActive(false);
}
void Update()
{
if (isGameOver) return;
timeRemaining -= Time.deltaTime;
if (timeRemaining <= 0f)
{
timeRemaining = 0f;
isGameOver = true;
gameOverUI.SetActive(true);
Time.timeScale = 0f;
}
int display = Mathf.CeilToInt(timeRemaining);
timerText.text = display.ToString();
}
}
Common mistakes
Forgetting to set the Game Over text to disabled before pressing Play — it's visible from the start. Using (int)timeRemaining instead of Mathf.CeilToInt() — truncation means the display jumps from 1 to 0 too early. Forgetting that Time.timeScale = 0 persists between Play sessions in the Editor — if you stop and re-enter Play mode, everything may still be frozen. Reset it in Start() to be safe.
Why Time.timeScale?
Setting Time.timeScale = 0f freezes all time-dependent systems: Update() still runs but Time.deltaTime becomes 0, physics stop, coroutines using WaitForSeconds pause, and animations freeze. This is the simplest way to implement a global game pause or game-over state.