Saving and Loading a procedurally generated planet?

I quite new to preocedural generated and Saving & Loading. I have been trying to save and load a procedurally generated planet, but with no succes. I believe I can accomplish with some kind of “Seed system” but I am not sure how to implement that.

This is the code:

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

public class PlanetGenerator : MonoBehaviour
{
    public int Seed { get; set; }

    [Header("Base Settings:")]
    public float Radius;
    public int IcosphereSubdivisions;
    public Material PlanetMaterial;
    public bool SmoothNormals;
    public bool Rotate;
    public float TurnSpeed;
    public List<ColorSetting> Colors = new List<ColorSetting>();

    [Header("Oceans:")]
    public bool DrawShore;
    public float MinShoreWidth;
    public float MaxShoreWidth;

    [Header("Continents:")]
    public int MaxAmountOfContinents;
    public float ContinentsMinSize;
    public float ContinentsMaxSize;
    public float MinLandExtrusionHeight;
    public float MaxLandExtrusionHeight;

    [Header("Mountains:")]
    public float MaxAmountOfMountains;
    public float MountainBaseSize;
    public float MinMountainHeight;
    public float MaxMountainHeight;

    [Header("Bumpiness:")]
    public float MinBumpFactor;
    public float MaxBumpFactor;

    private GameObject planetGameObject;
    private MeshRenderer meshRenderer;
    private MeshFilter meshFilter;
    private Mesh planetMesh;

    private List<MeshTriangle> MeshTriangles = new List<MeshTriangle>();
    private List<Vector3> Vertices = new List<Vector3>();

    private TriangleHashSet oceans;
    private TriangleHashSet continents;
    private TriangleHashSet continentsSides;
    private TriangleHashSet mountains;

    private MeshCollider meshCollider;

    public bool generatePlanet = true;

    public void Start()
    {
        if(generatePlanet)
        {
            StartGeneration();
        }
    }

    public Color FindColor(string _name)
    {
        for(int i = 0; i < Colors.Count; i++)
        {
            if(Colors[i].name == _name)
            {
                return Colors[i].color;
            }
        }

        return Color.magenta;
    }

    public void StartGeneration()
    {
        planetGameObject = this.gameObject;
        planetGameObject.transform.parent = transform;

        if(meshRenderer == null)
        {
            meshRenderer = planetGameObject.AddComponent<MeshRenderer>();
            meshRenderer.material = PlanetMaterial;
        }

        if(meshFilter == null)
        {
            meshFilter = planetGameObject.AddComponent<MeshFilter>();
        }

        if (meshCollider == null)
        {
            meshCollider = planetGameObject.GetComponent<MeshCollider>();
            if (meshCollider == null)
            {
                meshCollider = planetGameObject.AddComponent<MeshCollider>();
            }
        }
       
        planetMesh = new Mesh();
        GenerateIcosphere();
        CalculateNeighbors();

        AddContinents();
        AddOceans();
        AddMountains();
        GenerateMesh();
        GenerateCollider();

        // Add a sphere collider to the planetGameObject
        //SphereCollider sphereCollider = planetGameObject.AddComponent<SphereCollider>();
        //sphereCollider.radius = Radius / 8.7f;
    }

    private void AddContinents()
    {
        continents = new TriangleHashSet();

        for(int i = 0; i < MaxAmountOfContinents; i++)
        {
            float continentSize = Random.Range(ContinentsMinSize, ContinentsMaxSize);
            TriangleHashSet addedLandmass = GetTriangles(Random.onUnitSphere, continentSize, MeshTriangles);
            Debug.Log("continetSize:" + continentSize);
            continents.UnionWith(addedLandmass);
        }
        continents.ApplyColor(FindColor("GrassColor"));

        continentsSides = Extrude(continents, Random.Range(MinLandExtrusionHeight,MaxLandExtrusionHeight));
        continentsSides.ApplyColor(FindColor("DirtColor"));

        foreach(MeshTriangle triangle in continents)
        {
            Vector3[] currentVerts = new Vector3[3];
            for(int i = 0; i < triangle.VertexIndices.Count; i++)
            {
                currentVerts[i] = Vertices[triangle.VertexIndices[i]];
            }
            AddBumpyness(currentVerts);
            for(int i = 0; i < triangle.VertexIndices.Count; i++)
            {
                Vertices[triangle.VertexIndices[i]] = currentVerts[i];
            }
        }
    }

