Loading...

Unity Tutorial • Game Development • Mobile Games

How to Create an Endless Runner in Unity

A complete step-by-step guide to building your first endless runner game in Unity. Perfect for beginners looking to create engaging mobile games.

12 min read Cinetoon Studios

Why Build an Endless Runner?

Endless runners are fast to prototype, easy to extend, and packed with reusable systems—perfect for beginners or indie teams who want a polished release on mobile, PC, or WebGL. This step-by-step tutorial walks you through the exact process we use at Cinetoon Studios to ship production-ready runners.

You will end up with auto-forward movement, lane switching, procedural tiles, obstacle spawning, score tracking, difficulty ramps, and a clean UI—all built with beginner-friendly Unity and C#.

Project Setup Checklist

  • Unity 2021 LTS or newer (any recent version works).
  • Basic C# knowledge plus familiarity with the Unity editor.
  • 3D assets or primitives (Unity Asset Store has plenty of free packs).

Create a new 3D project named EndlessRunner. In the opening scene:

  • Add a Ground plane, test obstacles (cubes), and a Main Camera behind the player.
  • Create a capsule named Player with a Rigidbody and Capsule Collider. Disable gravity if you want floating movement, leave it on for jump-based runners.

Step 1: Auto Movement & Lane Switching

Attach PlayerController.cs to the Player object to handle auto forward motion and lane selection.

public class PlayerController : MonoBehaviour
{
    public float forwardSpeed = 10f;
    public float laneDistance = 3f;
    private int currentLane = 1; // 0 = left, 1 = center, 2 = right

    void Update()
    {
        transform.Translate(Vector3.forward * forwardSpeed * Time.deltaTime);

        if (Input.GetKeyDown(KeyCode.LeftArrow)) MoveLane(-1);
        if (Input.GetKeyDown(KeyCode.RightArrow)) MoveLane(1);
    }

    void MoveLane(int direction)
    {
        currentLane = Mathf.Clamp(currentLane + direction, 0, 2);
        Vector3 target = transform.position;
        target.x = (currentLane - 1) * laneDistance;
        transform.position = target;
    }
}

This foundation works with keyboard, swipe, or virtual buttons. Later you can swap the input layer without rewriting the movement logic.

Step 2: Infinite Track Generation

Create a 20-unit-long ground prefab populated with props or obstacles. Then use TileSpawner.cs to keep spawning new tiles ahead of the player while recycling old ones.

public class TileSpawner : MonoBehaviour
{
    public GameObject tilePrefab;
    public Transform player;
    public float tileLength = 20f;
    public int tilesOnScreen = 5;

    private readonly List<GameObject> tiles = new();
    private float spawnZ = 0f;

    void Start()
    {
        for (int i = 0; i < tilesOnScreen; i++) SpawnTile();
    }

    void Update()
    {
        if (player.position.z - 40 > spawnZ - tilesOnScreen * tileLength)
        {
            SpawnTile();
            DeleteTile();
        }
    }

    void SpawnTile()
    {
        var tile = Instantiate(tilePrefab, Vector3.forward * spawnZ, Quaternion.identity);
        tiles.Add(tile);
        spawnZ += tileLength;
    }

    void DeleteTile()
    {
        Destroy(tiles[0]);
        tiles.RemoveAt(0);
    }
}

This lightweight pooling pattern keeps draw calls low even on entry-level phones.

Step 3: Dynamic Obstacles

Inside each tile prefab, add empty child objects named SpawnPoints. Use the following script to randomly populate them with obstacles:

public class ObstacleSpawner : MonoBehaviour
{
    public GameObject obstaclePrefab;

    void Start()
    {
        foreach (Transform point in transform)
        {
            if (Random.value > 0.5f)
            {
                Instantiate(obstaclePrefab, point.position, point.rotation);
            }
        }
    }
}

Mix spawn probabilities, obstacle types, and even moving hazards to raise the skill ceiling.

Step 4: Collisions & Game Over Flow

Extend PlayerController with collision detection:

private void OnCollisionEnter(Collision collision)
{
    if (collision.gameObject.CompareTag("Obstacle"))
    {
        Time.timeScale = 0f;
        Debug.Log("GAME OVER!");
        // Trigger UI panel + restart button here.
    }
}

Pair this with an overlay UI that shows score, best distance, and CTA buttons for restart or home.

Step 5: Scoring & Difficulty Scaling

Track forward progress and display it in a HUD:

public class ScoreManager : MonoBehaviour
{
    public Text scoreText;
    public Transform player;

    void Update()
    {
        scoreText.text = Mathf.FloorToInt(player.position.z).ToString();
    }
}

Increase forwardSpeed gradually inside Update() to keep runs tense. You can also shorten tile lengths, spawn more hazards, or add moving obstacles as distance milestones are hit.

Step 6: UI, FX, and Mobile Export

  • UI: Build a start menu, pause screen, and game-over card with large tap targets.
  • Effects: Add particle bursts, motion blur, camera shake, and trail renderers for speed cues.
  • Audio: Layer running loops, swipe sounds, and impact effects.
  • Touch controls: Replace arrow keys with swipe gestures (left/right to lane swap, up for jump, down for slide).
  • Build settings: Switch to Android/iOS, set iconography, and test on physical hardware.

Final Tips for a Publishable Runner

  • Keep the core loop fun within the first five seconds.
  • Introduce new obstacles progressively; make death feel fair.
  • Use short daily missions or limited-time objectives to boost retention.
  • Profile frequently—object pooling, texture atlases, and Addressables keep mobile builds lean.
  • Ship updates often; community feedback is the fastest way to identify sticky features.

Endless runners are an ideal crash course in player movement, procedural content, scoring systems, and UI polish. Start small, iterate weekly, and you can transform this prototype into a commercial release.

Back to articles

Comments

0
  • Be the first to share feedback on this article.