Beginner Tutorial Mar 9, 2026

How to Build a Simple Endless Runner in Unity - Complete Beginner Tutorial

Learn how to build a simple endless runner in Unity step-by-step, from player movement and obstacle spawning to scoring, difficulty, and polish.

By GamineAI Team

Endless runners are one of the fastest ways to learn real, production-style game structure in Unity. You get movement, obstacles, scoring, difficulty, and juicy feedback — all without building a full story or level system first.

In this tutorial, you will build a simple endless runner from scratch in Unity. We will keep the art and code approachable so you can follow along even if this is one of your first Unity projects.

By the end, you will have:

  • A player that automatically runs forward and can jump or dodge.
  • Obstacles that spawn over time and move toward the player.
  • A scoring system that tracks distance or time survived.
  • Increasing difficulty as the run continues.
  • Simple polish like camera shake, particles, and sound hooks.

You can reuse this structure for your own mobile runner, an arcade minigame, or a prototype for a larger project.

Project Setup and Core Concept

Before writing any code, decide what kind of endless runner you want:

  • Side-scrolling 2D (like Jetpack Joyride, Subway Surfers in side view).
  • Forward-run 3D (like Temple Run).

For this tutorial, we will focus on a 2D side-scrolling endless runner because it is easier to understand visually and faster to prototype.

Create a New Unity Project

  1. Open Unity Hub and create a new 2D (URP optional) project.
  2. Name it EndlessRunnerTutorial.
  3. Once loaded, delete any sample objects from the default scene.
  4. Create a new scene called Game and save it.

Basic Scene Layout

You need three main things in the scene:

  • A ground the player can run on.
  • A player character with a collider and rigidbody.
  • A camera following the player or locked to a static view.

You can start with very simple placeholder art:

  • Ground: a stretched rectangle sprite.
  • Player: a colored square sprite.
  • Obstacles: rectangles or spikes later.

Keep the art simple at first. You can always swap it later.

Player Movement and Jumping

In a basic endless runner, the player usually runs automatically while you only control jumping, sliding, or lane switching.

We will:

  • Use a Rigidbody2D for physics.
  • Apply constant horizontal velocity.
  • Allow the player to jump when grounded.

Create the Player

  1. Create an empty GameObject called Player.
  2. Add a Sprite Renderer and assign a simple sprite (square).
  3. Add a Rigidbody2D.
    • Set Body Type to Dynamic.
    • Enable Freeze Rotation Z so the player does not tip over.
  4. Add a BoxCollider2D that slightly overlaps the sprite.

Player Controller Script

Create a new C# script called PlayerController and attach it to the Player.

Key responsibilities:

  • Constant horizontal movement.
  • Jump when the player taps or presses a key.
  • Detect if the player is grounded.

Here is a minimal example of what that script might look like:

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float moveSpeed = 5f;
    public float jumpForce = 7f;
    public LayerMask groundLayer;
    public Transform groundCheck;
    public float groundCheckRadius = 0.1f;

    private Rigidbody2D rb;

    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        // Constant horizontal velocity
        rb.velocity = new Vector2(moveSpeed, rb.velocity.y);

        // Jump input
        if (Input.GetButtonDown("Jump") && IsGrounded())
        {
            rb.velocity = new Vector2(rb.velocity.x, jumpForce);
        }
    }

    bool IsGrounded()
    {
        return Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
    }
}

To support this:

  • Create an empty child on the player called GroundCheck and place it slightly below the feet.
  • Assign groundCheck in the inspector.
  • Create a Ground object with a BoxCollider2D and mark it on a Ground layer.
  • Set the groundLayer field to that layer.

Now when you press Play, the player should automatically move to the right and jump when you press space or tap (depending on your input settings).

Building the Ground and Level Illusion

Endless runners often fake infinite ground instead of generating infinite level data.

Two simple options:

  1. Looping ground tiles that move left while the player stays near the center.
  2. Static ground with moving obstacles and a scrolling background.

For a beginner-friendly setup, we will:

  • Keep the camera fixed.
  • Move obstacles toward the player.
  • Use a long ground object that covers the camera width.

Simple Ground Setup

  1. Create a new GameObject called Ground.
  2. Add a Sprite Renderer with a simple rectangle sprite.
  3. Scale it so it spans the camera width.
  4. Add a BoxCollider2D and set it to not be a trigger.
  5. Put it on the Ground layer used by the PlayerController.

If the player ever runs out of visible ground, you can extend the sprite or create a second ground piece and move it ahead when it goes off-screen. That is a nice upgrade but not required for a first version.

Spawning Obstacles Over Time

Obstacles are what make the runner interesting. You want:

  • A prefab for a simple obstacle.
  • A spawner that periodically creates them.
  • A script that moves them left and destroys them when off-screen.

Create an Obstacle Prefab

  1. Create a new GameObject called Obstacle.
  2. Add a Sprite Renderer (spike, box, or pillar sprite).
  3. Add a BoxCollider2D.
  4. Add a script called Obstacle.
using UnityEngine;

public class Obstacle : MonoBehaviour
{
    public float moveSpeed = 5f;
    public float destroyX = -15f;