    private void AddOceans()
    {
        oceans = new TriangleHashSet();

        foreach (MeshTriangle triangle in MeshTriangles)
        {
            if (!continents.Contains(triangle))
                oceans.Add(triangle);
        }

        TriangleHashSet ocean = new TriangleHashSet(oceans);
        ocean.ApplyColor(FindColor("OceanColor"));
        if(DrawShore)
        {
            TriangleHashSet shore;
            shore = Inset(ocean, Random.Range(MinShoreWidth,MaxShoreWidth));
            shore.ApplyColor(FindColor("ShoreColor"));

            shore = Extrude(ocean, -0.02f);
            shore.ApplyColor(FindColor("OceanColor"));
       
            shore = Inset(ocean, 0.02f);
            shore.ApplyColor(FindColor("OceanColor"));
        }
    }

    private void AddMountains()
    {
        TriangleHashSet sides;

        for(int i = 0; i < MaxAmountOfMountains; i++)
        {
            mountains = GetTriangles(Random.onUnitSphere, MountainBaseSize, continents);
            mountains.ApplyColor(FindColor("DirtColor"));
            continents.UnionWith(mountains);
            sides = Extrude(mountains,Random.Range(MinMountainHeight,MaxMountainHeight));
            sides.ApplyColor(FindColor("DirtColor"));

            mountains.ApplyColor(FindColor("HillColor"));
            mountains = GetTriangles(Random.onUnitSphere, MountainBaseSize * -.33f, continents); 
            continents.UnionWith(mountains);
            sides = Extrude(mountains,Random.Range(MinMountainHeight,MaxMountainHeight));
            mountains.ApplyColor(FindColor("HillColor"));

            mountains = GetTriangles(Random.onUnitSphere, MountainBaseSize * -.66f, continents);
            continents.UnionWith(mountains);
            sides = Extrude(mountains,Random.Range(MinMountainHeight,MaxMountainHeight));
            mountains.ApplyColor(FindColor("HillColor"));
        }
    }

    private void Update()
    {
        if(Rotate)
        {
            transform.Rotate(Vector3.up, TurnSpeed * Time.deltaTime);
        }
    }


