How could I get rid of lag spike when generating new chunks in my infinite world?

No, I do, but for some reason I don’t see any of those versions.

wow, never knew about this… YOINK

For the record, coroutines are not threading. They are just a fancy way of doing a kind of callback that feels sorta like multithreading. It’s literally just standard IEnumerator functionality in C# which if you aren’t familiar with you should definitely look up. Typically it’s used for iterating over potentially long lists of data but Unity managed to turn it into something that allowed people to write logic that could execute over many frames without having to think too hard on it. It was a clever solution back when multi-threading in games was uncommon and hardware didn’t provide support for it. It allowed Unity to not have to worry about thread-safe APIs and still allow users to feel like they could write async logic.

These days however it’s pretty much nothing but overhead. I occasionally use them when I’m feeling lazy and the code is only running during startup or something but I absolutely stay away any time performance is even slightly important. for exactly the same reasons I stay away from Linq in any hot path during runtime. It’s allocating memory for absolute no benefit.

So what do you recommend I do?

I think they were primarily intending to clear up the prevalent misunderstanding of how and what coroutines are. Way too many people think they’re threading or what not. They’re just basic state machines.

All of Unity’s core API is single threaded. Even regular C# async code is performed on the main thread, but is usually non blocking.

Really you should be looking into why you can’t download new versions because being stuck on an old version is going to continue to hinder you.

I just downloaded the next 2022 LTS version. I don’t know why it wasn’t showing up in the hub, but I was able to download it from Unity directly. I will look into instantiate async.

Ok, so coroutines aren’t actually asynchronous, they just seem that way?

Asynchronous and threading are two different things. Coroutines are asynchronous, in the sense that their execution can be non-blocking, but they aren’t threaded.

