Runner , a minimal side-scroller

In this tutorial we'll create a very simple endless running game. You'll learn to generate a layered background;

reuse objects;

use simple 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'll learn to 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. Note that I will often omit chunks of code that have remained the same, only new code is shown. The context of the new code should be clear. This tutorial is quite old. I created it for Unity 3 and later updated it to Unity 4, but I won't update it to take advantage of the new featuers of Unity 5. I recommend you go through the Swirly Pipe tutorial instead, which is the spiritual successor of this one. Having said that, this tutorial still contains useful things that aren't mentioned in the new one.

You'll make a run for it.

Game Design

Before we get started, we should make some decisions about what we put in the game. We're going to make a very simple 2D side-scroller, but that's still very vague. Let's narrow it down a bit. 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

2 by 3 editor layout is a good one for this project, but you can use whatever layout you prefer. Let's make this game with a 16:10 display ratio in mind, so select this option in the Game view. Open a new project without any packages. The defaulteditor layout is a good one for this project, but you can use whatever layout you prefer. Let's make this game with a 16:10 display ratio in mind, so select this option in theview. 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 (120, 180, 250).

Light and camera.

GameObject / Create Other / Cube) with Z positions of 0, 50, and 100. Call them Runner, Skyline Close, and Skyline Far Away respectively. Remove the collider from both skyline cubes, because we won't be needing those. Create three cubes () with Z positions of 0, 50, and 100. Call them, andrespectively. Remove the collider from both skyline cubes, because we won't be needing those. 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).



The three cube configurations.

To keep things organized, create a Runner and a Skyline folder in the Project view via Create / Folder and put the materials in there.

Hierarchy, project, and game views.

Running

Runner to move to the right. So far it doesn't look like much and nothing's happening yet, but that will change. Let's start by creating a mock-up of the game in action by instructing theto move to the right. 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); } }

Main Camera onto Runner so is becomes a child of it. While it's not much, it already shows us a problem when entering play mode. The camera does not follow the cube. To fix this, dragontoso is becomes a child of it. 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.

Setup for moving camera.

Generating a Skyline

Now that we have rudimentary movement, let's generate a row of cubes to construct an endless skyline. The first thing we should realize is that only the visible part of the skyline needs to exist. As soon as a cube falls off the left side of the screen, it can be destroyed. Or better yet, it can be reused to build the next part of the skyline that's about to enter view. We can program this behaviour by generating a queue of cubes and constantly moving the front cube to the back as soon as it's no longer visible. 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; }

GameObject / Create Empty) named Managers which we'll use as a container for all of our manager objects. Create another empty object named Skyline Close Manager, make it a child of Managers, and create a SkylineManager component for it by dragging the script on it. To keep things organized, create a new empty object () namedwhich we'll use as a container for all of our manager objects. Create another empty object named, make it a child of, and create acomponent for it by dragging the script on it. 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.

Manager and prefabs.

We need a starting point from where we'll begin spawning cubes, so let's add a startPosition variable. We also need to determine how many cubes we need to spawn to fill the screen. Let's simply use a variable named numberOfCubes for that. To keep track of where the next cube needs to spawn we'll use a private variable named nextPosition.

public Transform prefab; public int numberOfObjects; public Vector3 startPosition; private Vector3 nextPosition; void Start () { nextPosition = startPosition; }

The next step is spawning the initial row of cubes. We'll use a simple loop for that, instantiating new objects, setting their position, and advancing nextPosition by the width of the object so they form an unbroken line. Because we're using unit cubes, their with is equal to their localScale.x .

void Start () { nextPosition = startPosition; for (int i = 0; i < numberOfObjects; i++) { Transform o = (Transform)Instantiate(prefab); o.localPosition = nextPosition; nextPosition.x += o.localScale.x; } }

Now set Start Position to (0, -1, 0) and set Number of Objects to 10. When entering play mode, we'll see a short row of cubes appear below Runner. However, once the cubes move out of view we'll never see them again.

Instantiating a skyline.

The idea is that we'll recyle objects once Runner has moved past them by some distance. For this to work, the manager must know how far Runner has traveled. We can provide for this by adding a static variable named 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; } }

