[Tutorial] – Create a Procedurally Generated Terrain for a 2D Infinite Runner Game in Unity

Have you ever been thinking of creating infinite runner game with the Unity? This kind of games can be quite challenging even for experienced developers. One of the things you most probably want to have in your game, is a procedurally generated terrain. Here we will show you how to achieve such feature using the Unity game engine.

Tiny Wings for iOS
Tiny Wings for iOS

The theory

We will write a script that will split the level to segments. Each segment is a constant size object including a Mesh. When the camera is about to render a segment, the Mesh is generated and set on the target position. Segment that is no longer visible is released back into the pool.

Sounds simple? Then let’s get started…

Set up

First, we will need a prefab with Mesh Filter and Mesh Renderer. We will use these to render segments.

Mesh Renderer and Mesh Filter prefab.
Mesh Renderer and Mesh Filter prefab.

Now let’s start writing our script. Let’s call it MeshGen.

public class MeshGen : MonoBehaviour
{
// the length of segment (world space)
    public float SegmentLength = 5;
    
    // the segment resolution (number of horizontal points)
    public int SegmentResolution = 32;
    
    // the size of meshes in the pool
    public int MeshCount = 4;
    
    // the maximum number of visible meshes. Should be lower or equal than MeshCount
    public int VisibleMeshes = 4;
    
    // the prefab including MeshFilter and MeshRenderer
    public MeshFilter SegmentPrefab;
    
    // helper array to generate new segment without further allocations
    private Vector3[] _vertexArray;

    // the pool of free mesh filters
    private List<MeshFilter> _freeMeshFilters = new List<MeshFilter>();
}

Next, when the script is awaken, we want to initialize its fields and build the mesh pool. We’re using a pool to minimize the garbage collection.

    void Awake()
    {
        // Create vertex array helper
        _vertexArray = new Vector3[SegmentResolution * 2];
        
        // Build triangles array. For all meshes this array always will
        // look the same, so I am generating it once 
        int iterations = _vertexArray.Length / 2 - 1;
        var triangles = new int[(_vertexArray.Length - 2) * 3];
        
        for (int i = 0; i < iterations; ++i)
        {
            int i2 = i * 6;
            int i3 = i * 2;
            
            triangles[i2] = i3 + 2;
            triangles[i2 + 1] = i3 + 1;
            triangles[i2 + 2] = i3 + 0;
            
            triangles[i2 + 3] = i3 + 2;
            triangles[i2 + 4] = i3 + 3;
            triangles[i2 + 5] = i3 + 1;
        }
        
        // Create colors array. For now make it all white.
        var colors = new Color32[_vertexArray.Length];
        for (int i = 0; i < colors.Length; ++i)
        {
            colors[i] = new Color32(255, 255, 255, 255);
        }
        
        // Create game objects (with MeshFilter) instances.
        // Assign vertices, triangles, deactivate and add to the pool.
        for (int i = 0; i < MeshCount; ++i)
        {
            MeshFilter filter = Instantiate(SegmentPrefab);
        
            Mesh mesh = filter.mesh;
            mesh.Clear();
            
            mesh.vertices = _vertexArray;
            mesh.triangles = triangles;
            
            filter.gameObject.SetActive(false);
            _freeMeshFilters.Add(filter);
        }
    }

One thing that definitely needs an explanation is a triangle order thing. You can see that it is kind of messed up. For unity meshes you need to define triangle indices. These indices are the indices of vertices passed just before. Each triangle has three vertices. There are two ways you can pass these vertices – clockwise or counter-clockwise. Since most Unity built-in shaders (and any shaders) are rendering counter-clockwise ordered triangles, and discarding (culling) clockwise ordered triangles, we have to follow the rule.

triangles

Here’s an example of 4-vertices shape. It can be displayed using two triangles. If vertices are defined as above (0, 1, 2, 3 in order), then the triangles should be defined as follows:

  • 0-2-1 (alternatives: 2-1-0 or 1-2-0)
  • 3-1-2 (alternatives: 1-2-3 or 2-3-1)

Height function

What we will need is a height function. This should be a Pure Function and can be modified freely to get different interesting results. For our case we made a combination of two sine functions.

// Gets the heigh of terrain at current position.
    // Modify this fuction to get different terrain configuration.
    private float GetHeight(float position)
    {
        return (Mathf.Sin(position) + 1.5f + Mathf.Sin(position * 1.75f) + 1f) / 2f;
    }

Generating segment function

When we have the height function, we need a function that will generate a mesh based on the returned value.

    // This function generates a mesh segment.
    // Index is a segment index (starting with 0).
    // Mesh is a mesh that this segment should be written to.
    public void GenerateSegment(int index, ref Mesh mesh)
    {
        float startPosition = index * SegmentLength;
        float step = SegmentLength / (SegmentResolution - 1);
        
        for (int i = 0; i < SegmentResolution; ++i)
        {
            // get the relative x position
            float xPos = step * i;
            
            // top vertex
            float yPosTop = GetHeight(startPosition + xPos); // position passed to GetHeight() must be absolute
            _vertexArray[i * 2] = new Vector3(xPos, yPosTop, 0);
            
            // bottom vertex always at y=0
            _vertexArray[i * 2 + 1] = new Vector3(xPos, 0, 0);         
        }
        
        mesh.vertices = _vertexArray;
        
        // need to recalculate bounds, because mesh can disappear too early
        mesh.RecalculateBounds();
    }

We’re computing as many vertices as defined by SegmentResolution field value. Also, we’re using _vertexArray,  because it is already allocated and it is not used by any other object (assigning the array to the mesh will copy it instead of passing the reference, but this does not generate any garbage). Vertex positions are relative, but the position passed to GetHeight() must be absolute.

