Runner
a minimal side-scroller

Introduction
- generate a layered background;
- reuse objects;
- use simply physics;
- detect input to make the player jump;
- implement a power-up;
- write a small event manager;
- switch stuff on and off on demand;
- make a minimal GUI.
You're assumed to know your way around Unity's editor and know the basics of creating C# scripts. If you've completed the Clock tutorial you're good to go. The Graphs tutorial is useful too, but not necessary.
Game Design
For gameplay, we'll have a runner who dashes towards the right of the screen. The player needs to jump from platform to platform for as long as possible. These platforms can come in different flavors, slowing down or speeding up the runner. We'll also include a single power-up, which is a booster that allows mid-air jumps.
For graphics, we'll simply use cubes and standard particle systems. The cubes will be used for the runner, power-up, platforms, and a skyline background. We'll use particle systems to add a trail effect and lots of floating stuff to give a better sense of speed and depth.
There won't be any sound or music.
Setting the Scene
Our game is basically 2D, but we want to keep a little feeling of 3D. An orthographic camera doesn't allow for 3D, so we stick to a perspective camera. This way we can also get a multilayered scrolling background by simply placing stuff at various distances. Let's say the foreground is at depth 0 and we have a background layer at depth 50 and another one at depth 100. Let's place three cubes at these depths and use them as guides to construct the scene. I went ahead and picked a view angle and color setup, but you're free to experiment and choose whatever you like.
Add a directional light (GameObject / Create Other / Directional Light) with a rotation of (20, 330, 0). This gives us a light source that's shining over our right shoulder. Because it's a directional light its position doesn't matter.
Reduce the Field of View of the Main Camera to 30, position it at (5, 15, -40), and rotate it by (20, 0, 0). Also change its Background color to (115, 140, 220).


Create a material for each in the Project view via Create / Material, naming them Runner Mat and so on, then assign them to the cubes by dragging. I used default diffuse shaders with the colors white, (100, 120, 220), and (110, 140, 220).





Running
Create a new C# script called Runner inside the Runner folder and attach it to our Runner cube. Write the following code to make it move.
using UnityEngine;
public class Runner : MonoBehaviour {
void Update () {
transform.Translate(5f * Time.deltaTime, 0f, 0f);
}
}
Now Runner remains at a fixed position in our view and we can see that the close skyline cube appears to move faster than the one further away.

Generating a Skyline
Create a new C# script in the Skyline folder and name it SkylineManager. We will use it to create two managers, one for each of the skyline layers. At minimum, it needs to know which prefab to use to generate the skyline, so let's start by adding a public variable for that.
using UnityEngine;
public class SkylineManager : MonoBehaviour {
public Transform prefab;
}
Now turn both skyline cubes into prefabs by dragging them into the Skyline project folder or via Create / Prefab and then dragging onto that. Afterwards, delete both cubes from the Hierarchy. Now drag the Skyline Close prefab onto the Prefab field of our Skyline Close Manager.

numberOfCubes for that. To keep track of where the next cube
needs to spawn we'll use a private variable named nextPosition.
using UnityEngine;
public class SkylineManager : MonoBehaviour {
public Transform prefab;
public int numberOfObjects;
private Vector3 nextPosition;
void Start () {
nextPosition = transform.localPosition;
}
}
nextPosition by the width of
the object so they form an unbroken line.
using UnityEngine;
public class SkylineManager : MonoBehaviour {
public Transform prefab;
public int numberOfObjects;
private Vector3 nextPosition;
void Start () {
nextPosition = transform.localPosition;
for(int i = 0; i < numberOfObjects; i++){
Transform o = (Transform)Instantiate(prefab);
o.localPosition = nextPosition;
nextPosition.x += o.localScale.x;
}
}
}


distanceTraveled to
Runner and making sure that it's always up to date.
using UnityEngine;
public class Runner : MonoBehaviour {
public static float distanceTraveled;
void Update () {
transform.Translate(5f * Time.deltaTime, 0f, 0f);
distanceTraveled = transform.localPosition.x;
}
}
recycleOffset variable to configure how far behind Runner this
reuse should occur. A value of 60 seems to work well.
using UnityEngine;
using System.Collections.Generic;
public class SkylineManager : MonoBehaviour {
public Transform prefab;
public int numberOfObjects;
public float recycleOffset;
private Vector3 nextPosition;
private Queue<Transform> objectQueue;
void Start () {
objectQueue = new Queue<Transform>(numberOfObjects);
nextPosition = transform.localPosition;
for(int i = 0; i < numberOfObjects; i++){
Transform o = (Transform)Instantiate(prefab);
o.localPosition = nextPosition;
nextPosition.x += o.localScale.x;
objectQueue.Enqueue(o);
}
}
void Update () {
if(objectQueue.Peek().localPosition.x + recycleOffset < Runner.distanceTraveled){
Transform o = objectQueue.Dequeue();
o.localPosition = nextPosition;
nextPosition.x += o.localScale.x;
objectQueue.Enqueue(o);
}
}
}

