Chunks load runtime

I have a problem with loading chunks. The code works, but when loading a new chunk, the game freezes for a second. Is there any method to load chunks into runtime more efficiently.

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

public class ChunkPlacer : MonoBehaviour
{
public Transform Player;
private Chunk[ ] Chunks;

public Chunk FirstChunk;

private List _spawnedChunks = new List();

public NavMeshSurface _surface;

void Start()
{
Chunks = Resources.LoadAll(“Prefabs”);
_spawnedChunks.Add(FirstChunk);
}

void Update()
{
if(Player.position.z > _spawnedChunks[_spawnedChunks.Count - 1]._end.position.z - 10){
SpawnChunk();
}
}

private void SpawnChunk(){
Chunk newChunk = Instantiate(Chunks[Random.Range(0, Chunks.Length)]);
newChunk.transform.position = _spawnedChunks[_spawnedChunks.Count - 1]._end.position - newChunk._begin.position;
_surface.BuildNavMesh();
_spawnedChunks.Add(newChunk);
}
}

You can use a pooling system to avoid calling expensive functions such as Instantiate() and Destroy() during gameplay. Basically, it instantiates an array of chunks once at startup and these get reused whenever you need to create or remove a chunk.

The built-in pooling system allows you to provide functions that are called when you take a chunk from the pool or return it, so you’d probably want to toggle the renderer and collider and generate a new navmesh.

If you’re using 2022.3.20 or later, you also have access to InstantiateAsync(). You’d have to change SpawnChunk() to a Coroutine or UniTask, but it would prevent the stutter caused by Instantiate() blocking the main thread.

You can also use it to instantiate many copies of a prefab at once, and even pass in NativeArrays of positions and rotations so they’re all spawned in different positions.

You can also generate a navmesh asynchronously so that could also reduce the stutter.

Edit: Here’s an example of using InstantiateAsync to reduce stutter.

I’d recommend using a pooling system for your chunk manager though, but I’m not sure if the built-in system would be suitable. It would need to check if a chunk is visible or within a certain distance before allowing it to be returned to the pool, but the ObjectPool class doesn’t seem to support this.

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

public class ChunkPlacer : MonoBehaviour
{
    // [SerializeField] allows you to set the object in the inspector without making the field public
    [SerializeField] private Transform Player;

    private Chunk[] Chunks;
    [SerializeField] private Chunk FirstChunk;

    // You can set the initial size of a List to avoid resizing the internal array each time it reaches its capacity
    // The default size of a List is 4 and it increases in powers of 2 when you exceed it
    private List<Chunk> _spawnedChunks = new List<Chunk>(256);

    [SerializeField] private NavMeshSurface _surface;

    void Start()
    {
        // No Resources.LoadAllAsync() function exists :(
        // Only available for AssetBundles or Addressables
        Chunks = Resources.LoadAll<Chunk>("Prefabs");
        _spawnedChunks.Add(FirstChunk);

        StartCoroutine(CheckPosition());
    }

    // By replacing Update() with a Coroutine, we can ensure only one chunk is instantiated at a time
    // Also this pauses the distance check when chunks are being created
    private IEnumerator CheckPosition()
    {
        // Coroutines share the lifetime of their MonoBehaviour, so this will stop when ChunkPlacer is destroyed
        // async functions and UniTasks do not automatically stop, so you need to attach a destroyCancellationToken to them
        while (true)
        {
            if (Player.position.z > _spawnedChunks[_spawnedChunks.Count - 1]._end.position.z - 10)
            {
                yield return SpawnChunk(); // Return after the Coroutine SpawnChunk() has completed
                // Calling StartCoroutine(SpawnChunk()); would call the Coroutine but not wait for it to finish
            }

            yield return null; // Return in the next frame, like Update()
        }
    }

    // Note that Coroutines cannot return an object, but UniTask can
    private IEnumerator SpawnChunk()
    {
        AsyncInstantiateOperation aio = InstantiateAsync(Chunks[Random.Range(0, Chunks.Length)]);

        // Instantiate the chunk in a background thread and return once complete
        yield return aio;

        // InstantiateAsync() always returns an array of Objects, even when count == 1
        // You'll need to cast the Object to the class it's supposed to be
        Chunk newChunk = aio.Result[0] as Chunk;

        // The position can also be set in the Instantiate function, reducing the number of calls to Unity's C++ code
        newChunk.transform.position = _spawnedChunks[_spawnedChunks.Count - 1]._end.position - newChunk._begin.position;

        // Can use NavMeshBuilder.UpdateNavMeshDataAsync()
        // UpdateNavMesh() also returns an AsyncOperation, which you can use when reusing chunks in a pool
        _surface.BuildNavMesh();
        _spawnedChunks.Add(newChunk);
    }
}