Procedural Generation in Unity with Perlin Noise

Illustration of terrain generated from a noise map

Procedural generation lets a small team build big worlds: endless terrain, fresh dungeon layouts every run, caves and islands that no one placed by hand. Most of it is built on one humble function, Mathf.PerlinNoise. This guide covers procedural generation in Unity with Perlin noise: how the noise works, octaves and seeds for natural-looking results, generating terrain heightmaps and 2D tilemaps, island falloff, caves with cellular automata, and splitting big worlds into chunks.

What Perlin Noise Is

Random.value gives completely unrelated values: neighbouring points jump wildly, like TV static. Perlin noise is smooth randomness. Nearby inputs give nearby outputs, so it produces rolling hills rather than spikes. Feed it a 2D coordinate and it returns a value from roughly 0 to 1 that changes gradually as you move.

Sampling Mathf.PerlinNoise

float h = Mathf.PerlinNoise(x * scale, y * scale);

Two rules explain most problems:

  • Integer inputs return the same value (about 0.465). PerlinNoise(1, 2) and PerlinNoise(5, 7) look identical. Always multiply by a scale such as 0.05 so you sample between grid points.
  • Scale controls feature size. A small scale (0.01) gives broad continents, and a large scale (0.3) gives busy, noisy detail.

Values can slightly exceed 0–1, so clamp with Mathf.Clamp01 if you index into arrays with them. Unity 6 also includes the Unity.Mathematics.noise class with noise.snoise (simplex) and noise.cnoise, which are faster, support 3D, and work in Burst jobs.

Octaves: From Blobs to Landscapes

A single layer of noise looks like soft blobs. Real terrain has big mountains and small bumps. Combine several layers (octaves), each with higher frequency and lower amplitude. This is called fractal Brownian motion (fBm):

public static float FractalNoise(float x, float y, int octaves, float persistence, float lacunarity, Vector2 offset)
{
    float total = 0f, amplitude = 1f, frequency = 1f, max = 0f;
    for (int i = 0; i < octaves; i++)
    {
        total += Mathf.PerlinNoise((x + offset.x) * frequency, (y + offset.y) * frequency) * amplitude;
        max += amplitude;
        amplitude *= persistence;   // each octave contributes less (e.g. 0.5)
        frequency *= lacunarity;    // and has finer detail (e.g. 2)
    }
    return total / max;             // normalise back to ~0..1
}
Parameter Typical value Effect
Scale 0.005–0.05 Overall feature size
Octaves 3–6 Amount of layered detail (more = slower)
Persistence 0.4–0.6 How rough the result is
Lacunarity 2 Detail growth per octave

Want sharp mountain ridges? Use ridged noise: 1 - Mathf.Abs(noise * 2 - 1) per octave. Want flat plains with occasional peaks? Raise the result to a power: Mathf.Pow(h, 3f).

Seeds and Reproducible Worlds

Mathf.PerlinNoise has no seed parameter. The standard trick is to offset the sample coordinates with values derived from a seed:

public static Vector2[] OctaveOffsets(int seed, int octaves)
{
    var rng = new System.Random(seed);
    var offsets = new Vector2[octaves];
    for (int i = 0; i < octaves; i++)
        offsets[i] = new Vector2(rng.Next(-100000, 100000), rng.Next(-100000, 100000));
    return offsets;
}

Give each octave its own offset to avoid visible repetition. Use System.Random with a seed rather than UnityEngine.Random, which is global state other scripts also use. Same seed, same world every time: perfect for sharing seeds or regenerating a level on load instead of saving it. Keep offsets under about 100,000, since very large coordinates lose floating-point precision and the noise becomes blocky.

Generating a Terrain Heightmap

Unity Terrain takes heights as a 2D float array (0–1), indexed [y, x]:

using UnityEngine;

[RequireComponent(typeof(Terrain))]
public class TerrainGenerator : MonoBehaviour
{
    public int seed = 1234;
    public float scale = 0.004f;
    [Range(1, 8)] public int octaves = 5;
    public float persistence = 0.5f, lacunarity = 2f;
    public AnimationCurve heightCurve = AnimationCurve.Linear(0, 0, 1, 1);

    [ContextMenu("Generate")]
    public void Generate()
    {
        TerrainData data = GetComponent<Terrain>().terrainData;
        int res = data.heightmapResolution;
        float[,] heights = new float[res, res];
        var rng = new System.Random(seed);
        Vector2 offset = new Vector2(rng.Next(-50000, 50000), rng.Next(-50000, 50000));

        for (int y = 0; y < res; y++)
        for (int x = 0; x < res; x++)
        {
            float h = FractalNoise(x * scale, y * scale, octaves, persistence, lacunarity, offset);
            heights[y, x] = heightCurve.Evaluate(h);
        }
        data.SetHeights(0, 0, heights);
    }

    // FractalNoise from the octaves section goes here
}

The AnimationCurve is a designer’s best friend: flatten the low end for beaches and plains, steepen the top for mountains. Right-click the component and choose Generate to run it in the Editor. Combine this with the painting and detail tools from our Unity terrain tutorial.

2D Tilemap Worlds and Islands

For 2D, sample noise per cell and choose a tile by thresholds:

