IT박스

StartCoroutine / yield return pattern은 Unity에서 실제로 어떻게 작동합니까?

itboxs 2020. 7. 11. 11:09
반응형

StartCoroutine / yield return pattern은 Unity에서 실제로 어떻게 작동합니까?


나는 코 루틴의 원리를 이해합니다. 나는 표준 얻을하는 방법을 알고 StartCoroutine/의 yield return예를 호출하는 방법은 반환, 유니티에서 C #에서 작업에 패턴을 IEnumerator통해 StartCoroutine, 그 방법으로 일을 할 yield return new WaitForSeconds(1);, 잠깐만 후 다른 일을 할 수 있습니다.

내 질문은 : 무대 뒤에서 실제로 무슨 일이 일어나고 있습니까? StartCoroutine실제로 무엇을 합니까? 어떻게 IEnumerator되어 WaitForSeconds반환? StartCoroutine호출 된 메소드의 "다른 것"부분으로 제어를 리턴 하는 방법은 무엇입니까? 이 모든 것이 Unity의 동시성 모델과 어떻게 상호 작용합니까 (코 루틴을 사용하지 않고 많은 일이 동시에 진행되고 있는가)?


상세 링크 에서 종종 참조 된 Unity3D 코 루틴 이 죽었습니다. 의견과 답변에서 언급되었으므로 기사의 내용을 여기에 게시 할 것입니다. 이 내용은 이 거울 에서 나온 것입니다 .


Unity3D 코 루틴 디테일

게임의 많은 프로세스가 여러 프레임 과정에서 발생합니다. 경로 찾기와 같은 '조밀 한'프로세스가있어 각 프레임마다 효과가 있지만 여러 프레임으로 분할되어 프레임 속도에 너무 큰 영향을 미치지 않습니다. 대부분의 프레임을 수행하지 않지만 때로는 중요한 작업을 수행해야하는 게임 플레이 트리거와 같은 '스파 스'프로세스가 있습니다. 그리고 둘 사이에 여러 프로세스가 있습니다.

멀티 스레딩없이 여러 프레임에서 발생하는 프로세스를 만들 때마다 작업을 프레임 당 실행할 수있는 청크로 분할하는 방법을 찾아야합니다. 중앙 루프가있는 알고리즘의 경우 상당히 분명합니다. 예를 들어 A * 패스 파인더는 노드 목록을 반영구적으로 유지하도록 구성 할 수 있으며 시도하지 않고 열린 프레임에서 소수의 노드 만 처리합니다. 한 번에 모든 작업을 수행합니다. 대기 시간을 관리하기 위해 약간의 균형이 이루어져야합니다. 결국 프레임 속도를 초당 60 또는 30 프레임으로 고정하는 경우 프로세스는 초당 60 또는 30 단계 만 수행하므로 프로세스가 중단 될 수 있습니다. 전체적으로 너무 길다. 깔끔한 디자인은 한 수준에서 가장 작은 작업 단위를 제공 할 수 있습니다. 예 : 단일 A * 노드를 처리하고 작업을 더 큰 청크로 그룹화하는 방법 위에 배치합니다. 예를 들어 A * 노드를 X 밀리 초 동안 계속 처리합니다. (일부 사람들은 이것을 '타임 슬라이싱'이라고 부릅니다.)

그럼에도 불구하고 이러한 방식으로 작업을 분할하면 한 프레임에서 다음 프레임으로 상태를 전송해야합니다. 반복 알고리즘을 중단하는 경우 반복에서 공유되는 모든 상태와 다음에 수행 할 반복을 추적하는 수단을 유지해야합니다. 일반적으로 그렇게 나쁘지는 않습니다. 'A * 패스 파인더 클래스'의 디자인은 상당히 분명합니다. 그러나 다른 경우도 있습니다. 때로는 프레임마다 다른 종류의 작업을 수행하는 긴 계산에 직면하게됩니다. 상태를 캡처하는 객체는 한 프레임에서 다음 프레임으로 데이터를 전달하기 위해 유지되는 반 유용한 '로컬'의 큰 혼란으로 끝날 수 있습니다. 그리고 드문 프로세스를 처리하는 경우 작업이 언제 완료되어야하는지 추적하기 위해 작은 상태 머신을 구현해야하는 경우가 종종 있습니다.