First, consider that both initially placing and later on recycling a cube is basically doing the same
thing. Let's put this code in its own Recycle method and rewrite our Start
and Updatemethods to both use it.
using UnityEngine;
using System.Collections.Generic;
public class SkylineManager : MonoBehaviour {
public Transform prefab;
public int numberOfObjects;
public float recycleOffset;
private Vector3 nextPosition;
private Queue<Transform> objectQueue;
void Start () {
objectQueue = new Queue<Transform>(numberOfObjects);
for(int i = 0; i < numberOfObjects; i++){
objectQueue.Enqueue((Transform)Instantiate(prefab));
}
nextPosition = transform.localPosition;
for(int i = 0; i < numberOfObjects; i++){
Recycle();
}
}
void Update () {
if(objectQueue.Peek().localPosition.x + recycleOffset < Runner.distanceTraveled){
Recycle();
}
}
private void Recycle () {
Transform o = objectQueue.Dequeue();
o.localPosition = nextPosition;
nextPosition.x += o.localScale.x;
objectQueue.Enqueue(o);
}
}
using UnityEngine;
using System.Collections.Generic;
public class SkylineManager : MonoBehaviour {
public Transform prefab;
public int numberOfObjects;
public float recycleOffset;
public Vector3 minSize, maxSize;
private Vector3 nextPosition;
private Queue<Transform> objectQueue;
void Start () {
objectQueue = new Queue<Transform>(numberOfObjects);
for(int i = 0; i < numberOfObjects; i++){
objectQueue.Enqueue((Transform)Instantiate(prefab));
}
nextPosition = transform.localPosition;
for(int i = 0; i < numberOfObjects; i++){
Recycle();
}
}
void Update () {
if(objectQueue.Peek().localPosition.x + recycleOffset < Runner.distanceTraveled){
Recycle();
}
}
private void Recycle () {
Vector3 scale = new Vector3(
Random.Range(minSize.x, maxSize.x),
Random.Range(minSize.y, maxSize.y),
Random.Range(minSize.z, maxSize.z));
Vector3 position = nextPosition;
position.x += scale.x * 0.5f;
position.y += scale.y * 0.5f;
Transform o = objectQueue.Dequeue();
o.localScale = scale;
o.localPosition = position;
nextPosition.x += scale.x;
objectQueue.Enqueue(o);
}
}
Let's go ahead and add the second skyline layer as well. Duplicate Skyline Close Manager and change its name to Skyline Far Away Manager. Change its Prefab to the Skyline Far Away prefab. Set its position to (-100, -100, 100), its Recycle Offset to 75, its Min Size to (10, 50, 10), and its Max Size to (30, 100, 10). Of course you can use any values you like instead.




Generating Platforms
Create a new folder in the Project view named Platform. Create a new C# script in there called PlatformManager and copy the code from SkylineManager into it. Then change the code as shown below to make if conform to our needs.
using UnityEngine;
using System.Collections.Generic;
public class PlatformManager : MonoBehaviour {
public Transform prefab;
public int numberOfObjects;
public float recycleOffset;
public Vector3 minSize, maxSize, minGap, maxGap;
public float minY, maxY;
private Vector3 nextPosition;
private Queue<Transform> objectQueue;
void Start () {
objectQueue = new Queue<Transform>(numberOfObjects);
for(int i = 0; i < numberOfObjects; i++){
objectQueue.Enqueue((Transform)Instantiate(prefab));
}
nextPosition = transform.localPosition;
for(int i = 0; i < numberOfObjects; i++){
Recycle();
}
}
void Update () {
if(objectQueue.Peek().localPosition.x + recycleOffset < Runner.distanceTraveled){
Recycle();
}
}
private void Recycle () {
Vector3 scale = new Vector3(
Random.Range(minSize.x, maxSize.x),
Random.Range(minSize.y, maxSize.y),
Random.Range(minSize.z, maxSize.z));
Vector3 position = nextPosition;
position.x += scale.x * 0.5f;
position.y += scale.y * 0.5f;
Transform o = objectQueue.Dequeue();
o.localScale = scale;
o.localPosition = position;
objectQueue.Enqueue(o);
nextPosition += new Vector3(
Random.Range(minGap.x, maxGap.x) + scale.x,
Random.Range(minGap.y, maxGap.y),
Random.Range(minGap.z, maxGap.z));
if(nextPosition.y < minY){
nextPosition.y = minY + maxGap.y;
}
else if(nextPosition.y > maxY){
nextPosition.y = maxY - maxGap.y;
}
}
}





Jumping and Falling
As movement will be accomplished by gliding across the platforms, let's create a physic material (Create / Physic Material) with no friction whatsoever. Set all its fields to zero and both combine options to maximum. This way friction will be determined by whatever it's gliding across.
Name the new physic material Runner PMat, put it in the Runner folder, and assign it to the Material field of the Box Collider of Runner.
Reposition Runner to (0, 2, 0) so that it will begin by falling down on the first platform. Then try out play mode to see what happens!



Update
method. We should leave its movement to the physics engine and instead apply forces to it.
Remove the call to Translate from the Update method of Runner.
Instead, we'll use two of Unity's collision event methods – OnCollisionEnter and
OnCollisionExit – to detect when we touch or leave a platform.
As long as we're touching a platform, we apply an acceleration to make us run faster.
Let's make the acceleration configurable and set it to 5 in the editor.
using UnityEngine;
public class Runner : MonoBehaviour {
public static float distanceTraveled;
public float acceleration;
private bool touchingPlatform;
void Update () {
distanceTraveled = transform.localPosition.x;
}
void FixedUpdate () {
if(touchingPlatform){
rigidbody.AddForce(acceleration, 0f, 0f, ForceMode.Acceleration);
}
}
void OnCollisionEnter () {
touchingPlatform = true;
}
void OnCollisionExit () {
touchingPlatform = false;
}
}
Now our platforms provide a little friction, but Runner has a large enough acceleration pick up speed while moving across them.




Runner so we can configure its jump velocity. We'll use a vector
instead of just a float so we can profide both a vertical and horizontal component. Set the
corresponding field in the editor to (1, 7, 0).
We want Runner to jump only when it's touching a platform while the jump button is
pressed. Let's add code for this to the Update method.
using UnityEngine;
public class Runner : MonoBehaviour {
public static float distanceTraveled;
public float acceleration;
public Vector3 jumpVelocity;
private bool touchingPlatform;
void Update () {
if(touchingPlatform && Input.GetButtonDown("Jump")){
rigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange);
}
distanceTraveled = transform.localPosition.x;
}
void FixedUpdate () {
if(touchingPlatform){
rigidbody.AddForce(acceleration, 0f, 0f, ForceMode.Acceleration);
}
}
void OnCollisionEnter () {
touchingPlatform = true;
}
void OnCollisionExit () {
touchingPlatform = false;
}
}

