블로그 이미지
Every unexpected event is a path to learning for you.

카테고리

분류 전체보기 (2737)
Unity3D (817)
Programming (474)
Server (33)
Unreal (4)
Gamebryo (56)
Tip & Tech (228)
협업 (58)
3DS Max (3)
Game (12)
Utility (136)
Etc (96)
Link (32)
Portfolio (19)
Subject (90)
iOS,OSX (53)
Android (14)
Linux (5)
잉여 프로젝트 (2)
게임이야기 (3)
Memories (20)
Interest (38)
Thinking (38)
한글 (30)
PaperCraft (5)
Animation (408)
Wallpaper (2)
재테크 (18)
Exercise (3)
나만의 맛집 (3)
냥이 (10)
육아 (16)
Total
Today
Yesterday
04-19 00:04

[출처http://theeye.pe.kr/archives/2725#disqus_thread

유니티에서 사용되는 코루틴(Coroutine)은 왜 필요한가?

유니티에서 화면의 변화를 일으키기 위해서는 Update() 함수 내에서 작업을 하게 됩니다. 이 Update() 함수는 매 프레임을 그릴때마다 호출되며 60fps의경우라면 초당 60번의 Update() 함수 호출이 발생하게 됩니다. 하나의 프레임 안에서 어떤 작업을 한다면 이 Update() 함수에 코드를 작성하면 될 것입니다.

하지만 다수의 프레임을 오가며 어떤 작업을 수행해야 한다면 어떻게 해야 할까요? 혹은 특정 시간, 가령 3초 동안 특정 작업을 수행해야 한다면 어떻게 해야 할까요? 3초니깐 3 x 60 = 180 프레임동안 작업을 수행하도록 하면 될까요?

안타깝게도 기기의 성능이나 상황에 따라 프레임 드랍(Frame drop)이라는 상황이 발생하게 됩니다. 60fps의 게임일지라 하더라도 디바이스의 성능에 따라 그 이하로 떨어질 수 있다는 의미가 됩니다. 이렇게 되면 더더욱 3초 동안 작업을 수행한다는게 쉽지 않은 일이 됩니다.

예로 다음의 코드를 준비하였습니다. 특정 오브젝트를 페이드 아웃(Fade out) 시키는 예제 코드입니다. 이 코드를 수행하면 스프라이트의 알파값이 점점 작아져서 결국 화면에서 사라지게 됩니다.






위의 코드를 보면 매 프레임마다 스프라이트 렌더러의 알파값을 0.1씩 감소시키고 있습니다. Update() 함수가 10번 호출되면 사라지게 되겠네요. 이는 1/6 초만에 사라지게 된다는것을 의미합니다. 이것도 1/6초만에 사라질지 보장받기가 어렵습니다.

그럼 혹시, 1초에 걸쳐 (60fps가 정상적으로 보장될 경우 60 프레임에 걸쳐) 사라지게 하려면 어떻게 하면 될까요? 대충 알파값을 0.017씩 감소시키면 될까요? 프레임이 아닌 시간 단위로 특정 작업을 수행할 수 있을까요? 여기서 생각할 수 있는 수단은 Time.deltaTime 이 있습니다.

하지만 우리가 여기서 알아보고자 하는것은 델타 타임이 아닌 코루틴이므로 코루틴에 대해서 알아보도록 하겠습니다. 코루틴은 프레임과 상관없이 별도의 서브 루틴에서 원하는 작업을 원하는 시간만큼 수행하는 것이 가능합니다.

다음은 코루틴을 사용하여 1초동안 페이드 아웃을 진행하는 예제 코드입니다.








이전 코드에서는 Update() 에서 모든 작업을 처리하던것을 Start() 에서 RunFadeOut() 코루틴을 실행하는것으로 변경된 것을 볼 수 있습니다. 여기서 주목해야 하는 부분은 yield return new WaitForSeconds(0.1f); 부분입니다.

이 복잡해 보이는 코드는 0.1초 동안 잠시 멈추라는 의미를 가진 코드입니다. 이제 위의 코드를 통해서 페이드 아웃 효과는 0.1초씩 10번을 수행하며 1초동안 사라지는 모습을 보여주게 됩니다. 이 코루틴은 Update() 함수에 종속적이지 않으며 마치 별도의 쓰레드와 같이 동작을 하게 됩니다. 이와 같은 코드로 프레임율에 영향을 받지 않는 시간 기반의 서브루틴을 구동할 수 있게 되었습니다.

IEnumerator와 yield는 무엇이며 어떤 관계가 있는가?

그렇다면 여기서 궁금증을 유발하는 부분이 몇가지 있는데요 RunFadeOut의 리턴 타입은 IEnumerator(열거자) 입니다. 또한 while 문 내부에 보면 yield(양보)라는 구문이 보이는군요. 그 뒤로 return이 따라나오는 것도 일반적인 언어에서 보기 힘든 문법입니다. 이것들이 어떤 관계를 가지고 있는지 알아보겠습니다.

우선 다음의 일반적인 C# 코드를 한번 살펴보도록 하겠습니다.





위의 Main() 함수를 실행하게 되면 다음과 같은 결과물이 출력됩니다.



조금 헷갈리지만 알고보면 어렵지 않은 코드입니다. 이 코드는 다음과 같은 순서로 동작하게 됩니다.

  1. SomeNumbers() 함수를 실행한 결과를 IEnumerator 열거자로 받습니다. 정확히는 실행된 결과가 아닙니다. enumerator 에 함수의 실행결과가 할당 되었다고 생각될만한 코드지만 여기서는 SomeNumbers() 함수는 한줄도 실행되지 않은 상태입니다. 함수의 포인터를 받았다고 생각하시는게 이해하시기 편할 것 같습니다.
  2. while 문을 만나면서 처음으로 enumerator의 MoveNext()가 호출됩니다. 여기서 SomeNumbers()가 실행이 되며 딱 yield 문을 만날때까지 실행이 됩니다.
  3. 첫번째 yield 문인 yield return 3; 을 만납니다. 여기서는 표현식 그대로 return 3에게 양보한다는 느낌으로 코드를 읽으시면 될 것 같습니다. 우선 여기까지 오면 3을 리턴하는것에 양보가 일어납니다. 이때에 리턴되는 값이 존재하므로 MoveNext()의 결과값으로 true가 반환됩니다.
  4. 이제 enumerator의 Current를 통해서 현재 반환된 값을 꺼내올 수 있습니다. MoveNext()를 통해서 yield return 되는 값이 있는지를 알 수 있고 반환된 값이 있다면 Current에 담겨 있게 됩니다.
  5. Debug.Log를 사용하여 Current를 출력해보면 처음으로 양보 반환된 3이 출력되게 됩니다.
  6. 다시한번 while문이 실행되며 MoveNext()가 호출되면 정말 재미있게도 가장 마지막에 yield return이 일어났던 위치의 다음줄부터 재실행이 되게 됩니다. 다시한번 yield 문을 만날때까지 진행이 됩니다.
  7. 이번에는 두번째 yield문인 yield return 5를 만나게 됩니다. 결과적으로 MoveNext() 의 결과값은 true가 되게 됩니다.
  8. 현재 Current에 할당된 값은 MoveNext()의 양보 반환된 값인 5가 될 것입니다.
  9. Debug.Log를 통해 값을 출력해보면 5가 출력됩니다.
  10. 다시한번 while문의 MoveNext()를 호출하면 yield return 5; 다음줄부터 재시작이 되게 되면 yield return 8;까지 진행이 되게 됩니다.
  11. 8이 양보 반환되었으므로 MoveNext()의 값은 true가 되며 Current에는 8이 들어가있게 됩니다.
  12. Debug.Log로 8이 출력됩니다.
  13. 다시한번 MoveNext() 가 호출되며 yield return 8; 이후의 코드부터 실행이 되지만 함수의 끝을 만나게 되므로 더이상 yield가 일어나지 않습니다.
  14. MoveNext() 의 결과 값으로 false가 반환되며 while 문이 종료됩니다.

조금 특이하지만 함수의 반환값이 IEnumerable, IEnumerable<T>, IEnumerator, IEnumerator<T> 인 경우에는 위와 같은 동작을 하게 됩니다. 함수의 동작이 비동기적으로 동작하게 되므로 파라미터에 ref나 out을 사용할 수 없다는 제약 사항이 있습니다. 위의 코드 동작 예시는 코루틴이 어떻게 동작하는지 알기위한 기본적인 코드라고 생각됩니다. 이제 다시 코루틴으로 돌아가 보겠습니다.







StartCoroutine을 직접 구현해 본다면 위와 같은 형태의 코드가 될 것 같습니다. 먼저 코루틴 함수의 포인터 역할을 하는 열거자를 받은 다음에 MoveNext()를 통해 첫번째 yield를 만날때까지 수행하고 그 결과값을 받습니다. 그리고 그 결과값에 맞는 작업을 수행해줍니다. 그리고 이것을 함수가 완료될 때까지 반복합니다.

위의 코드에서는 4번의 MoveNext()가 호출될 것이며 3번의 yield문을 만날 것입니다. 마지막 MoveNext()에서는 false가 반환될 것이므로 코루틴이 종료됩니다. 만약 함수의 실행이 완료되기 이전에 임의로 코루틴을 종료시키고 싶다면 yield break를 호출하면 됩니다. 즉시 MoveNext()에서 false가 반환되어 종료됩니다.

결론적으로 StartCoroutine은 IEnumerator를 반환하는 함수를 인자로 받으며 이 함수는 특이하게도 실행된 결과를 의미하는것이 아니라 함수 포인터와 같은 개념으로 사용이 됩니다.




이 코드를 한번 봐보겠습니다. 일반적인 함수의 개념으로 보자면 StartCoroutine에는 RunCoroutine() 함수의 결과값이 파라미터로 넘겨지게 되어있습니다. 하지만 여기서 RunCoroutine()은 단 한줄도 실행이 되지 않습니다. 함수의 포인터 역할을 하는 IEnumerator가 넘겨지게 되고 MoveNext()를 호출할 때마다 yield 문을 만날때까지 수행됩니다. 만나게 되면 MoveNext()가 true를 반환하고 함수가 끝나거나 yield break; 를 만나게 되면 false를 반환하게 됩니다. true를 반환할 경우 Current를 통해 반환된 값을 꺼내볼 수 있습니다.

StartCoroutine을 수행할 때 사용할 수 있는 두가지 방법

public Coroutine StartCoroutine(IEnumerator routine);

일반적으로 사용할 수 있는 방법입니다. 수행하고자 하는 코루틴의 IEnumerator 열거자를 넘겨서 실행되도록 합니다. 다음과 같은 방법으로 사용이 가능합니다.




위와 같은 방법은 일반적인 방법으로 waitTime 파라미터 값을 넘길 수 있으며 코루틴이 실행되는데에 추가적인 오버헤드가 전혀 없는 방법입니다. 뿐만 아니라 Start() 함수의 반환값을 IEnumerator로 변경하여 아예 코루틴이 실행 완료될때까지 기다리도록 의존적인 방법으로 실행하는 것도 가능합니다.




위의 코드는 WaitAndPrint(waitTime) 코루틴이 실행 완료된 이후에야 Done이 출력되는 과정을 보여줍니다.

public Coroutine StartCoroutine(string methodName, object value = null);

대부분의 경우는 StartCoroutine을 사용하기 위해 전자의 방법을 사용합니다. 하지만 StartCoroutine을 문자열 형태의 코루틴 함수 이름으로도 호출하는 것이 가능합니다. 이렇게 호출하면 StopCoroutine 역시 함수 이름만으로 호출하는것이 가능해 집니다.





위의 코드는 DoSomething(someParameter) 코루틴 함수를 함수 이름과 넘겨질 파라미터를 통해 호출하는 과정을 보여주고 있습니다. 그리고 1초 기다린 뒤에 실행했었던 DoSomething 코루틴을 종료시킵니다. 이러한 함수 이름을 문자열로 넘겨 실행하는 방법은 StartCoroutine을 수행하는데에 오버헤드가 크고 파라미터를 한개밖에 넘길 수 없다는 제약사항이 있습니다. 물론 배열을 넘기는것 역시 가능합니다.

yield return에서 사용할 수 있는 것들

위에서 본 예시에는 WaitForSeconds 클래스를 양보 반환함으로써 원하는 시간(초)만큼 기다리는 것이 가능하다는것을 알 수 있었습니다. 추가로 더 알아 보도록 하겠습니다.

yield return new WaitForSecondsRealtime (float time);

WaitForSeconds와 하는 역할은 동일하지만 결정적으로 다른것이 있습니다. 유니티상의 시간은 임의로 느리게 하거나 빠르게 하는 것이 가능합니다. 이를 Time.timeScale을 통해서 조정을 할 수 있습니다. 매트릭스에서 보던 총알이 느리게 날아오면서 그것을 피하는 모션을 구현해 본다면 이 값을 1보다 낮추게 되면 현재 시간의 진행보다 느려지게 되며 1보다 빠르게 변경하면 현재의 시간의 진행보다 빨라지게 됩니다. 하지만 WaitForSecondsRealtime는 이러한 Scaled Time의 영향을 받지 않고 현실 시간 기준으로만 동작을 하게 됩니다.

yield return new WaitForFixedUpdate ();

다음 FixedUpdate() 가 실행될때까지 기다리게 됩니다. 이 FixedUpdate()는 Update()와 달리 일정한 시간 단위로 호출되는 Update() 함수라고 생각하시면 됩니다.

yield return new WaitForEndOfFrame ();

하나의 프레임워 완전히 종료될 때 호출이 됩니다. Update(), LateUpdate() 이벤트가 모두 실행되고 화면에 렌더링이 끝난 이후에 호출이 됩니다. 특수한 경우에 사용하면 될 것 같습니다만 잘 모르겠군요.

yield return null;

WaitForEndOfFrame를 이야기 했다면 이것을 꼭 이야기 해야 할 것 같습니다. yield return null; 을 하게 되면 다음 Update() 가 실행될때까지 기다린다는 의미를 갖게 됩니다. 좀 더 정확하게는 Update()가 먼저 실행되고 null을 양보 반환했던 코루틴이 이어서 진행 됩니다. 그 다음에 LateUpdate()가 호출됩니다.

yield return new WaitUntil (System.Func<Bool> predicate);

이번엔 특정 조건식이 성공할때까지 기다리는 방법입니다. WaitUntil에 실행하고자 하는 식을 정의해 두면 매번 Update() 와 LateUpdate() 이벤트 사이에 호출해 보고 결과값이 true면 이후로 재진행을 하게 됩니다. 다음의 예제 코드를 보겠습니다.








이 코드는 Update() 함수를 통해 매 프레임마다 frame 멤버 변수값을 1씩 최대 10까지 증가시키게 됩니다. 실행중인 코루틴은 frame값이 10또는 10보다 커질때까지 기다리다가 이 식이 충족되게 되면 다음으로 진행을 하게 됩니다. 여기서 사용되는 식은 람다 표기법이 사용됩니다. 다음과 같은 느낌이라고 생각하시면 될 것 같습니다.

yield return new WaitWhile(System.Func<Bool> predicate);

WaitWhile은 WaitUntil과 동일한 목적을 가지고 있지만 한가지만 다릅니다. WaitUntil은 람다식 실행 결과값이 true가 될때까지 기다린다면 WaitWhile은 false가 될때까지 기다립니다. 즉 WaitWhile은 결과가 true인 동안 계속 기다리게 됩니다.








위의 코드는 첫프레임부터 람다식의 결과가 true이게 됩니다. 10프레임에 도달하면 false가 되어서 이후 진행이 되겠네요.

yield return StartCoroutine (IEnumerator coroutine);

이번에는 심지어 코루틴 내부에서 또다른 코루틴을 호출할 수 있습니다. 물론 그 코루틴이 완료될 때까지 기다리게 됩니다. 의존성 있는 여러작업을 수행하는데에 유리하게 사용 될 수 있습니다.







위와 같은 코드를 실행해 본다면 결과는 다음과 같이 출력됩니다.

Coroutine 중단하기

public void StopCoroutine(IEnumerator routine);

이 방법은 기존에 StartCoroutine을 실행할 때 넘겨주었던 코루틴 함수의 열거자를 파라미터로 사용하여 그것을 중단시키는 방법입니다. 다음과 같은 사용이 가능합니다.








Start() 함수에서 WaitAndPrint(waitTime) 코루틴 함수의 열거자를 획득하여 클래스의 멤버 변수로 설정해 두고 이 코루틴을 실행합니다. 이 코루틴은 1초에 한번씩 WaitAndPrint 를 출력하게 되며 유저가 스페이스키를 누르게 되면 멤버 변수에 담겨 있는 기존 코루틴의 열거자를 이용하여 실행중인 코루틴을 중단시킵니다.

public void StopCoroutine(string methodName);

이 방법은 이전 방식보다 오버헤드는 크지만 간편하게 사용할 수 있는 방법입니다. 다음과 같이 멤버 변수 없이도 간편하게 사용할 수 있습니다.







이때에 주의할 점으로는 StopCoroutine을 문자열로 종료시키려면 StartCoroutine 역시 문자열로 실행했었어야 한다는 점입니다. StartCoroutine(IEnumerator routine) 으로 실행한 다음에 StopCoroutine(string methodName) 으로 종료시킬 수 없습니다.

public void StopAllCoroutines();

마지막으로 현재 Behaviour (클래스라고 이해하면 될 것 같습니다)에서 실행한 모든 코루틴을 한번에 종료시키는 함수입니다. 이와 같은 방법으로 현재 클래스에서 실행한 모든 코루틴을 한번에 중단시키게 됩니다.





어디선가 Example()을 실행하게 되면 DoSomething 코루틴이 실행되게 되면 곧바로 StopAllCoroutines() 이 호출되어 모든 코루틴이 종료됩니다.

참고 :
http://docs.unity3d.com/kr/current/Manual/Coroutines.html
http://docs.unity3d.com/ScriptReference/MonoBehaviour.StartCoroutine.html


반응형
Posted by blueasa
, |

이전에 Coroutine을 사용했을때는 그냥 사용했는데.. 어디에 제대로 사용하는 것이 맞는것일까? 고민도 하구..
unityStudy 사이트에 너무 잘 설명해 놓은 글이 있어서 차라리 그 글을 한번 정독하면 큰 도움이 될것입니다
.

URL : 
[스크립팅] 코루틴(Coroutine) 기본 개념  활용 
두번째 해당 사이트는 영문입니다. : http://unitygems.com/coroutines/

아래는 백과사전 및 위 사이트에 대한 부분을 이해하기 위해 정리했습니다.

 

1.    동시 실행(협동) 루틴이라 불리 우는 Coroutine 정의

A.     호출 루틴과 피호출 루팅이 대등 관계를 유지하는 처리 절차.
부차적 프로그램의 수행이 완전히 끝나기 전에 제어가 호출 루틴으로 돌아가는 것이 동시 실행 과정이다. 그리고 제어가 부차적 프로그램으로 돌아 왔을 때는 중단된 부분부터 다시 수행이 계속 된다. 주종 관계를 갖지 않고 서로 호출하는 둘 이상의 모듈들.서브루틴의 제어 전달 개념과 유사한것. 각 호출에서 초기화되는 서브루틴과는 달리, 호출 시 관련된 모든 정보를 보존하는 능력을 갖는다. 그리고 다음에 다시 시작할 때에는 이전에 실행했던 다음부터 실행 할 수 있는 논리를 갖는다.

B.     프로그램에서 순서는 일반적으로 불려지는 쪽이 부르는 쪽에 속하고 있는 것이 대부분이지만 어느 쪽도 종속 관계가 아니라 대등한 관계로 서로 호출하는 것이다.
예를 들면, 게임 프로그램에서 각 플레이어 루틴은 서로 Coroutine 된다. 복수 프로세스 간에서 한정된 형태의 통신을 행하는 프로그램을 순차 제어로 실현한 것으로 볼 수 있다.

C.     C언어 등에서 일반적으로 사용하는 함수는 시작할 때 진입하는 지점이 하나 존재하고 함수가 모두 실행되거나,  return 구문에 의해서 종료되는 지점을 설정 할 수 있다.
이러한 함수를 서브 루틴(subroutine)이라 부르는데, 코루틴은 이를 더 일반화한 개념으로 진입하는 지점까지 여러 개를 가질 수 있는 함수를 의미. (서브 루틴도 코루틴의 한 종류)

D.     Coroutine 은 프로그램 상에서 보면 멀티 쓰레드와 비슷한 개념이다.
별도의 스택과 지역변수들을 가진 실행 중, 각자의 메모리 영역과 실행 명령 포인터를 가지고 있어
,
재개(Resume)와 양보(yield)가 가능한 구조로 되어 있다
.
중지(yield)하면 실제 프로그램(함수)는 종료되지 않고 위치를 대기시킨 후 값이 만들어지는 시점부터 다시 시작(resume) 할수 있다는 말이다.

 

2.    왜 필요한 것인가?

유니티에서 코루틴과 상호 작용하는 주체는 엔진이다
.
코루틴을 사용 할 때는 C#의 언어적인 기능을 익혀서 모든 것을 해결한다는 개념보다 스크립트에서 엔진이 가지는 기능을 활용하는 또 한가지의 관점으로 접근해야 한다
.
스크립트에서 StartCoroutine 함수를 통해 코루틴을 구동하면 코루틴은 첫 번째 데이터를 엔진에 전달하고 이 데이터를 분석한 엔진은 내부 루푸를 돌면서 필요한 때가 되면 다음 데이터를 전달하도록 코루틴을 다시 호출해 준다. 이때 엔진에 전달할 데이터가 정말 중요한데, 데이터들은 코루틴이 유니티 엔진에게 보내는 일종의 명령으로 생각하면 좋다
.



 

3.    자주 등장하는 조연 배우들…

A.     Yield 
Coroutine 
은 Yield 를 사용하여 중간에 제어권을 다른 함수에 넘겨주는 역할을 한다.
yield 를 사용하여 일정시간 대기 할 수 있는 함수도 있다
.
yield return new WaitForSecond(3.0f); 
3초  대기 후에 리턴을 해주는 순서다.  여기서 기본적인 추가 특징으로 update 함수상에서는 yield 문의 직접적인 사용이 금지가 됩니다.

B.     IEnumerator
(msdn) 
제네릭이 아닌 컬렉션을 단순하게 반복할 수 있도록 지원하는 인터페이스
우리말로 열거자 라고 하는데 , 데이터의 목록을 하나씩 넘겨 줄 때 사용되는 인터페이스 이다.
코루틴은 호출한 함수와 서로 상호작용하면서 진행하도록 설계되어 있습니다
.
코루틴은 자신을 호출한 함수에 데이터를 하나 넘겨주고 쉰다
.
받은 측에서는 데이터를 받고 나서 처리한 후에 코루틴에게 다음 데이터를 달라고 깨운다
.
쉬고 있던 코루틴은 일어나서 다시 데이터를 전달하고 .. 이를 계속 반복하는 구조로 동작한다
.
이런 작업에 적절한 인터페이스가 IEnumerator이며, C# 에서 코루틴을 사용할 때는 이를 사용한다
.
일반적으로 호출한 함수에게 데이터를 전달 할때, return 구문을 사용하게 되면 데이터를 전달하고 함수는 종료 된다.

C.     StartCoroutine 함수
IEnumerator 반환형을 받아 yield 이 만날 때 함수를 일시 정지하고, 다른 스크립트나 함수에게 활동을 넘긴다. 다시 말해 코루틴 함수를 구동하는 함수다.



 

4.    엔진이 제공하는 데이터들과 이를 수행하는 명령들.

A.     Yield return null : 다음 프레임까지 대기

B.     Yield return new WaitForSeconds(flat) : 지정된 초(float) 만큼 대기

C.     Yield return new WaitForFixedUpdate() : 다음 물리 프레임까지 대기

D.     Yield return new WaitForEndOfFrame() : 모든 렌더링 작업이 끝날 때 까지 대기

E.     Yield return StartCoroutine(string) : 다른 코루틴이 끝날 때 까지 대기

F.      Yield return new WWW(string) : 웹 통신 작업이 끝날 때까지 대기

G.     Yield return new AsyncOperation : 비동기 작업이 끝날 때 까지 대기 (씬로딩)

•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

 


 

 

 

5.    Coroutine 의 장점

A.     성능
yield return new WaitForSecond(3.0f); 
이라는 명령을 수행하면, 코루틴은 유니티 엔진에게 WaitForSeconds(3.0f) 라는 데이터를 보내고 쉬기 시작합니다.
코루틴이 없이 일반적으로 이를 구현한다면, Update 문에서 Time.deltaTime 을 사용하여 매 프레임마다 시간을 더해서 10초가 지났는지 감지해야 하는데, 프레임의 평균 소요 시간이 0.01초 라고 한다면 아이러니하게 10초 동안 대기 하기 위해 스크립트는Update 함수를 1000번 호출해야 합니다
.
특히 모바일 기기에서 코루틴의 활용은 성능 향상에 큰 영향을 미친다
.

B.     가독성
코루틴을 사용하면 읽기 좋은 코드가 만들어 진다. 같은 결과물을 나타내는 두개의 코드로 비교하면

//update 상에서 코루틴을 사용하지 않고 직접 구현시

public class UpdateTimer : MonoBehaviour {
    float accumulator = 0.0f;
    bool wait1Finished = false;
    bool wait2Finished = false;
    float waitTime1 = 1.0f;
    float waitTime2 = 2.0f;

    void Start() {
        Debug.Log ("Action Start!");
    }
    void Update () {
        accumulator += Time.deltaTime;
        if(!wait1Finished && !wait2Finished) {
            if(accumulator >= waitTime1) {
                Debug.Log ("Action1 End");
                wait1Finished = true;
                accumulator = 0.0f;  
            }
        } else if(wait1Finished) {
            if(accumulator >= waitTime2) {
                Debug.Log ("Action2 End");
                wait2Finished = true;
                accumulator = 0.0f;  
            }
        }
    }
}

//Start 
함수를 IEnumerator 로 선언시. (엔진이 자동으로 코루틴으로 실행해줌.
public class CoRoutineTimer : MonoBehaviour {
    float waitTime1 = 1.0f;
    float waitTime2 = 2.0f;   

    IEnumerator Start () {
        Debug.Log ("Action Start");
        yield return new WaitForSeconds(waitTime1);
        Debug.Log ("Action1 End");
        yield return new WaitForSeconds(waitTime2);
        Debug.Log ("Action2 End");
    }     
}

 

6.    특징 및 활용

A.     가장 많이 사용되는 건 WaitForSeconds 명령

멀티 쓰레드 프로그래밍의 Thread.Sleep 함수 처럼 원하는 시간 만큼 잘수 있다. 하지만 더욱 좋은 점은 엔진과 동일한 싱글 쓰레드로 돌아가기 때문에, 멀티 쓰레드 프로그래밍의 어려운 부분인 자원 관리 및 컨텍스트 스위칭(Context Switching)과 같은 다양한 고려 사항의 필요가 없음.

B.     두번째로 많인 사용하는 부분은 비동기 작업

웹에서의 데이터 수신, 씬 로딩과 같이 오래 걸리는 작업은 엔진에게 넘기고, 자신은 진행상황을 점검하면서 UI 처리만 수행하는 형태로 사용합니다.
아래와 같이 WWW 클래스가 대표적인 비동기 작업인데, 웹에서 데이터를 다운로드 받기 위해 스크립트는 엔진에게 www 클래스를 만들어 데이터를 받아올 URL과 함께 넘기고 자신은 쉰다
.
이를 받은 엔진은 비동기 처리를 위한 쓰레드를 생성하고, 네이티브 라이브러리를 사용하여 웹에 접속한  후 데이터를 다운로드 하면서 스크립트로부터 넘겨 받은 www 객체에 직장 상사에게 보고하듯 현재 진행상황을 저장해 둔다.(이 때 클래스의 레퍼런스를 넘겼기 때문에 다른 스크립트에서 진행상황을 읽어 들이는 것이 가능함
.)
엔진에서 다운로드가 끝나면 www 객체에는 완료 상황보고와 함께 받은 데이터가 저장되며, 엔진은 스크립트를 깨운다. 스크립트에서는 바로 그 떄부터 그것을 가지고 사용한다
.

//코루틴이 쉬는 동안 엔진은 열심히 다운로드 받고, Update 함수는 이를 보면서 열심히 진행상황을 로그에 남기는 예제임
.
public class WebBasic : MonoBehaviour {
    public string url;
    WWW www;
    bool isDownloading = false;

    IEnumerator Start()
    {
        www = new WWW(url);
        isDownloading = true;
        yield return www;
        isDownloading = false;
        Debug.Log ("Download Completed!");
    }

    void Update()
    {
        if(isDownloading)
            Debug.Log (www.progress); 
    }
}

//update 에서 이런 처리 부담스러워…프로그래스 바를 넣는식으로 변경하는 방법으로
..
public class WebAdvanced : MonoBehaviour {
    public string url;
    WWW www;

    IEnumerator Start()
    {
       www = new WWW(url);
       StartCoroutine("CheckProgress");
       yield return www;
       Debug.Log ("Download Completed!");
    }

    IEnumerator CheckProgress()
    {
        Debug.Log (www.progress);
        while(!www.isDone)
        {
            yield return new WaitForSeconds(0.5f);
            Debug.Log (www.progress);
        }
    }
}

C.     마지막으로 코루틴을 사용하여 작업을 원하는 대로 순차적으로 수행 할수 있다.

C#과 같은 고급 언어(High Level Language) 에서 지원하는 리플렉션(Reflection) 기능과 코루틴 기능이 혼합된 활용성에 있어서 백미라고 할수 있다
.
이의 활용 예를 위해 RPG 게임등에서 흔히 보이는 패턴인 A 와 B 중 하나를 선택하게 하는 다이얼로그를 띄운 후 , 사용자의 결정에 따라 A 를 선택하면 ActionA 를 수행하고  B 를 선택하면 ActionB를 수행하는 구문을 프로그래밍 할 때 이를 코루틴으로 구현하기 위해, 먼저 모든 행동을 코루틴으로 정의해 준다
.
그리고 각 행동들의 순서를 제어하는 코루틴을 만들어주고, 코루틴 안에서 행동들이 순차적으로 실행되도록 yield return new StartCoroutine 구문으로 차례대로 지정해 준다
.
첫번째 코루틴의 결과에 따라 다음 행동이 A 가 될지 B 가 될지 결정되므로, 첫 번째 결과 값을 string 변수로 지정하고, 변수 값을 StartCoroutine 에 넣으면 실시간으로 결과에 따른 해당 코루틴이 실시간으로 실행 된다. 아래는 실 구현 예제 


public class DialogExample : MonoBehaviour {
    bool showDialog = false;
    string answer = "";

    IEnumerator Start()
    {
        yield return StartCoroutine("ShowDialog");
        yield return StartCoroutine(answer);
    }

    IEnumerator ShowDialog()
    {
        showDialog = true;
        do
        {
            yield return null;
        } while(answer == "");
        showDialog = false;
    }

    IEnumerator ActionA()
    {
        Debug.Log ("Action A");
        yield return new WaitForSeconds(1f);
    }

    IEnumerator ActionB()
    {
        Debug.Log ("Action B");
        yield return new WaitForSeconds(2f);
    }
    void OnGUI()
    {
        if(showDialog)
        {
            if(GUI.Button(new Rect(10f, 10f, 100f, 20f), "A"))
            {
                answer = "ActionA";   
            } else if(GUI.Button(new Rect(10f, 50f, 100f, 20f), "B")) {
                answer = "ActionB";
            }
        }
    }
}

//
결과 값의 유효성을 강화하고 싶다면 아래와 같이 enum 를 보조적으로 사용하는 것도 좋은 방법이 될수 있다.

using UnityEngine;
using System.Collections;

public class DialogExampleEnum : MonoBehaviour {
    enum DialogActions {
        ShowDialog,
        ActionA,
        ActionB
    };

    bool showDialog = false;
    DialogActions action = DialogActions.ShowDialog;
    IEnumerator Start()
    {
        yield return StartCoroutine(action.ToString());
        yield return StartCoroutine(action.ToString());
    }

    IEnumerator ShowDialog()
    {
        showDialog = true;
        do
        {
            yield return null;
        } while(action == DialogActions.ShowDialog);
        showDialog = false;
    }
    IEnumerator ActionA()
    {
        Debug.Log ("Action A");
        yield return new WaitForSeconds(1f);
    }
    IEnumerator ActionB()
    {
        Debug.Log ("Action B");
        yield return new WaitForSeconds(2f);
    }
    void OnGUI()
    {
        if(showDialog)
        {
            if(GUI.Button(new Rect(10f, 10f, 100f, 20f), "A"))
            {
                action = DialogActions.ActionA;         
            } else if(GUI.Button(new Rect(10f, 50f, 100f, 20f), "B")) {
                action = DialogActions.ActionB;
            }
        }
    }
}
이러한 특징을 잘 홀용하면 AI 를 위한 유한상태 기계(Finite State Machine) 을 구현할 때에도 별도의 클래스를 만들지 않고 쉽게 코딩이 가능해 진다
.

D.     Coroutine 는 for 루프 내에서는 사용할 수 없지만, while 문이나 foreach 문에서는 사용이 가능하다.
IEnumerator Foo()
{
 while(someCondition)
 { 
   // 여기서 시간이 오래 걸리는 처리를 수행한다
.
   // 다음 프레임 전에 일시 정지
.
 }
}


*Tip : 이전부터 Debug.Log 을 사용하는 것을 꺼려 했다. 실제 빌드 후에 자동으로 사라지는 것이 아니기 때문에,
이 함수를 별도로 따로 빼서 클래스를 생성해서, 그 클래스를 직접 사용하는 편이 가장 좋다
.
http://answers.unity3d.com/questions/126315/debuglog-in-build.html

public static void Log (String method)
  {
       Debug.Log (method);
    }



[출처] http://kimseunghyun76.tistory.com/304

반응형
Posted by blueasa
, |

코루틴(Coroutine)++

Unity3D/Tips / 2015. 10. 19. 11:32

출처 : http://unityindepth.tistory.com/21


원문은 이곳에서 보실수 있습니다.



그는 코루틴으로 속임수를 쓴다.
그들은 실행되고,  왜 때때로 작동하지 않는가!!!
또, 컷 시퀀스를 만드는 방법과 강력한 기술을 사용하여 
오랫동안 실행시키는 방법에 대해서 배워 보자

목적


만약 코루틴에 대해서 확실하게 모르거나, 원하는 작업을 하고자 할 때 문제가 발생했다면 당신은 반드시 이 튜토리얼을 읽어야 한다.


코루틴은 다음과 같은 훌륭한 방법이다:

  • 특정 일을 단계적으로 발생하게 한다.

  • 시간이 흐름에 따라 발생하는 루틴을 작성할 수 있다.

  • 다른 연산이 완료될때까지 기다리는 루틴을 작성할 수 있다.
예를 들어, 컷 씬 시퀀스를 조직하는데 코루틴을 사용하는 것을 쉽게 생각할 수 있다. 혹은 단순히 적이 죽는 애니메이션을  기다린 다음, 재생성을 할 수 있을 것이다.

코루틴은 유니티의 매우 강력한 부분이지만, 종종 초심자에 의해서 이 주제에 대해 많은 오해를 받는다. 이 튜토리얼은 당신이 강력하고 융통성있는 코루틴을 파악하고, 그들이 어떻게 작동하는지 이해하는데 도움을 줄 것이다.


만약 단지 몇 초 동안 특정 연산을 지연시키기를 원한다면, Invoke를 사용하는 것을 고려해봐라. 물론 코루틴을 사용할 수는 있지만, 초보자들에게는 Invoke가 더 쉽다.

쓰레드와 코루틴


우선, 이들에 대해서 명확하게 하자, 코루틴은 쓰레드가 아니다. 코루틴은 비동기가 아니다
(not asynchronous).
(역자 주 : 비동기가 아니다라는 말은, 쉽게 말해서 동시에 발생하지 않는다는 뜻이다.)
쓰레드의 비동기와 동기의 개념이 잘 이해가 안된다면

쓰레드는 프로그램에서 다른 쓰레드와 함께 비동기로 실행된다. 멀티프로세서 기계에서, 쓰레드는 실제 다른 쓰레드와 함께 동시에 코드가 실행될 수 있다. 이는 쓰레드를 프로그래밍하는 것을 이해하기 복잡하게 만드는데, 왜냐하면 하나의 쓰레드는, 다른 쓰레드가 특정 부분을 읽고 있는 동시에, 그것을 변경할 수 있기 때문이다.

이는 더 복잡한데, 왜냐하면 당신이 작성한 코드는 컴파일러에 의해서 어셈블리어로 변경이되는데, 이는 다른 쓰레드가 당신의 소스코드의 한 부분을 처리하면서, 실제로 게임 중에 특정 부분을 변경할 수 있다는 것을 의미한다.이 때문에, 당신은 공유 메모리영역을 만들지 않거나, 공유된 자원을 읽거나 변경시키고 있을 때에는, 공유된 자원으로부터 다른 쓰레드를 잠궈버림으로서, 이런 상황을 발생시키지 않도록 하기 위해서 고생을 겪을 것이다.

쓰레드는 다음 글의 주제가 될 것이다.

그래서 코루틴이 무엇인가?


우리는 분명하게 코루틴이 쓰레드가 아니라는 것을 확실하게 했다. 이는 한번에 오직 하나의 코루틴이 실행되고, 게임의 주된 쓰레드에서 그것이 실행된다는 것을 의미한다. 사실, 한때 게임의 핵심 코드로 실행되는 것들 중 하나에 불과하다.


코루틴을 프로그래밍할 때, 당신은 절대 동기화와 특정 값을 잠금하는 것에 대해서 걱정할 필요가 없다. 당신의 코드에서 yield를 실행하기 전까지 완전한 통제를 할 수 있을 것이다.

그래서 여기 코루틴에 대한 정의가 나와있다:

코루틴은 부분적으로, 그리고 특정한 상황이 맞아 떨어졌을 때 실행되는 함수로써,
그 작업이 완료가 되기전까지, 미래의 어느시점에 재개될 수 있다.

유니티는 코루틴을 하나 혹은 여러개를 가지는 모든 객체에 대해서 게임의 모든 프레임마다 코루틴을 처리한다.
이 처리는 대부분의 yield 문에 대해서 Update가 끝나고, LateUpdate가 시작하기전에발생하는데, 그러나 여기 특별한 경우가 있다:

코루틴이 활성화되었을 때, 바로 다음 yield 문까지 실행될 것이고, 그것은 다시 시작될 때까지 일시정지할 것이다. 위에 다이어 그램을 통해서, 당신이 yield한 것을 기반으로 어디에서 재시작되는지 알 수 있을 것이다.


정말 간단한 코루틴을 보자:
1
2
3
4
5
6
7
8
IEnumerator TestCoroutine()
{
      while(true)
      {
           Debug.Log(Time.time);
           yield return null;
      }
}

이 코루틴은 명백하게 영원히 작동하는 루프를 가지고 있다. 이 코루틴은 현재 시간으로 로그를 남기고, 그 다음 yield한다. 다시 재개될 때, 다시 루프를 돌아서 로그를 남기고, 한번 더 yield될 것이다.

루프안에 있는 코드는 정확하게 Update 함수와 같다. 단지 Update 루틴이 실행된 후에(만약 Update문이 있다면), 모든 프레임마다 한번 실행이 되는 것이다.


StartCoroutine(TestCoroutine())를 호출할 때, 코드는 첫번째 yield를 만날때까지 즉시 실행되며,
유니티 프로세스가 이 객체에 대해서 코루틴을 처리할 때, 다시 재개된다.


Start, Update, OnCollisionEnter함수에서처럼 게임 객체의 처리 앞부분에 코루틴을 시작한다면, 코루틴은 즉시 첫번째 yield까지 실행될 것이고, 만약 yield return null을 사용했다면 같은 프레임 동안 다시 재개가 될 것이다. 만약 당신이 이것을 이해하지 못한다면 때로 이상한 효과를 초래할 수도 있다.

Start와 OnColiisionEnter를 코루틴으로 만드는 것은 문제가 되지 않는다.

한계를 넘어

자 이제 한가지 더 - 우리의 테스트 코루틴에서의 명백한 무한 루프는 무한하지 않다는 것이 판명되었다. 

만약 게임 객체에 대해서 코루틴을 중단하는 함수를 호출하거나, 객체가 파괴가 된다면, 이 무한 루프는 절대 다시 실행이 되지 않을 것이다. 또한 스크립트가 직접적으로 또는 SetActiveResursively(false)를 통해서 비활성화(disable)되면, 코루틴은 멈추게 될 것이다.

I Yield Sir

나는 마지막 섹션에 문장을 하나 만들었는데, 당신은 아마 의문이 들 것이다. 


하나 혹은 여러개의 코루틴을 가지는 게임내 모든 객체에 대해서 유니티는 매프레임마다 코루틴을 처리한다.
당신은 아마, 만약 yield return new WaitForSeconds(1) 같은 문장을 사용한다면, 그것은 1초동안 어떤 것도 처리하지 않기때문에  "오, 그렇지 않다" 라고 생각할지도 모른다. 

실제로 유니티는 매 프레임마다 올바른 시간이 경과되었는지 검사를 하면서, 코루틴을 처리한다.
- 이것은 당신의 코드에서 처리하지 않는다. 그러나 당신의 코드를 둘러 싸고 있는 랩퍼인 코루틴을 처리한다.

몇가지 값들을 yield함으로써 우리의 코드를 효과적으로 멈출수 있다는 것을 알았다. 여기에는 우리가 yield할 수 있는 것들이다.

  • null - Update구문의 수행이 완료될 때까지 대기한다.

  • WaitForEndOfFrame - 현재 프레임의 렌더링 작업이 끝날 때까지 대기한다.

  • WaitForFixedUpdate - FixedUpdate구문의 수행이 완료될 때까지 대기한다.

  • WaitForSeconds - 지정한 초만큼 대기한다.

  • WWW - 웹에서 데이터의 전송이 완료될 때까지 대기한다.
    (WaitForSeconds or null처럼 재시작한다.)

  • Another coroutine - 새로운 코루틴은 yielder가 재시작되기 전에 완료 될 것이다.


또한 yield break; 명령어를 실행할 수 있다. 이는 즉시 코루틴을 멈춘다.


WaitForEndOfFrame때문에,모든 카메라가 렌더링과 GUI의 출력이 완료되었을 때,  코루틴은 render texture로부터 정보를 얻는데 사용될 수 있다.
만약 Time.timeScale이 0으로 설정되어 있다면, yield return new WaitForSeconds(x)를 사용하는 것은 절대 재시작 되지 않는다.
물론, 이 모든 것들에 대해 가장 좋은 것은 특정 기간 동안 실행되는 코드를 작성하거나, 혹은 약간의 외부 이벤트가 일어나기를 기다리는 코드를 작성하고, 그것들을 당신의 코드를 훨씬 더 읽기 쉽게 만드는 하나의 함수 안에서 관리하는 것이다.(만약 여러 개의 함수를 작성해야 했거나, 상태를 확인하기 위해서 많은 코드를 작성해야 했다면)

요약

  1. 코루틴은 시간에 따라 혹은 외부의 처리가 완료되었을 때, 연산이 일어나는 순서를 정하는 정말 멋진 방법이다.

  2. 코루틴은 쓰레드가 아니다. 그리고 비동기가 아니다(동시에 일어나지 않는다.)

  3. 코루틴이 실행되고 있을 때, 다른 어떤 것들도 실행되지 않는다.

  4. 코루틴은 yield 문의 조건을 만족시켰을 때, 재개될 것이다.

  5. 코루틴은 스크립트가 비활성화(disabled)되거나, 객체가 파괴되었을 때, 비활성화 된다.

  6. yield return new WaitForSeconds는 Time.timeScale에 의해 영향을 받는다.

코루틴의 활용


바라건대, 우리는 코루틴이 무엇인지 그리고 그들이 어떻게 작동하는지 알아봤다. 우리의 상급 튜토리얼은 그들 뒤에 있는 기술에 대해서 살펴볼 것이다.

어떤 일을 하는 코루틴을 사용해보자. 몇몇 간단한 헬퍼 함수는 실제로 코루틴을 사용해서 쉽게 컷 시퀀스를 만드는 것을 도와준다.

우리는 물체를 목표 위치와, 회전값으로 움직이게하는 코루틴을 작성할 수 있다. 또 우리는 애니메이션이 일정 비율에 도달 했을 때까지 기다리는 코루틴을 작성할 수도 있다. 이 두가지 도구를 사용하여, 우리는 쉽게 하나의 함수로 전체 컷 시퀀스를 읽기 쉽게 스크립트 할 수 있을 것이다. 


코루틴에서 어떤 것을 움직일 때를 기다리는 것은, 동시에 객체의 위치를 조작하는 다른 코루틴 또는 업데이트 함수간의 충돌이 없다는 것을 보장한다.

당신은 오직 한번에 하나의 객체에 영향을 미치는 코루틴을 만들어야하고, 객체를 움직이게 하는 Update는 비활성화 시켜야 한다.

여기 애니메이션이 특정 부분 완료되기까지를 기다리는 코루틴의 예가 있다:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
//Wait for an animation to be a certain amount complete
IEnumerator WaitForAnimation(string name, float ratio, bool play)
{
    //Get the animation state for the named animation
    var anim = animation[name];
    //Play the animation
    if(play) animation.Play(name);
 
    //Loop until the normalized time reports a value
    //greater than our ratio.  This method of waiting for
    //an animation accounts for the speed fluctuating as the
    //animation is played.
    while(anim.normalizedTime + float.Epsilon + Time.deltaTime < ratio)
        yield return new WaitForEndOfFrame();
 
}

당신은 애니메이션을 기다리는 코루틴을 다음과 같이 작성할 수 있다.
01
02
03
04
05
06
07
08
09
10
11
IEnumerator Die()
{
       //Wait for the die animation to be 50% complete
       yield return StartCoroutine(WaitForAnimation("die",0.5f, true));
       //Drop the enemies on dying pickup
       DropPickupItem();
       //Wait for the animation to complete
       yield return StartCoroutine(WaitForAnimation("die",1f, false));
       Destroy(gameObject);
 
}

비디오는 당신에게 더 많은 것들을 보여준다. 예제 프로젝트는 여기서 얻을 수 있다.

비디오


만약 강력한 코루틴 도구를 어떻게 만들고, easing 함수를 적용하고, 코루틴을 다음 단계로 개발하는 방법을 알고싶다면, 반드시 이 비디오를 봐야한다.
이 비디오는 꽤 길지만, 그것은 정말 멋지다. 코루틴을 사용해서 전체의 컷 시퀀스를 만드는 방법을 배워라. 그리고 어떻게 easing 함수에 yield 문을 적용하는지를 봐라.

비디오의 끝 무렵에, 우리는 어떻게 고급 코루틴 도구를 만드는지와 멋진 효과를 만들기위해 그것들을 사용하는 방법에 대해 알게 될 것이다. 다운로드한 프로젝트는 비디오에서 보여주었던 모든 부분을 담고 있다.











반응형
Posted by blueasa
, |


링크 : http://www.slideshare.net/MrDustinLee/ss-33346625

반응형

'Unity3D > Script' 카테고리의 다른 글

Simple C# Unity Serializer  (0) 2014.09.25
ClipboardHelper  (0) 2014.05.15
ScreenWipe CrossFade with C#  (0) 2014.04.22
A simple cross fade shader for Unity  (0) 2014.04.22
어플을 내렸을때, 어플을 종료할때의 처리  (3) 2014.04.04
Posted by blueasa
, |