Randomness is a useful tool in game development to get a lot of content with less work, if utilized correctly. A common use for randomness is level generation.
Random level generation can be implemented in several ways. The following example is kind of faux random level generation, as it uses pre-made “blocks”, akin to rooms, to create one continuous level. For more “true” random generation, I recommend looking into the Unity tutorial on using Cellular Automata for procedural cave generation.
Let’s get to the actual script:
using UnityEngine; using System.Collections; using System.Collections.Generic; public class LevelGenerationController : MonoBehaviour { public Transform player; public GameObject floor; public GameObject[] parts; // level parts used public float allowedDistanceUntilEnd = 10f; // if distance from end is lower than this value, moving is triggered public GameObject currentPart; private ConnectionPointManager currentConnectionPoints; public GameObject nextPart; private ConnectionPointManager nextConnectionPoints; private int currentPartIndex; // for preventing the use of the same part twice in a row // Use this for initialization void Start () { // find player and floor player = GameObject.FindGameObjectWithTag("Player").transform; floor = GameObject.FindGameObjectWithTag("Floor"); // assign the origin level part or block as the current part currentPart = floor.transform.FindChild("Origin").gameObject; currentConnectionPoints = currentPart.GetComponent<ConnectionPointManager>(); // pick randomly the next part to be moved to the end of the level int nextPartIndex = Random.Range(0, parts.Length); nextPart = parts[nextPartIndex]; nextConnectionPoints = nextPart.GetComponent<ConnectionPointManager>(); currentPartIndex = nextPartIndex; // cycle indexes } // Update is called once per frame void Update () { ObservePlayerDistance(); } // move the randomly chosen next part to the end of the level // the next part is placed so that it seamlessly joins the previous part void MoveNextPart() { float nextY = currentConnectionPoints.endPoint.position.y - nextConnectionPoints.beginPoint.localPosition.y; nextPart.transform.position = new Vector3(currentPart.transform.position.x + 50, nextY, 0); } void ObservePlayerDistance() { // if the player is close to the end of the level (or more than allowed) if (currentConnectionPoints.endPoint.position.x - player.transform.position.x < allowedDistanceUntilEnd) { MoveNextPart(); // move next part to the end of the level CycleParts(); // assign the moved part to the current part and choose a new next part } } void CycleParts() { currentPart = nextPart; currentConnectionPoints = nextConnectionPoints; // choose a random number and use it as index to pick a new part int nextPartIndex = Random.Range(0, parts.Length); // prevent the same part from being used twice while (nextPartIndex == currentPartIndex) { nextPartIndex = Random.Range(0, parts.Length); } nextPart = parts[nextPartIndex]; currentPartIndex = nextPartIndex; // cycle indexes nextConnectionPoints = nextPart.GetComponent<ConnectionPointManager>(); } }
So what is actually going on there? The process is rather simple (hence the name of the post).
- During Start(), the script looks for the player and floor (requires a GameObject with tag “Floor” to exist), after which the current part is assigned and next part is chosen randomly from the collection of given parts, from which to build the level from.
- During Update(), the player character’s position is observed. If the player character is closer to end of the level than allowedDistanceUntilEnd specifies, MoveNextPart() is run. This moves the randomly chosen part to the end of the level, to a position in which it will seamlessly connect to the previous part. Determining what that position is what I will discuss later in this post.
- In Update(), after MoveNextPart() has been run, the parts are cycled in CycleParts(). Here the script assigns the previously moved “next part” as the current part and picks a new next part.
Now, to answer how the next position is determined: the controller is told where the beginning points and end points of the current and next parts are. These beginning points and end points are GameObjects that are placed to the end and beginning coordinates of each part or “block”. Each part or “block” also has a script called ConnectionPointManager, which search for the start and end points when the scene is loaded. Basically, the functioning of this script depends on the correct placement of these beginning and end points.
Here is what the ConnectionPointManager looks like:
using UnityEngine; using System.Collections; public class ConnectionPointManager : MonoBehaviour { public Transform beginPoint; public Transform endPoint; // Use this for initialization void Start () { Transform[] points = GetComponentsInChildren<Transform>(); foreach (Transform point in points) { if (point.tag.Equals("Begin Point")) { beginPoint = point; } if (point.tag.Equals("End Point")) { endPoint = point; } } } }
In Start(), all Transform components are collected from the child GameObjects of the part or “block”, after which they are looped through. Depending on the tag of the GameObject, the Transform is assigned to either beginPoint or endPoint.
If you want to try out the code, you can download the UnityPackage with all the assets and a functioning test scene from here.
All the visual assets are the creations of Kenney. You can download the asset pack (Platformer Pack Redux) from here.