Creating an infinite 3D runner game in Unity (like Temple Run, Subway Surfers), part 2

This is the second part of  an infinite 3D runner game tutorial. If you haven’t read the first part, you’re highly encouraged to do it by clicking here. As always, you can find the source code here on GitHub and play the game here on a WebGL enabled browser.

The levels

In the second and last part of the tutorial, we’ll see how the two game levels were built. We’ll begin with the “straight paths” level.

Straight Paths Level

Let’s take a look at the objects contained in our game hierarchy.

GameObject hierarchy in the straight paths level.

First, there is the straight path game object which, by nature, is wider than the one in the “rotated paths” level. The path is a prefab (as designated by its blue color) and all subsequent paths will be clones of this one. Scene also contains some cubical shaped 3D objects (named Cubes), which act as floor of our path. It also contains the NewPathSpawn game object (a simple location in the 3D space), and some other locations called SpawnPoints. The SpawnPoints are positions in which new stuff will be generated. This stuff can be either a candy, or obstacle, or nothing at all.

Recall that Max runs from path to path. As we are not generating all paths at the beginning of the game (because we want to save on memory and mainly because we do not know how far in the game the player will proceed, i.e. how many paths to generate), we need a mechanism to generate the N+1 path, where N is the path that Max currently steps on. We’ve used a simple trigger BoxCollider to implement this. When Max collides with it, a new path is generated via the PathSpawnCollider script (described in a while). In the straight paths level, the new path is instantiated in the “NewPathSpawn” position, which conveniently happens to be positioned at the far end of the current path.

When the player collides with the PathSpawnCollider, a new path is generated on the NewPathSpawn position.
The NewPathSpawnPosition, where a new straight path will be instantiated.

The Character game object holds our Camera and the Max model. It also has a Character Controller component (which allows us to easily move our object) and a CharacterSidewaysMovement script (described below). Making the Character game object move will move both Camera and Max (since they are child game objects). This is the classic way of making 3rd person games, since in this way, camera will always be at the same position during the game, behind and a bit higher than Max.

The Canvas game object holds our simple UI (two Text objects) whereas the Directional Light is a simple light and the ScriptHolder game object holds our GameManager singleton script, which was described in the first part of the tutorial.


This script is used on both levels in order to generate the next path, following the one Max is currently running on.

public class PathSpawnCollider : MonoBehaviour {

    public float positionY = 0.81f;
    public Transform[] PathSpawnPoints;
    public GameObject Path;
    public GameObject DangerousBorder;

Script begins by declaring some public variables, to be set outside the script in the Unity editor.
– PositionY is used to properly place the redBorder on the Y axis
– The PathSpawnPoints array is used to host the locations that the next path and borders will be instantiated. In the “straight paths” level the array will have only one member (since we’ll only instantiate the next path) whereas in the “rotated paths” level the array will hold three locations, in one of which there will be the new path and in the rest two there will be the red borders that will kill Max upon collision
– The Path object holds the path prefab
– The DangerousBorder array holds the RedBorder prefab in the “rotated paths” level whereas it is null in the “straight path” level (where we do not need it)

  void OnTriggerEnter(Collider hit)
        //player has hit the collider
        if (hit.gameObject.tag == Constants.PlayerTag)
            //find whether the next path will be straight, left or right
            int randomSpawnPoint = Random.Range(0, PathSpawnPoints.Length);
            for (int i = 0; i < PathSpawnPoints.Length; i++)
                //instantiate the path, on the set rotation
                if (i == randomSpawnPoint)
                    Instantiate(Path, PathSpawnPoints[i].position, PathSpawnPoints[i].rotation);
                    //instantiate the border, but rotate it 90 degrees first
                    Vector3 rotation = PathSpawnPoints[i].rotation.eulerAngles;
                    rotation.y += 90;
                    Vector3 position = PathSpawnPoints[i].position;
                    position.y += positionY;
                    Instantiate(SpawnBorder, position, Quaternion.Euler(rotation));


When Max collides with the PathSpawnCollider game object, game engine randomly chooses whether the next path will be straight, left or right. In the case of the “straight paths” level, we have only one entry in the PathSpawnPoints array (which corresponds to the straight location), so randomSpawnPoint will be 0 and the next path will be instatiated at the straight location. In the “rotated paths” level, we instatiate the next path on the chosen location and we also instantiate the RedBorder prefabs on the other two locations, while we are rotating them by 90 degrees to make them fit properly.

Stuff Spawner

Stuff Spawner script is responsible of generating the candy and the obstacles in both levels. It begins by declaring some public variables that the developer can set in the Unity editor.

