Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Speeding Up Procedural Generation

Discussion in 'Scripting' started by DoggyBoggy, Mar 7, 2020.

  1. DoggyBoggy

    DoggyBoggy

    Joined:
    Jan 19, 2018
    Posts:
    20
    so, im making a terraria-like game which has procedurally generated tilemaps and i have this script attatched to a grid:

    Code (CSharp):
    1. using UnityEngine;
    2. using AccidentalNoise;
    3. using System.Collections.Generic;
    4. using UnityEngine.Tilemaps;
    5. using System;
    6.  
    7. public class CompileTerrain : MonoBehaviour
    8. {
    9.  
    10.     public TileBase dirtTile;
    11.     public TileBase grassTile;
    12.     public TileBase stoneTile;
    13.  
    14.     public List<GameObject> fractalLayers = new List<GameObject>();
    15.  
    16.     public Tilemap grid;
    17.     public int width;
    18.     public int height;
    19.  
    20.     public float seed;
    21.     public int caveSmoothness = 2;
    22.  
    23.     void Start()
    24.     {
    25.         grid.ClearAllTiles();
    26.  
    27.         int touchCount = 0;
    28.         Vector3Int newPos;
    29.         double nx, ny;
    30.  
    31.         ModuleBase combinedTerrain = CavesAndMountains((uint)seed);
    32.         List<Vector3Int> terrainCoords = new List<Vector3Int>();
    33.         SMappingRanges ranges = new SMappingRanges();
    34.  
    35.         for (int x = 0; x < width; x++)
    36.         {
    37.             for (int y = 0; y < height; y++)
    38.             {
    39.                 nx = (ranges.mapx0 + ((double)x / (double)width) * (ranges.mapx1 - ranges.mapx0)) * 3;
    40.                 ny = (ranges.mapy0 + ((double)y / (double)height) * (ranges.mapy1 - ranges.mapy0)) * 3;
    41.  
    42.                 if (combinedTerrain.Get(nx, ny) > 0f)
    43.                 {
    44.                     terrainCoords.Add(new Vector3Int(x, height - y, 0));
    45.                 }
    46.             }
    47.         }
    48.  
    49.         List<Tuple<int, int>> neighbors = new List<Tuple<int, int>>() {Tuple.Create(1, 1), Tuple.Create(-1, -1),
    50.                                                                        Tuple.Create(0, 1), Tuple.Create(1, 0),
    51.                                                                        Tuple.Create(0, -1), Tuple.Create(-1, 0),
    52.                                                                        Tuple.Create(-1, 1), Tuple.Create(1, -1)};
    53.  
    54.         for (int index = 0; index < terrainCoords.Count; index++)
    55.         {
    56.             if (index == terrainCoords.Count)
    57.             {
    58.                 break;
    59.             }
    60.  
    61.             touchCount = 0;
    62.  
    63.             for (int posAdd = 0; posAdd < neighbors.Count; posAdd++)
    64.             {
    65.                 newPos = new Vector3Int(terrainCoords[index].x + neighbors[posAdd].Item1, terrainCoords[index].y + neighbors[posAdd].Item2, 0);
    66.                 touchCount += terrainCoords.Contains(newPos) ? 1 : 0;
    67.             }
    68.  
    69.             if (touchCount < 2)
    70.             {
    71.                 terrainCoords.Remove(terrainCoords[index]);
    72.             }
    73.  
    74.         }
    75.  
    76.         for (int j = 0; j < caveSmoothness; j++)
    77.         {
    78.             for (int x = 0; x < width; x++)
    79.             {
    80.                 for (int y = 0; y < height; y++)
    81.                 {
    82.                     if (!terrainCoords.Contains(new Vector3Int(x, y, 0)))
    83.                     {
    84.                         touchCount = 0;
    85.  
    86.                         for (int posAdd = 0; posAdd < neighbors.Count; posAdd++)
    87.                         {
    88.                             newPos = new Vector3Int(x + neighbors[posAdd].Item1, y + neighbors[posAdd].Item2, 0);
    89.                             touchCount += terrainCoords.Contains(newPos) ? 1 : -1;
    90.                         }
    91.  
    92.                         if (touchCount > 1)
    93.                         {
    94.                             terrainCoords.Add(new Vector3Int(x, y, 0));
    95.                         }
    96.  
    97.                     }
    98.  
    99.                 }
    100.  
    101.             }
    102.         }
    103.  
    104.         foreach (Vector3Int blck in terrainCoords)
    105.         {
    106.             grid.SetTile(blck, stoneTile);
    107.         }
    108.  
    109.         terrainCoords.Sort((x, y) => x.x == y.x ? x.y.CompareTo(y.y) : x.x.CompareTo(y.x));
    110.         terrainCoords.Reverse();
    111.  
    112.         TileBase selectedTile;
    113.         int depth = 0;
    114.         int lastx = 0;
    115.         int lasty = terrainCoords[0].y + 1;
    116.  
    117.         foreach (Vector3Int blck in terrainCoords)
    118.         {
    119.             depth = blck.x != lastx ? 0 : depth;
    120.             lasty = blck.x != lastx ? blck.y + 1 : lasty;
    121.  
    122.             selectedTile = depth < 4 ? grassTile : stoneTile;
    123.             selectedTile = 3 < depth && depth < 30 ? dirtTile : selectedTile;
    124.  
    125.             grid.SetTile(blck, selectedTile);
    126.  
    127.             lastx = blck.x;
    128.             depth += lasty - blck.y;
    129.             lasty = blck.y;
    130.         }
    131.  
    132.         int layerNum = 1;
    133.         List<Vector3Int> posList = new List<Vector3Int>();
    134.  
    135.         foreach (GameObject layer in fractalLayers)
    136.         {
    137.             GetPerlinLayer component = layer.GetComponent<GetPerlinLayer>();
    138.  
    139.             for (int k = 0; k < component.populateCount; k++)
    140.             {
    141.                 layerNum++;
    142.                 foreach(Vector3Int pos in component.GetFractalCoords(width, height, (uint)(seed * layerNum)))
    143.                     if(grid.GetTile(pos) != null && grid.GetTile(pos) != grassTile)
    144.                     {
    145.                         grid.SetTile(pos, component.defaultTile);
    146.                     }
    147.             }
    148.         }
    149.     }
    150.  
    151.     public static ModuleBase CavesAndMountains(uint seed)
    152.     {
    153.         AccidentalNoise.Gradient ground_gradient = new AccidentalNoise.Gradient(0, 0, 0, 1);
    154.  
    155.         // lowlands
    156.         Fractal lowland_shape_fractal = new Fractal(FractalType.BILLOW, BasisTypes.GRADIENT, InterpTypes.QUINTIC, 2, 0.25, seed);
    157.         AutoCorrect lowland_autocorrect = new AutoCorrect(lowland_shape_fractal, 0, 1);
    158.         ScaleOffset lowland_scale = new ScaleOffset(0.125, -0.45, lowland_autocorrect);
    159.         ScaleDomain lowland_y_scale = new ScaleDomain(lowland_scale, null, 0);
    160.         TranslatedDomain lowland_terrain = new TranslatedDomain(ground_gradient, null, lowland_y_scale);
    161.  
    162.         // highlands
    163.         Fractal highland_shape_fractal = new Fractal(FractalType.FBM, BasisTypes.GRADIENT, InterpTypes.QUINTIC, 4, 2, seed);
    164.         AutoCorrect highland_autocorrect = new AutoCorrect(highland_shape_fractal, -1, 1);
    165.         ScaleOffset highland_scale = new ScaleOffset(0.25, 0, highland_autocorrect);
    166.         ScaleDomain highland_y_scale = new ScaleDomain(highland_scale, null, 0);
    167.         TranslatedDomain highland_terrain = new TranslatedDomain(ground_gradient, null, highland_y_scale);
    168.  
    169.         // mountains
    170.         Fractal mountain_shape_fractal = new Fractal(FractalType.RIDGEDMULTI, BasisTypes.GRADIENT, InterpTypes.QUINTIC, 8, 1, seed);
    171.         AutoCorrect mountain_autocorrect = new AutoCorrect(mountain_shape_fractal, -1, 1);
    172.         ScaleOffset mountain_scale = new ScaleOffset(0.3, 0.15, mountain_autocorrect);
    173.         ScaleDomain mountain_y_scale = new ScaleDomain(mountain_scale, null, 0.15);
    174.         TranslatedDomain mountain_terrain = new TranslatedDomain(ground_gradient, null, mountain_y_scale);
    175.  
    176.         // terrain
    177.         Fractal terrain_type_fractal = new Fractal(FractalType.FBM, BasisTypes.GRADIENT, InterpTypes.QUINTIC, 3, 0.125, seed);
    178.         AutoCorrect terrain_autocorrect = new AutoCorrect(terrain_type_fractal, 0, 1);
    179.         ScaleDomain terrain_type_y_scale = new ScaleDomain(terrain_autocorrect, null, 0);
    180.         AccidentalNoise.Cache terrain_type_cache = new AccidentalNoise.Cache(terrain_type_y_scale);
    181.         Select highland_mountain_select = new Select(terrain_type_cache, highland_terrain, mountain_terrain, 0.55, 0.2);
    182.         Select highland_lowland_select = new Select(terrain_type_cache, lowland_terrain, highland_mountain_select, 0.25, 0.15);
    183.         AccidentalNoise.Cache highland_lowland_select_cache = new AccidentalNoise.Cache(highland_lowland_select);
    184.         Select ground_select = new Select(highland_lowland_select_cache, 0, 1, 0.5, null);
    185.  
    186.         // caves
    187.         Fractal cave_shape = new Fractal(FractalType.RIDGEDMULTI, BasisTypes.GRADIENT, InterpTypes.QUINTIC, 1, 4, seed);
    188.         Bias cave_attenuate_bias = new Bias(highland_lowland_select_cache, 0.65);
    189.         Combiner cave_shape_attenuate = new Combiner(CombinerTypes.MULT, cave_shape, cave_attenuate_bias);
    190.         Fractal cave_perturb_fractal = new Fractal(FractalType.FBM, BasisTypes.GRADIENT, InterpTypes.QUINTIC, 6, 3, seed);
    191.         ScaleOffset cave_perturb_scale = new ScaleOffset(0.5, 0, cave_perturb_fractal);
    192.         TranslatedDomain cave_perturb = new TranslatedDomain(cave_shape_attenuate, cave_perturb_scale, null);
    193.         Select cave_select = new Select(cave_perturb, 1, 0, 0.75, 0);
    194.  
    195.         return new Combiner(CombinerTypes.MULT, cave_select, ground_select) as ModuleBase;
    196.     }
    197. }
    which i have so graciously borrowed and modified from the fine folks at accidental noise, and i made an empty gameobject which i attached this script to:

    Code (CSharp):
    1. using UnityEngine;
    2. using AccidentalNoise;
    3. using System.Collections.Generic;
    4. using UnityEngine.Tilemaps;
    5.  
    6. public class GetPerlinLayer : MonoBehaviour
    7. {
    8.  
    9.     public TileBase defaultTile;
    10.     public float threshold = 0.5f;
    11.     public int populateCount = 5;
    12.  
    13.     public List<Vector3Int> GetFractalCoords(int width, int height, uint seed)
    14.     {
    15.         double nx, ny;
    16.  
    17.         ModuleBase combinedTerrain = new Fractal(FractalType.FBM, BasisTypes.GRADIENT, InterpTypes.QUINTIC, 6, 2, seed);
    18.         List<Vector3Int> fractalCoords = new List<Vector3Int>();
    19.         SMappingRanges ranges = new SMappingRanges();
    20.  
    21.         for (int x = 0; x < width; x++)
    22.         {
    23.             for (int y = 0; y < height; y++)
    24.             {
    25.                 nx = (ranges.mapx0 + ((double)x / (double)width) * (ranges.mapx1 - ranges.mapx0)) * 3;
    26.                 ny = (ranges.mapy0 + ((double)y / (double)height) * (ranges.mapy1 - ranges.mapy0)) * 3;
    27.  
    28.                 if (combinedTerrain.Get(nx, ny) > threshold)
    29.                 {
    30.                     fractalCoords.Add(new Vector3Int(x, height - y, 0));
    31.                 }
    32.             }
    33.         }
    34.  
    35.         return fractalCoords;
    36.     }
    37.  
    38. }
    and i attached different colored square sprites for each of those gameobjects, and saved them as a prefab. Once i had that prefab, i attatched that to the
    fractalLayers
    list in my previous script to generate ores. And although it runs fine on a lower scale, I cant run it on a larger scale. And since there's no cure-all for making code run faster (aside from refactoring, which i don't know how to do), and i probably couldve made parts of my code more efficient since im a novice, i would really like some insight from the eyes of a proffesional on how to make my code run better. I know i didnt explain everything about my project but its really just a barebones project those are the only scripts and unique parts about it, you can just infer what i did and fill in the blanks. Any help is appreciated. Thank you! =)
     
  2. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    first of all
    • chunking is your friend. sadly it's that kind of friend that kinda attaches to your leg and won't let go.

    second of all
    • you want a dedicated chunk generator that runs really fast
    • you want a chunk manager that is able to cache the chunks, occlude them with the input from camera, help the pathfinder to be able to do its job, and generally make chunk-generating requests
    • if you really need more speed, make a chunk generator that runs on separate threads
    • if you need persistent data but not entire chunks sitting in memory, consider caching what is crucial about them in tiny textures
    • in general textures can also be used for overlays, because once baked, you can quickly render them on the GPU just by uploading them
    • if you really need just data, consider how would you make connectivity graphs between the chunks for the pathfinding system afterwards
    • consider line of sight problems, do the sun rays affect your underground world? etc

    thirdly, well it seems that you're doing the generation part properly
    • you want to model all kinds of noise, try looking for faster algorithms, perlin is ok, simplex is much slower, don't get into extravagant noises, it's just an overkill, try to roll something of your own perhaps, look up fast hash functions as well
    • try to really harness the power of combining a fast, seeded PRNG and hash functions, that's the fastest and most manageable thing you can throw in; obviously area noise has its uses, don't throw that out just yet
     
    hippocoder likes this.
  3. DoggyBoggy

    DoggyBoggy

    Joined:
    Jan 19, 2018
    Posts:
    20
    While this all sounds like really good information, and most of it I understand, I could really only get myself to work on some of it right now due to my lack of knowledge in those fields. There's just so much stuff out there, and along with your recommendations which I will read more about so I can come back with an educated mind to internalize more, I would really love if you could reference me some general videos/articles to point me in the right direction. Thank you for you in depth reply as well! =)
     
  4. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    start with that link and check out unity videos on the subject.
     
  5. DoggyBoggy

    DoggyBoggy

    Joined:
    Jan 19, 2018
    Posts:
    20
    all of the unity videos that show for that link talk about infinite world generation, while i only want a finite amount of complex terrain.
     
  6. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    if you've run into a performance bottleneck, you could as well make the world infinite.
     
  7. DoggyBoggy

    DoggyBoggy

    Joined:
    Jan 19, 2018
    Posts:
    20
    oh, i thought making the world infinite would slow down the generation
     
  8. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    obviously that's what chunking is for.

    chunk is the right "bite size" for whatever random spatial algorithm you're trying to make.

    when you get to a point where your world is too big, you're likely to experience an amount of lag disproportional to the amount of new material you're introducing to the system.

    this is because you're increasing the bandwidth of the application, and not just its memory footprint. in other words, the slow down will occur in a non-linear fashion, and this is not only because 2D or 3D worlds tend to grow exponentially: you'll find this is the case with only increasing the size in one dimension as well, it will still slow down exponentially. even with 1 tile high worlds.

    there are numerous technical reasons why is this the case, mostly it's because of memory latency, but also because many low-level counters tend to grow in size, and this will add up, also many unity features will turn off if you make your objects too big, but let's not go in there. it doesn't matter.

    chunks solve this by introducing a predictable "bite size" and from that point on you can simply manage them and make sure they're stitched together in a controllable manner, like you normally do with tiles. chunks are simply units of higher organizational order. if your tiles are 128x128 pixels, imagine your chunks being 64x64 tiles. imagine your world being 64x64 chunks. this is how you manage big structures and big worlds in general. (that's 524,288x524,288 pixels btw, and this is not something unprecedented in size; but imagine a texture that big, that's 275 gigapixels 16 gigapixels (edit: still mindboggling even if I don't know what a gigapixel is) no I was close the first time: 256 gigapixels!)
     
    Last edited: Mar 8, 2020