0
\$\begingroup\$

So, I have a procedural mesh generation system with chunks that load in and generate as the player walks into a nearby cell, and get removed when the player walks out. When each set of 9 cells is getting generated, this takes around 1500 ms. When they are being generated, the game thus freezes. Is there a way to make the game NOT freeze and allow the player to continue doing things in their current chunk while the area is being generated? Like having the terrain gen happening in the background? EDIT- From @DmGregory- the ob struct seems like it would work, but I am a bit confused as to whether or not I can include all of mymesh geneartion and update function in it,or if I need to find a way to pass out the mesh and verts from the job. What is the best way to do this?

I haven't fully started writing code in the struct as i am unsure of where to begin in terms of the mesh update system. Essentially, my code is as follows- In a for loop, it generates vertices, and for every one of these vertices, it uses a noise function to generate height. It then enters another for loop that connects the verts together via triangles. Without the noise function, it essentially forms a plane. The noise function is almost certainly the culprit for the lag, as it gets repeated 65025 times per chunk. I do want to convert the entire gen function into a job, however.

EDIT 2- I am using the coroutine system suggested by @DMGregory, and so have written my coroutine mesh gen as such. It is a combination of my update mesh and mesh generation methods. This now DOES NOT freeze the game, but fails to generate the mesh at all, however, the game is smooth. This is what the code looks like for the co-routine-

IEnumerator meshGen(){ 
       
        vertices = new Vector3[(xSize + 1) * (zSize + 1)];
    
   for (int i=0, z = 0; z <= zSize; z++){
       for(int x = 0; x <= xSize; x++){

          
           vertices[i] = new Vector3(x, generateNoise(x, z), z);
           i++;
           yield return null;
       }
   }

   triangles = new int[6 * xSize * zSize];

    int vert = 0;

    int tris = 0;

    for( int z = 0; z < zSize; z ++){
        for(int x = 0; x < xSize; x++)
        {
         triangles[0 + tris] = vert + 0;
         triangles[1 + tris] = vert + xSize + 1;
         triangles[2 + tris] = vert + 1;
            triangles[3 + tris] = vert + 1;
         triangles[4 + tris] = vert + xSize + 1;
         triangles[5 + tris] = vert + xSize + 2;

            vert ++;

            tris +=6;
         }
         vert ++;
    }

    mesh.Clear();
    mesh.vertices = vertices;
    mesh.triangles = triangles;
    
    mesh.RecalculateNormals();

    
    }

And this is what the code looks like for the generateNoie(x, z) function-