Now we'll store our skyline objects in a queue and keep checking whether the first object in it should be recycled. If so, we'll reposition it and move it to the back of the queue. Let's use a recycleOffset variable to configure how far behind Runner this reuse should occur. Set it to 10 for now.

using UnityEngine; using System.Collections.Generic; public class SkylineManager : MonoBehaviour { public Transform prefab; public int numberOfObjects; public float recycleOffset; public Vector3 startPosition; private Vector3 nextPosition; private Queue<Transform> objectQueue; void Start () { objectQueue = new Queue<Transform>(numberOfObjects); nextPosition = startPosition; 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); } } }

Recycling skyline configuration.

This works! After entering play mode, you'll see the cubes reposition themselves. However, it doesn't look much like a skyline yet. To make it more like a real irregular skyline, let's randomly scale the cubes whenever they're placed or recycled. First, consider that both initially placing and later recycling a cube is basically doing the same thing. Let's put this code in its own Recycle method and rewrite our Start and Update methods to both use it.

void Start () { objectQueue = new Queue<Transform>(numberOfObjects); for (int i = 0; i < numberOfObjects; i++) { objectQueue.Enqueue( (Transform)Instantiate(prefab) ); } nextPosition = startPosition; 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); }

Next, we'll introduce two variables to configure the maximum and minimum allowed size and use them to randomly scale our objects. After picking a scale, we'll make sure to position the object so they're all aligned at the bottom. For that we'll need to offset by half their size, because the objects are centered around their position.

public Vector3 minSize, maxSize; 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); }

Start Position to (-60, -60, 50), set Min Size to (10, 20, 10), set Max Size to (30, 60, 10), and set Recycle Offset to 60. To get a nice skyline effect, setto (-60, -60, 50), setto (10, 20, 10), setto (30, 60, 10), and setto 60. 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 Start 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.

The complete skyline.

Generating Platforms

Adding platforms to the game is basically doing the same thing as generating a skyline, with only a few differences. The elevation of the platforms needs to change at random and there need to be gaps between them. Also, we want to constrain the elevation of the platforms to make sure our skyline remains properly in view. If a platform is placed outside this range, we should bounce it back. 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 startPosition; 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 = startPosition; 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; } } }

Now let's make a prefab for the platforms, along with a material for it. You can do this by duplicating one of the skyline materials and changing its color to (255, 60, 255). Call it Platform Regular Mat. Then create a new cube, assign the material to it, and turn it into a prefab called Platform. Put them both in the Platform folder.

Platform prefab.

Also create a new empty object named Platform Manager and make it a child of Managers. Give it a Platform Manager component by draggin the script onto it. Assign our new Platform prefab to its Prefab field. Set its Number Of Objects to 6, its Recycle Offset to 20, its Start Position to (0, 0, 0), its Min Size to (5, 1, 1), and its Max Size to (10, 1, 1). Then set the new fields Min Gap, Max Gap, Min Y, and Max Y to (2, -1.5, 0), (4, 1.5, 0), -5, and 10 respectively. Once again, you can experiment with different settings.

Platform manager.

Jumping and Falling

Rigidbody component to Runner via Component / Physics / Rigidbody. We don't want it to rotate or disappear into the distance, so constrain it by freezing its Z position and all rotation axes. Now that we have platforms, it's time to upgrade our runner. We'll use Unity's physics engine to make it jump, fall, and collide with the platforms, so add acomponent tovia. We don't want it to rotate or disappear into the distance, so constrain it by freezing its Z position and all rotation axes. 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!

Runner with physics.

Runner falls on the platform and then moves to the right. It even falls again after it moves past the platform. But when it happens to collide with the side of the next platform it behaves a bit weird. This is because we're still changing its position in an Update method. We should leave its movement to the physics engine and instead apply forces to it. Sofalls on the platform and then moves to the right. It even falls again after it moves past the platform. But when it happens to collide with the side of the next platform it behaves a bit weird. This is because we're still changing its position in anmethod. 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; } }

Runner PMat, rename it to Platform Regular PMat and move it to the Platform folder. Set both friction fields to 0.05 and drag the physic material onto the Platform prefab. One more thing we need to do before this works is assign a physic material to out platforms. Duplicate, rename it toand move it to thefolder. Set both friction fields to 0.05 and drag the physic material onto theprefab. Now our platforms provide a little friction, but Runner has a large enough acceleration pick up speed while moving across them.

