The linked Unity3D coroutines link has been disabled in detail . Since in the comments and answers I mention the content of the article here. This content comes from this mirror .
Detailed description of Unity3D
Many processes in games occur over several frames. You have "dense processes, such as pathfinding," which work every frame, but are split into several frames so as not to affect the frame rate too much. You have "rare processes, such as gameplay triggers" that do nothing in most frames, but sometimes require critical work. And you have various processes between them.
Whenever you create a process that will run on multiple frames — without multithreading — you need to find some way to break the work into pieces that can be run one by one. For any algorithm with a central loop, it is pretty obvious: for example, A * pathfinder can be structured so that it stores its node lists semi-permanently, processing only a few nodes from the open list every frame, instead trying to do all the work at once. There’s some balancing to control latency - after all, if you block the frame rate of 60 or 30 frames per second, then your process will only take 60 or 30 steps per second, and this can lead to the process taking too long in general and overall. An optimal design can offer the smallest unit of work possible at the same level - for example, process one A * node - and the layer above, a way of grouping, work together into larger pieces - for example, continue to process A * nodes for X milliseconds. (Some people call it "timelicing", although I do not know).
However, while allowing work to be broken in this way, you must transfer state from one frame to another. If you break the iterative algorithm up, then you need to save all the state divided between iterations, as well as a means of tracking the subsequent iteration. This is usually not so bad - the design of the "A * Pathfinder" class is pretty obvious, but there are other cases that are less enjoyable. Sometimes you will encounter long calculations that perform different types of work from frame to frame; an object that has captured its state may be in a big mess of semi-useful "local residents" designed to transmit data from one frame to another. And if you're dealing with a sparse process, you often have to implement a small end machine to keep track of when work needs to be done at all.
It would not be tidy if, instead of explicitly tracking all this state over several frames, and instead of having multi-threading and controlling synchronization and locking, etc., you could just write your function as a single piece of code, and mark the specific places where the function should "pause and continue later"
Unity - along with a number of other environments and languages ​​- provides this in the form of Corouts.
How do they look? In "Unityscript" (Javascript):
function LongComputation() { while(someCondition) {
In C #:
IEnumerator LongComputation() { while(someCondition) {
How do they work? Let me say quickly that I do not work for Unity Technologies. Ive not seen the source code for Unity. Ive never seen the guts of a Unitys coroutine engine. However, if they implemented it in a way that is radically different from what I'm going to describe, then Ill will be very surprised. If someone from UT wants to call back and talk about how it works, then it will be great.
Big keys are in the C # version. First, note that the return type for the function is IEnumerator. And secondly, note that one of the statements is return yield. This means that profitability should be the keyword, and since Unitys C # support is vanilla C # 3.5, it should be the C # 3.5 vanilla keyword. Indeed, here it resides on MSDN - speaking of something called "iterator blocks". So what's going on?
The first is this type of IEnumerator. The IEnumerator type acts like a cursor over a sequence, providing two meaningful elements: Current, which is a property that provides you with the element that is now in the cursor, and MoveNext (), a function that moves to the next element in the sequence. Since IEnumerator is an interface, it does not exactly determine how these elements are implemented; MoveNext () can simply add one toCurrent or load a new value from a file or load an image from the Internet and hash it and save the new hash in Current ... or it can even do one for the first element in the sequence and something completely different for the second . You could even use it to generate an infinite sequence if you want. MoveNext () calculates the next value in the sequence (returns false if there are no more values), and Current retrieves the value that it calculated.
Usually, if you want to implement an interface, you will have to write a class, implement elements, etc. Iterator blocks are a convenient way to implement IEnumerator without any hassle - you just follow a few rules, and the IEnumerator implementation is automatically generated by the compiler.
A block iterator is a regular function that (a) returns an IEnumerator, and (b) uses the yield keyword. So what does the yield keyword actually do? He declares that the next value in the sequence - or that there are no more values. The point at which the code encounters a return X or break break is the point at which IEnumerator.MoveNext () should stop; the return income of X causes MoveNext () to return true andCurrent, which is assigned the value X, while the yield of break calls MoveNext () to return false.
Now, this is a trick. It doesn't matter what the actual values ​​returned by the sequence are. You can call MoveNext () repeatedly and ignore Current; the calculations will still be performed. Each time MoveNext () is called, your iterator block is run in the next yield statement, no matter what expression it actually gives. So you can write something like:
IEnumerator TellMeASecret() { PlayAnimation("LeanInConspiratorially"); while(playingAnimation) yield return null; Say("I stole the cookie from the cookie jar!"); while(speaking) yield return null; PlayAnimation("LeanOutRelieved"); while(playingAnimation) yield return null; }
and what you actually wrote is an iterator block that generates a long sequence of null values, but the side effects of the work that it does to calculate them are significant. You can run this coroutine using a simple loop:
IEnumerator e = TellMeASecret(); while(e.MoveNext()) { }
Or, more useful, you could mix it with another job:
IEnumerator e = TellMeASecret(); while(e.MoveNext()) { // If they press 'Escape', skip the cutscene if(Input.GetKeyDown(KeyCode.Escape)) { break; } }
Everything in timelines As you have seen, each return yield statement must provide an expression (for example, null), so the iterator block must actually assign IEnumerator.Current something. A long sequence of zeros is not exactly useful, but more interested in side effects. Arent we?
We can do something convenient with this expression, in fact. What if, instead of simply yielding to null and ignoring it, we gave something that was indicated when we expect you to need to work harder? Often you need to perform right on the next frame, of course, but not always: there will be many times when we want to continue the game after the animation or sound has finished the game, or after a certain time. Those while (playAnimation) return return null; The designs are a bit tedious, don't you think?
Unity declares the base type of YieldInstruction and provides several specific derived types that indicate specific types of expectation. You have WaitForSeconds, which resumes a coroutine after a specified period of time. You have a WaitForEndOfFrame that resumes a coroutine at a specific point later in the same frame. You have the Coroutine type itself, which, when coroutine A gives coroutine B, pauses coroutine A until coroutine B.
What does it look like in terms of runtime? As I said, I do not work for Unity, so Ive never seen their code; but I'd imagine it might look something like this:
List<IEnumerator> unblockedCoroutines; List<IEnumerator> shouldRunNextFrame; List<IEnumerator> shouldRunAtEndOfFrame; SortedList<float, IEnumerator> shouldRunAfterTimes; foreach(IEnumerator coroutine in unblockedCoroutines) { if(!coroutine.MoveNext()) // This coroutine has finished continue; if(!coroutine.Current is YieldInstruction) { // This coroutine yielded null, or some other value we don't understand; run it next frame. shouldRunNextFrame.Add(coroutine); continue; } if(coroutine.Current is WaitForSeconds) { WaitForSeconds wait = (WaitForSeconds)coroutine.Current; shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine); } else if(coroutine.Current is WaitForEndOfFrame) { shouldRunAtEndOfFrame.Add(coroutine); } else /* similar stuff for other YieldInstruction subtypes */ } unblockedCoroutines = shouldRunNextFrame;
It's not hard to imagine how to add additional YieldInstruction subtypes to handle other cases - for example, support for signal level at the engine level can be added with support for YieldInstruction WaitForSignal (SignalName). By adding more YieldInstructions, the coroutines themselves can become more expressive - the yield return new WaitForSignal ("GameOver") is better to read then (! Signals.HasFired ("GameOver")) return return null if you ask me, completely regardless of what to do this is faster in the engine than doing this in a script.
A few unobvious consequences Theres a few useful things about all this that people sometimes overlook, that I thought I should point out.
First, profitability simply gives expression — any expression — and YieldInstruction is the usual type. This means that you can do things like:
YieldInstruction y; if(something) y = null; else if(somethingElse) y = new WaitForEndOfFrame(); else y = new WaitForSeconds(1.0f); yield return y;
Specific rows return a new WaitForSeconds (), yield return new WaitForEndOfFrame (), etc., are common, but theyre not actually special forms in their own right.
Secondly, since these coroutines are just blocks of iterators, you can iterate over them yourself, if you want, you do not need the engine to do this for you. Ive used this to add interrupt conditions to a coroutine before:
IEnumerator DoSomething() { } IEnumerator DoSomethingUnlessInterrupted() { IEnumerator e = DoSomething(); bool interrupted = false; while(!interrupted) { e.MoveNext(); yield return e.Current; interrupted = HasBeenInterrupted(); } }
Thirdly, the fact that you can give it on other coroutines can allow you to implement your own YieldInstructions, although not as if they were implemented by the engine. For example:
IEnumerator UntilTrueCoroutine(Func fn) { while(!fn()) yield return null; } Coroutine UntilTrue(Func fn) { return StartCoroutine(UntilTrueCoroutine(fn)); } IEnumerator SomeTask() { yield return UntilTrue(() => _lives < 3); }
however, I would not recommend this - the cost of launching Coroutine is a bit heavy for me.
Conclusion Hope this clarifies some of what really happens when you use Coroutine in Unity. C # stereo blocks are a small groovy construct, and even if you are not using Unity, you might find it useful to use them in the same way.