[SerializeField] Tilemap tilemap;
[SerializeField] TileBase water, sand, grass, forest, rock;
[SerializeField] int width = 200, height = 200;

void GenerateMap(int seed)
{
    var offset = new Vector2(seed % 10000, seed / 10000 % 10000);
    for (int x = 0; x < width; x++)
    for (int y = 0; y < height; y++)
    {
        float n = FractalNoise(x * 0.03f, y * 0.03f, 4, 0.5f, 2f, offset);
        n -= Falloff(x, y);                    // island shape
        TileBase t = n < 0.30f ? water : n < 0.35f ? sand : n < 0.55f ? grass : n < 0.70f ? forest : rock;
        tilemap.SetTile(new Vector3Int(x, y, 0), t);
    }
}

float Falloff(int x, int y)
{
    // 0 in the centre, rising toward 1 at the edges
    float nx = x / (float)width * 2f - 1f, ny = y / (float)height * 2f - 1f;
    float d = Mathf.Max(Mathf.Abs(nx), Mathf.Abs(ny));
    return Mathf.Pow(d, 3f) / (Mathf.Pow(d, 3f) + Mathf.Pow(2.2f - 2.2f * d, 3f));
}

Subtracting a falloff map guarantees water at the map edges, turning infinite noise into an island. For large maps, build arrays of positions and tiles and call tilemap.SetTiles once. Calling SetTile 40,000 times is slow. Rule Tiles then handle edges automatically (see our Tilemap and Rule Tile tutorial).

Caves With Cellular Automata

Noise makes landscapes. For caves and dungeons, cellular automata produce natural, connected-looking caverns:

bool[,] GenerateCave(int w, int h, int seed, float fill = 0.47f, int steps = 5)
{
    var rng = new System.Random(seed);
    var map = new bool[w, h];   // true = wall
    for (int x = 0; x < w; x++)
    for (int y = 0; y < h; y++)
        map[x, y] = x == 0 || y == 0 || x == w - 1 || y == h - 1 || rng.NextDouble() < fill;

    for (int s = 0; s < steps; s++)
    {
        var next = new bool[w, h];
        for (int x = 0; x < w; x++)
        for (int y = 0; y < h; y++)
        {
            int walls = 0;
            for (int dx = -1; dx <= 1; dx++)
            for (int dy = -1; dy <= 1; dy++)
            {
                int nx = x + dx, ny = y + dy;
                if (dx == 0 && dy == 0) continue;
                if (nx < 0 || ny < 0 || nx >= w || ny >= h || map[nx, ny]) walls++;
            }
            next[x, y] = walls > 4 || (map[x, y] && walls == 4);
        }
        map = next;
    }
    return map;
}

Start with random noise, then repeatedly apply “become a wall if most neighbours are walls”. After 4–6 steps the static becomes smooth caves. Use a flood fill afterwards to find disconnected pockets and either fill them in or carve tunnels to connect them, so the player can reach everything.

🌍 Generation taking forever?

Large worlds and repeated bakes are CPU-hungry. If your laptop is the bottleneck, our dev hardware guide covers machines with the cores for it.

See our laptop picks →

Chunks and Performance

An infinite world is generated in chunks (say 32×32 tiles or 64×64 m) around the player:

  • Track which chunk the player is in, and generate chunks within a radius. Unload or pool chunks that fall outside it.
  • Sample noise in world coordinates (chunkX * size + localX), not local ones, so chunk edges line up seamlessly.
  • Generate off the main thread with the Job System and Burst using Unity.Mathematics.noise. It’s often 10–50× faster than Mathf.PerlinNoise in a plain loop.
  • Spread work across frames: generate one chunk per frame, or await between chunks (see Awaitable vs coroutines).
  • Profile with the Unity Profiler before optimizing. Mesh building and collider generation usually cost more than the noise itself.

Troubleshooting

Perlin noise returns the same value everywhere

You’re passing whole numbers. Multiply coordinates by a scale like 0.05 so samples fall between integer grid points.

The world looks the same with every seed

The seed isn’t affecting the sample coordinates. Use it to generate offsets, and pass them into every noise call.

Noise looks blocky or stepped far from the origin

Floating-point precision loss at large coordinates. Keep offsets below about 100,000, or use double-precision noise for huge worlds.

Visible seams between chunks

Each chunk samples local coordinates or uses its own offset. Sample using global world coordinates with one shared offset.

Terrain looks like repeating blobs

Use more octaves, give each octave a different offset, and reshape heights with a curve or ridged noise.

FAQ

How do I use Perlin noise in Unity?

Call Mathf.PerlinNoise(x * scale, y * scale) for each point, where scale is a small number like 0.05. Combine several octaves for natural detail.

How do I seed Mathf.PerlinNoise?

It has no seed parameter. Generate random offsets from your seed with System.Random and add them to the sample coordinates.

Perlin or simplex noise?

Simplex (Unity.Mathematics noise.snoise) is faster, has fewer directional artifacts, and supports 3D. Mathf.PerlinNoise is fine for simple 2D use.

How do I make an island shape?

Subtract a falloff map that’s 0 in the centre and 1 at the edges from your noise, so the edges always end up as water.

Can I make 3D caves with noise?

Yes. Sample 3D noise per voxel and treat values below a threshold as empty, then build a mesh with marching cubes.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *