Is there a cleaner way to load and unload big chunks of data in parallel?

So I’m building a game with a procedurally generated world and therefore need to generate/load and unload big chunks of data without blocking the main thread. I currently use tasks for this and just create and run a new task whenever a chunk enters or leaves render distance.

All the chunks can be loaded in parallel without problems. But if a chunk enters and then leaves the render distance quickly, it’ll get an unload command before the loading is finished. Which would lead to both of the tasks executing in parallel and at least one of them failing and causing lots of issues.

Now I’ve tried using a bool to mark a chunk as busy when a task is created and then prevent the creation of new tasks for this chunk while its busy. But this seems clunky and leads to other issues like having to check if there are active chunks outside of the render distance that couldn’t be unloaded because they were busy.

Another thing I’ve tried is creating a loadingOperation struct and giving each chunk a concurrent queue for these structs.
Then I would do the following:

  • If a chunk needs to be loaded or unloaded, create a loadingOperation and add it to its concurrent queue

  • Add the chunk itself to another concurrent queue called chunksWithTasks but only if its not already in there or currently executing a loadingOperation (marked with another “busy” bool)

  • Have an indefinitely running task constantly checking the chunksWithTasks queue. If it has chunks in it:

  • Dequeue

  • Create a new task thats running all the loadingOperations in order

  • set a “busy” bool in the chunk to true so I can check if its currently executing a loadingOperation

  • if the task is done running and the queue is empty set the bool to false

This seems to be working but I don’t know if having a constantly running background task should maybe be avoided? The whole solution just feels clunky and like there should be an easier way I just don’t know about, maybe including the job system?

So tl;dr: I need to load and unload a lot of chunks in parallel. But loading and unloading always need to be executed in order in case one gets triggered before the other finished. Whats the best way to accomplish this?

You need some form of “cancellation token”.

You can also simply allow that chunk to complete loading but mark it as unused. Any decent chunking system ought to have a separate caching or threshold mechanism because you’re more likely than not to need that same chunk again in the very near future. Imagine the player running zig-zag across chunk boundaries. You don’t want to load/unload every time the player crosses that boundary!

“What is Update()?”

Reminded me of that TV show. :slight_smile:

I mean you have chunks so you ought to have a system (MonoBehaviour likely) that checks if a chunk should be loaded and which could be unloaded. This takes the player position and checks for chunk boundaries and determines you need to load another 3 chunks to the west, something like that. You already have that, right?

If so, it should be easy and it would be trivial to test every frame which of the presumably no more than 20 to 200-ish chunks could be unloaded. You could also defer this task to run only a few times per second, after all the player won’t notice that a chunk far off in the distance isn’t starting to load within the same frame. But it’s unlikely that this sort of “200 position checks” is going to affect performance at all.

Thanks for the answer, the whole caching system definitely seems to be a step in the right direction. I had something similar implemented before with not only having a loaded and unloaded state per chunk but also an active and inactive state. This just seemed to be a bit convoluted so I got rid of it …

I reckon I should have a list of all the unused chunks and only start unloading the oldest ones once it reaches a certain length? But wouldn’t that just move the problem to when an unused chunk is unloaded? Like what if I run in a circle and happen to end back up at the start right when the unused list reaches my threshold … then the chunks at the start of the circle would try to unload the same moment I’m trying to activate/load them again, resulting in the same issue.
I guess there’s just no way around having some sort of task queue for each chunk then …

Also I thought about building a way to cancel loading and unloading tasks, its just that cancelling them halfway through might lead to even bigger problems so I’d really like them to finish whatever they’re doing before starting the next task.

Also you’re right, I originally kept it away from the update function to have everything concerning the chunking system be done away from the main thead. But maybe thats a bit overkill, checking the coordinates of a few dozen chunks really shouldn’t be too taxing on the main thread :slight_smile:

Ok so it looks like the simplest solution was to just define a second “unrender distance” at which chunks get unloaded and make that one chunk farther than the render distance …
That seems to work as long as the player doesn’t move farther than a single chunk in the time it takes a chunk to load, which is never the case anyways :slight_smile:

It also prevents the issue of rapidly loading and unloading chunks if a player zig-zags across chunk borders and was a lot simpler to implement than the whole task-queue system

Yes I think you have a cached outOfRange1 of chunks loading which may come into view, and then a outOfRange2 with a countdown of say 1 second to being adding to an unload queue. That way if the player is moving in X direction the cached chunks are ready to view, while the -X are becoming outOfRange2 and will expire being held in cache. Just means if the player suddenly moves -X then you there is a time delay in loading/unloading, with time to streamline between priority loading and secondary unloading.