Acceleration and regular platform.

To make Runner jump, we need to detect the player's input. Unity's default settings for the jump action, found under Edit / Project Settings / Input, is to be triggered by pressing the space bar. We'll use that, and also configure 'x' as an alternative by putting it in the Alt Positive Button fied.

Jump input configuration.

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'll add a variable toso 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.

public Vector3 jumpVelocity; void Update () { if(touchingPlatform && Input.GetButtonDown("Jump")){ rigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange); } distanceTraveled = transform.localPosition.x; }

Runner jumping.

Now we can jump! However, if Runner hits a platform from the side, we can perform multiple jumps while it's still touching the platform, launching ourselves out of the gap. To prevent this, we'll decree that once jumped we are no longer touching the platform, even if we really are. This allows for one jump after colliding, which usually isn't enough to escape from a gap.

void Update () { if(touchingPlatform && Input.GetButtonDown("Jump")){ rigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange); touchingPlatform = false; } distanceTraveled = transform.localPosition.x; }

Platform Variety

Runner down a bit, while the other speeds it up. We'll accomplish this by adding two physic materials, accompanied by two new colors, and pick which to use per platform at random. To spice things up, let's add two new platform types. One slowsdown a bit, while the other speeds it up. We'll accomplish this by adding two physic materials, accompanied by two new colors, and pick which to use per platform at random. 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.





Slowdown and speedup platforms.

We now have to modify 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.

public Material[] materials; public PhysicMaterial[] physicMaterials; 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; } } }

Now it's a matter of assigning stuff to the arrays, either by dragging or by setting the array's size and using the dots. Make sure that both arrays are ordered the same way. Furthermore, if we'd like some platform types to be more common than others, simply include them multiple times. By including the regular materials twice and the others just once, the regular option has a 50% chance of occurring, while the others have a 25% chance each.

Platform variety.

Game Events

Right now, the game starts as soon as we enter play mode and it doesn't end at all. What we want instead is that the game begins with a title screen and ends with a game over notice, where pressing one of the jump buttons starts a new game. 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 inside it. Note that the manager isn't a MonoBehaviour and won't be attached to any Unity object.

Game event manager.

public static class GameEventManager { public delegate void GameEvent(); }

Next, we use the new 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; }

Finally, we need to include a way to trigger these events. We'll add two methods for this. Care should be taken to only call an event if anyone is subscribed to it, otherwise it will be 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

Now that we have a game start event, let's create a GUI and a manager that uses it. 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), just 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; }

GUI Text.

Now add a 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.

void Start () { gameOverText.enabled = false; } void Update () { if(Input.GetButtonDown("Jump")){ GameEventManager.TriggerGameStart(); } }

Now it's time to include a method to handle our game-start event, let's appropriately name it 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.

private void GameStart () { gameOverText.enabled = false; instructionsText.enabled = false; runnerText.enabled = false; enabled = false; }

The last step is informing our event manager that it should call the 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

Let's also add a handler for the game-over event to our gui manager. We go about the same way as for the game start event, but in this case we need to enable the manager again, along with the instructions and game over text.

void Start () { GameEventManager.GameStart += GameStart; GameEventManager.GameOver += GameOver; gameOverText.enabled = false; } private void GameOver () { gameOverText.enabled = true; instructionsText.enabled = true; enabled = true; }

The game over event should be triggered whenever Runner falls below the platforms. We'll simply add a Game Over Y field to Runner with a value of -6, then check each update whether we dropped below it. If so, we trigger the game over event.

public float gameOverY; 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(); } }

Game over threshold.

Using the Events

Runner to take them into account. Now that our game events are triggered correctly, it's time forto take them into account. We want Runner to be disabled before the first game is started, though we want the camera inside of it to stay active. Disabling the runner means we have to deactivate its renderer and the runner component itself. We also switch its rigidbody to kinematic mode to freeze it in place. We can do this in its Start method, then undo this change when the game-start event is triggered, and then redo it when the game-over 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.

