Help with procedural terrain efficiency.

Hello!

So for the past few hours I’ve been working on a 2D procedural terrain system (Basically trying to clone the terraria terrain generation). However I quickly encountered a problem that I should’ve seen coming.


As you can see it can generate a fairly simple terrain currently. However the problem is with efficiently, I quickly discovered that there is no way my generation script will be able to handle generating a terraria sized world (I tried and Unity ran out of memory). So I was wondering if there was an easy shortcut to solve my problem. The terrain generation currently generates the world in chunks so I was wondering if I should make it so that the chunk is generated, serialized and deleted so that it can be loaded up when it needs to be rendered. Another problem is that it takes waaaay too long to generate even a world this size (10 seconds or so, which may not seem like much but when I want to generate a much larger terrain with biomes it is going to take forever).

Here’s my current script:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[System.Serializable]
public class Tile
{
    public GameObject tile; // The tile you want to spawn
    public float pernlinMultiplier; // how big the veins are
    public float popThreshold; // The chance of it spawning (the lower the more common and the larger the groups)
    public int seed;
}

[System.Serializable]
public class Biome
{
    public string name;
    public GameObject baseTile; // This will be the main tile in the biome
    public int rarity; // Rarity of the biome
    public List<Tile> tiles = new List<Tile>(); // List of tiles that can spawn in the biome
    public int maxHeight; // Max height that the biome can spawn at
    public int minHeight; // Min height the biome can spawn at
}
public class Terraingen : MonoBehaviour
{
    public int worldSeed;

    public int chunkWidth; // Width of the rendering chunks
    public int chunkDepth; // Depth of the rendering chunks

    public int worldWidth;
    public int worldDepth;

    public int biomeMaxSize;
    public int biomeMinSize;

    public float hillMultiplier; // Hill depth
    public float hillSample; // Hill closeness
    public int hillSeed;

    public float perlinMultiplier; // Basically determines the size of caves

    public float popThreshold = 0.5f; // Chance of caves, the smaller the more common they are

    public List<Biome> biomes = new List<Biome>();

    public List<GameObject> chunks = new List<GameObject>();

    void Start ()
    {
        Generate();
    }

    void Generate()
    {
        hillSeed = worldSeed;

        foreach (Biome b in biomes)
        {
            for (int i = 0; i < b.tiles.Count; ++i)
            {
                b.tiles[i].seed = worldSeed * (i + 1);
            }
        }

        for (int i = 0; i < Mathf.Round(worldWidth / chunkWidth); ++i) // Figure out how many chunks will fit in the world width wise
        {
            for (int i1 = 0; i1 < Mathf.Round(worldDepth / chunkDepth); ++i1) // figure out how many chunks will fit in the world height wise
            {
                GenerateChunk(i * chunkWidth, i1 * chunkDepth); // Spawn the chunks according to the world size vs chunk size
            }
        }
    }

    void GenerateChunk(int x, int y)
    {
        GameObject chunk = new GameObject(); // Instantiate the chunk parent
        chunk.name = "Chunk";
        chunk.AddComponent<BoxCollider2D>();
        BoxCollider2D bc = chunk.GetComponent<BoxCollider2D>();
        bc.offset = new Vector2(chunkWidth / 2, chunkDepth / 2);
        bc.size = new Vector2(chunkWidth, chunkDepth);
        chunk.transform.position = new Vector3(x, y, 1);
        for (int i = 0; i < chunkWidth; ++i) // Cycle through each row of the chunk
        {
            for (int i1 = 0; i1 < chunkDepth; ++i1) // Cycle through each column of the chunk
            {
                if (!((i1 + y) > worldDepth - (Mathf.PerlinNoise(x + i / hillSample, hillSeed) * hillMultiplier))) // This part is to generate hills
                {
                    PlaceTile(i + x, i1 + y, chunk, GetOccupyingTile(i + x, i1 + y, 0));
                }
            }
        }
        chunks.Add(chunk);
    }

    GameObject GetOccupyingTile(int x, int y, int biome)
    {
        GameObject tile = biomes[biome].baseTile;
        foreach (Tile t in biomes[biome].tiles)
        {
            float val = Mathf.PerlinNoise((x + t.seed) * t.pernlinMultiplier, (y + t.seed) * t.pernlinMultiplier);
            if (val >= t.popThreshold)
            {
                tile = t.tile;
            }
        }
        return tile;
    }

    void PlaceTile(int x, int y, GameObject chunk, GameObject tile)
    {
        GameObject tileToPlace = (GameObject)GameObject.Instantiate(tile, new Vector3(x, y, 1), Quaternion.identity);
        tileToPlace.transform.parent = chunk.transform;
    }
}

Any help or suggestions would be appreciated,

Many thanks,

Aiden Leeming

What it comes down to is that you can’t have a separate GameObject for every tile. There isn’t really any easy way to solve it (well, aside from just buying an asset to take care of it for you). One possibility is to create meshes using the Mesh class, but what I did with SpriteTile is to create only enough sprites to fill the screen, and recycle them as the camera moves. You can see with this demo that you can procedurally generate a million tiles (using Perlin noise similar to what you did, in fact) in a small fraction of a second; also it doesn’t use much RAM (~7MB in this case).

–Eric

1 Like

Oh wow, that’s actually a really good idea, thanks I’ll give it a shot