(I've only included the Update method in the code below, nothing else changed.)
void Update () {
if(touchingPlatform && Input.GetButtonDown("Jump")){
rigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange);
touchingPlatform = false;
}
distanceTraveled = transform.localPosition.x;
}
Platform Variety
Duplicate Platform Regular PMat twice and name them Platform Slowdown PMat and Platform Speedup PMat. Also duplicate Platform Regular Mat twice and name them in a similar fashion. Set the friction values to 0.15 and 0, and their colors to (255, 255, 0) and (60, 130, 255), respectively.





PlatformManager so it will assign these materials. We'll add two
arrays for the materials and pick from them at random when recycling a platform.
using UnityEngine;
using System.Collections.Generic;
public class PlatformManager : MonoBehaviour {
public Transform prefab;
public int numberOfObjects;
public float recycleOffset;
public Vector3 minSize, maxSize, minGap, maxGap;
public float minY, maxY;
public Material[] materials;
public PhysicMaterial[] physicMaterials;
private Vector3 nextPosition;
private Queue<Transform> objectQueue;
void Start () {
objectQueue = new Queue<Transform>(numberOfObjects);
for(int i = 0; i < numberOfObjects; i++){
objectQueue.Enqueue((Transform)Instantiate(prefab));
}
nextPosition = transform.localPosition;
for(int i = 0; i < numberOfObjects; i++){
Recycle();
}
}
void Update () {
if(objectQueue.Peek().localPosition.x + recycleOffset < Runner.distanceTraveled){
Recycle();
}
}
private void Recycle () {
Vector3 scale = new Vector3(
Random.Range(minSize.x, maxSize.x),
Random.Range(minSize.y, maxSize.y),
Random.Range(minSize.z, maxSize.z));
Vector3 position = nextPosition;
position.x += scale.x * 0.5f;
position.y += scale.y * 0.5f;
Transform o = objectQueue.Dequeue();
o.localScale = scale;
o.localPosition = position;
int materialIndex = Random.Range(0, materials.Length);
o.renderer.material = materials[materialIndex];
o.collider.material = physicMaterials[materialIndex];
objectQueue.Enqueue(o);
nextPosition += new Vector3(
Random.Range(minGap.x, maxGap.x) + scale.x,
Random.Range(minGap.y, maxGap.y),
Random.Range(minGap.z, maxGap.z));
if(nextPosition.y < minY){
nextPosition.y = minY + maxGap.y;
}
else if(nextPosition.y > maxY){
nextPosition.y = maxY - maxGap.y;
}
}
}


Game Events
For this approach we can identify three events that might require objects to take action. The first,
game launch, is effectively handled by the Start methods. The other two, game start and
game over, require a custom approach. We will create a very simple event manager class to handle them.
Create a new folder named Managers and put a new C# script named
GameEventManager in it. We make GameEventManager a static class
that defines a GameEvent delegate type. Note that the manager isn't a
MonoBehaviour and won't be attached to any Unity object.

public static class GameEventManager {
public delegate void GameEvent();
}
gameEvent type to add two events to our manager,
GameStart and GameEnd. Now other scripts can subscribe to these events by
assigning methods to them, which will be called when the events are triggered.
public static class GameEventManager {
public delegate void GameEvent();
public static event GameEvent GameStart, GameOver;
}
null and the call will result in an error.
public static class GameEventManager {
public delegate void GameEvent();
public static event GameEvent GameStart, GameOver;
public static void TriggerGameStart(){
if(GameStart != null){
GameStart();
}
}
public static void TriggerGameOver(){
if(GameOver != null){
GameOver();
}
}
}
GUI and Game Start
Let's add some text labels to our scene. To keep things organized, we'll use a container object to group them, so create a new empty game object with position (0, 0, 0) and name it GUI. Create three empty child objects for it and give each a GUIText component via Component / Rendering / GUIText. Set their Anchor fields to middle center so their text gets centered on their position.
Name the first object Game Over Text, set its Text field to "GAME OVER", set its Font Size to 40, and set its Font Style to bold. Change its position to (0.5, 0.2, 0) so it ends up near the bottom center of the screen.
Name the second object Instructions Text, also bold but with a font size of 20, and set its text to "press Jump (x or space) to play". Change its position to (0.5, 0.1, 0), right below the game over text.
Name the third object Runner Text, with text "RUNNER", bold, and a font size of 60. It's position should be (0.5, 0.5, 0), right in the middle of the screen.
Now create a C# script named GUIManager in the Managers folder
and give it a GUIText variable for each text object we just made. Create a new object
named GUI Manager and assign the script as a component. Make it a child of
Managers. Then assign the text objects to the manager's corresponding fields.
using UnityEngine;
public class GUIManager : MonoBehaviour {
public GUIText gameOverText, instructionsText, runnerText;
}






