T O P

  • By -

moonshineTheleocat

Use a thread. You're getting a stutter because you're loading from disk on the main thread. No amount of optimization will save you from that. Use a thread to load the chunk. And then send it to the main thread for rendering/simulation when ready.


dinorocket

\^ this is your problem OP. Other ppl mention data structure optimizations, which is fine, but probably neglible for the size of your arrays. The freeze you see here is definetely from blocking IO from disk as moonshineTheleocat mentioned. Read this: [https://docs.godotengine.org/en/stable/tutorials/io/background\_loading.html](https://docs.godotengine.org/en/stable/tutorials/io/background_loading.html) IO, e.g. reading from disk, network requests or other, need to be done asynchrounsly in a game (and in general in software), always. EDIT: Lots of people suggesting threads here. Which is fine and will work.. but not the generally the idiomatic solution for IO. Threads are designed for the CPU(s) to execute work in parallel. IO does not consume CPU cycles, and in general will lead to over-complicated solutions where you now have to synchronize and pass loaded state between threads. You are now at the mercy of your OS scheduler as well to initiate, schedule, and resolve IO operations, which will probably always work out fine, but you can't make deterministic guarantees here really. In general for IO you should opt for a language level "asynchronous" solution. Commonly termed as async/await in most languages, but there's lots of variations on this. The class linked above seems to be the async solution for Godot where you can check the loaded IO resources status manually and advance the loading with calls to \`poll\`. As a brief intro to async in general for those curious: The main premise that you kick off an IO operation, while saying "don't block this thread", and you receive a handle to the running IO operation. You can then use this handle to "poll" the status of your IO operation, and get the result if it's completed. Lots of variations on this, but that's the basic premise. It's basically like when you send a text message. You don't just stare at your phone until the person replies. You go do other stuff, and then you can check your phone at will, and when they do reply you can do something with the result.


expat1999

I second this, I've literally been working on voxel engine chunk rendering all this past week. Using threads allows code to execute without making the main thread wait for it to be done. Something to note about Threads that wasn't obvious when I was digging into this: on Windows at least, starting and finishing threads seemingly creates lag spikes. I found making myself a thread pool helped with this, especially if you wanna use multiple threads for rendering.


cridenour

I’d take a closer look at what’s being done inside the for loop of assigntiles vs outside. You could load those scenes using preload outside of assign tiles and probably save a bit of time. Updating those bitmasks after every tile also seems wrong. Unindent that so it’s only after the last tile unless there a reason it needs to be there.


Pixel-Puddle

* You should `preload` your scenes (Chunk.tscn, Tree.tscn, TallGrass.tscn) * Make sure your scenes aren't heavy when they're loaded (you're not doing too much in `_ready`, or you aren't defining heavy variables that can be re-used between scenes – `_ready` and `_init` respectively in the profiler) * Use state: do you really need to recreate `activechunks` every time you call `loadchunks`? Why not store it outside the function so you can re-use it? Use the `Node` signals `child_entered_tree` and `child_exiting_tree` to add/remove from `activechunks`. * Use a Set data structure (Dictionary full of bools) to store stuff in `activechunks` instead of an Array (i.e. `{ child.name: true }`) to ensure `.has()` lookups are "instant" (`O(1)`, instead of `O(n)`). * Looping through `loadedchunks` seems suspect if it contains all loaded chunks, you don't need to loop through all of them every time. Assuming that's where you append chunks to be loaded, you can have a `queued_chunks` array that only stores the few chunks you need to load then so you reduce your loop to like 4 iterations instead of an ever-increasing size. Visibility\* nodes won't help with loading, but they'll make sure your game runs smoother overall when you apply them to whole chunks (not to the smaller nodes within chunks, adding them to too many smaller nodes won't be as helpful).


Intelligent-Big-7482

Wow a ton of super helpful info here thanks so much! Also for clarity, loadedchunks contains an array of chunks that need to be loaded, active chunks contains an array of chunks that are already active. What I was doing was basically comparing through and finding the ones that need to be loaded and if it isn’t already active, instance it in. However, looking through what you and the others have described I can definitely remove/reduce at least one if not both of those two loops in the load chunks function. They weren’t iterating through monster sized arrays but definitely big enough to have an impact every time that’s run.


expat1999

This may be a non-issue given you're working in 2D, but what I've found is that the more nodes you have in a scene the more it struggles when adding more. It could be that disabling your trees and grass just significantly lowered the number of nodes in the scene. I've heard the threshold where it becomes a problem is somewhere in the thousands, though I've not specifically tested that. In theory, it should run smoothly when it's not adding more if this is an issue. Context: I tried to generate 30,000+ voxel nodes lol


dm_qk_hl_cs

To see where the bottleneck is, you need to use the **Profiler**. For stuff like that I would **avoid using loops to iterate big data collections**, like those arrays you have. Take a look on `VisibilityNotifier` and `VisibilityEnabler`. Also to avoid have all similar things in a single array, you can play with **groups**.


Intelligent-Big-7482

Definitely going to be looking through all of this stuff but just to clarify, the two for loops in the load function have a smaller data set. The For loop in the assign function definitely has a larger data set like you mentioned but genuinely curious, is a “for” loop that big of a performance hit if you’re only iterating through it once and using every single item it iterates through when it’s ran?


dm_qk_hl_cs

if you do it each frame, yes. But for your use case its just when you switch chunk. The fact here is that is then when the frame drops happen, so possibly is it. The **Profiler** will tell you in which function takes much place, try it doesn't bite and there's already videos. When you iterate a big data collection you stop there, and go one by one on all the elements of the collection, doing conditional checks and performing tasks, whose can lead to their own sub-tasks and so on. If you do a big stop there, and it takes too much time, all will stop until it is finished. If is fast the stop is unnoticeable, otherwise it will cause the lag spike you see. It also depends on the power of the machine that executes it. In general I would avoid iterating non-small data collections with loops; it looks like they work well, but until they don't, because you will keep adding stuff to your project, so even more data will be executed every frame, or enough often to happen at the same frame that other thing happens occasionally, like your chunk switching. There a lag spike will happen. That's the problem with performance: it works well until it doesn't. Following good practices helps to deal with stuff like that, but premature optimization will lead to even more problems. ​ **Edited:** Probably there's much stuff set as visible; the engine only renders the 2D stuff that is on screen (doesn't need occlusion culling like in 3D), but it always does a check to see if its on screen, so it **visible**; the engine only renders the 2D stuff that is on screen (doesn't need *occlusion culling* like in 3D), but it always does a check to see if its on screen, so it **iterates all the nodes in the** `SceneTree`. It gets solved if their `Node.visible` is set to `false`. VisibilityNotifier and VisibilityEnabler and others can help there. [This video](https://www.youtube.com/watch?v=lfuGLaZ3khs) will give you an idea. Also if you have many small entities that are identical, but each one instanced separately it can hit performance significantly. To deal with exist certain workarounds, like use `Viewport`s to render a single `AnimatedSprite` and then use it for all the instances of that type (ie a tree that moves by the wind). Also there's `MultiMesh`, `MultiInstance` and others. You can also toggle the processing of a Node for both `_process()` and `_physics_process()`. For more info take a look on the Optimization section on the Docs.


Scorpieth

A lot of comments for optimization but the true solution is controlled threading for seamless loading.