Mobile Endless Runner Game Development Series With Unity3D — Part 1, Player Controls [Beginner]
If you are interested in developing games then you have probably heard about Unity3D has become one of the most popular game engines in the mobile game development community. First of all, it’s free for personal use and you are not restricted from the main features of it. Also, it has cross-platform support that made the development process easier and less time consuming for multiple platform deployments. Of course there are some features that need to be adapted for individual platforms but still, you can dig up Unity’s asset store to find a packed solution like IAP and notifications works for both platforms.
What Will We Achieve?
In this series, I will try to explain how can we use Unity3D to develop a cross-platform endless runner game with low poly art which I found from the Unity asset store. With this part, we will find out how to detect finger swipes using touch phases and touch positions in Unity3D. Next, we’re gonna write their callbacks and trigger some functions in them. Beginner level knowledge of Unity3D is enough to understand what is going on in this project. But still, you can take a glance at this series to find out how Unity handles this kind of processes even if you are not touched Unity before. I will add the complete script to end of the post but we’re gonna continue with code blocks for a better explanation.
Here some information on tools and stuff that I will use to develop my project:
- Unity3D 2017.3.1f1,
- C# for coding,
- [Optional]Some 3D assets for visualization,(Primitive objects are enough for this but it’s funnier this way)
- Visual Studio 2017 Community Edition (or Mono Develop if you are Mac OS user),
- Unity Profiler for performance testing, (Will be used for detecting infinite road spawn system’s performance impact later,
- Mobile Device for Testing. This is important because you can’t simulate gestures on Play mode without additional development.
Let’s start with Touch. According to Unity Documentation, Touch API is “Structure describing the status of a finger touching the screen.” But it’s not enough to detect whether the player is swiped or not. To understand finger gestures, Unity is providing us TouchPhase that defined as “An enum type that contains the states of possible finger touches.” This information is enough at this stage of the post but of course, you can read more about TouchPhase at Unity Documentation. So we have Touch and TouchPhase to develop a simple swipe detection script.
Let’s fill our Update function now:
[1]: detectSwipeAfterRelease: Set this bool is true if you want to see an action performed after the player released finger from the screen. But this is a bad idea for our use case because it feels laggy and slow. We want the player to react as fast as possible and change lanes with cat reflexes using this simple trick.
Now it’s time to check swipes at our TODO lines. First of all, we need to detect if swipe vertical of horizontal. For achieve this let’s create two simple functions named as VerticalMoveValue () and HorizontalMoveValue().
We could use those values directly but it is more flexible if you divide your code functions by their main purposes. With doing this, you will have more control over your code.
Now we are ready to create swipe checker function that uses these values and calls related callback functions with directions. But first, let me explain what swipe threshold is and why we need it. For instance, imagine that you’ve placed a pause button to the up right corner. Since we are getting finger positions on the screen, (not from canvas or other components) swipes on button’s surface are still affects our functions. What if we do not have a threshold and user clicks button during the gameplay? Yes. It might trigger some of our callbacks and that is an unwanted situation for us.
In the example below, I defined float SWIPE_THRESHOLD and set it to 20f.
For vertical swipes:
For horizontal swipes:
Next thing to do is put together our code pieces and see the big picture. This is our main swipe checker function’s skeleton.
Finally, our SwipeDetector script is shaped like this:
Let’s summarize what we talked about in this post:
- Decide what your function is serving for and extract other codes with different purposes,
- Always use thresholds for avoiding unwanted swipe detections,
- Decide swipe detection phase that suits best to your gameplay mechanics.
Here you can see a simple demonstration of our near-complete SwipeDetector script. Of course, we can always add some animations and stuff to improve overall quality but simply, it works like a charm!
For testing it your self, place a UI Text component on screen and change its text in callbacks.
That was all for this part. We’re gonna take a look at how can we move the player with code and create performance-friendly infinite road creation system at part 2. Until then, stay well.
Runner , a minimal side-scroller
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.
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 (120, 180, 250).
Light and camera.
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).
Hierarchy, project, and game views.
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.
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.
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.
Instantiating a skyline.
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.
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
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.
Platform prefab.
Platform manager.
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!
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.
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.
Jump input configuration.
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.
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.