Start method to our new manager and use it to disable gameOverText
so it won't be shown anymore. Also add an Update method that checks whether a jump button
was pressed, and if so triggers the game-start event.
using UnityEngine;
public class GUIManager : MonoBehaviour {
public GUIText gameOverText, instructionsText, runnerText;
void Start () {
gameOverText.enabled = false;
}
void Update () {
if(Input.GetButtonDown("Jump")){
GameEventManager.TriggerGameStart();
}
}
}
GameStart. We use this method to disable all text. We also disable the manager itself,
so its Update method will no longer be called. If we didn't, each time we jump there'd
be a new game-start event.
using UnityEngine;
public class GUIManager : MonoBehaviour {
public GUIText gameOverText, instructionsText, runnerText;
void Start () {
gameOverText.enabled = false;
}
void Update () {
if(Input.GetButtonDown("Jump")){
GameEventManager.TriggerGameStart();
}
}
private void GameStart () {
gameOverText.enabled = false;
instructionsText.enabled = false;
runnerText.enabled = false;
enabled = false;
}
}
GameStart method of
our manager object, whenever the game-start event is triggered. We do this by adding our method
to the event in the Start method.
void Start () {
GameEventManager.GameStart += GameStart;
gameOverText.enabled = false;
}
Game Over
using UnityEngine;
public class GUIManager : MonoBehaviour {
public GUIText gameOverText, instructionsText, runnerText;
void Start () {
GameEventManager.GameStart += GameStart;
GameEventManager.GameOver += GameOver;
gameOverText.enabled = false;
}
void Update () {
if(Input.GetButtonDown("Jump")){
GameEventManager.TriggerGameStart();
}
}
private void GameStart () {
gameOverText.enabled = false;
instructionsText.enabled = false;
runnerText.enabled = false;
enabled = false;
}
private void GameOver () {
gameOverText.enabled = true;
instructionsText.enabled = true;
enabled = true;
}
}
using UnityEngine;
public class Runner : MonoBehaviour {
public static float distanceTraveled;
public float acceleration;
public Vector3 jumpVelocity;
public float gameOverY;
private bool touchingPlatform;
void Update () {
if(touchingPlatform && Input.GetButtonDown("Jump")){
rigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange);
touchingPlatform = false;
}
distanceTraveled = transform.localPosition.x;
if(transform.localPosition.y < gameOverY){
GameEventManager.TriggerGameOver();
}
}
void FixedUpdate () {
if(touchingPlatform){
rigidbody.AddForce(acceleration, 0f, 0f, ForceMode.Acceleration);
}
}
void OnCollisionEnter () {
touchingPlatform = true;
}
void OnCollisionExit () {
touchingPlatform = false;
}
}