여러 프레임에서이 상태를 명시 적으로 추적하는 대신 동기화 및 잠금 등을 멀티 스레딩하고 관리하는 대신 함수를 단일 코드 덩어리로 작성하는 것만으로도 깔끔하지 않습니까? 기능이 '일시 정지'되어야하는 특정 장소를 표시하고 나중에 계속 수행합니까?

Unity는 다른 여러 환경 및 언어와 함께 코 루틴 형식으로 제공합니다.

어떻게 보이나요? “Unityscript”(자바 스크립트)에서 :

function LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

        // Pause here and carry on next frame
        yield;
    }
}

C #에서 :

IEnumerator LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

        // Pause here and carry on next frame
        yield return null;
    }
}

그들은 어떻게 작동합니까? Unity Technologies에서 근무하지 않는다고 빨리 말씀 드리겠습니다. Unity 소스 코드를 보지 못했습니다. 나는 Unity의 코 루틴 엔진의 내장을 본 적이 없다. 그러나 그들이 설명하려고하는 것과 근본적으로 다른 방식으로 그것을 구현했다면, 나는 매우 놀랄 것입니다. UT 출신의 누군가가 실제로 어떻게 작동하는지에 대해 이야기하고 싶다면 그것이 좋을 것입니다.

큰 실마리는 C # 버전입니다. 먼저 함수의 반환 유형은 IEnumerator입니다. 둘째, 성명서 중 하나는 수익률 반환입니다. 즉 수익률은 키워드 여야하고 Unity의 C # 지원은 바닐라 C # 3.5이므로 바닐라 C # 3.5 키워드 여야합니다. 실제로 MSDN 에는 '반복자 블록'이라는 것이 있습니다. 무슨 일이야?

첫째,이 IEnumerator 유형이 있습니다. IEnumerator 형식은 시퀀스에서 커서처럼 작동하여 두 가지 중요한 멤버를 제공합니다. Current는 현재 커서가있는 요소를 제공하는 속성 인 Current와 시퀀스의 다음 요소로 이동하는 함수 인 NextNext ()입니다. IEnumerator는 인터페이스이므로 이러한 멤버가 구현되는 방식을 정확하게 지정하지 않습니다. MoveNext ()는 하나의 toCurrent를 추가하거나 파일에서 새 값을로드하거나 인터넷에서 이미지를 다운로드하여 해시하여 Current에 새 해시를 저장할 수 있습니다. 순서에있는 요소, 두 번째와는 완전히 다른 것입니다. 원하는 경우이를 사용하여 무한 시퀀스를 생성 할 수도 있습니다. MoveNext ()는 시퀀스에서 다음 값을 계산합니다 (값이 없으면 false를 반환 함).

일반적으로 인터페이스를 구현하려면 클래스를 작성하고 멤버를 구현하는 등의 작업을 수행해야합니다. 반복자 블록은 번거 로움없이 IEnumerator를 구현하는 편리한 방법입니다. 몇 가지 규칙 만 따르면 IEnumerator 구현이 컴파일러에 의해 자동으로 생성됩니다.

반복자 블록은 (a) IEnumerator를 반환하고 (b) yield 키워드를 사용하는 일반 함수입니다. 수익률 키워드는 실제로 무엇을합니까? 시퀀스의 다음 값이 무엇인지 또는 더 이상 값이 없음을 선언합니다. 코드에서 수익률 반환 X 또는 수익률 중단이 발생하는 지점은 IEnumerator.MoveNext ()가 중지해야하는 지점입니다. 수율 반환 X는 MoveNext ()가 true를 반환하고 현재 값에 X가 할당되도록하는 반면 수율 중단은 MoveNext ()가 false를 반환합니다.