    //points where stuff will spawn :)
    public Transform[] StuffSpawnPoints;
    //meat gameobjects
    public GameObject[] Bonus;
    //obstacle gameobjects
    public GameObject[] Obstacles;

    public bool RandomX = false;
    public float minX = -2f, maxX = 2f;

– StuffSpawnPoints contains the locations of the stuff (either candy or obstacles) to be instantiated
– Bonus array contains the prefabs for the candy
– Obstacles array contains the prefabs for the obstacles
– RandomX variable is true only at the “straight paths” level. It allows the candy or obstacle prefab to be instantiated on a random X position (in the range of [minX, maxX] in order to have it on a different “lane”.

  void CreateObject(Vector3 position, GameObject prefab)
        if (RandomX) //true on the straight paths level, false on the rotated one
            position += new Vector3(Random.Range(minX, maxX), 0, 0);

        Instantiate(prefab, position, Quaternion.identity);

CreateObject is a simple method that will instantiate a new prefab at the selected position, taking into account whether it will be moved somewhere on the X axis (on the “straight paths” level).

 void Start()
        bool placeObstacle = Random.Range(0, 2) == 0; //50% chances
        int obstacleIndex = -1;
        if (placeObstacle)
            //select a random spawn point, apart from the first one
            //since we do not want an obstacle there
            obstacleIndex = Random.Range(1, StuffSpawnPoints.Length);

            CreateObject(StuffSpawnPoints[obstacleIndex].position, Obstacles[Random.Range(0, Obstacles.Length)]);

        for (int i = 0; i &amp;amp;amp;amp;amp;lt; StuffSpawnPoints.Length; i++)
            //don't instantiate if there's an obstacle
            if (i == obstacleIndex) continue;
            if (Random.Range(0, 3) == 0) //33% chances to create candy
                CreateObject(StuffSpawnPoints[i].position, Bonus[Random.Range(0, Bonus.Length)]);


The Start method
– Decides whether it will create an obstacle on this path (50% chance). If true, it creates one, randomly selecting one of the given prefabs.
– For all the other locations in the StuffSpawnPoints array, it decides whether to create a candy (33% chance). If true, then it creates one, randomly selected out of the Bonus array.

Bear in mind that


This script is responsible for moving Max onto the “straight paths” level. Remember that Max has a CharacterController component. Reader is encouraged to take a look at Unity’s documentation for some insights on how to move a game object with Character Controller component.

    private Vector3 moveDirection =;
    public float gravity = 20f;
    private CharacterController controller;
    private Animator anim;

    private bool isChangingLane = false;
    private Vector3 locationAfterChangingLane;
    //distance character will move sideways
    private Vector3 sidewaysMovementDistance = Vector3.right * 2f;

    public float SideWaysSpeed = 5.0f;

    public float JumpSpeed = 8.0f;
    public float Speed = 6.0f;
    //Max gameobject
    public Transform CharacterGO;

    IInputDetector inputDetector = null;

We declare some variables to assist with the script creation, their usage can be easily inferred by their name.

   void Start()
        moveDirection = transform.forward;
        moveDirection = transform.TransformDirection(moveDirection);
        moveDirection *= Speed;


        GameManager.Instance.GameState = GameState.Start;

        anim = CharacterGO.GetComponent();
        inputDetector = GetComponent();
        controller = GetComponent();

The Start method sets the moveDirection vector so that Max can move straight ahead for the given Speed. It also sets proper values to the component related variables and modifies the game state to start the game.

    private void CheckHeight()
        if (transform.position.y &lt; -10)

Max can jump so you can imagine that it is not that difficult for him to leave the designated paths and fall. CheckHeight method checks if Max has dropped at a certain level, so game is over.

    private void DetectJumpOrSwipeLeftRight()
        var inputDirection = inputDetector.DetectInputDirection();
        if (controller.isGrounded && inputDirection.HasValue && inputDirection == InputDirection.Top && !isChangingLane)
            moveDirection.y = JumpSpeed;
            anim.SetBool(Constants.AnimationJump, true);
            anim.SetBool(Constants.AnimationJump, false);

DetectJumpOrSwipeLeftRight method first checks whether player has swiped left, right or upwards (or has used left, right or up arrow keys). If player has swiped upwards, we have to check whether Max is touching the ground (we disallow double jumping) or is not currently changing lane (i.e. moving left or right). If Max is not already jumping or moving sideways, we set a motion on the Y axis and transition Max to the jump animation.

        if (controller.isGrounded && inputDirection.HasValue && !isChangingLane)
            isChangingLane = true;

            if (inputDirection == InputDirection.Left)
                locationAfterChangingLane = transform.position - sidewaysMovementDistance;
                moveDirection.x = -SideWaysSpeed;
            else if (inputDirection == InputDirection.Right)
                locationAfterChangingLane = transform.position + sidewaysMovementDistance;
                moveDirection.x = SideWaysSpeed;

Next, we see if the player has swiped left or right. If this happens, we move player left or right, provided Max is not currently jumping or already moving sideways. We do this by adding the SideWaysSpeed variable to the x axis of the moveDirection vector. We also save the final position of Max, the one he when the lane changing is finished.

Let’s now check the Update method. This method has a switch statement that handles the three different game states.

void Update()
        switch (GameManager.Instance.GameState)
            case GameState.Start:
                if (Input.GetMouseButtonUp(0))
                    anim.SetBool(Constants.AnimationStarted, true);
                    var instance = GameManager.Instance;
                    instance.GameState = GameState.Playing;


On the Start state, game checks if player has tapped on screen. When this happens, we transition Max to the run state (by setting the AnimationStarted variable to true) and transition the game state to Playing.

            case GameState.Playing:



                //apply gravity
                moveDirection.y -= gravity * Time.deltaTime;

                if (isChangingLane)
                    if (Mathf.Abs(transform.position.x - locationAfterChangingLane.x) < 0.1f)
                        isChangingLane = false;
                        moveDirection.x = 0;

                //move the player
                controller.Move(moveDirection * Time.deltaTime);


On the playing state, we first check Max’s current position in the Y axis. Then, we move Max left or right, or we make him jump, depending on user’s input (via DetectJumpLeftOrRightSwipe method). If Max is changing lane, we check his current X position compared to the final one. If the difference between them is (very) small, this means that Max has finished his movement so we can safely set the isChangingLane variable to false and preventing Max from moving more. Finally, we use the CharacterController’s Move method to move Max.

 case GameState.Dead:
                anim.SetBool(Constants.AnimationStarted, false);
                if (Input.GetMouseButtonUp(0))


The last part of Update method runs when Max has dies. There, we transition Max to the idle state by setting the AnimationStarted variable to false and we prompt the player to tap the screen in order to restart the level.

 public void OnControllerColliderHit(ControllerColliderHit hit)
        //if we hit the left or right border
        if(hit.gameObject.tag == Constants.WidePathBorderTag)
            isChangingLane = false;
            moveDirection.x = 0;

The OnControllerHit method is used in the “straight paths” level. The left and right borders of the path are tagged as WidePathBorder and when Max is touching them we consider that lane changing has finished. If we fail to do this, then Max continously tries to move towards the wall, resulting in a nasty bug.

Rotated paths level

Game objects hierarchy in the “rotated paths” level shares some similarities with the “straight paths” level. However, here we have a different path prefab (a narrower path), more NewPathSpawns and we also have a SwipeCollider.

Rotated paths level Hierarchy
PathSpawnCollider and SwipeCollider depicted. PathSpawnCollider triggers the creation of the next path and RedBorder prefabs whereas SwipeCollider is the only place where player is allowed to swipe. We also see the SpawnPoints, where candy or obstacles may be created.

When Max enters the PathSpawnCollider, a new path is instantiated in a random location, chosen from the three points in the NewPathSpawn array.

Left NewPathSpawnPoint
Right NewPathSpawnPoint
Straight NewPathSpawnPoint

Out of these three locations, one is randomly chosen for the next path location. In the two remaining locations, the game engine will instantiate RedBorder prefabs.

Stuff Spawner and Path Spawn Collider

These scripts are exactly the ones used in the “straight paths” level, so we will not repeat them here.

Swipe Collider

In the “rotated paths” level, there is a certain space in the path where player is allowed to swipe to move Max. This is because we do not want the player to mistakenly swipe when Max is in the middle of the path, this would create an unnecessary hurdle for our player. So, we have placed an invisible game object with a collider at the point where our player has to swipe for Max to continue on the path that follows after the current one. Hence, player is only allowed to swipe when Max is colliding with the Swipe Collider game object. Let’s see the collider’s code!

public class SwipeCollider : MonoBehaviour

    // Use this for initialization
    void OnTriggerEnter(Collider hit)
        if (hit.gameObject.tag == Constants.PlayerTag)
            GameManager.Instance.CanSwipe = true;

    void OnTriggerExit(Collider hit)
        if (hit.gameObject.tag == Constants.PlayerTag)
            GameManager.Instance.CanSwipe = false;

The SwipeCollider script basically sets a public variable on the GameManager object, which allows/disallows left/right swipe on Max, as already described. The OnTriggerEnter method is executed when Max enters the collider and the OnTriggerExit method is executed upon Max’s exit.

CharacterRotateMovement script

This script shares a lot of similarities with the CharacterSidewaysMovement script, described in the “straight paths” level. Main difference is in the DetectJumpOrSwipeLeftRight method, where Max needs to turn left or right, if the player has swiped (and is allowed to do so!). We use Quaternion.AngleAxis method to rotate Max.

if (GameManager.Instance.CanSwipe && inputDirection.HasValue &&
         controller.isGrounded && inputDirection == InputDirection.Right)
            transform.Rotate(0, 90, 0);
            moveDirection = Quaternion.AngleAxis(90, Vector3.up) * moveDirection;
            //allow the user to swipe once per swipe location
            GameManager.Instance.CanSwipe = false;
        else if (GameManager.Instance.CanSwipe && inputDirection.HasValue &&
         controller.isGrounded && inputDirection == InputDirection.Left)
            transform.Rotate(0, -90, 0);
            moveDirection = Quaternion.AngleAxis(-90, Vector3.up) * moveDirection;
            GameManager.Instance.CanSwipe = false;

Having seen both movement scripts, you may wonder why we did not use some OOP inheritance to create both classes. Well, we certainly could! However, I prefer it this way since it will be easier for you to pick just the one class you need in your game. If you create a game that features both movement mechanics though, I strongly encourage you to refactor to an inheritance model which will make your code more manageable.

Possible enhancements

Some enhancements that come to mind:

  • To save in memory and performance, we could use an Object Pooler class to help us with game object’s creation. We’ve made one in our Tower Defense tutorial, check it out here.
  • The “wide paths” level is pretty easy, should be more difficult. Consider increasing the appearance chances of the obstacles on this level.
  • Complete the animations on Max. The model has also a “death” animation, try adding it to the game.

Development IDE(s)

This tutorial was developed on Visual Studio/Windows (using the awesome Visual Studio Tools for Unity) and on Mac OSX, on the awesome and free Visual Studio Code editor which can be of great assistance when working with Unity projects.


Hope you enjoyed this tutorial! I tried to implement the logic behind a classic infinite runner game without wasting too much time on unnecessary stuff. Still, there is much room for improvement (like making the game harder!), feel free to improve it. Do not forget to check the source code here on GitHub and play the game here on a WebGL enabled browser.

18 thoughts on “Creating an infinite 3D runner game in Unity (like Temple Run, Subway Surfers), part 2

  1. Hi

    I’d firstly like to thank you for creating this blog. I’m a South African teenager and have learnt a lot from these tutorials. I’ve been all over the web but can’t find what I’m looking for. or rather wherever I find it, its not good quality or good enough for the game I made. I’d like to get a tutorial for creating a menu for a game. A good looking attractive eye-catching menu. I’ve found videos that don’t really help much. please help. and thanks again for all the work so far.


      • yes i am. I would like to get something like what you find on games like bubblewitch saga or Candy Crush. most of the videos I have found all basically give instructions for a menu consisting of a “Play” and “Quit” button. I need more. Stuff like “Settings”, “High Score”, “Help” ,”Connect Facebook”, “Leaderboard” and “Multiplayer”. I’d like to add colour and sound on the menu. That’s basically it.


      • Oh and one more thing, inspired by your match-3 game tutorial, it is possible to have a circular grid instead of your usual rectangular/square grid for the candies ? if so, how can it be implemented.


  2. Hey!
    First of all thanks for such an awesome tutorial!
    Just one request can you plz mail me all the scripts because it is quite difficult for me to understand where to put each scripts and so all.
    So please help me out!

    Liked by 1 person

  3. hi there, I got this errors – the namespace ‘global’:: already contains a definition for ‘constants’ and the namespace ‘global’:: already contains a definition for ‘obstacles


  4. IInputDetector doesn’t seem to work for me. What am I missing?
    I also had to change the “inputDetector = GetComponent();” to inputDetector = GetComponent();
    not sure if that’s right though.


  5. I figured out how to move left and right in the rotated levels as i only have the one but ive run into an even bigger problem. it’s been ongoing but i figured id cross that bridge when it came time. Anyways the problem im having is that when i spawn a path it spawns from it’s center point (the X,Y,Z axis navigation thing that is on all game objects). As a temporary fix I moved the spawn locations to the needed postions and removed the border prefabs because they’d just spawn out of place anyways and get in the way. But now after I get to the next path it spawns a path on the previous path.
    Start on Path_1 -> Hit the spawn box -> Spawn Path_2 -> Hit the spawn box -> Path_3 Spawns where Path_2 spawned.
    Im not sure how to fix this at all.
    Ive tried a great number of things, one of which was starting from scratch and making separate Right, Left and forward paths but that would have been a nightmare and It made me realize that rotating the One path would be the best option.
    Im lost and i could really use your help.


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s