Using the Events
We want Runner to be hidden before the first game is started. We can do this
by simply deactivating it in its Start method and activating it when the game start
event is triggered. We'll also remember its starting position so we can reset it each game start.
Let's reset distanceTraveled too, so it's immediately up to date.
(I've omitted the contents of unmodified methods in the code below.)
using UnityEngine;
public class Runner : MonoBehaviour {
public static float distanceTraveled;
public float acceleration;
public Vector3 jumpVelocity;
public float gameOverY;
private bool touchingPlatform;
private Vector3 startPosition;
void Start () {
GameEventManager.GameStart += GameStart;
startPosition = transform.localPosition;
gameObject.active = false;
}
void Update () { … }
void FixedUpdate () { … }
void OnCollisionEnter () { … }
void OnCollisionExit () { … }
private void GameStart () {
distanceTraveled = 0f;
transform.localPosition = startPosition;
gameObject.active = true;
}
}
Update method running after the game ends, so we should disable the runner component
until a new game begins.
using UnityEngine;
public class Runner : MonoBehaviour {
public static float distanceTraveled;
public float acceleration;
public Vector3 jumpVelocity;
public float gameOverY;
private bool touchingPlatform;
private Vector3 startPosition;
void Start () {
GameEventManager.GameStart += GameStart;
GameEventManager.GameOver += GameOver;
startPosition = transform.localPosition;
gameObject.active = false;
}
void Update () { … }
void FixedUpdate () { … }
void OnCollisionEnter () { … }
void OnCollisionExit () { … }
private void GameStart () {
distanceTraveled = 0f;
transform.localPosition = startPosition;
rigidbody.isKinematic = false;
gameObject.active = true;
enabled = true;
}
private void GameOver () {
rigidbody.isKinematic = true;
enabled = false;
}
}
We can achieve this by initially instantiating the platforms somewhere far behind the camera and
moving the recyle loop to a new GameStart method.
using UnityEngine;
using System.Collections.Generic;
public class PlatformManager : MonoBehaviour {
public Transform prefab;
public int numberOfObjects;
public float recycleOffset;
public Vector3 minSize, maxSize, minGap, maxGap;
public float minY, maxY;
public Material[] materials;
public PhysicMaterial[] physicMaterials;
private Vector3 nextPosition;
private Queue<Transform> objectQueue;
void Start () {
GameEventManager.GameStart += GameStart;
GameEventManager.GameOver += GameOver;
objectQueue = new Queue<Transform>(numberOfObjects);
for(int i = 0; i < numberOfObjects; i++){
objectQueue.Enqueue((Transform)Instantiate
(prefab, new Vector3(0f, 0f, -100f), Quaternion.identity));
}
enabled = false;
}
void Update () { … }
private void Recycle () { … }
private void GameStart () {
nextPosition = transform.localPosition;
for(int i = 0; i < numberOfObjects; i++){
Recycle();
}
enabled = true;
}
private void GameOver () {
enabled = false;
}
}
SkylineManger the exact same treatment, so all parts of the game respond nicely
to our events.
using UnityEngine;
using System.Collections.Generic;
public class SkylineManager : MonoBehaviour {
public Transform prefab;
public int numberOfObjects;
public float recycleOffset;
public Vector3 minSize, maxSize;
private Vector3 nextPosition;
private Queue<Transform> objectQueue;
void Start () {
GameEventManager.GameStart += GameStart;
GameEventManager.GameOver += GameOver;
objectQueue = new Queue<Transform>(numberOfObjects);
for(int i = 0; i < numberOfObjects; i++){
objectQueue.Enqueue((Transform)Instantiate
(prefab, new Vector3(0f, 0f, -100f), Quaternion.identity));
}
enabled = false;
}
void Update () { … }
private void Recycle () { … }
private void GameStart () {
nextPosition = transform.localPosition;
for(int i = 0; i < numberOfObjects; i++){
Recycle();
}
enabled = true;
}
private void GameOver () {
enabled = false;
}
}


Power-Up
Create a new folder named Booster. In it, create a new material named Booster Mat. Because it's spinning, we'll use the Specular shader for the material, giving it a green (0, 255, 0) color and a white specular color.
Now create a new cube, name is Booster, and set its scale to 0.5 to make it small. To make it a bit easier to hit, increase its collider's size to 1.5, which ends up being 0.75 due to the scale. Then assign its material to it.
Mark the collider as a trigger, by checking its Is Trigger field. We do this because we want Runner to pass right through it, instead of colliding.


using UnityEngine;
public class Booster : MonoBehaviour {
public Vector3 offset, rotationVelocity;
public float recycleOffset, spawnChance;
}


SpawnIfAvailable to Booster for this. It requires a platform position so
we know where to place the booster.
using UnityEngine;
public class Booster : MonoBehaviour {
public Vector3 offset, rotationVelocity;
public float recycleOffset, spawnChance;
public void SpawnIfAvailable(Vector3 position){
}
}
PlatformManager to which we assign Booster.
Inside the Recycle method, we'll call its PlaceIfAvailable method after
we've determined the new platform's position.
using UnityEngine;
using System.Collections.Generic;
public class PlatformManager : MonoBehaviour {
public Transform prefab;
public int numberOfObjects;
public float recycleOffset;
public Vector3 minSize, maxSize, minGap, maxGap;
public float minY, maxY;
public Material[] materials;
public PhysicMaterial[] physicMaterials;
public Booster booster;
private Vector3 nextPosition;
private Queue<Transform> objectQueue;
void Start () { … }
void Update () { … }
private void Recycle () {
Vector3 scale = new Vector3(
Random.Range(minSize.x, maxSize.x),
Random.Range(minSize.y, maxSize.y),
Random.Range(minSize.z, maxSize.z));
Vector3 position = nextPosition;
position.x += scale.x * 0.5f;
position.y += scale.y * 0.5f;
booster.SpawnIfAvailable(position);
Transform o = objectQueue.Dequeue();
o.localScale = scale;
o.localPosition = position;
int materialIndex = Random.Range(0, materials.Length);
o.renderer.material = materials[materialIndex];
o.collider.material = physicMaterials[materialIndex];
objectQueue.Enqueue(o);
nextPosition += new Vector3(
Random.Range(minGap.x, maxGap.x) + scale.x,
Random.Range(minGap.y, maxGap.y),
Random.Range(minGap.z, maxGap.z));
if(nextPosition.y < minY){
nextPosition.y = minY + maxGap.y;
}
else if(nextPosition.y > maxY){
nextPosition.y = maxY - maxGap.y;
}
}
private void GameStart () { … }
private void GameOver () { … }
}

SpawnIfAvailable method so it activates and positions the
booster, but only if it's not already active, and also taking the spawn chance into account. Also,
to make this work the booster must begin deactivated and must also deactivate when the game ends.
using UnityEngine;
public class Booster : MonoBehaviour {
public Vector3 offset, rotationVelocity;
public float recycleOffset, spawnChance;
void Start () {
GameEventManager.GameOver += GameOver;
gameObject.active = false;
}
public void SpawnIfAvailable (Vector3 position) {
if(gameObject.active || spawnChance <= Random.Range(0f, 100f)) {
return;
}
transform.localPosition = position + offset;
gameObject.active = true;
}
private void GameOver () {
gameObject.active = false;
}
}
Update method to it. Recycling
is achieved by simple deactivation, as that makes it eligible for a respawn via
SpawnIfAvailable. Rotation is achieved by rotating based on the elapsed time since the
last frame.
using UnityEngine;
public class Booster : MonoBehaviour {
public Vector3 offset, rotationVelocity;
public float recycleOffset, spawnChance;
void Start () { … }
void Update () {
if(transform.localPosition.x + recycleOffset < Runner.distanceTraveled){
gameObject.active = false;
return;
}
transform.Rotate(rotationVelocity * Time.deltaTime);
}
public void SpawnIfAvailable (Vector3 position) { … }
private void GameOver () { … }
}

OnTriggerEnter to
Booster, which is called whenever something hits its trigger collider. Because we know
that the only thing that could possibly hit the booster is our runner, we can go ahead and give it
a new booster power-up whenever there's a trigger. Let's assume Runner has a static method
named AddBoost for this purpose, and use that. We also deactivate the booster, because
it's been consumed.
using UnityEngine;
public class Booster : MonoBehaviour {
public Vector3 offset, rotationVelocity;
public float recycleOffset, spawnChance;
void Start () { … }
void Update () { … }
void OnTriggerEnter () {
Runner.AddBoost();
gameObject.active = false;
}
public void SpawnIfAvailable (Vector3 position) { … }
private void GameOver () { … }
}
AddBoost method to Runner. To keep
things simple, let's just add a private static variable to remember how many boosts we have
accumulated.
using UnityEngine;
public class Runner : MonoBehaviour {
public static float distanceTraveled;
public float acceleration;
public Vector3 jumpVelocity;
public float gameOverY;
private bool touchingPlatform;
private Vector3 startPosition;
private static int boosts;
void Start () { … }
void Update () { … }
void FixedUpdate () { … }
void OnCollisionEnter () { … }
void OnCollisionExit () { … }
private void GameStart () {
boosts = 0;
distanceTraveled = 0f;
transform.localPosition = startPosition;
rigidbody.isKinematic = false;
gameObject.active = true;
enabled = true;
}
private void GameOver () { … }
public static void AddBoost(){
boosts += 1;
}
}
using UnityEngine;
public class Runner : MonoBehaviour {
public static float distanceTraveled;
public float acceleration;
public Vector3 boostVelocity, jumpVelocity;
public float gameOverY;
private bool touchingPlatform;
private Vector3 startPosition;
private static int boosts;
void Start () { … }
void Update () {
if(Input.GetButtonDown("Jump")){
if(touchingPlatform){
rigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange);
touchingPlatform = false;
}
else if(boosts > 0){
rigidbody.AddForce(boostVelocity, ForceMode.VelocityChange);
boosts -= 1;
}
}
distanceTraveled = transform.localPosition.x;
if(transform.localPosition.y < gameOverY){
GameEventManager.TriggerGameOver();
}
}
void FixedUpdate () { … }
void OnCollisionEnter () { … }
void OnCollisionExit () { … }
private void GameStart () { … }
private void GameOver () { … }
public static void AddBoost(){ … }
}

