How does the original StartCoroutine / yield template really work in Unity?

I understand the principle of coroutine. I know how to get the standard StartCoroutine / yield return template for working in C # in Unity, for example. call the method that returns IEnumerator via StartCoroutine , and in this method do something, do yield return new WaitForSeconds(1); to wait a second, then do something else.

My question is: what is really going on behind the scenes? What makes StartCoroutine really? What does IEnumerator return WaitForSeconds ? How does StartCoroutine return control to the "something else" part of the called method? How does all this interact with the Unity concurrency model (where many things happen simultaneously without using coroutines)?

+83
coroutine c # unity3d
Oct 17 '12 at 10:30
source share
4 answers

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) { /* Do a chunk of work */ // Pause here and carry on next frame yield; } } 

In C #:

 IEnumerator LongComputation() { while(someCondition) { /* Do a chunk of work */ // Pause here and carry on next frame yield return null; } } 

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.

+56
Sep 09 '15 at 0:09
source share

The first heading below is a direct answer to the question. These two headers are more useful for everyday programmers.

Perhaps drilling Coroutines command execution information

Coroutines are explained in Wikipedia and elsewhere. Here I will just give some details from a practical point of view. IEnumerator , yield , etc. C # language features that are used for some other purpose in Unity.

Simply put, IEnumerator claims to have a set of values ​​that you can query one by one, sort of like List . In C #, a function with a signature to return IEnumerator should not actually create and return one, but may allow C # to provide an implicit IEnumerator . The function can then provide the contents of the returned IEnumerator in the future in a lazy way using yield return . Each time the caller requests a different value from the implicit IEnumerator , the function executes until the next yield return , which provides the next value. As a by-product, the function stops until the next request.

In Unity, we do not use them to provide future values, we use the fact that the function is paused. Because of this exploitation, a lot of things about coroutines in Unity make no sense (that IEnumerator has something to do with something? What is yield ? Why new WaitForSeconds(3) etc.). What happens “under the hood”, the values ​​you provide through IEnumerator are used by StartCoroutine() to decide when to request the next value, which determines when your coroutine will again stop.

Your Unity Game Single Thread (*)

Korotin are not streams. There is one main Unity loop, and all those functions that you write are called by the same main thread in order. You can verify this by placing while(true); to any of your functions or coroutines. It will freeze all this, even the Unity editor. This indicates that everything works in one main thread. This link , which Kay mentions in his previous comment, is also a great resource.

(*) Unity calls your functions from a single thread. So, if you yourself do not create a thread, the code you wrote is single-threaded. Of course, Unity uses other threads, and you can create themes yourself if you want.

Practical Description of Coroutines for Game Programmers

Basically, when you call StartCoroutine(MyCoroutine()) , it exactly resembles a regular call to the MyCoroutine() function, until the first yield return X , where X is something like null , new WaitForSeconds(3) , StartCoroutine(AnotherCoroutine()) , break , etc. This is when it starts to differ from the function. Unity “pauses” this function right in this line of yield return X , continues working with another business, and some frames go through, and when again, Unity resumes this function right after this line. It remembers the values ​​for all local variables in the function. That way, you can have a for loop that cycles through every two seconds, for example.

When Unity resumes your coroutine, it depends on what X was in your yield return X For example, if you used yield return new WaitForSeconds(3); It resumes after 3 seconds. If you used yield return StartCoroutine(AnotherCoroutine()) , it resumes after the completion of AnotherCoroutine() , which allows you to embed behavior in time. If you just used yield return null; He resumes right in the next frame.

+80
Mar 14 '13 at 15:08
source share

It couldn't be simpler:

Unity (and all game engines) are frame based.

The whole point, the whole point of Oneness, is that it is based on frames. The engine makes "every frame" for you. (Animation, rendering of objects, physics, etc.)

You may ask: "Oh, that's great. What if I want the engine to do something for me in each frame? How can I get the engine to do something in the frame?"

Answer...

This is precisely the “coroutine”.

It is just that simple.

And consider this ....

You know the update function. Quite simply, everything you insert there is executed every frame. This is literally the same thing, no difference whatsoever from the coroutine-yield syntax.

 void Update() { this happens every frame, you want Unity to do something of "yours" in each of the frame, put it in here } ...in a coroutine... while(true) { this happens every frame. you want Unity to do something of "yours" in each of the frame, put it in here yield return null; } 

There is no difference.

Footnote: As everyone notes, Unity just has no threads . "Frames" in Unity or in any game engine are not connected in any way with streams.

Coroutines / yield is just accessing frames in Unity. It. (And indeed, this is exactly the same as the Update () function provided by Unity.) All it needs is simple.

+6
Feb 08 '16 at 22:05
source share

Will fit into this recently, wrote a message here - http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/ - shed light on the insides (with examples of dense code), the basic IEnumerator interface and how It is used for coroutines.

Using collection counters for this purpose still seems a little odd to me. This is the inverse of what encoders are for. The enumeration point is the return value for each access, but the Coroutines point is the code between the return value. The actual return value in this context is pointless.

+4
Jan 30 '15 at 10:37
source share



All Articles