    void Update()
    {
        transform.Translate(Vector3.left * moveSpeed * Time.deltaTime);

        if (transform.position.x < destroyX)
        {
            Destroy(gameObject);
        }
    }
}
  1. Drag the Obstacle GameObject from the Hierarchy into a Prefabs folder to create a prefab.
  2. Delete the instance from the scene (you will spawn it via script).

Obstacle Spawner Script

Create an empty GameObject called ObstacleSpawner and place it off to the right side of the visible camera area.

Add a ObstacleSpawner script:

using UnityEngine;

public class ObstacleSpawner : MonoBehaviour
{
    public GameObject obstaclePrefab;
    public float minSpawnTime = 1.2f;
    public float maxSpawnTime = 2.5f;

    float timer;
    float nextSpawnTime;

    void Start()
    {
        ScheduleNextSpawn();
    }

    void Update()
    {
        timer += Time.deltaTime;

        if (timer >= nextSpawnTime)
        {
            SpawnObstacle();
            ScheduleNextSpawn();
        }
    }

    void SpawnObstacle()
    {
        Instantiate(obstaclePrefab, transform.position, Quaternion.identity);
    }

    void ScheduleNextSpawn()
    {
        timer = 0f;
        nextSpawnTime = Random.Range(minSpawnTime, maxSpawnTime);
    }
}

Assign the obstaclePrefab field to your Obstacle prefab.

Now when you press Play:

  • Obstacles spawn on the right.
  • Move toward the player.
  • Get destroyed off-screen to the left.

Detecting Collisions and Game Over

You need to stop the run when the player hits an obstacle.

Two common approaches:

  • Handle collision in the PlayerController.
  • Handle collision in the Obstacle script.

A simple option is to use OnTriggerEnter2D on the obstacle:

  1. Set the BoxCollider2D on the Obstacle prefab to Is Trigger.
  2. Ensure the player collider is not a trigger.

Update the Obstacle script:

void OnTriggerEnter2D(Collider2D other)
{
    if (other.CompareTag("Player"))
    {
        // Notify a GameManager that the run ended
        GameManager.Instance.GameOver();
    }
}

You also need a basic GameManager singleton to handle game state.

using UnityEngine;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    bool isGameOver;

    void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    public void GameOver()
    {
        if (isGameOver) return;
        isGameOver = true;

        // You can show a UI panel here instead of reloading
        Invoke(nameof(ReloadScene), 1f);
    }

    void ReloadScene()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }
}

Add this script to a GameManager object in your scene.

Scoring and Difficulty Ramp

Endless runners feel satisfying when:

  • Your score goes up continuously.
  • The game slowly gets harder.

Distance / Time Score

Create a ScoreManager script:

using UnityEngine;

public class ScoreManager : MonoBehaviour
{
    public static ScoreManager Instance { get; private set; }

    public int Score { get; private set; }
    public float pointsPerSecond = 10f;

    bool isRunning = true;

    void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    void Update()
    {
        if (!isRunning) return;

        Score += Mathf.RoundToInt(pointsPerSecond * Time.deltaTime);
    }

    public void Stop()
    {
        isRunning = false;
    }
}

You can show this score in a simple UI Text or TextMeshProUGUI element by reading ScoreManager.Instance.Score every frame.

When the game ends, call ScoreManager.Instance.Stop() and optionally show a “Best Score” using PlayerPrefs.

Increasing Difficulty Over Time

There are many ways to ramp difficulty:

  • Increase obstacle move speed.
  • Decrease spawn interval.
  • Introduce new obstacle types.

For a simple starting point, you can:

  • Increase the moveSpeed on obstacles slightly every 10–20 seconds.
  • Decrease minSpawnTime and maxSpawnTime gradually.

One quick way is to add a difficulty script that references your spawner and obstacle speed.

Polishing the Runner: Juice and Feedback

Even simple games feel much better with feedback. Some easy wins:

  • Camera shake on jump or collision.
  • Particles when landing or hitting an obstacle.
  • Sound effects for jump, hit, and score milestones.
  • UI feedback when your score crosses thresholds.

Ideas you can add quickly:

  • A tiny scale punch on the player when they jump.
  • Dust particles on landings.
  • A background that scrolls slower than the obstacles to fake parallax.

If you want more inspiration, check out your existing tutorials like How to Build a Simple Save/Load System in Unity for code structure patterns you can reuse.

Common Mistakes to Avoid

  • Overcomplicating art too early: gray boxes and simple shapes are fine until the core loop feels good.
  • Too steep difficulty ramp: if players die in the first 5 seconds every time, they will not learn the pattern.
  • No clear collision rules: make sure it is obvious what is safe vs dangerous.
  • No feedback: hits, jumps, and milestones should all feel noticeable.

Treat this first version as a learning project. Once the core loop works and feels fun, you can rebuild it cleaner or more stylish.

Where to Go Next

Once your basic endless runner works, here are natural next steps:

  • Add coins or pickups and a simple shop screen.
  • Implement a save system so best scores and currency persist (you can adapt patterns from your save/load tutorial).
  • Build a level-based runner by mixing endless sections with hand-crafted segments.
  • Port the project to mobile, add touch controls, and test performance.

Endless runners are perfect prototypes for testing new ideas: power-ups, enemy types, camera tricks, monetization, and more. Start small, get one polished run working, and then iterate.