float generateNoise(float x, float z){
    
    //float k = (-1 * noise.Evaluate(new Vector3(x * 0.01f, 0, z * 0.01f)));
    float k = (-1 * evalNoise(x, z, 0.01f));
    k = Mathf.Pow(k, 2);
            
  //  float ty = Mathf.Clamp(1 - Mathf.Abs(noise.Evaluate(new Vector3(x * 0.015f, 0, z * 0.015f))), 0, 1);
  float ty = Mathf.Clamp(1 - Mathf.Abs(evalNoise(x, z, 0.015f)), 0, 1);


    //float y = (1 - Mathf.Abs(noise.Evaluate(new Vector3(x * 0.03f, 0, z * 0.03f)))) * 5f + 5f;
    float y = (1 - Mathf.Abs(evalNoise(x, z, 0.03f))) * 5f + 5f;

    y*= ty;

    y *= k;

   y -= (1 - k) * 1;

   float freq = 1;
   float ampl = 1;
    if(y > CutOffHeight && y < maxCutOffHeight){
        for(int i =1; i <= octaves; i++){
            freq *= 2;
            ampl *= 0.5f;
            //y += noise.Evaluate(new Vector3((x * freq),0, (z*freq)/octaves)) * (ty * 2) * k * ampl;
            y += evalNoise(x,z, freq) * (ty * 2) * k * ampl;
      //print(i);
    }
    }
           
   // y -= 1- Mathf.Abs(noise.Evaluate(new Vector3(x * 0.05f, 0, z * 0.05f))) * ty;
   y -= 1- Mathf.Abs(evalNoise(x,z,0.05f)) * ty;
    
    //float uy = Mathf.Clamp(Mathf.Pow(noise.Evaluate(new Vector3(x * 0.01f, 0, z * 0.01f)), 2), 0, 1);
    float uy = Mathf.Clamp(Mathf.Pow(evalNoise(x,z,0.01f), 2), 0, 1);
    
    y+= uy;

    y -= ty;
    
    y *= 2f;

    

    float r = evalNoise(x,z, 0.002f);

    y *= 1- Mathf.Clamp(Mathf.Pow(r, 3), 0, 1);

   
    
    if(y <= IslandHeight){
        float tempGrad = Mathf.Abs(y - IslandHeight);
        y -= Mathf.Abs(tempGrad);
    }
         
    return y;
}
\$\endgroup\$
4
  • \$\begingroup\$ Are you looking for how not to freeze the main thread in Unity? Or perhaps using the asynchronous job system? \$\endgroup\$ – DMGregory Jul 17 '20 at 12:07
  • \$\begingroup\$ @DMGregory Thanks, added the code! I was able to make some slight tweaks in terms of placement, now it no longer freezes, but instead fails to generate code. \$\endgroup\$ – Spaceguy99 Jul 20 '20 at 0:36
  • \$\begingroup\$ It looks like you're yielding a frame for every vertex. That means for a 1000x1000 map, we'd expect this generator to run for a million frames before producing visible output, or a little over four and a half hours at 60 fps. Did you wait long enough for a map of your size to finish to confirm that no generation occurred? Or did you maybe want to be a bit more sparing with your yields, so you do a bigger chunk of work each frame and finish in a more reasonable number of frames? \$\endgroup\$ – DMGregory Jul 20 '20 at 11:52
  • \$\begingroup\$ @DMGregory, Well my problem is, where can I put the Yield Returns that DOESN'T do that. midway through the vertex calculation? \$\endgroup\$ – Spaceguy99 Jul 20 '20 at 15:50
0
\$\begingroup\$

If you want to keep using your coroutine approach, you'll want to the coroutine do more work each time it takes a turn, so that it finishes after a smaller number of frames.

How much work is enough to finish in a decent amount of time, but small enough that it won't cause a noticeable stutter? That might vary depending on the hardware, or the current load on the system. So one common trick to use here is to allocate it a time budget, and do as much as you can within that limit.

IEnumerator MeshGen(){          
        // Set up a clock to watch how long we're running.
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();

        float millisecondBudget = 3.0f; // Adjust this to suit your needs.
        long tickBudget = (long)(System.Diagnostics.Stopwatch.Frequency
                          * millisecondBudget/1000f);

        vertices = new Vector3[(xSize + 1) * (zSize + 1)];

        for (int i=0, z = 0; z <= zSize; z++){
            for(int x = 0; x <= xSize; x++){              
                vertices[i] = new Vector3(x, generateNoise(x, z), z);
                i++;
            }

            // After finishing each line of vertices, check if we're running overtime:
            if(stopwatch.ElapsedTicks > tickBudget) {
                // If so, let the game run for a frame, and resume the work next frame.
                yield return null;
                stopwatch.Restart();
            }
        }

You can pepper similar yields in other places where your method takes a long time in your testing.

One potential area of risk is mesh.RecalculateNormals(); which has to run to completion - we don't have an opportunity to inject yields midway through that function. If you find that this line alone takes longer than your budget for a frame, and causes a noticeable hitch, then you may want to consider generating your normals as you go. Many noise functions can be adapted to return not just a noise value, but also an analytic or partial derivative in a single pass, letting you assign the normals one-by-one in the same code that generates your vertex positions, letting you split the work into bite-sized chunks that won't cause a stutter.

Of course, this work is what we call embarrassingly parallel, so using threads, jobs, or even GPU compute is a significantly better solution here on platforms where you can. But that's a more complicated answer I'll have to save for another time (or maybe someone else will beat me to it).

A simple threaded version can be found in the link I gave you earlier - you'll just have to defer the interaction with the Mesh class to the main thread after your worker thread has finished populating all the arrays for it to use.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.