Infinite random terrain generation is something I have been interested in since first playing Minecraft nearly a decade ago. During my second year at university, I set about trying to achieve it using Perlin noise. (Video at bottom of post.)

There are several key things I felt I learnt about over the course this project:

  • Meshes
  • Advanced code optimisation
  • Using threads (safely), and their limitations – such as not modifying Unity datatypes within one.
  • Unity’s Animation Curves can be used to modify data!
  • I should really start using object pooling.
  • Pseudo random number generators – great for seeds – System.Random prng = new System.Random(seed);
  • Creating levels of detail (LODs) by reducing the number of vertices (and thus tris) in a mesh.

 

An early version of the terrain generator, with a load of interesting info and debugging text. (As you can see from the speed, I really liked trying to push things!)

During this project I learnt a lot about the safe usage of threads, and an awful lot about optimisations. I used thread queues to queue map/mesh data handling methods, however I read about the benefits of concurrent queues – but couldn’t use these as the version of Unity I was working in didn’t appear to support .NET 4 features. 🙁

I spent a long time debugging my code to find unused variables and data that I forgot to clean up, that for some reason Unity’s garbage collector was unwilling to take! I ended up using several different approaches to the garbage collection issue, discovering the following two methods that turned out to be very helpful:

GameObject.DestroyImmediate(); A dangerous method to be using as it can delete assets and cause errors due to changing script execution order, however I found it to help and was able to use it without damaging anything! Nevertheless I will remain wary of using this in future.

Resources.UnloadUnusedAssets(); Running this upon chunk updates worked wonders.

The below extract shows how I created a noise map using Perlin noise. ‘x’ and ‘y’ refer to the coordinates within the noise map. I think my comments explain it fairly decently, however I will reiterate. The float ‘noiseHeight’ is the final vertical height that this point of terrain will be. In the extract I’m running through every coordinate in the noise map, and generating noise. I subtract half the map’s width from the x values, and half the map’s height from the y values to keep the map centred.

Having multiple octaves of noise allows for more detail to be in the noise map. I reduce the amplitude by persistance, and increase frequency by lacunarity in order to have more detailed noise but ensure the more detailed noise affects the final map less.

For example, the first octave is the general flow of the land, mountains and seas. The next octave adds smaller features, like hills and valleys. Further octaves add even smaller features, like little peaks or troughs. Reducing the amplitude of each extra octave ensures that you don’t end up with a load of spiky land.

Extract from NoiseGenerator.cs

                amplitude = 1;
                frequency = 1;
                float noiseHeight = 0;

                for (int i = 0; i < octaves; i++)
                {
                    /* Use the Perlin noise function built into Unity for smooth randomness.
                    /  Multiply by 2 and -1 to allow more negative noise
                    /  (Else Mathf.PelinNoise would just give us a 0-1 value)
                    /
                    /  Adding octave offset else we'd just be making the same noise over again.
                    */
                    noiseHeight += (Mathf.PerlinNoise((x - halfMapWidth + octOffsets[i].x) / scale * frequency, 
                    (y - halfMapHeight + octOffsets[i].y) / scale * frequency) * 2 - 1) * amplitude;

                    // Increase frequency and decrease amplitude
                    amplitude *= persistance;
                    frequency *= lacunarity;
                }

I used a pseudo random number generator for populating the landscape with trees, boats, snowmen etc. In hindsight, running through every single vertex in every mesh may not be the best way to randomly spawn flora, however for this project it worked fine and didn’t appear to have much of an impact on frame rate or anything else.

Extract from ObjectGenerator.cs

// Called from Chunk.UpdateChunk()

public void GenerateObjects(Mesh mesh, GameObject parent, int seed, int lod)
{
    System.Random prng = new System.Random(seed);

    foreach (Vector3 vert in mesh.vertices)
    {
        // We generate rotation value here, as if it was done in SpawnObject()
        // it seemed to always return the same value.
        float rotationValue = prng.Next(0, 360);

        // Generic flora
        if(vert.y > minFloraHeight && vert.y < maxFloraHeight)
        {
            if(prng.Next(prngMin, prngMax) < treeChance )
            {
                SpawnObject(treePrefab, vert, parent, rotationValue);
            }
            // Christmassy flora
        }else if (vert.y > minXmasHeight && vert.y < maxXmasHeight)
        {
            if (prng.Next(prngMin, prngMax) < snowManChance)
            {
                SpawnObject(snowmanPrefab, vert, parent, rotationValue);
            }
            if(prng.Next(prngMin, prngMax) < xmasTreeChance)
            {
                SpawnObject(xmasTreePrefab, vert, parent, rotationValue);
            }
        }
            // Boats!
        if(vert.y < maxBoatHeight && vert.y > minBoatHeight)
        {
            if (prng.Next(prngMin, prngMax) < boatChance)
            {
                SpawnObject(boatPrefab, vert, parent, rotationValue);
            }
        }
        
    }
}

 

Here is a video of the finished thing!

After submitting this for an assignment, I created a multiplayer horror game using it. I used Unity’s UNET for this, (which was quite the learning curve from having used Photon’s PUN plugin beforehand!) I had one client host, and other connect, requesting and receiving the seed from the client, meaning all players’ games would generate the same random map as the host.