Informative GUI
Create a new object with a GUIText component as a child of GUI. Position it at (0.01, 0.99, 0), set its Anchor to upper left, give it font size 20 and a normal style. Name it Boosts Text.
Create another such object, naming it Distance Text. Set its position to (0.5, 0.99, 0), with font size 30 and bold style. Its Anchor should be set to upper center.
Add two variables to GUIManager for these new objects and assign them.
using UnityEngine;
public class GUIManager : MonoBehaviour {
public GUIText boostsText, distanceText, gameOverText, instructionsText, runnerText;
void Start () { … }
void Update () { … }
private void GameStart () { … }
private void GameOver () { … }
}




GUIManager which Runner can use to notify
the GUI of changes to its distance traveled and boost count. Because the manager needs to use
nonstatic variables in those methods, we add a static variable that references itself. That way the
static code can get to the component instance which actually has the gui text elements.
using UnityEngine;
public class GUIManager : MonoBehaviour {
public GUIText boostsText, distanceText, gameOverText, instructionsText, runnerText;
private static GUIManager instance;
void Start () {
instance = this;
GameEventManager.GameStart += GameStart;
GameEventManager.GameOver += GameOver;
gameOverText.enabled = false;
}
void Update () { … }
private void GameStart () {
gameOverText.enabled = false;
instructionsText.enabled = false;
runnerText.enabled = false;
enabled = false;
}
private void GameOver () { … }
public static void SetBoosts(int boosts){
instance.boostsText.text = boosts.ToString();
}
public static void SetDistance(float distance){
instance.distanceText.text = distance.ToString("f0");
}
}
Runner call those methods whenever its distance or amount
of boosts changes.
using UnityEngine;
public class Runner : MonoBehaviour {
public static float distanceTraveled;
public float acceleration;
public Vector3 boostVelocity, jumpVelocity;
public float gameOverY;
private bool touchingPlatform;
private Vector3 startPosition;
private static int boosts;
void Start () { … }
void Update () {
if(Input.GetButtonDown("Jump")){
if(touchingPlatform){
rigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange);
touchingPlatform = false;
}
else if(boosts > 0){
rigidbody.AddForce(boostVelocity, ForceMode.VelocityChange);
boosts -= 1;
GUIManager.SetBoosts(boosts);
}
}
distanceTraveled = transform.localPosition.x;
GUIManager.SetDistance(distanceTraveled);
if(transform.localPosition.y < gameOverY){
GameEventManager.TriggerGameOver();
}
}
void FixedUpdate () { … }
void OnCollisionEnter () { … }
void OnCollisionExit () { … }
private void GameStart () {
boosts = 0;
GUIManager.SetBoosts(boosts);
distanceTraveled = 0f;
GUIManager.SetDistance(distanceTraveled);
transform.localPosition = startPosition;
rigidbody.isKinematic = false;
gameObject.active = true;
enabled = true;
}
private void GameOver () { … }
public static void AddBoost(){
boosts += 1;
GUIManager.SetBoosts(boosts);
}
}

Particle Effects
Create a new a new particle system (GameObject / Create Other / Particle System) and make it a child of Runner with a position of (20, 0, 0) so it'll always stay to the right of the camera view. Set its Ellipsoid emitter to (10, 20, 20) so dust spawns in a large area. Set its Word Velocity to (-2, 0, 0) so the particles start with a velocity opposite to Runner and set Rnd Velocity to (2, 2, 2). The Emitter Velocity Scale should be 0 so movement of the emitter doesn't affect the initial velocity of the particles. To increase variety, set Min Size to 0.2, set Max Size to 0.8, increase Max Energy to 5, and decrease Min Emission to 10. Name the new object Dust Emitter.
Next, create another particle system, also a child of Runner, and name it Trail Emitter. We'll use this one for a condensation trail effect left behind by Runner. We'll use a Mesh Particle Emitter this time, so remove its Ellipsoid Particle Emitter component and then add a new one via Component / Particles / Mesh Particle Emitter. Give it a Min Size of 0.2, a Max Size of 0.4, a Min Energy of 1, a Max Energy of 2, and set its Min Emission to 10. Select a cube for its Mesh field (by clicking on the dot). Make sure that the object's position is (0, 0, 0), so it overlaps with the runner's cube.



The only thing that ParticleEmitterManager has to do is switch the particle emitters
on and off at the appropriate time. We'll use an array variable named emitters to hold
references to all emitters that need to be managed. In this case, that's the two emitters we just
created, but the manager can deal with any additional emitters you'd like to create.
Assign our two particle emitters by dragging them to the Emitters field.
using UnityEngine;
public class ParticleEmitterManager : MonoBehaviour {
public ParticleEmitter[] emitters;
void Start () {
GameEventManager.GameStart += GameStart;
GameEventManager.GameOver += GameOver;
GameOver();
}
private void GameStart () {
for(int i = 0; i < emitters.Length; i++){
emitters[i].ClearParticles();
emitters[i].emit = true;
}
}
private void GameOver () {
for(int i = 0; i < emitters.Length; i++){
emitters[i].emit = false;
}
}
}



