Unity Coroutine Drill Down!

Wikipedia에서 Coroutine 정의를 보면 다음과 같다.

Coroutines
 are computer program components that generalize subroutines to allow multiple entry points for suspending and resuming execution at certain locations.

“코루틴은 프로그램 컴포넌트로써, 특정 위치에서의 대기와 재시작 수행을 위한 다수 개의 진입 점을 가지는 서브 루틴(흔히 function이라고 부름)을 일반화한 컴포넌트이다.”

Unity Gems에서는 이렇게 정의한다.

A coroutine is a function that is executed partially and, presuming suitable conditions are met, will be resumed at some point in the future until its work is done.

코루틴은 부분적으로 실행되는 함수이며,  함수의 작업을 마칠 때까지 특정 조건이 만족되는 시점에서 다시 특정 위치에서 재시작 하는 함수이다.

이 coroutine을 이용하면 함수를 부분적으로 실행할 수도, 멈추고 대기를 탈 수도, 각각 원하는 시간에 다시 시작하게 할 수도 있다.
한번 살펴보자.

Elementary Step>
—————————————————————————————————————————————————————————

게임을 만들 때, 순차적인 상태 변환이나 호출을 만들어 낼 때 고전적으로는 아래와 같은 로직을 많이 쓴다.

그러나, Unity C# 에서는 yield statement를 이용해서 유사한 작업을 깔끔하게 할 수 있다.

yield statement는 return의 특별한 형태다.
함수가 다음번에 호출 될 때 해당 라인 다음 라인이 실행 되는 것을 보장하는 special keyword 이다.

아래 코드의 출력을 예상해보자.

위 코드를 실행시키면 출력을 다음과 같다.

Start
1
End
2
3

Coroutine에 생소한 개발자들은 return 이후에 함수의 라인들이 실행된다는 점에 신선한 충격을 받을 것이다.
이처럼 코루틴은 yield return 이후의 라인에 대해서도 조건(Yield Instruction)에 맞춰 실행하게 해준다.

아래는 “1” 이 호출 된 후, 1초후에 “2”가 반복 호출 되고, 이 루틴이 반복하는 것을 보여준다.

 

몇초 기다리게 할 수도 있다.

아래처럼 사용하면 “This is printed immediately” 가 즉시 호출 된 후에 2초 지난 다음 “Do 2 seconds later”가 호출 된다.

즉시 호출 되지 않고, 함수가 완료 된 다음 호출 되게 하기 위해서는

 

이렇게 StartCoroutine 호출 자체를 yield return 으로 해주면,
“This is after the Do coroutine has finished execution” 이 마지막에 찍힌다.