    public void GenerateIcosphere()
    {
        this.transform.localScale = Vector3.one * Radius;
        MeshTriangles = new List<MeshTriangle>();
        Vertices = new List<Vector3>();

        float t = (1.0f + Mathf.Sqrt(5.0f)) / 2.0f;

        Vertices.Add(new Vector3(-1,  t,  0).normalized);
        Vertices.Add(new Vector3( 1,  t,  0).normalized);
        Vertices.Add(new Vector3(-1, -t,  0).normalized);
        Vertices.Add(new Vector3( 1, -t,  0).normalized);
        Vertices.Add(new Vector3( 0, -1,  t).normalized);
        Vertices.Add(new Vector3( 0,  1,  t).normalized);
        Vertices.Add(new Vector3( 0, -1, -t).normalized);
        Vertices.Add(new Vector3( 0,  1, -t).normalized);
        Vertices.Add(new Vector3( t,  0, -1).normalized);
        Vertices.Add(new Vector3( t,  0,  1).normalized);
        Vertices.Add(new Vector3(-t,  0, -1).normalized);
        Vertices.Add(new Vector3(-t,  0,  1).normalized);

        MeshTriangles.Add(new MeshTriangle( 0, 11,  5));
        MeshTriangles.Add(new MeshTriangle( 0,  5,  1));
        MeshTriangles.Add(new MeshTriangle( 0,  1,  7));
        MeshTriangles.Add(new MeshTriangle( 0,  7, 10));
        MeshTriangles.Add(new MeshTriangle( 0, 10, 11));
        MeshTriangles.Add(new MeshTriangle( 1,  5,  9));
        MeshTriangles.Add(new MeshTriangle( 5, 11,  4));
        MeshTriangles.Add(new MeshTriangle(11, 10,  2));
        MeshTriangles.Add(new MeshTriangle(10,  7,  6));
        MeshTriangles.Add(new MeshTriangle( 7,  1,  8));
        MeshTriangles.Add(new MeshTriangle( 3,  9,  4));
        MeshTriangles.Add(new MeshTriangle( 3,  4,  2));
        MeshTriangles.Add(new MeshTriangle( 3,  2,  6));
        MeshTriangles.Add(new MeshTriangle( 3,  6,  8));
        MeshTriangles.Add(new MeshTriangle( 3,  8,  9));
        MeshTriangles.Add(new MeshTriangle( 4,  9,  5));
        MeshTriangles.Add(new MeshTriangle( 2,  4, 11));
        MeshTriangles.Add(new MeshTriangle( 6,  2, 10));
        MeshTriangles.Add(new MeshTriangle( 8,  6,  7));
        MeshTriangles.Add(new MeshTriangle( 9,  8,  1));

        Subdivide();
    }

    public void Subdivide()
    {
        var midPointCache = new Dictionary<int, int>();

        for (int i = 0; i < IcosphereSubdivisions; i++)
        {
            var newPolys = new List<MeshTriangle>();
            foreach (var poly in MeshTriangles)
            {
                int a = poly.VertexIndices[0];
                int b = poly.VertexIndices[1];
                int c = poly.VertexIndices[2];

                int ab = GetMidPointIndex(midPointCache, a, b);
                int bc = GetMidPointIndex(midPointCache, b, c);
                int ca = GetMidPointIndex(midPointCache, c, a);

                newPolys.Add(new MeshTriangle(a, ab, ca));
                newPolys.Add(new MeshTriangle(b, bc, ab));
                newPolys.Add(new MeshTriangle(c, ca, bc));
                newPolys.Add(new MeshTriangle(ab, bc, ca));
            }
           
            MeshTriangles = newPolys;
        }
    }

    private void GenerateCollider()
    {
        Mesh mesh = new Mesh();
        mesh.vertices = Vertices.ToArray();
        mesh.triangles = GetTriangleIndices().ToArray();
        mesh.RecalculateNormals();

        meshCollider.sharedMesh = mesh;
    }

    public int GetMidPointIndex(Dictionary<int, int> cache, int indexA, int indexB)
    {
        int smallerIndex = Mathf.Min(indexA, indexB);
        int greaterIndex = Mathf.Max(indexA, indexB);
        int key = (smallerIndex << 16) + greaterIndex;

        // If a midpoint is already defined, just return it.

        int ret;
        if (cache.TryGetValue(key, out ret))
            return ret;

        // If we're here, it's because a midpoint for these two
        // vertices hasn't been created yet. Let's do that now!

        Vector3 p1 = Vertices[indexA];
        Vector3 p2 = Vertices[indexB];
        Vector3 middle = Vector3.Lerp(p1, p2, 0.5f).normalized;

        ret = Vertices.Count;
        Vertices.Add(middle);

        // Add our new midpoint to the cache so we don't have
        // to do this again.

        cache.Add(key, ret);
        return ret;
    }

    public void CalculateNeighbors()
    {
        foreach (MeshTriangle poly in MeshTriangles)
        {
            foreach (MeshTriangle other_poly in MeshTriangles)
            {
                if (poly == other_poly)
                    continue;

                if (poly.IsNeighbouring(other_poly))
                    poly.Neighbours.Add(other_poly);
            }
        }
    }