private Vector3 startPosition; void Start () { GameEventManager.GameStart += GameStart; GameEventManager.GameOver += GameOver; startPosition = transform.localPosition; renderer.enabled = false; rigidbody.isKinematic = true; enabled = false; } private void GameStart () { distanceTraveled = 0f; transform.localPosition = startPosition; renderer.enabled = true; rigidbody.isKinematic = false; enabled = true; } private void GameOver () { renderer.enabled = false; rigidbody.isKinematic = true; enabled = false; }

Runner reacts properly to the game events, but once the first platform has been recycled all new games will be over rather quickly, as Runner immediately plummets. Let's modify our platform manager so it reacts to the events as well. It should only be enabled when a game is in progress and the platforms should only become visible after the first game start event has been triggered. Nowreacts properly to the game events, but once the first platform has been recycled all new games will be over rather quickly, asimmediately plummets. Let's modify our platform manager so it reacts to the events as well. It should only be enabled when a game is in progress and the platforms should only become visible after the first game start event has been triggered. We can achieve this by having PlatformManager initially place the platforms somewhere far behind the camera and relocating its recycle loop to a new GameStart method.

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; } private void GameStart () { nextPosition = startPosition; for(int i = 0; i < numberOfObjects; i++){ Recycle(); } enabled = true; } private void GameOver () { enabled = false; }

Now give SkylineManager the exact same treatment, so all parts of the game respond nicely to our events.

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; } private void GameStart () { nextPosition = startPosition; for(int i = 0; i < numberOfObjects; i++){ Recycle(); } enabled = true; } private void GameOver () { enabled = false; }

Game start and game over.

Power-Up

Let's include a power-up that allows for mid-air boosts. We'll make it a spinning cube that appears above platforms at random. We decide to have at most one such booster cube active in the scene at any moment, so we can suffice with one instance and reuse it. 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.

Booster cube.

Now create a new C# script named Booster in the corresponding folder and assign it to the Booster object. We start by giving it four public variables used to configure it. First, we need an offset from the platform's center to place the booster. Let's set it to (0, 2.5, 0). Second, we need a rotation velocity to make it spin. Let's use (45, 90, 1) to make it a bit lively. Third, we need a recycle offset, just as for platforms, in case Runner misses the power-up. Let's use a distance of 20. Fourth, we include a spawn chance to make the appearance of the booster somewhat unpredictable. A 25% chance per platform is fine.

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

Booster configuration.

One way to make the spawning work is by requesting a booster placement each time a platform is recycled. Then it's up to the booster itself whether it'll be placed. We'll add a method named SpawnIfAvailable to Booster for this. It requires a platform position so we know where to place the booster. We leave it empty for now.

public void SpawnIfAvailable(Vector3 position){ }

We then add a variable to 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.

public Booster booster; 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; } }

Platform manager knows about booster.

Now that everything is connected, we need to update the 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 Booster must begin deactivated and must also deactivate when the game ends.

void Start () { GameEventManager.GameOver += GameOver; gameObject.SetActive(false); } public void SpawnIfAvailable (Vector3 position) { if(gameObject.activeSelf || spawnChance <= Random.Range(0f, 100f)) { return; } transform.localPosition = position + offset; gameObject.SetActive(true); } private void GameOver () { gameObject.SetActive(false); }

To make the booster spin and recycle, we have to add an 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.

void Update () { if(transform.localPosition.x + recycleOffset < Runner.distanceTraveled){ gameObject.SetActive(false); return; } transform.Rotate(rotationVelocity * Time.deltaTime); }

Rotating booster.

At the moment Runner passed right through Booster, nothing happened. To change this, we add the Unity event method 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.

void OnTriggerEnter () { Runner.AddBoost(); gameObject.SetActive(false); }

To make this work, we have to add an AddBoost method to Runner . To keep things simple, let's just add a private static variable to remember how many boosts we have accumulated.

private static int boosts; private void GameStart () { boosts = 0; distanceTraveled = 0f; transform.localPosition = startPosition; renderer.enabled = true; rigidbody.isKinematic = false; enabled = true; } public static void AddBoost () { boosts += 1; }

To actually allow mid-air jumps by consuming boosts, we need to modify the code that checks whether a jump is possible. Let's define a seperate boost velocity as well and set it to (10, 10, 0) for a nice boost.