Checking if segment is seeable by the camera

You have to check if segment is about to be rendered by the camera. This is done using this method:

private bool IsSegmentInSight(int index)
    {
        Vector3 worldLeft = Camera.main.ViewportToWorldPoint(new Vector3(0, 0, 0));
        Vector3 worldRight = Camera.main.ViewportToWorldPoint(new Vector3(1, 0, 0));
        
        // check left and right segment side
        float x1 = index * SegmentLength;
        float x2 = x1 + SegmentLength;
        
        return x1 <= worldRight.x && x2 >= worldLeft.x;
    }

Storing data about visible segments

If a segment will be displayed, we have to store this information in some way. We will need segment index and also we need to know what MeshFilter has been used to draw that segment. Then, we can put it back into the pool when the segment is no longer visible. We will create a helper struct within MeshGen class:

    private struct Segment
    {
        public int Index { get; set; }
        public MeshFilter MeshFilter { get; set; }
    }

Then, within the MeshGen class there will be one more private field:

    // the list of used segments
    private List<Segment> _usedSegments = new List<Segment>();

We’re using struct instead of class because creating new struct does not generate any garbage.

Checking if segment is currently visible

We will need to check if a segment is currently visible, so we don’t use more than one MeshFilters to render the single segment.

    private bool IsSegmentVisible(int index)
    {
        return SegmentCurrentlyVisibleListIndex(index) != -1;
    }
    
    private int SegmentCurrentlyVisibleListIndex(int index)
    {
        for (int i = 0; i < _usedSegments.Count; ++i)
        {
            if (_usedSegments[i].Index == index)
            {
                return i;
            }
        }
        
        return -1;
    }

The name of SegmentCurrentlyVisibleListIndex can be a little confusing. It’s looking for a segment of given index, and if found, it returns an index of this segment within _usedSegments list.

Making the segment visible

Now, it’s the most important part, making the segment visible! To do this, we created the EnsureSegmentVisible() method. It takes segment index and makes sure that given segment index will be visible after executing this method. If this segment is already visible, it does nothing.

    private void EnsureSegmentVisible(int index)
    {
        if (!IsSegmentVisible(index))
        {
            // get from the pool
            int meshIndex = _freeMeshFilters.Count - 1;
            MeshFilter filter = _freeMeshFilters[meshIndex];
            _freeMeshFilters.RemoveAt(meshIndex);
            
            // generate
            Mesh mesh = filter.mesh;
            GenerateSegment(index, ref mesh);
            
            // position
            filter.transform.position = new Vector3(index * SegmentLength, 0, 0);
            
            // make visible
            filter.gameObject.SetActive(true);
            
            // register as visible segment
            var segment = new Segment();
            segment.Index = index;
            segment.MeshFilter = filter;
            
            _usedSegments.Add(segment);
        }
    }

Hiding the segment

When the segment is no longer visible by the camera, it should be removed and the MeshFilter should be given back to the pool. We did that with  EnsureSegmentNotVisible() method. It is opposite of the previous method.

    private void EnsureSegmentNotVisible(int index)
    {
        if (IsSegmentVisible(index))
        {
            int listIndex = SegmentCurrentlyVisibleListIndex(index);
            Segment segment = _usedSegments[listIndex];
            _usedSegments.RemoveAt(listIndex);
            
            MeshFilter filter = segment.MeshFilter;
            filter.gameObject.SetActive(false);
            
            _freeMeshFilters.Add(filter);
        }
    }

Connecting it all

Now, the sweet part. The update function! It should hide all the segments that are no longer visible and display segments that should be visible. The order is important here, because otherwise we can run out of free MeshFilters.

    void Update()
    {
        // get the index of visible segment by finding the center point world position
        Vector3 worldCenter = Camera.main.ViewportToWorldPoint(new Vector3(0.5f, 0.5f, 0));
        int currentSegment = (int) (worldCenter.x / SegmentLength);
        
        // Test visible segments for visibility and hide those if not visible.
        for (int i = 0; i < _usedSegments.Count;)
        {
            int segmentIndex = _usedSegments[i].Index;
            if (!IsSegmentInSight(segmentIndex))
            {
                EnsureSegmentNotVisible(segmentIndex);
            } else {
                // EnsureSegmentNotVisible will remove the segment from the list
                // that's why I increase the counter based on that condition
                ++i;
            }
        }
        
        // Test neighbor segment indexes for visibility and display those if should be visible.
        for (int i = currentSegment - VisibleMeshes / 2; i < currentSegment + VisibleMeshes / 2; ++i)
        {
            if (IsSegmentInSight(i))
            {
                EnsureSegmentVisible(i);
            }
        }
    }

Procedurally generated terrain result

It is working? Let’s move the camera position and check it out.

generated terrain gif

The package

Here you can download the unitypackage suitable for Unity 5.3.1 and above. Feel free to modify it to your needs! If you have any questions, please add those as a comment to this post. We will be more than happy to help you!

related
BasicsGuideTutorial
Improved Prefab Workflow
Introduction At the end of October on the Unity Developer conference Unite LA, Unity team...
0
Tutorial
Coroutines in Unity – Encapsulating with Promises [Part 1]
Every Unity developer should be familiar with Coroutines. For many they are the way to script...
6
AdvancedAugmented RealityTutorial
Corner and surface detection in AR Part 1
Introduction AR technology is getting more and more popular these days. Two big companies...
0
Call The Knights!
We are here for you.
Please contact us with regards to a Unity project below.



The Knights appreciate your decision!
Expect the first news soon!
hire us!