    public List<int> CloneVertices(List<int> old_verts)
    {
        List<int> new_verts = new List<int>();
        foreach (int old_vert in old_verts)
        {
            Vector3 cloned_vert = Vertices[old_vert];
            new_verts.Add(Vertices.Count);
            Vertices.Add(cloned_vert);
        }
        return new_verts;
    }

    public TriangleHashSet StitchPolys(TriangleHashSet polys, out BoarderHashSet stitchedEdge)
    {
        TriangleHashSet stichedPolys = new TriangleHashSet();

        stichedPolys.IterationIndex = Vertices.Count;

        stitchedEdge      = polys.CreateBoarderHashSet();
        var originalVerts = stitchedEdge.RemoveDublicates();
        var newVerts      = CloneVertices(originalVerts);

        stitchedEdge.Seperate(originalVerts, newVerts);

        foreach (TriangleBoarder edge in stitchedEdge)
        {
            // Create new polys along the stitched edge. These
            // will connect the original poly to its former
            // neighbor.

            var stitch_poly1 = new MeshTriangle(edge.OuterVertices[0],
                                           edge.OuterVertices[1],
                                           edge.InnerVertices[0]);
            var stitch_poly2 = new MeshTriangle(edge.OuterVertices[1],
                                           edge.InnerVertices[1],
                                           edge.InnerVertices[0]);
            // Add the new stitched faces as neighbors to
            // the original Polys.
            edge.InnerTriangle.UpdateNeighbour(edge.OuterTriangle, stitch_poly2);
            edge.OuterTriangle.UpdateNeighbour(edge.InnerTriangle, stitch_poly1);

            MeshTriangles.Add(stitch_poly1);
            MeshTriangles.Add(stitch_poly2);

            stichedPolys.Add(stitch_poly1);
            stichedPolys.Add(stitch_poly2);
        }

        //Swap to the new vertices on the inner polys.
        foreach (MeshTriangle poly in polys)
        {
            for (int i = 0; i < 3; i++)
            {
                int vert_id = poly.VertexIndices[i];
                if (!originalVerts.Contains(vert_id))
                    continue;
                int vert_index = originalVerts.IndexOf(vert_id);
                poly.VertexIndices[i] = newVerts[vert_index];
            }
        }

        return stichedPolys;
    }

    public TriangleHashSet Extrude(TriangleHashSet polys, float height)
    {
        BoarderHashSet stitchedEdge;
        TriangleHashSet stitchedPolys = StitchPolys(polys, out stitchedEdge);
        List<int> verts = polys.RemoveDublicates();

        // Take each vertex in this list of polys, and push it
        // away from the center of the Planet by the height
        // parameter.

        foreach (int vert in verts)
        {
            Vector3 v = Vertices[vert];
            v = v.normalized * (v.magnitude + height);
            Vertices[vert] = v;
        }

        return stitchedPolys;
    }

    public TriangleHashSet Inset(TriangleHashSet polys, float insetDistance)
    {
        BoarderHashSet stitchedEdge;
        TriangleHashSet stitchedPolys = StitchPolys(polys, out stitchedEdge);

        Dictionary<int, Vector3> inwardDirections = stitchedEdge.GetInwardDirections(Vertices);

        // Push each vertex inwards, then correct
        // it's height so that it's as far from the center of
        // the planet as it was before.

        foreach (KeyValuePair<int, Vector3> kvp in inwardDirections)
        {
            int     vertIndex       = kvp.Key;
            Vector3 inwardDirection = kvp.Value;

            Vector3 vertex = Vertices[vertIndex];
            float originalHeight = vertex.magnitude;

            vertex += inwardDirection * insetDistance;
            vertex  = vertex.normalized * originalHeight;
            Vertices[vertIndex] = vertex;
        }

        return stitchedPolys;
    }