Slowdown and speedup platforms.
Platform variety.
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 inside it. Note that the manager isn’t a MonoBehaviour and won’t be attached to any Unity object.
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), 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.
Game Over
Game over threshold.
Using the Events
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.
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.
Game start and game over.
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.
Booster configuration.
Platform manager knows about booster.
Boost velocity.
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.

Boosts and distance.
Particle Effects
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.
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.
Downloads
Questions & Answers
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 Instantiate do? The Object class, which every MonoBehaviour inherits from, contains the static Instantiate method. This method creates a clone of whatever Object instance 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 Instantiate returns an Object reference. 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 code x = x + y;
There are other operators that behave in a similar fashion, like -= , *= , and /= . Why is distanceTraveled static? 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 to distanceTraveled .
Of course, we could just add a Runner variable to SkylineManager and 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.Generic namespace contains the Queue class, 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.
Queue is a generic class that can deal with any one type of content. In this case, we use Queue<Transform> to declare a queue of Transform references.
You can add to the end of the queue by using the Enqueue method. Taking out the first item is done with the Dequeue method. Additionaly, you can get to the first item without removing it via the Peek method. What does Random.Range do? Random is a utility class that contains some stuff to create random values. Its Range method can be used to generate a random value within some range.
There are two versions of the Range method. 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 Range with 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, like someArray[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 RigidBody components, 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 FixedUpdate called? 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 FixedUpdate method works like Update , except that it’s called once per physics step instead of once per frame. In other words, FixedUpdate is independent of the frame rate. What does AddForce do? The AddForce method 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.Acceleration option. If you want to directly adjust the velocity, you can use ForceMode.VelocityChange . What does && do? The && operator is used for boolean logic and stands for ‘and also’. In other words, x && y is only true if both x and y are true.
Note that if x is found to be false, there’s no point in checking y anymore. If y were 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’. So x || y is true if at least one of them is. Also, if x is found to be true, then y will not be considered. What does Input.GetButtonDown do? Input is 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 GetButtonDown method can be used to check whether the user just pressed down a key associated with some button or action. Correspondingly, the GetButtonUp method can be used the check whether the user just released it. Also, the GetButton method 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 Update is equivalent to writing code that would activate once in the next FixedUpdate . 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 += myMethod and myEvent -= myMethod . A delegate also allows a direct assignment, via myDelegate = 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.
Also, events can only be invoked by the class that defines them. Outsiders can only register and unregister methods to them. 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’s null results in an error. You can test for this value to make sure that doesn’t happen. You can also set such a variable to null yourself, in case you no longer need whatever it was referencing. What does != do? The != operator checks whether two things are different. For example, 1 != 2 is true, while 2 != 2 is false. In our case, we’re checking whether our event isn’t null , 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 Update method. 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, GUIManager will respond to the event just fine. Why immediately reset the distance? Leaving it to the Update method to override distanceTraveled could 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.identity is 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 OnCollisionEnter method 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 OnTriggerEnter method to detect when something enters the collider’s volume. What does setActive() do? You use this method to either activate or deactivate an entire game object, not just a single component. Also, when deactivating a game object all its child game objects will be deactivated as well. So a game object is only really active when both itself and all of its parents are active. You can check whether this is the case via the property activeInHierarchy . You can also check activeSelf which disregards the hierarchy, which is what we do because our Booster has no parents. What does return do? You use the return keyword 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 ? Time is a utility class for time-related stuff. Its deltaTime property contains the amount of seconds passed since the last frame, or since the last fixed time step if called inside FixedUpdate . What’s this ? The this keyword 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 this to access it. For example, inside the Update method, transform is the same as this.transform . What does ToString() do? The ToString method can be used to create string representations of data. We use it for an int and a float . The first conversion is simply the decimal representation of the number (its real form is binary). We do the same for the float , except we also add a format description which tells the method to no display the fractional part. 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 Runner class 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 Runner call the GUI manager at all. Then it would be up to GUIManager to get the boost count from Runner instead. 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 GameOver in Start ? 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 GameOver method does, we call it instead of writing the same code twice.
Endless Runner in Unity
In video games, no matter how large the world is, it always has an end. But some games try to emulate the infinite world, such games fall under a category called Endless Runner.
Endless Runner is a type of game where the player is constantly moving forward while collecting points and avoiding obstacles. The main objective is to reach the end of the level without falling into or colliding with the obstacles, but oftentimes, the level repeats itself infinitely, gradually increasing the difficulty, until the player collides with the obstacle.

However, considering that even modern computers/gaming devices have limited processing power, it’s impossible to make a truly infinite world.
So how do some games create an illusion of an infinite world? The answer is by reusing the building blocks (a.k.a. object pooling), in other words, as soon as the block goes behind or outside the Camera view, it’s moved to the front.
To make an endless-runner game in Unity, we will need to make a platform with obstacles and a player controller.
Step 1: Create the Platform
We begin by creating a tiled platform that will be later stored to Prefab:
- Create a new GameObject and call it «TilePrefab»
- Create new Cube (GameObject -> 3D Object -> Cube)
- Move the Cube inside «TilePrefab» object, change its position to (0, 0, 0) and scale to (8, 0.4, 20)

- Optionally you can add Rails to the sides by creating additional Cubes, like this:

For the obstacles, I will have 3 obstacle variations, but you can make as many as needed:
- Create 3 GameObjects inside «TilePrefab» object and name them «Obstacle1», «Obstacle2» and «Obstacle3»
- For the first obstacle, create a new Cube and move it inside «Obstacle1» object
- Scale new Cube to around the same width as the platform and scale its height down (the player will need to jump to avoid this obstacle)
- Create new Material, name it «RedMaterial» and change its color to Red, then assign it to the Cube (this is just so the obstacle is distinguished from the main platform)


- For the «Obstacle2» create a couple of cubes and place them in a triangular shape, leaving one open space at the bottom (the player will need to crouch to avoid this obstacle)

- And lastly, the «Obstacle3» is going to be a duplicate of «Obstacle1» and «Obstacle2», combined together


- Now select all the Objects inside Obstacles and change their tag to «Finish», this will be needed later to detect the collision between Player and Obstacle.
To generate an infinite platform we will need a couple of scripts that will handle Object Pooling and Obstacle activation:
- Create a new script, call it «SC_PlatformTile» and paste the code below inside it:
SC_PlatformTile.cs
- Create a new script, call it «SC_GroundGenerator» and paste the code below inside it:
SC_GroundGenerator.cs
- Attach SC_PlatformTile script to «TilePrefab» object
- Assign «Obstacle1», «Obstacle2» and «Obstacle3» object to Obstacles array
For the Start Point and End Point we need to create 2 GameObjects that should be placed at the start and the end of the platform respectively:


- Assign Start Point and End Point variables in SC_PlatformTile

- Save «TilePrefab» object to Prefab and remove it from the Scene
- Create a new GameObject and call it «_GroundGenerator»
- Attach SC_GroundGenerator script to «_GroundGenerator» object
- Change Main Camera position to (10, 1, -9) and change its rotation to (0, -55, 0)
- Create new GameObject, call it «StartPoint» and change its position to (0, -2, -15)
- Select «_GroundGenerator» object and in SC_GroundGenerator assign Main Camera, Start Point, and Tile Prefab variables
Now press Play and observe how the platform moves. As soon as the platform tile goes out of the camera view, it’s moved back to the end with a random obstacle being activated, creating an illusion of an infinite level (Skip to 0:11).
The Camera must be placed similarly to the video, so the platforms go towards the Camera and behind it, otherwise the platforms won’t repeat.
![]()
Step 2: Create the Player
Player Instance will be a simple Sphere using a controller with the ability to jump and crouch.
- Create new Sphere (GameObject -> 3D Object -> Sphere) and remove its Sphere Collider component
- Assign previously created «RedMaterial» to it
- Create a new GameObject and call it «Player»
- Move the Sphere inside «Player» object and change its position to (0, 0, 0)
- Create a new script, call it «SC_IRPlayer» and paste the code below inside it:
SC_IRPlayer.cs
-
SC_IRPlayer script to «Player» object (you’ll notice that it added another component called Rigidbody)
- Add BoxCollider component to «Player» object

- Finally, place «Player» object slightly above the «StartPoint» object, right in front of the Camera
Press Play and use the W key to jump and the S key to crouch. The objective is to avoid red Obstacles:
Мобильная 3D игра на Unity3D менее чем за 90 часов
Приветствую! Сегодня я расскажу вам о своем опыте разработки игры на Unity для платформы Android, менее чем за 90 часов, на примере создания простенького «раннера». В процессе повествования я затрону некоторые детали и ключевые этапы, с описанием всех возможных подводных камней и методов их решения. Данная история описывает процесс создания игры для мобильных платформ, начиная от концепции и заканчивая готовым продуктом. Надеюсь, она вдохновит вас на создание собственного проекта, либо поможет пролить свет на некоторые особенности движка Unity. Без лишних слов, приступим к делу!
Этап-1: концепция
Как правило, начинающие разработчики, наступают на свои первые и самые значимые грабли уже на данном этапе, потому что перед тем, как приступить к созданию чего-либо, неплохо было бы оценить собственные возможности. Просто задайте себе вопрос: хватит ли у вас сил, времени и умений на создание проекта ААА класса? Ответ – нет! Отбросьте эту идею в долгий ящик, и не возвращайтесь к ней до тех пор, пока не реализуете чертову дюжину удачных проектов. К слову, под удачей мы подразумеваем количество установок от 500 тысяч, рейтинг свыше 3,5 по 5-ти бальной шкале и коммерческий успех. Для начала, займитесь более простыми, я бы даже сказал приземленными проектами, вроде аркад в стиле addictive games, сочетающих в себе все необходимые нами критерии «удачного» проекта.
Преимущества стиля addictive games:
- Затягивающий, «залипающий» геймплей;
- Отсутствие сюжета;
- Простое и интуитивно понятное управление, требующее от игрока минимум действий;
- Минимальные требования к графике.
Этап-2: создание наброска
Вот мы и подошли к самому главному этапу разработки, и пока с вашего лица еще не сошла разочаровывающая улыбка, позвольте вам напомнить, что набросок – это видение продукта. Создав его, вы фактический утверждаете техническое задание будущей игры, благодаря которому все дальнейшие шаманства и танцы будут исходить именно из этого задания. Степень проработки эскиза определять вам и только вам. В конце концов, этот эскиз вы создаете для себя, а не для галереи искусств. На данном этапе, я просто беру ручку и блокнот, после чего начинаю рисовать, изредка оставляя краткие комментарии и пояснения:

Из наброска видно, что игра предназначена для мобильных платформ, и запускается она будет в портретном режиме. Геймплей также бесхитростен: задача игрока заключается в преодолении опасного машрута на предоставленном игрой автомобиле, попутно собирая кристаллы. За каждый собранный кристалл и удачно пройденный поворот, игрок получает вознаграждение в виде очков бонуса. Касание по экрану заставляет изменять направление движения автомобиля по осям X и Z.
Этап-3: создание прототипа
Имея под рукой подробный план действий, можно смело приступать к созданию «мокапа» или прототипа будущей игры. По сути, данный этап – начало работы с Unity, и начинать его следует с настройки окружения. Вот, как это настроено у меня:

В левой части экрана расположились редактор Scene и Game. Последний отображает то, как именно игра выглядит на устройствах. В правой части: панели Hierarchy и Inspector, а чуть ниже расположены панели Project и Console.
Этап-3.1: под капотом
Внимание! Ниже будет описан простейший код реализации игры, рассчитанный на новичков, и демонстрирующий то, насколько быстро и просто можно добиться результата в Unity. Финальный код игры реализован на более глубоких познаниях языка, включающий проблемы хранения данных, оптимизации и монетизации проекта, однако, по понятным причинам, в данной статье о них говориться не будет. Все скрипты мы будем писать на C#, а тем, кому это не интересно, предлагаю смело переходить к Этапу-4: визуальный дизайн
Мое прототипирование всегда начинается с болванок, то есть в качестве актеров я всегда использую примитивные элементы, вроде кубов и сфер. Такой подход заметно упрощает процесс разработки, позволяя абстрагироваться от всего, что не связано с механикой игры. На первом шаге мы формируем базовое понимание облика будущей игры, а так как по задумке наша игра создается в изометрическом стиле, первое что нам необходимо проделать, это настроить камеру. Тут мы подходим к одной из ключевой особенности Unity. Дело в том, что можно долго экспериментировать с параметрами настройки камеры, подбирая нужные значения… Но проще просто выставить понравившийся ракурс с помощью панели View, а затем активировать GameObject -> Align With View, после чего ваша камера тотчас же примет необходимые значения. Вот такой вот shortcut от создателей Unity.

Итак, сцена готова, но как придать персонажу движение? Для начала, произведем некоторые манипуляции с объектом Sphere, добавив в него такие компоненты как Rigidbody и только что созданный скрипт sphereBehavior. Не забудьте отключить галочку Use Gravity, так как на данном этапе он нам не понадобится.

Если вкратце, то компонент Rigidbody позволяет объекту ощутить на себе все прелести физического мира, таких как масса, гравитация, сила тяжести, ускорение и.т.д. Вот почему для нас он так важен! А теперь, чтобы заставить тело двигаться в нужном нам направлении, нам всего лишь нужно слегка изменить параметр velocity, но делать это мы будем при помощи кода. Давайте заставим сферу двигаться по оси Х, для этого внесём изменения в скрипт sphereBehavior:
В Unity, тела описывают своё положение и направление, посредством специальных векторов, хранящих значения по осям x, y и z. Изменяя эти значения, мы добиваемся необходимого нам направления или положения конкретного тела. Строка rb.velocity = new Vector3(speed, 0f,0f) задает новое направление телу по оси X, тем самым придавая нашей сфере нужное нам направление.
Если вы сделали всё в точности, как и я, то ваша сфера отправится в бесконечное путешествие по оси X, со скоростью speed.
Теперь давайте заставим нашу сферу изменять свое направление, при каждом клике левой клавиши мыши так, как это реализовано в игре ZIGZAG. Для этого мы вновь вернемся к коду sphereBehavior и изменим его следующим образом:
Условимся, что когда сфера движется по оси X, то это движение называется движением «вправо», а по оси Z – «влево». Таким образом мы легко можем описать направление нашего тела специальной булевой переменной isMovingRight.
Этот кусочек кода отслеживает нажатие левой клавиши мыши, и если данная клавиша все же была нажата, запускает функцию changeDirection(), с простой логикой: если на момент нажатия левой клавиши мыши, переменная isMovingRight имела значение true, то теперь она стала false и наоборот. Напомню, что булевая переменная позволяет нам ответить на один простой вопрос: истинно ли утверждение о том, что тело движется по оси X, или нет? Иными словами, нажатие на левую клавишу мыши постоянно изменяет значение isMovingRight, то на true(тело движется вправо), то на false(тело движется влево).
Альтернативно, функцию changeDirection() можно записать в одну строку:
И последнее, что необходимо сделать, это переписать метод направления движения с учетом переменной isMovingRight:
Если isMovingRight имеет значение true (если сфера действительно движется вправо), тогда значение velocity принимает новый вектор направления rb.velocity = new Vector3 (speed, 0f, 0f); Если isMovingRight имеет значение false, значит тело более не движется вправо, а значит пришло время изменить вектор направления на rb.velocity = new Vector3 (0f, 0f, speed);
Запустите игру, проделайте несколько кликов мыши, и если вы сделали все в точности, как и я, то увидите, как сфера начнет описывать зигзаги.
Круто? Конечно нет! Ведь сфера движется, а мы стоим на месте. Давайте доработаем игру так, чтобы мы могли двигаться вместе со сферой и не упускали её из виду. Для этого нам нужно создать скрипт cameraFollow и прикрепить его к объекту Main Camera:

А вот код скрипта cameraFollow:
Как же осуществить слежение за объектом? Для начала нам нужно рассчитать разницу смещения между объектами Camera и Sphere. Для этого достаточно вычесть от позиции камеры, координаты сферы, а полученную разницу сохранить в переменной offset. Но прежде, необходимо получить доступ к координатам сферы. Для этого нам необходима переменная player, представляющая собой простой GameObject. Так как наша сфера находится в постоянном движении, мы должны синхронизировать координаты камеры с координатами сферы, приплюсовав полученное ранее смещение. Осталось только указать в поле player наш объект слежения, и можно смело любоваться результатом. Просто перетащите объект Sphere в поле Player, скрипта cameraFollow, как это показано на картинке (Main Camera при этом должна оставаться выделенной):

Теперь же, давайте подумаем над генерацией дороги, по которой могла бы двигаться наша сфера, ведь сейчас она в буквальном смысле парит в воздухе. Начнем с настройки объекта Cube, представляющий, по нашему мнению, участок пути.
Если в вашем списке нет тэга Ground, то его необходимо создать во вкладке Add Tag.
Следующее, что нам предстоит совершить, это создать в корне проекта специальную папку с названием Prefabs, и перетащить в нее наш Cube, прямо из инспектора. Если после этого, имя объекта Cube стало синего цвета, значит вы все сделали правильно.

Префабы – это особый тип объектов, позволяющий хранить GameObject, а также все его значения и свойства в одном месте. Префабы позволяют создавать бесконечное множество объекта, а любое его изменение немедленно отражаются на всех его копиях. Иными словами, теперь мы можем вызывать участок пути Cube, прямо из папки Prefabs, столько раз, сколько необходимо.
Теперь давайте создадим пустой GameObject, (щелчок правой кнопки мыши по Hierarchy) переименуем его в RoadContainer и прикрепим к нему только что созданный скрипт roadBehavior:


А вот и сам код roadBehavior:
Что же тут на самом деле происходит? Как видите, у нас есть переменная, которая позже, вручную будет привязана к нашему префабу Cube, и есть объект Vector3, хранящий координаты последнего установленного префаба (сейчас значения равны нулю).
Этот участок кода выполняет следующее: до тех пор, пока i < 10, мы будем брать префаб, устанавливать его позицию с учетом последней позиции lastpos + позиция с учетом смещения по X, сохранять последнюю позицию. То есть в результате, мы получим 10 префабов Cube установленных в точности друг за другом. Перед проверкой, не забываем назначить переменной road наш объект Cube из папки Prefabs:


Ок, но что делать дальше? А дальше нам нужно продолжить установку блоков в произвольном порядке. Для этого нам понадобится генератор псевдослучайных чисел random. Подправим скрипт roadBehavior с учетом нововведений:
Строчка InvokeRepeating («SpawnPlatform», 1f, 0.2f) предназначена для активации функции SpawnPlatform() спустя 1 секунду после начала игры, и повторного её вызова каждые 0.2 секунды. Что касается самой функции, то тут, как говорится все проще пареной репы! Каждые 0.2 секунды, система загадывает случайное число между цифрами от 0 до 1. Если система загадала 0 – мы устанавливаем новый префаб по оси X, а если 1 – то по оси Z. Вот и вся магия!

И наконец, давайте заставим сферу падать каждый раз, когда она сходит с дистанции. Для этого создадим новый скрипт playerFalls и прикрепим его к нашему объекту Sphere:

А вот и сам код скрипта playerFalls:
Raycast – специальный луч на подобии лазера, который излучается по направлению к сцене. В случае, если луч отражается от объекта, он возвращает информацию об объекте, с которым столкнулся. И это очень круто, потому что именно так, посредством такого луча, направленного из центра сферы вниз, мы будем проверять, находимся ли мы на платформе Cube или нет (проверяем, имеет ли объект тэг «Ground»). И как только мы покинем регионы дорожного полотна, мы автоматом активируем параметр Gravity нашей сферы (помните, как мы заведомо отключили его в самом начале?), после чего сфера, под воздействием гравитации, рухнет вниз, ха-ха!
Этап-4: визуальный дизайн
Когда все работы по игровой механике закончены, в пору переходить к визуальной части проекта. Все таки геймплей – это хорошо, а приятный геймплей – еще лучше. И несмотря на то, что в самом начале мы обозначили графику, как далеко не самое главное, хочется все же привнести некоторую изюминку, добавив красок в создаваемую игру. После недолгих раздумий, в голову пришла следующая идея:

По замыслу, вы управляете автомобилем, несущимся по бескрайним морским просторам, спасаясь от надвигающего катаклизма. Промедление сродни смерти, так как платформы то и дело норовят опрокинуться в морскую пучину, увлекая игрока в бездну позора и разочарования. Плюс ко всему, время от времени, платформы начинают менять цвет, а автомобиль, самопроизвольно увеличивать скорость. Всё это призвано привнести в игру некое подобие «челленджа». Как и было сказано, за каждый удачно пройденный поворот или собранный кристалл, игрок вознаграждается «инкамом» — местным подобием зарплаты. Зарплату в последствии можно обменять в лавке на авто с более высоким «инкамом». Концепция подарила звучное название «Income Racer».
Все ассеты были смоделированы в Blender’е – бесплатном 3D редакторе. В нём же были созданы необходимые текстуры, впоследствии доведенные до приемлемого вида в Photoshop’е. Приятным моментом оказалось то, что Unity легко импортирует 3D модели из Blender’а, без лишней головной боли, делая процесс создания приятным и безболезненным.
Этап-5: полировка
Доводка проекта – те еще грабли, ведь всегда найдется место тому, что можно улучшить, или переделать. Зачастую, случается так, что именно на этапе полировки и доводки, процесс разработки значительно теряет во времени, а то и вовсе заходит в тупик. Причина заключается в том, что вы уже заметно подустали: игра вам кажется однообразной и недостаточно интересной. Иногда, под конец разработки, приходит откровение того, что вы способны на переделку игры с нуля, улучшив её как минимум в два раза! Отбросьте эти мысли и вспомните о плане, о том, с чего все начиналось. Лучше дополнять игру уже после релиза, путем выкатки обновлений, чем затягивать разработку на неопределенный срок. В противном случае, вы рискуете погубить проект, поставив на нём жирный крест. К примеру, на момент написания этих строк, игра даже не имела вступительного экрана. Причиной тому тот факт, что по плану я не мог выйти за рамки в 90 часов, отведенные на процесс разработки. Конечно, можно было бы потратить еще несколько часов на создание вступительного экрана, однако на то он и план, чтобы ему следовать. И это нормально, что некоторые моменты добавляются в игру уже после её релиза.
Последним, остается создать презентационные документы: краткое описание, видео, а также иконку игры. Этому этапу следует уделить как можно больше внимания, ведь именно по иконке, пользователи начинают судить ваш проект.
В итоге получилось то, что получилось. На всё про всё было затрачено чуть более 90 часов, что по меркам современного геймдева не так уж и много. По прошествии этого времени, игра была загружена в Play Market и выставлена на всеобщий суд, вот такая история! Если вам понравилась статья, или просто есть о чем поговорить, то добро пожаловать в комментарии. Буду рад ответить на ваши вопросы.