자, 여기 속임수가 있습니다. 시퀀스에서 반환 된 실제 값이 무엇인지는 중요하지 않습니다. MoveNext ()를 반복해서 호출하고 Current를 무시할 수 있습니다. 계산은 여전히 ​​수행됩니다. MoveNext ()가 호출 될 때마다 반복자 블록은 실제로 생성되는 표현식에 관계없이 다음 'yield'문으로 실행됩니다. 따라서 다음과 같이 작성할 수 있습니다.

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;
}

실제로 작성한 것은 긴 null 값 시퀀스를 생성하는 반복자 블록이지만 중요한 것은 값을 계산하는 작업의 부작용입니다. 다음과 같은 간단한 루프를 사용하여이 코 루틴을 실행할 수 있습니다.

IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }

또는 더 유용하게 다른 작업과 혼합 할 수 있습니다.

IEnumerator e = TellMeASecret();
while(e.MoveNext()) 
{ 
  // If they press 'Escape', skip the cutscene
  if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}

지금까지 살펴본 바와 같이 각 yield return 문은 null 블록과 같은 식을 제공하여 반복자 블록이 실제로 IEnumerator.Current에 할당 할 항목을 갖도록해야합니다. 긴 null 시퀀스는 유용하지 않지만 부작용에 더 관심이 있습니다. 우리 아닌가요?

실제로 우리는 그 표현으로 할 수있는 편리한 일이 있습니다. 널 (null)을 산출하고 무시하는 대신 더 많은 작업이 필요할 것으로 예상되는 것을 산출하면 어떻게 될까요? 애니메이션이나 사운드가 재생을 마치거나 특정 시간이 지난 후에도 계속하고 싶은 시간이 많이있을 것입니다. while (playingAnimation) yield는 null을 반환합니다. 구조는 약간 지루합니다.

Unity는 YieldInstruction 기본 유형을 선언하고 특정 종류의 대기를 나타내는 몇 가지 구체적 파생 유형을 제공합니다. 지정된 시간이 지나면 코 루틴을 다시 시작하는 WaitForSeconds가 있습니다. WaitForEndOfFrame이 있는데, 나중에 동일한 프레임의 특정 지점에서 코 루틴을 다시 시작합니다. 코 루틴 유형 자체가 있습니다. 코 루틴 A가 코 루틴 B를 생성하면 코 루틴 B가 완료 될 때까지 코 루틴 A를 일시 중지합니다.

이것은 런타임 관점에서 어떤 모습입니까? 내가 말했듯이, 나는 Unity에서 일하지 않으므로 코드를 본 적이 없다. 하지만 다음과 같이 보일 수 있습니다.

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 difficult to imagine how more YieldInstruction subtypes could be added to handle other cases – engine-level support for signals, for example, could be added, with a WaitForSignal("SignalName")YieldInstruction supporting it. By adding more YieldInstructions, the coroutines themselves can become more expressive – yield return new WaitForSignal("GameOver") is nicer to read thanwhile(!Signals.HasFired("GameOver")) yield return null, if you ask me, quite apart from the fact that doing it in the engine could be faster than doing it in script.

A couple of non-obvious ramifications There’s a couple of useful things about all this that people sometimes miss that I thought I should point out.

Firstly, yield return is just yielding an expression – any expression – and YieldInstruction is a regular type. This means 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;

The specific lines yield return new WaitForSeconds(), yield return new WaitForEndOfFrame(), etc, are common, but they’re not actually special forms in their own right.

Secondly, because these coroutines are just iterator blocks, you can iterate over them yourself if you want – you don’t have to have the engine do it for you. I’ve used this for adding 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 yield on other coroutines can sort of allow you to implement your own YieldInstructions, albeit not as performantly 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 wouldn’t really recommend this – the cost of starting a Coroutine is a little heavy for my liking.