    public TriangleHashSet GetTriangles(Vector3 center, float radius, IEnumerable<MeshTriangle> source)
    {
        TriangleHashSet newSet = new TriangleHashSet();

        foreach(MeshTriangle p in source)
        {
            foreach(int vertexIndex in p.VertexIndices)
            {
                float distanceToSphere = Vector3.Distance(center, Vertices[vertexIndex]);

                if (distanceToSphere <= radius)
                {
                    newSet.Add(p);
                    break;
                }
            }
        }

        return newSet;
    }

    public void GenerateMesh()
    {
        int vertexCount = MeshTriangles.Count * 3;

        int[] indices = new int[vertexCount];

        Vector3[] vertices = new Vector3[vertexCount];
        Vector3[] normals  = new Vector3[vertexCount];
        Color32[] colors   = new Color32[vertexCount];
        Vector2[] uvs      = new Vector2[vertexCount];

        for (int i = 0; i < MeshTriangles.Count; i++)
        {
            var poly = MeshTriangles[i];

            indices[i * 3 + 0] = i * 3 + 0;
            indices[i * 3 + 1] = i * 3 + 1;
            indices[i * 3 + 2] = i * 3 + 2;

            vertices[i * 3 + 0] = Vertices[poly.VertexIndices[0]];
            vertices[i * 3 + 1] = Vertices[poly.VertexIndices[1]];
            vertices[i * 3 + 2] = Vertices[poly.VertexIndices[2]];

            uvs[i * 3 + 0] = poly.UVs[0];
            uvs[i * 3 + 1] = poly.UVs[1];
            uvs[i * 3 + 2] = poly.UVs[2];

            colors[i * 3 + 0] = poly.Color;
            colors[i * 3 + 1] = poly.Color;
            colors[i * 3 + 2] = poly.Color;

            if(SmoothNormals)
            {
                normals[i * 3 + 0] = Vertices[poly.VertexIndices[0]].normalized;
                normals[i * 3 + 1] = Vertices[poly.VertexIndices[1]].normalized;
                normals[i * 3 + 2] = Vertices[poly.VertexIndices[2]].normalized;
            }
            else
            {
                Vector3 ab = Vertices[poly.VertexIndices[1]] - Vertices[poly.VertexIndices[0]];
                Vector3 ac = Vertices[poly.VertexIndices[2]] - Vertices[poly.VertexIndices[0]];

                Vector3 normal = Vector3.Cross(ab, ac).normalized;

                normals[i * 3 + 0] = normal;
                normals[i * 3 + 1] = normal;
                normals[i * 3 + 2] = normal;
            }
        }

        planetMesh.vertices = vertices;
        planetMesh.normals  = normals;
        planetMesh.colors32 = colors;
        planetMesh.uv       = uvs;

        planetMesh.SetTriangles(indices, 0);

        meshFilter.mesh = planetMesh;

    }

    private List<int> GetTriangleIndices()
    {
        List<int> triangleIndices = new List<int>();
        foreach (MeshTriangle triangle in MeshTriangles)
        {
            triangleIndices.Add(triangle.VertexIndices[0]);
            triangleIndices.Add(triangle.VertexIndices[1]);
            triangleIndices.Add(triangle.VertexIndices[2]);
        }
        return triangleIndices;
    }

    Vector3[] AddBumpyness(Vector3[] verts) {
       Dictionary<Vector3, List<int>> dictionary = new Dictionary<Vector3, List<int>>();
       for (int x = 0; x < verts.Length; x++) {
          if (!dictionary.ContainsKey(verts[x])) {
               dictionary.Add(verts[x], new List<int>());
           }
           dictionary[verts[x]].Add(x);
       }
       foreach (KeyValuePair<Vector3, List<int>> pair in dictionary) {
         Vector3 newPos = pair.Key * Random.Range(MinBumpFactor, MaxBumpFactor);
           foreach (int i in pair.Value) {
               verts[i] = newPos;
          }
       }
        return verts;
    }
}