Basically, when you start a coroutine you are asking unity to create a wrapper object around your function that acts as an iterator ( again, iterators and enumerators are a basic thing in C# so once you know what those are this all can be inferred). It then calls that function you supplied. Inside you can execute any number of instructions and the main thread of the engine will do nothing until you are done. If you yield then through the normal mechanisms of C# it will essetially save the spot your execution was in for that function and return controll to Unity for the rest of the frame. But it will stil have that coroutine stored in a list of pending operations and next time the engine gets back to processing them either next frame or later that same frame if you specify, then it will pick up where you left off. But this is in no way non-blocking. Nothing else is running while your coroutine logic is. Its entirely single threaded. Its really just a fancy way of writing a loop where you can leave off and pickup at a later time. If you write a loop in your coroutine and never yield then the engine will entirely lock up until the loop ends.

My suggestion is the same as Spiny’s, consider an upgrade. I concur that early versions of 2022.3 were a complet mess. Broken rendering and memory leaks being a big part of it! The latest version of 2022.3 is very solid though. Barring that then you are really stuck with coroutines and you’ll have to find a workaround that involves doing less work per frame.

Okay, so my main question form this is: what’s the difference splitting everything over a bunch of frames from threading? The coroutines are just instantiating a bunch of objects over a certain period of time. What then, is the difference from doing that and doing it in a separate thread?

Coroutines are on the Main Thread. In Unity, threading takes some care because you can’t so easily interact with Unity Objects (they are not threadsafe). Since coroutines are on the main thread, you can continue to interact with Unity Objects without worry. So coroutines can help you can distribute work over frames, but not to other threads. If you could put it in another thread, then it would alleviate the burden on the Main Thread which is often the bottleneck.

Then there are other APIs like the Jobs System which are more advanced, which help you efficiently distribute work to all your CPU cores.

A small change to reduce the garbage your coroutines generate is:
yield return new WaitForSeconds(0.01f);
to
yield return null;
which does almost the same thing… it yields a frame. What’s leftover is the garbage cost of the coroutine itself, which is small.

About twenty years ago there would be no difference between the two. Threading was just a programming tool to make writing async code easier to reason about.

However, CPUs are now multi-cored and hyper-trheaded and OSes support that at the kernel level. Essentially, this means you can offload actual work and time spent on that work to one part of the CPU while logic continues on another. Literally, multi-threading nowadays can actually allow multiple processes to happen at the same physical time.

Ok, so what I’m gathering form this and outside research, is that coroutines run “kind of” at the same time. The code is run out of order with the other code to create an illusion of asynchronous-ness

Like:
Code
Coroutine Code
Coroutine Code
Code
Code
Coroutine Code

However, threading literally makes it run at the same time which is better as it would “actually” delay and run things over multiple frames rather than a Coroutine that would still stop the execution of code?

How does waiting cause garbage? Does it have something to do with creating a timer?

Threads can execute parts at the same time. It doesn’t always mean that they will. There’s a lot more to it than that. It’s mostly just the fact that you can write code one way and not worry about what the processor is physically doing. The main difference is that you don’t block other threads from executing as long as their logic isn’t waiting on the completion of that thread. So you can write logic like it is running physically at the same time regardless of whether or not it actually is.

It’s not the waiting that causes garbage, its the creation of a managed objects that is used to track active Coroutines. To be fair, you can also allocate when doing threading but depending on how clever you are about it, that can be mitigated significantly. Coroutines have their place. Sometimes you just need to access something in the Unity API but you want the logic to occur over a series of frames. Then it boils down to creating your own object to manage this state or letting Unity do it with Coroutines.

Like Lo-renzo said earlier though, if you want to reduce stutters you need to do less work per frame. AsyncInstatiate will go a long way to helping you with that. You can continue to use Coroutines (I’ve done it in the past with pooling systems). But you’ll need to ensure that you don’t go over a certain amount of time per frame. That means keeping track of how long you spend instantiating objects within your Coroutine and yielding when it goes over some maximum allowed amount of time. This of course means that chunks won’t be loaded in instantly as far as your logic goes but rather over a series of frames parts of it will be. AsyncInstantiate will basically do the same thing but much more efficiently due to it’s low-level integration with the engine itself as well as without the need to write a bespoked coroutine just to handle all of the async stuff.

new WaitForSeconds(.01f) instantiates an object, whereas yield return null does not. WaitForSeconds is a class. More broadly, C#'s distinction between class and struct can be important when optimizing.

So depending on what you want to do threading and coroutines are useful? For simple things coroutines are good, but if what you are doing can potentially cause code to stop for a noticeable time until the Coroutine is done, you use threading?

Coroutines are a bit of an old artifact of the engine, like Tags. What you can do with them you can probably do better with regular C# async code these days anyway. Packages like UniTask also exist to make it more performant and Unity friendly.

Though important to note that code you write for threading, such as jobs, is very different to normal Unity code you will write.

The world generation in my current project just uses regular async code, so not threaded but not blocking either.

Generally I try to stay as simple as possible and avoid all of it! I’m a simple person making simple games :upside_down_face:

The standard Task library is my usual goto tool for actual threading. I’ve yet to try Unitask but I’ve heard great things about it. I’ve only used the Jobs system a few times and it has come absolutely in clutch but the reason I used it was because there was no way to do what I needed to using normal threading (I had to move a lot of gameobject transforms around and you simple can’t do that in Unity using your own threads).

Coroutines are rare for me these days but it’s usually that I need to delay exactly 1 frame because the engine requires it and spinning up a whole thread would be overkill and I’m yet again feeling too lazy to install Unitask and get started using it. In all cases it’s usually something done at startup or close to it where performance isn’t critical. One thing’s for certain - I cringe any time I look at code and see coroutines all over the place because people are using them for timers (I’m looking at you Daggerfall Unity!!). Whatever you do, please don’t do that.

UniTask is very useful for those times when you want to wait for just one frame, and even one frame and after a very specific point in the player loop - and in a non-Game Object context, such as within UI Toolkit visual element code. That’s probably the majority of my usage of it as of late. The high-performance is just a cherry on top.