public Vector3 boostVelocity, jumpVelocity; 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(); } }

Boost velocity.

Informative GUI

Runner can boost itself while in flight. It would be useful to actually see how many boost are available, so let's add a display for it to the GUI. While we're at it, let's show the distance traveled so far as well. It works! As long as we have boosts remaining,can boost itself while in flight. It would be useful to actually see how many boost are available, so let's add a display for it to the GUI. While we're at it, let's show the distance traveled so far as well. 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.

public GUIText boostsText, distanceText, gameOverText, instructionsText, runnerText;



Boosts and distance.

Let's add two static methods to 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.

private static GUIManager instance; void Start () { instance = this; GameEventManager.GameStart += GameStart; GameEventManager.GameOver += GameOver; gameOverText.enabled = false; } public static void SetBoosts(int boosts){ instance.boostsText.text = boosts.ToString(); } public static void SetDistance(float distance){ instance.distanceText.text = distance.ToString("f0"); }

Now all we need to do is let Runner call those methods whenever its distance or amount of boosts changes.

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(); } } private void GameStart () { boosts = 0; GUIManager.SetBoosts(boosts); distanceTraveled = 0f; GUIManager.SetDistance(distanceTraveled); transform.localPosition = startPosition; renderer.enabled = true; rigidbody.isKinematic = false; enabled = true; } public static void AddBoost(){ boosts += 1; GUIManager.SetBoosts(boosts); }

Complete GUI.

Particle Effects

By now we have a functional game, but it feels a bit empty. Let's add some dust particles to fill the empty space and enhance the sense of depth and speed. Create a new a new particle system (GameObject / Create Other / Particle System) named Dust Emitter. Make it a child of Runner with a position of (25, 0, 0) and reset its rotation, so it'll always stay to the right of the camera view. Set Start Lifetime to Random Between Two Constants with values 6 and 10, and set Start Speed to 0 so we get stationary particles with varied lifetimes to start with. Also set Simulation Space to World so the particles don't move with Runner. To increase variety, set Start Size to Random Between Two Constants with values 0.2 and 0.8. Change the shape to a box with dimensions (1, 30, 10) so we get a large spawning area, and increase the Rate of Emission to 20. Activate Velocity Over Lifetime, set it to use world space and a random range between two constants, using the vectors (-1, -1, 0) and (-4, 1, 0). This way the particles have some individual movement. Finally activate Color over Lifetime and change to gradient so it has an alpha value of 0 at 100%. This adds some fading to the particles. Next, duplicate this particle system, keep it a child of Runner, reset its position, and name it Trail Emitter. We'll use this one for a condensation trail effect left behind by Runner. Change its Shape to Mesh and set it to a cube (by clicking on the dot), and deactivate Velocity over Lifetime. Decrease Start Lifetime to between 1 and 2 and Start Size to between 0.2 and 0.4, to keep the trail subtle and short.

Particle systems.

Managers folder and name it ParticleSystemManager. Also create an appropriately named child object for Managers and assign the manager script to it. As we only want to spawn particles when a game is in progress, we'll create a manager for them. Add a new C# script in thefolder and name it. Also create an appropriately named child object forand assign the manager script to it. The only thing that ParticleSystemManager has to do is switch the particle systems on and off at the appropriate time. We'll use an array variable named particleSystems 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 Particle Systems field.

using UnityEngine; public class ParticleSystemManager : MonoBehaviour { public ParticleSystem[] particleSystems; void Start () { GameEventManager.GameStart += GameStart; GameEventManager.GameOver += GameOver; GameOver(); } private void GameStart () { for(int i = 0; i < particleSystems.Length; i++){ particleSystems[i].Clear(); particleSystems[i].enableEmission = true; } } private void GameOver () { for(int i = 0; i < particleSystems.Length; i++){ particleSystems[i].enableEmission = false; } } }

Particles in action.

That's it, we've finished the game! We can run and jump, leave a trail, collect power-ups, see our score, and have a scrolling background. It's a nice prototype that, with a lot of polish, you can transform into a finished game. Enjoyed the tutorial? Help me make more by becoming a patron!

Downloads