This line: public int Seed { get; set; } is no longer in the code. Forgot to delete it when I uploaded the code.

What does this mean?

How to report your problem productively in the Unity3D forums:

http://plbm.com/?p=220

This is the bare minimum of information to report:

  • what you want
  • what you tried
  • what you expected to happen
  • what actually happened, log output, variable values, and especially any errors you see
    - links to documentation you used to cross-check your work (CRITICAL!!!)

Otherwise, break down the problem. You won’t find it by posting 550+ lines of random procgen code with the comment “no success.”

If you want to do seed-based regeneration, check out youtube tutorials for using random seeds. As this is software engineering, this method has both advantages and drawbacks. Understand what those are by working through the tutorials, such as these:

9123874--1266199--Screen Shot 2023-07-04 at 8.51.06 AM.jpg

Tutorials and example code are great, but keep this in mind to maximize your success and minimize your frustration:

How to do tutorials properly, two (2) simple steps to success:

Step 1. Follow the tutorial and do every single step of the tutorial 100% precisely the way it is shown. Even the slightest deviation (even a single character!) generally ends in disaster. That’s how software engineering works. Every step must be taken, every single letter must be spelled, capitalized, punctuated and spaced (or not spaced) properly, literally NOTHING can be omitted or skipped.
Fortunately this is the easiest part to get right: Be a robot. Don’t make any mistakes.
BE PERFECT IN EVERYTHING YOU DO HERE!!

If you get any errors, learn how to read the error code and fix your error. Google is your friend here. Do NOT continue until you fix your error. Your error will probably be somewhere near the parenthesis numbers (line and character position) in the file. It is almost CERTAINLY your typo causing the error, so look again and fix it.

Step 2. Go back and work through every part of the tutorial again, and this time explain it to your doggie. See how I am doing that in my avatar picture? If you have no dog, explain it to your house plant. If you are unable to explain any part of it, STOP. DO NOT PROCEED. Now go learn how that part works. Read the documentation on the functions involved. Go back to the tutorial and try to figure out WHY they did that. This is the part that takes a LOT of time when you are new. It might take days or weeks to work through a single 5-minute tutorial. Stick with it. You will learn.

Step 2 is the part everybody seems to miss. Without Step 2 you are simply a code-typing monkey and outside of the specific tutorial you did, you will be completely lost. If you want to learn, you MUST do Step 2.

Of course, all this presupposes no errors in the tutorial. For certain tutorial makers (like Unity, Brackeys, Imphenzia, Sebastian Lague) this is usually the case. For some other less-well-known content creators, this is less true. Read the comments on the video: did anyone have issues like you did? If there’s an error, you will NEVER be the first guy to find it.

Beyond that, Step 3, 4, 5 and 6 become easy because you already understand!

Finally, when you have errors, don’t post here… just go fix your errors! Here’s how:

Remember: NOBODY here memorizes error codes. That’s not a thing. The error code is absolutely the least useful part of the error. It serves no purpose at all. Forget the error code. Put it out of your mind.

The complete error message contains everything you need to know to fix the error yourself.

The important parts of the error message are:

  • the description of the error itself (google this; you are NEVER the first one!)
  • the file it occurred in (critical!)
  • the line number and character position (the two numbers in parentheses)
  • also possibly useful is the stack trace (all the lines of text in the lower console window)

Always start with the FIRST error in the console window, as sometimes that error causes or compounds some or all of the subsequent errors. Often the error will be immediately prior to the indicated line, so make sure to check there as well.

Look in the documentation. Every API you attempt to use is probably documented somewhere. Are you using it correctly? Are you spelling it correctly?

All of that information is in the actual error message and you must pay attention to it. Learn how to identify it instantly so you don’t have to stop your progress and fiddle around with the forum.