아래는 웹으로 데이터를 받아오는 동시에 로딩 Progress를 체크하는 로직이다. 코루틴 2개가 쓰였다.
(ref : http://www.unitystudy.net/bbs/board.php?bo_table=writings&wr_id=43)

추가로 몇가지 사용 예를 들어보면, 아래는 1 초 간격으로 5번 “Hello”를 출력하는 coroutine 로직이다.

특정 위치에 도달할 때까지 이동하는 것을 coroutine으로 작성한 로직이다.

글의 초반에 코드를 조금 변형해보자.
아래의 코드를 실행하면 결과값이 어떻게 출력이 되겠는가?

정답은 아래와 같다.

Start
1
2
3
End

코루틴을 코루틴으로 호출할 경우 해당 코루틴의 실행 종료 시점까지 기다려 준다.

이런걸 물론 코루틴이 없이도 Update(), SetState() 등으로 충분히 만들 수 있으나,
이처럼 코루틴을 사용하면 고전적으로 굉장히 지저분해 질 수 있는 코드를 깔끔하게 처리해주어서,
로직의 흐름을 쉽게 제어하여 코딩과 관리의 효율성이 높아지게 된다.

 

Step Further>
—————————————————————————————————————————————————————————

아래 함수를 보자 .

이 경우 현재 시간을 출력하고, 다시 재시작되었을 때 while(true) 구문으로 인해서 현재 시간을 다시 출력하기를 반복한다.

while 문 안쪽은 Update 함수와 같이 이 Object의 매프레임 호출 마다 정확히 1번 호출 된다. (Update가 호출 된 직후!)
StartCoroutine(TestCoroutine()) 으로 호출하면 해당 라인이 호출 되는 즉시 TestCoroutine() 이 한번 수행된다.

추가로, StopCoroutine(TestCoroutine())을 호출하거나, Object가 Destroy되거나, SetActiveRecursively(false) 가 호출 되거나 하면 위의 코루틴 호출은 종료된다.

 

라고 하면 10초동안 Unity Process가 아무것도 안하므로 CPU 상의 이득이 있다고 이해한다면 큰 오해다. 유니티는 매 프레임마다 coroutine wrapper에서 각 코루틴의
재시작 시점을 체크한다. 따라서 연산상의 이득이라기보다는 코드 제어상의 편의성과 readability를 최대 장점으로 봐야할 것이다.

yield return에서 사용하는 pausing 코드의 종류는 아래와 같다.

• null – the coroutine executes the next time that it is eligible
• WaitForEndOfFrame – the coroutine executes on the frame, after all of the rendering and GUI is complete
• WaitForFixedUpdate – causes this coroutine to execute at the next physics step, after all physics is calculated
• WaitForSeconds – causes the coroutine not to execute for a given game time period
• WWW – waits for a web request to complete (resumes as if WaitForSeconds or null)
• Another coroutine – in which case the new coroutine will run to completion before the yielder is resumed

yield break; 는 해당 코루틴을 즉시 종료한다.

코루틴의 장점이다.

1. Coroutines are a really good way of making a sequence of operations happen over time or when some external process is completed
2. Coroutines are not threads and are not asynchronous
3. Nothing else is running when your coroutine is executing
4. Your coroutine will resume when the conditions of your yield statement are met
5. Coroutines are inactive when the script is disabled or the object is destroyed
6. yield return new WaitForSeconds is dependent on game time which is affected by Time.timeScale

요약하면,
특정 프로세스가 완료되거나 순차적으로 이어지는 로직들을 만들 때 매우 좋음.
thread도 asyc도 아님(따라서 코루틴이 돌 때 아무것도 수행 안됨)
yield 문의 조건이 성립되면 재시작됨
스크립트가 disable되거나, object가 destroy가 되면 비활성화됨.
yield return new WaitForSeconds는 Time.timeScale의 값에 영향을 받으니 주의할 것

아래는 “die” 에니메이션이 50% 완료되었을 때 DropPickupItem()이 호출 되고,
100% 완료되었을 때, Destroy(gameObject);를 호출 하는 로직이다.
이걸 update 로직 안에서 처리하려면 여기저기 변수를 뿌려대고 아마 난리가 날 것이다.
깔끔하지 않은가? Elegance Code!

 

Step Deeper>

—————————————————————————————————————————————————————————

 

한걸음 더 나아가보면, 코루틴은 기본적으로 C#의 Iterator Block을 이용한 것이다. Iterator Block은 IEnumerator와 yield을 쌍으로 이루면서,
특정 코드 블럭을 반복할 수 있게 하는 기능이다. (ref : http://msdn.microsoft.com/ko-kr/library/dscyy5s0.aspx)
Iterator Block을 사용하기 위해서는 IEnumeratable 형태의 Get 접근자가 반드시 필요하다.
IEnumerator는 Current 속성과 MoveNext(), Reset() 메쏘드를 가지고 있다. 이 속성값과 메쏘드를 이용해서 반복 루틴을 제어하는 것이다.
(ref : http://msdn.microsoft.com/ko-kr/library/system.collections.ienumerator.aspx)

Current는 현재 요소이고, MoveNext()는 다음 요소로 이동하는 메쏘드다.
현재 yield return이 뱉은 값이 Current에 저장되고, yield return 다음에 수행할 코드 블럭을 MoveNext() 호출시에 수행하게 된다.

아래 예는 피보나치 수열을 IEnumerator로 구현한 예이다.

IEnumerator 변수인 fib 은 MoveNext()로 다음 실행 코드를 실행시키고,
Current로 현재 yield return에서 Get 한 값을 출력하는 것이다.

유니티 내부에서는
StartCoroutine(IEnumerator) 함수 호출시에 Coroutine Object가 생성이 될 것이다.
최초 실행 후에 yield 값을 저장한다. YieldInstruction의 종류는 다양할 것임(WaitForSeconds, WaitForEndOfFrame, WWW, Coroutine,null 등)

게임 로직 자체가 frame별로 수행이 되기 때문에, 한프레임당 MonoBehaviors의 Update 로직도 한번 수행될 것이다.
해당 프레임의 여러 포인트에서 저장된 Coroutine 오브젝트들이 있을 경우 조건에 맞는 지 체크한다.

WWW 은 isDone을 체크해서 완료되면 MoveNext() 호출
WaifForSeconds 는 게임 오브젝트들 Update가 호출 된 후에 시간이 지났는지 체크해서 MoveNext() 호출
null, 특정 값 은 게임 오브젝트들의 Update 호출 후 MoveNext()
WaitForEndOfFrame 은 모든 카메라의 Render가 호출 된 후에 MoveNext가 호출

만약 MoveNext() 값이 false 값을 리턴하면 해당 코루틴을 종료하는 형식으로 내부 동작이 이뤄진다.
코루틴이 코루틴을 기다리게 할 수 있다. 이걸 잘 이해하고 이용해야 코루틴을 잘 사용하는 것이다.

코루틴의 에러를 체크하려면,

조금 더 발전시켜서 타입 까지 체크하게 하면 아래와 같이 하면 된다.

아래와 같이 Generic 클래스와 MonoBehavior 클래스의 Extension을 이용해서 에러를 처리하는 방식을 만들 수 있다.

사용은 이렇게.

아래는 인터널 루틴에서 에러를 Catch해서 Coroutine 오브젝트에서 저장하여 에러를 체크하는 로직이다.

 

추가로, Coroutine을 이용해서 AI쪽 FSM을 개발하면 괜찮은 코드가 나온다.
또한 Coroutine을 관리하는 Manager Class를 만들어 코루틴의 Start, Pause, Resume을 관리하게 하는 것이 더 좋다.

reference :
http://unitygems.com/coroutines/
http://docs.unity3d.com/412/Documentation/ScriptReference/index.Coroutines_26_Yield.html
http://twistedoakstudios.com/blog/Post83_coroutines-more-than-you-want-to-know