Downloads
- runner.zip
- The finished project.
Questions & Answers
- What's a collider?
- A collider is a physics concept used by Unity's physics engine. They can function as either obstacles, causing collisions, or detectors, triggering events.
- Why remove the colliders?
- Because the cubes will be used for the noninteractive background only, nothing will ever collide with then. There's no point in the physics engine keeping track of their colliders, so we simply remove them.
- What's a material?
-
Materials are used to define the visual properties of objects. They can range from very simple,
like a constant color, to very complex.
Materials consist of a shader and whatever data the shader needs. Shaders are basically scripts that tell the graphics card how an object's polygons should be drawn.
The standard diffuse shader uses a single color and optionally a texture, along with the light sources in the scene, to determine the appearance of polygons.
- What about the fourth color component?
- Although colors have four components, I'm mentioning only three. The fourth component is the alpha value, which represents the opacity of the color. I assume this value is 255 by default, though it doesn't really matter as we won't create any materials that take alpha into account.
- What does it mean to be a child?
-
Beyond the obvious effect on the object hierarcy, being a child means that you are subject to
the Transform component of your parent. Your own transformation is
relative to your parent's. When it moves, so do you. When it rotates, you orbit around its
pivot. When it scales, both your size and your relative position scale as well.
From a low-level graphics point of view, the hierarchy corresponds to how the transformation matrix stack is created. The parent's matrix is pushed first, then the child's matrix.
- What's a prefab?
- A prefab is a Unity object – or hierarchy of objects – that doesn't exist in the scene and hasn't been activated. You use it as a template, creating clones of it and adding those to the scene.
- What does
Instantiatedo? -
The
Objectclass, which everyMonoBehaviourinherits from, contains the staticInstantiatemethod. This method creates a clone of whateverObjectinstance you pass to it. Optionally, you can supply a new position and rotation for the clone, otherwise it keeps the values of the original.Note that
Instantiatereturns anObjectreference. If you want to do something with the new clone, you have to cast it to the correct type.Typically, this method is used with prefabs, but you can also clone objects that already exist in the scene.
- What does
+=do? -
The code
x += y;adds x and y together and assigns the result back to x. You can consider it a short alternative for the codex = x + y;There are other operators that behave in a similar fashion, like
-=,*=, and/=. - Why is
distanceTraveledstatic? -
Because static variables exist independent of object instances, we can access it everywhere via
Runner.distanceTraveled. If it were nonstatic, we first need to get a reference to our runner instance before we could get todistanceTraveled.Of course, we could just add a
Runnervariable toSkylineManagerand assign Runner to it. However, this approach gets unwieldy when we'll need the value in multiple scripts later. - What's a
Queue? -
The
System.Collections.Genericnamespace contains theQueueclass, which can be used to represent a first in, first out queue. By constantly moving the first entry in the queue to the end of it, we effectively get a rotating ring.Queueis a generic class that can deal with any one type of content. In this case, we useQueue<Transform>to declare a queue ofTransformreferences.You can add to the end of the queue by using the
Enqueuemethod. Taking out the first item is done with theDequeuemethod. Additionaly, you can get to the first item without removing it via thePeekmethod. - What does
Random.Rangedo? -
Randomis a utility class that contains some stuff to create random values. ItsRangemethod can be used to generate a random value within some range.There are two versions of the
Rangemethod. You can call it with two floats, in which case it returns a float between the minimum and maximum value, both inclusive.Alternatively, you can call
Rangewith two integers, in which case it returns an integer between the minimum, inclusive, and maximum, exclusive. A typical use for this version is selecting an index at random, likesomeArray[Random.Range(0, someArray.Length)]. - What's a rigidbody?
-
A rigidbody is a physics concept, literally a rigid body that doesn't deform. Unity's physics
engine will simulate real-world physics behavior for all objects with
RigidBodycomponents, causing them to fall, move, and collide with other stuff.It is also possible to have soft bodies, which do deform, like cloth.
- What's a physic material?
- Physic materials are like regular materials, except they deal with collision instead of visual properties. When objects collide, what happens depends on whether they're made of stone, wood, ice, rubber, or some other substance. You use physic materials to simulate this behavior by configuring friction and bounciness.
- When is
FixedUpdatecalled? -
The physics engine works by dividing time into little discrete steps – by default 0.02
seconds – during which it moves objects and then checks for collisions and triggers. It
keeps doing that in a loop until it has caught up with real time.
The
FixedUpdatemethod works likeUpdate, except that it's called once per physics step instead of once per frame. In other words,FixedUpdateis independent of the frame rate. So if you want your code to be independent of the frame rate, put it in this method. - What does
AddForcedo? -
The
AddForcemethod applies a force to a rigidbody, which might result in an acceleration, which builds up velocity, which results in movement.There are actually various ways to use this method, which you control with the second parameter. For example, if you want to apply a specific acceleration, regardless of an object's mass, you can use the
ForceMode.Accelerationoption. If you want to directly adjust the velocity, you can useForceMode.VelocityChange. - What does
&&do? -
The
&&operator is used for boolean logic and stands for 'and also'. In other words,x && yis only true if bothxandyare true.Note that if
xis found to be false, there's no point in checkingyanymore. Ifywere a method call, it won't be invoked. Because of this, when Runner isn't touching the platform, the input won't be checked at all.The companion of
&&is the||operator, which stands for 'or else'. Sox || yis true if at least one of them is. Also, ifxis found to be true, thenywill not be considered. - What does
Input.GetButtonDowndo? -
Inputis a utility class that contains stuff to detect the player's input. This can be anything from button presses to mouse movement to joystick motion.The
GetButtonDownmethod can be used to check whether the user just pressed down a key associated with some button or action. Correspondingly, theGetButtonUpmethod can be used the check whether the user just released it. Also, theGetButtonmethod tells you whether the button is current held down. - Shouldn't the jump be in
FixedUpdate? -
When the player presses a jump button, we want the velocity change to happed exactly once.
For single instantaneous events, putting the code in
Updateis equivalent to writing code that would activate once in the nextFixedUpdate. - Why is the class static?
- By marking a class as static you require that its contents are static as well. There can't be any nonstatic variables or methods and it cannot be used to create object instances. In other words, a static class is not a blueprint for objects.
- What's a
delegate? -
Besides simple values and object references, you also store method references in a variable.
Such a variable is known as a delegate.
You define a delegate type as if you're creating a method, except there's no code body. After that, you can use this type to create a delegate variable, to which you can assign any method that matches the type. You can then treat this variable like a method. In fact, you can treat a delegate like a list and add multiple methods to it. All of them will be called when you invoke the variable.
The Graphs tutorial uses delegates to dynamically select what kind of graph to generate.
- What's an
event? -
For our purposes, an event is a restricted form of a delegate, forced to behalve like a list.
We could use a regular delegate variable instead and it would work just fine.
Both events and delegates allow methods to be added and removed from them, via
myEvent += myMethodandmyEvent -= myMethod. A delegate also allows a direct assignment, viamyDelegate = myMethod. Doing so replaces whatever other methods had been added to it before. We only want the former functionality and not the latter. By disallowing it altogether, we protect ourselves from a potentially hard to find bug caused by forgetting to write a single+somewhere. - What's
null? -
The default value of a variable that's not a simple value is
null. This means that the variable doesn't reference anything yet. Trying to invoke or access anything from a variable that'snullresults in an error. You can test for this value to make sure that doesn't happen. You can also set such a variable tonullyourself, in case you no longer need whatever it was referencing. - What does
!=do? -
The
!=operator checks whether two things are different. For example,1 != 2is true, while2 != 2is false. In our case, we're checking whether our event isn'tnull, which it would be if no methods had been added to it.In contrast, the
==operator checks whether two things are equal.Note that for object references, equality is usually a matter identity. Two different objects with the exact same contents are not considered equal.
- What's with the text positions?
- The GUI text is not drawn in 3D but in 2D, relative to the screen. A position of (0, 0) corresponds to the lower left corner, while (1, 1) corresponds to the top right corner.
- Why trigger and handle the same event?
-
If we trigger the game start event, why not simply put the disabling code right after the call
to
TriggerGameStart?Any code that deals with the game start has nothing to do with the
Updatemethod. Regardless how a game start is triggered, it should simply work. That's why we put the code in the appropriate event handler method. If we ever add another way to start a new game,GUIManagerwill respond to the event just fine. - Why immediately reset the distance?
-
Leaving it to the
Updatemethod to overridedistanceTraveledcould lead to bugs. For example, if Platform Manager happens to be updated before Runner, it would recycle based on the old distance. If this distance is far ahead, the first platform will be recycled immediately, causing Runner to plummet to its doom.There are ways to enforce the order in which components are updated, but it is better to guarantee correct results regardless of update order. If you provide public data, make sure it's always up to date.
- What does it mean to be kinematic?
- A kinematic rigidbody will not be moved by the physics engine. However, other things will still react to it appropriately. In a way, it's a physics object that defies the laws of physics.
- What's
Quaternion.identity? -
Quaternion.identityis a static property that corresponds to the identity quaternion, which results in no rotation. - Why a specular shader?
-
The default specular shader works like the diffuse shader, except that it also has a specular
color and a shininess value. The shader uses these to add a highlight to the visuals.
We use this shader for Booster because it results in more vivid color changes while it rotates.
- What does it mean to be a trigger?
-
By default, a collider acts like a solid object. You can use the
OnCollisionEntermethod to detect when something hits it.If a collider is a trigger, it's like a ghost and does not influence the movement of other physics object. Instead, it acts like a radar or alarm. You can use the
OnTriggerEntermethod to detect when something enters the collider's volume. - What does
returndo? -
You use the
returnkeyword to incidate that a method is finished. Implicitly, it's at the end of every method. You can use it to add multiple exit paths to a method.In our case, we check whether Booster shouldn't be spawned, either because it's already active or because of the spawn chance. If we shouldn't spawn, we simply return back to where the method was called.
In case a method produces some result – like a number, shown in the Graphs tutorial – you need to explicitly declare what result it returns.
- What's
Time.deltaTime? -
Timeis a utility class for time-related stuff. ItsdeltaTimeproperty contains the amount of seconds passed since the last frame, or since the last fixed time step is called insideFixedUpdate. - What's
this? -
The
thiskeyword is a reference to an object itself. As a consequence, it can only be used inside nonstatic methods.Whenever you're accessing a variable of an object inside one of its methods, you're implicitly using
thisto access it. For example, inside theUpdatemethod,transformis the same asthis.transform. - Why not set the labels from
Runner? -
By putting a manager in between Runner and the GUI, we make both independent
of each other. The
Runnerclass doesn't deal with GUI details, only with runner details.If we were to change the GUI – like using icons to display boosts instead of a label – we only need to modify
GUIManager, the rest of the game doesn't care about the change.We could go one step further and not make
Runnercall the GUI manager at all. Then it would be up toGUIManagerto get the boost count fromRunnerinstead. However, then the manager must know details about the runner, which it really shouldn't. Complete decoupling might be achieved by using an event for this, but that's a rather heavy-handed approach for a straightforward case like this. A simple call to a manager is fine. - Why call
GameOverinStart? -
Initially, we want our particle systems to not emit, until the first game-start event is triggered.
So we need to loop over all emitters and shut them off. Because that's the exact same thing that
our
GameOvermethod does, we call it instead of writing the code thing twice.