Conclusion I hope this clarifies a little some of what’s really happening when you use a Coroutine in Unity. C#’s iterator blocks are a groovy little construct, and even if you’re not using Unity, maybe you’ll find it useful to take advantage of them in the same way.


The first heading below is a straight answer to the question. The two headings after are more useful for the everyday programmer.

Possibly Boring Implementation Details of Coroutines

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

To put it very simply, an IEnumerator claims to have a collection of values that you can request one by one, kind of like a List. In C#, a function with a signature to return an IEnumerator does not have to actually create and return one, but can let C# provide an implicit IEnumerator. The function then can provide the contents of that returned IEnumerator in the future in a lazy fashion, through yield return statements. Every time the caller asks for another value from that implicit IEnumerator, the function executes till the next yield return statement, which provides the next value. As a byproduct of this, the function pauses until the next value is requested.

In Unity, we don't use these to provide future values, we exploit the fact that the function pauses. Because of this exploitation, a lot of things about coroutines in Unity do not make sense (What does IEnumerator have to do with anything? What is yield? Why new WaitForSeconds(3)? etc.). What happens "under the hood" is, the values you provide through the IEnumerator are used by StartCoroutine() to decide when to ask for the next value, which determines when your coroutine will unpause again.

Your Unity Game is Single Threaded (*)

Coroutines are not threads. There is one main loop of Unity and all those functions that you write are being called by the same main thread in order. You can verify this by placing a while(true); in any of your functions or coroutines. It will freeze the whole thing, even the Unity editor. This is evidence that everything runs in one main thread. This link that Kay mentioned in his above comment is also a great resource.

(*) Unity calls your functions from one thread. So, unless you create a thread yourself, the code that you wrote is single threaded. Of course Unity does employ other threads and you can create threads yourself if you like.

A Practical Description of Coroutines for Game Programmers

Basically, when you call StartCoroutine(MyCoroutine()), it's exactly like a regular function call to MyCoroutine(), until the first yield return X, where X is something like null, new WaitForSeconds(3), StartCoroutine(AnotherCoroutine()), break, etc. This is when it starts differing from a function. Unity "pauses" that function right at that yield return X line, goes on with other business and some frames pass, and when it's time again, Unity resumes that function right after that line. It remembers the values for all the local variables in the function. This way, you can have a for loop that loops every two seconds, for example.

When Unity will resume your coroutine 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 have passed. If you used yield return StartCoroutine(AnotherCoroutine()), it resumes after AnotherCoroutine() is completely done, which enables you to nest behaviors in time. If you just used a yield return null;, it resumes right at the next frame.


It couldn't be simpler:

Unity (and all game engines) are frame based.

The whole entire point, the whole raison d'etre of Unity, is that it is frame based. The engine does things "each frame" for you. (Animates, renders objects, does physics, and so on.)

You might ask .. "Oh, that's great. What if I want the engine to do something for me each frame? How do I tell the engine to do such-and-such in a frame?"

The answer is ...

That's exactly what a "coroutine" is for.

It's just that simple.

And consider this....

You know the "Update" function. Quite simply, anything you put in there is done every frame. It's literally exactly the same, no difference at all, 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 absolutely no difference.

Footnote: as everyone has pointed out, Unity simply has no threads. The "frames" in Unity or in any game engine have utterly no connection to threads in any way.

Coroutines/yield are simply how you access the frames in Unity. That's it. (And indeed, it's absolutely the same as the Update() function provided by Unity.) That's all there is to it, it's that simple.


Have dig into this lately, wrote a post here - http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/ - that shed a light on the internals (with dense code examples), the underlying IEnumerator interface, and how it is used for coroutines.

Using collection enumerators for this purpose still seems a bit weird for me. It is the inverse of what enumerators feels designed for. The point of enumerators is the returned value on every access, but the point of Coroutines is the code in-between the value returns. The actual returned value is pointless in this context.

참고URL : https://stackoverflow.com/questions/12932306/how-does-startcoroutine-yield-return-pattern-really-work-in-unity

반응형