종종 참조 상세 링크 에서 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 ()가 중지해야하는 지점입니다. yield return X는 MoveNext ()가 true를 반환하고 Current에 값 X가 할당되도록하는 반면 yield break는 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;
다른 경우를 처리하기 위해 더 많은 YieldInstruction 하위 유형을 추가 할 수있는 방법을 상상하는 것은 어렵지 않습니다. 예를 들어 WaitForSignal ( "SignalName") YieldInstruction을 지원하여 신호에 대한 엔진 레벨 지원을 추가 할 수 있습니다. YieldInstructions를 더 추가하면 코 루틴 자체가보다 표현력이 향상 될 수 있습니다. 새로운 수익률 반환 WaitForSignal ( "GameOver")은 (! Signals.HasFired ( "GameOver"))보다 반환하기가 더 좋습니다. 엔진에서 수행하는 것이 스크립트에서 수행하는 것보다 빠를 수 있다는 사실.
명백하지 않은 결과 두 가지이 모든 것에 대해 사람들이 때때로 내가 지적해야한다고 생각하지 못하는 몇 가지 유용한 것들이 있습니다.
첫째, 수익률 반환은 표현식 (어떤 표현식이든)을 산출하는 것이며 YieldInstruction은 일반 유형입니다. 즉, 다음과 같은 작업을 수행 할 수 있습니다.
YieldInstruction y;
if(something)
y = null;
else if(somethingElse)
y = new WaitForEndOfFrame();
else
y = new WaitForSeconds(1.0f);
yield return y;
특정 라인 yield yield new WaitForSeconds (), yield return new WaitForEndOfFrame () 등은 일반적이지만 실제로는 고유 한 형태가 아닙니다.
둘째, 이러한 코 루틴은 반복자 블록 일 뿐이므로 원하는 경우 직접 반복 할 수 있습니다. 엔진을 사용하지 않아도됩니다. 나는 전에 코 루틴에 인터럽트 조건을 추가하기 위해 이것을 사용했다 :
IEnumerator DoSomething()
{
/* ... */
}
IEnumerator DoSomethingUnlessInterrupted()
{
IEnumerator e = DoSomething();
bool interrupted = false;
while(!interrupted)
{
e.MoveNext();
yield return e.Current;
interrupted = HasBeenInterrupted();
}
}
셋째, 다른 코 루틴에서 생산할 수 있다는 사실은 마치 엔진에서 구현 한 것처럼 성능 적으로는 아니지만 자체 YieldInstructions를 구현할 수있게 해줍니다. 예를 들면 다음과 같습니다.
IEnumerator UntilTrueCoroutine(Func fn)
{
while(!fn()) yield return null;
}
Coroutine UntilTrue(Func fn)
{
return StartCoroutine(UntilTrueCoroutine(fn));
}
IEnumerator SomeTask()
{
/* ... */
yield return UntilTrue(() => _lives < 3);
/* ... */
}
그러나, 나는 이것을 정말로 추천하지 않을 것입니다 – 코 루틴을 시작하는 비용은 내가 좋아하는 데 약간 무겁습니다.
결론 이것이 Unity에서 Coroutine을 사용할 때 실제로 일어나는 일 중 일부를 분명히하기를 바랍니다. C #의 반복자 블록은 그루비 한 작은 구성이며, Unity를 사용하지 않더라도 동일한 방식으로 활용하는 것이 유용 할 수 있습니다.
IEnumerator
/ 를 반환IEnumerable
하고yield
키워드 를 포함하는 메서드를 변환 합니다. 반복자를 찾아보십시오.