-
Coroutine 완벽하게 이해하기Unity, C#/Coroutine 2019. 2. 15. 19:48
자꾸 잊어 먹어서 제 나름대로 정리해 놓고 있습니다.
늦은 나이에 시작해서 많이 부족합니다.
틀린 부분이 있으면 댓글로 가르쳐 주시면 바로 수정하겠습니다.
(아이군의 블로그를 참고 : http://theeye.pe.kr/archives/2725)
기본적인 Coroutine 예제
예제1) 1초 동안 이미지가 자연스럽게 사라지도록 하는 코드12345678910111213141516171819202122public SpriteRenderer spriteRenderer = null;void Start(){StartCoroutine("RunFadeOut");}IEnumerator RunFadeOut(){Color color = spriteRenderer.color;while (color.a > 0.0f){color.a -= 0.1f;spriteRenderer.color = color;// 0.1초 동안 잠시 멈추라는 의미를 가진 코드, Update()// 함수에 종속적이지 않으며 마치 별도의 쓰레드와 같이 동작yield return new WaitForSeconds(0.1f);}}cs Coroutine 설명
예제2) IEnumerator(열거자)와 yield(양보)의 관계
1234567891011121314151617181920212223242526void Start(){IEnumerator enumerator = SomeNumbers();Debug.Log("=====================================");// MoveNext()에서 실제로 SomeNumbers()가 실행됩니다.// 처음에는 "yield return 3;" 까지만 실행됩니다.while (enumerator.MoveNext()){object result = enumerator.Current;Debug.Log("Number: " + result);}}IEnumerator SomeNumbers(){Debug.Log("yield return 1");yield return 3;Debug.Log("yield return 2");yield return 5;Debug.Log("yield return 3");yield return 8;// 이후에는 함수의 끝을 만나게 되므로 더이상 yield(양보)가 일어나지 않습니다.}cs 동작순서1. enumerator 에 SomeNumbers() 함수의 포인터 값이 저장됩니다. SomeNumbers() 함수의 결과값이저장되는 것이 아닙니다.2. while 문을 만나면서 처음으로 enumerator.MoveNext()가 호출됩니다. 여기서 SomeNumbers()가실행이 되며 yield문을 만날때까지 실행이 됩니다.3. 첫번째 yield문인 yield return 3;을 만납니다. 표현식 그대로 return 3에게 양보한다고 생각하시면될 듯 합니다. 이때에 리턴되는 값이 존재하므로 MoveNext()의 결과값으로 true가 반환됩니다.4. 이제 enumerator.Current를 통해 현재 반환된 값을 꺼내올 수 있습니다. 즉 MoveNext()를 통해서yield return 되는 값이 있는지를 알 수 있고, 반환된 값이 있다면 Current에 담겨 있게 됩니다.5. Debug.Log를 사용해서 Current를 출력해보면 처음으로 양보 반환된 3이 출력되게 됩니다.6. 다시 한번 while문이 실행되며 MoveNext() 호출되면 yield return이 일어났던 위치의 다음줄부터재실행이 되게 됩니다. 다시한번 yield 문을 만날때까지 진행이 됩니다.7. 이런식으로 yield return 8; 까지 진행이 됩니다.8. 다시 한번 MoveNext()가 호출되면 yield return 8; 이후의 코드부터 실행이 되지만 함수의 끝을만나게 되므로 더이상 yield가 일어나지 않습니다.9. MoveNext()의 결과값으로 false가 반환되면 while 문이 종료됩니다.특이하게도 함수의 반환값이 IEnumerable, IEnumerable<T>, IEnumerator, IEnumerator<T> 인경우에는 위와 같이 동작하게 됩니다.함수의 동작이 비동기적으로 동작하게 되므로, 파라미터에 ref나 out을 사용할 수 없다는 제약 사항이있습니다.위의 코드 동작 예시는 코루틴이 어떻게 동작하는지 알기위한 기본적인 코드라고 생각됩니다.* 위의 예제를 보면 알수 있듯이
1.
StartCorutine(SomeNumbers());
2.
IEnumerator enumerator = SomeNumbers();
while(runCoroutine.MoveNext()) // runCoroutine.MoveNext() == false 가 될때까지 반복합니다.
{
yield를 만날때까지 수행한 뒤 yield return 된 결과값에 맞게 작업을 처리합니다.
}
1과 2의 코드 역할이 똑같다고 할 수 있습니다.
예제3) 다시 한번 StartCoroutine을 구현해 보겠습니다.12345678910111213141516171819void Start () {//StartCoroutine ("RunCoroutine")IEnumerator runCoroutine = RunCoroutine ();while (runCoroutine.MoveNext ()) {object result = runCoroutine.Current;if (result is WaitForSeconds) {// 원하는 초만큼 기다리는 로직 수행// 여기 예제에서는 1초만큼 기다리게 될 것임을 알 수 있음}else if ...}}IEnumerator RunCoroutine() {yield return new WaitForSeconds(1.0f);yield return new WaitForSeconds(1.0f);yield return new WaitForSeconds(1.0f);}cs StartCoroutine을 직접 구현해 본다면 위와 같은 형태의 코드가 될 것 같습니다.
먼저 코루틴 함수의 포인터 역할을 하는 열거자를 받은 다음에 MoveNext()를 통해 첫번째 yield를
만날때까지 수행하고 그 결과값을 받습니다. 그리고 그 결과값에 맞는 작업을 수행해 줍니다.
그리고 이것을 함수가 완료될 때까지 반복합니다.
만약 함수의 실행이 완료되기 이전에 임의로 코루틴을 종료시키고 싶다면 yield break;를 호출하면
됩니다. 즉시 MoveNext()에서 false가 반환되어 종료됩니다.
결론적으로 StartCoroutine은 IEnumerator를 반환하는 함수를 인자로 받으면 이 함수는 특이하게도
실행된 결과를 의미하는 것이 아니라 함수 포인터와 같은 개념으로 사용이 됩니다.
예제4) 예제 3) 을 StartCoroutine을 사용해서 작성하면 이렇습니다.
1234567891011121314151617181920212223void Start(){// 일반적인 함수의 개념으로 보자면 StartCoroutine에는// Runcoroutine() 함수의 결과값이 파라미터로 넘겨지게 되어 있습니다.// 하지만 여기서 RunCoroutine()은 단 한줄도 실행이 되지 않습니다.// 함수의 포인터 역할을 하는 IEnumerator가 넘겨지게 되고// MoveNext()를 호출할 때마다 yield문을 만날때까지 수행됩니다.// 만나게 되면 MoveNext()가 true를 반환하고// 함수가 끝나거나 yield break;를 만나게 되면 false를 반환하게 됩니다.// true를 반환할 경우 Current를 통해 반환된 값을 꺼내볼 수 있습니다.StartCoroutine(Runcoroutine());}IEnumerator Runcoroutine(){Debug.Log("첫번째 코루틴");yield return new WaitForSeconds(3.0f);Debug.Log("두번째 코루틴");yield return new WaitForSeconds(3.0f);Debug.Log("세번째 코루틴");yield return new WaitForSeconds(3.0f);}cs
MonoBehaviour.StartCoroutine을 사용하는 두가지 방법
첫번째
public Coroutine StartCoroutine(IEnumerator routine);
수행하고자 하는 코루틴의 IEnumerator 열거자를 넘겨서 실행되도록 합니다.
추가적인 오버헤드가 전혀 없는 방법입니다.
123456789101112void Start(){StartCoroutine(WaitAndPrint(5.0f));}IEnumerator WaitAndPrint(float waitTime){yield return new WaitForSeconds(waitTime);// waitTime 뒤에 실행됩니다.Debug.Log("Done. " + Time.time);}cs 12345678910111213IEnumerator Start(){yield return StartCoroutine(WaitAndPrint(5.0f));Debug.Log("Done. " + Time.time);}IEnumerator WaitAndPrint(float waitTime){yield return new WaitForSeconds(waitTime);// waitTime 뒤에 실행됩니다.Debug.Log("WaitAndPrint. " + Time.time);}cs 위의 두번째 코드는 Start() 함수의 반환값을 IEnumerator로 변경하여 코루틴이 실행 완료될때까지
기다리도록 하는 방법입니다.
1. 위의 코드는 WaitAndPrint(float waitTime) 코루틴이 마지막까지 실행 완료된 이후에야
"Done" 이 출력되는걸 알수 있습니다.
앞서 StartCoroutine을 직접 구현할 때 while문을 사용해서 코루틴 함수의 마지막까지 실행한 후
코루틴이 실행 완료된다는걸 알 수 있었습니다.
2. 여기선 Start()의 반환값을 IEnumerator로 변경하여 코루틴이 실행 완료된 이후에
다음행이 실행 되도록 만들었습니다.
3. Start()는 Unity에선 무조건 실행되므로 따로 StartCoroutine()이 필요하지 않습니다.
4. 결과적으로 WaitAndPrint(float waitTime) 코루틴이 MoveNext()를 통해 return값이
false 가 될때까지 진행되므로 "WaitAndPrint "로그가 먼저 찍힌 후 "Done" 로그가 찍히게 됩니다.
두번째
public Coroutine StartCoroutine(string methodName, object value = null);
문자열 형태의 코루틴 함수 이름으로도 호출하는 것이 가능합니다.
123456789101112131415IEnumerator Start(){StartCoroutine("DoSomething", 2.0f);Debug.Log("Start1 : "+Time.time);yield return new WaitForSeconds(3.0f);Debug.Log("Start2 : " + Time.time);StopCoroutine("DoSomething");}IEnumerator DoSomething(float someParameter){while(true){Debug.Log("DoSomething Loop");yield return null; // 다음 Update()까지 기다린 후 실행을 계속합니다.}cs 1. StartCoroutine("DoSomething",2.0f); 실행합니다.
2. DoSomething(float someParameter) 코루틴 함수가 실행됩니다.
3. 처음 yield return 까지 실행되므로 Debug.Log("DoSomething Loop");를 출력합니다.
4. yield return null;을 반환하고 다음 Update() 까지 기다립니다.
5. Debug.Log("Start1 : "+Time.time); 출력합니다.
yield return StartCoroutine("DoSomething",2.0f) 였다면 DoSomething 코루틴이 완료될때까지 기다려야
하겠지만 여기선 첫번째 yield 문까지 간 뒤 다음 Update()때까지 기다리므로, 즉 비동기로 바뀌므로
StartCoroutine 다음 줄로 계속 실행됩니다.
6. yield return new WaitForSeconds(3.0f); 3초간 기다립니다.
7. 3초간 Update() 마다 Debug.Log("DoSomething Loop"); 을 출력합니다.
8. 3초 후 Debug.Log("Start2 : " + Time.time); 출력합니다.
9. StopCoroutine("DoSomething"); DoSomething 코루틴을 종료합니다.
* 즉 여기서 StartCoroutine("DoSomething", 2.0f) 실행 후 첫번째 yield return 값을 리턴한 후 리턴값의
처리 시간에 관계없이 StartCoroutine("DoSomething", 2.0f) 다음 라인을 실행하는 것을 알 수 있습니다.
* 함수 이름을 문자열로 넘겨서 실행하는 방법은 StartCoroutine을 수행하는 데에 오버헤드가 크고 파라미터를
한개밖에 넘길 수 없다는 제약사항이 있습니다. 물론 배열을 넘기는 것도 가능합니다.
파라미터로 배열을 넘긴 예
12345678910111213void Start () {object[] parms = new object[2] { 3.14f, "파이의 값 : " };StartCoroutine("ValueOfPi", parms);}IEnumerator ValueOfPi(object[] pi){yield return new WaitForSeconds(1.0f);float piNum = (float)pi[0];string piStr = (string)pi[1];Debug.Log(piStr + piNum);}cs yield return에서 사용할 수 있는 것들yield return new WaitForSecond(float time);WaitForSecond 클래스를 양보 반환함으로써 원하는 시간(초)만큼 기다리게 하는 것입니다.yield return new WaitForSecondRealtime(float time);WaitForSecond 와 역할이 동일합니다.틀린 부분은유니티상의 시간은 Time.timeScale을 통해서 빠르게 또는 느리게 조정 할 수 있습니다.하지만 WaitForSecondRealTime은 이 timeScale의 영향을 받지 않고 현실 시간 기준으로만동작을 하게됩니다.yield return new WaitForFixedUpdate();다음 FixedUpdate() 가 실행될 때까지 기다립니다.Update()는 기기의 성능에 따라 초당 실행횟수가 변경되지만,FixedUpdate()는 일정한 시간 단위로 호출되는 함수라고 생각하시면 됩니다.yield return new WaitForEndOfFrame();하나의 프레임이 완전히 종료될 때 호출이 됩니다.Update(), LateUpdate() 이벤트가 모두 실행되고 화면에 렌더링이 끝난 이후에 호출이 됩니다.
yield return null;다음 Update() 가 실행될 때까지 기다린다는 의미입니다.null 양보 반환 --> Update() 실행 --> null 양보 반환했던 코루틴이 이어서 진행됩니다. --> LateUpdate() 실행위의 상태를 반복합니다.yield return new WaitUntil(system.Func<Bool> predicate);특정 조건식이 성공할 때까지 기다리는 방법입니다.WaitUntil에 실행하고자 하는 식을 정의해 두면 Update()와 LateUpdate() 이벤트 사이에 호출해 보고 결과값이
true가 될때까지 계속 기다립니다. 아래는 예제 코드입니다.
123456789101112131415161718192021public int frame = 0;void Start(){StartCoroutine(Example());}IEnumerator Example(){Debug.Log("공주를 구출하기 위해 기다리는 중...");yield return new WaitUntil(() => frame >= 10);Debug.Log("공주를 구출했다!!!");}void Update(){if(frame < 10){frame++;Debug.Log("Frame : " + frame);}}cs 여기서 사용되는 식은 람다 표기법이 사용됩니다.
람다표기법
12Func<int, int> func1 = (int x) => x + 1;Func<int, int> func2 = (int x) => { return x + 1; };cs yield return new WaitWhile(system.Func<Bool> predicate);WaitUntil 과 동일한 목적을 가집니다.다른 점은 람다식 실행 결과값이 false가 될때까지 기다립니다.123456789101112131415161718192021public int frame = 0;void Start(){StartCoroutine(Example());}IEnumerator Example(){Debug.Log("공주를 구출하기 위해 기다리는 중...");yield return new WaitWhile(() => frame < 10);Debug.Log("공주를 구출했다!!!");}void Update(){if(frame < 10){frame++;Debug.Log("Frame : " + frame);}}cs yield return StartCoroutine(IEnumerator coroutine);코루틴 내부에서 또다른 코루틴을 호출할 수 있습니다. 물론 그 코루틴이 완료될 때까지 기다리게됩니다. 의존성 있는 여러 작업을 수행하는데에 유리하게 사용 될 수 있습니다.12345678910111213141516171819202122void Start(){StartCoroutine(TestRoutine());}IEnumerator TestRoutine(){Debug.Log("Run TestRoutine");yield return StartCoroutine(OtherRoutine());Debug.Log("Finish TestRoutine");}IEnumerator OtherRoutine(){Debug.Log("Run OtherRoutine #1");yield return new WaitForSeconds(1.0f);Debug.Log("Run OtherRoutine #2");yield return new WaitForSeconds(1.0f);Debug.Log("Run OtherRoutine #3");yield return new WaitForSeconds(1.0f);Debug.Log("Finish OtherRoutine");}cs Coroutine 중단하기
public void StopCoroutine(IEnumerator routine);StartCoroutine을 실행할 때 넘겨주었던 코루틴 함수의 열거자를 파라미터로 사용하여그것을 중단시키는 방법입니다. 다음과 같이 사용 가능합니다.123456789101112131415161718192021222324private IEnumerator coroutine;void Start(){coroutine = WaitAndPrint(1.0f);StartCoroutine(coroutine);}public IEnumerator WaitAndPrint(float waitTime){while(true){yield return new WaitForSeconds(waitTime);Debug.Log("WaitAndPrint " + Time.time);}}private void Update(){if (Input.GetKeyDown("space")){StopCoroutine(coroutine);Debug.Log("Stopped " + Time.time);}}cs Start() 함수에서 WaitAndPrint(waitTime) 코루틴 함수의 열거자를 획득하여 클래스의 멤버 변수로설정해 두고 이 코루틴을 실행합니다.이 코루틴은 1초에 한번씩 WaitAndPrint를 출력하게 되며 유저가 스페이스키를 누르게 되면멤버 변수에 담겨 있는 기존 코루틴의 열거자를 이용하여 실행중인 코루틴을 중단시킵니다.public void StopCoroutine(string methodName);이전 방식보다 오버헤드는 크지만 간편하게 사용할 수 있는 방법입니다.(오버헤드(Overhead) : 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간, 메모리 등을 말한다.)다음과 같이 멤버 변수 없이도 간편하게 사용할 수 있습니다.123456789101112131415161718192021void Start(){StartCoroutine("WaitAndPrint", 1.0f);}public IEnumerator WaitAndPrint(float waitTime){while(true){yield return new WaitForSeconds(waitTime);Debug.Log("WaitAndPrint " + Time.time);}}private void Update(){if (Input.GetKeyDown("space")){StopCoroutine("WaitAndPrint");Debug.Log("Stopped " + Time.time);}}cs 여기서 주의할 점은 StopCoroutine을 문자열로 종료시키려면 StartCoroutine 역시 문자열로
실행해야 한다는 점입니다.
public void StopAllCoroutine();현재 Behaviour(클래스라고 이해하시면 될 것 같습니다.)에서 실행한 모든 코루틴을 한번에종료시키는 함수입니다.1234567891011121314void Start(){StartCoroutine("DoSomething");Debug.Log("StopAllCoroutines()");StopAllCoroutines();}public IEnumerator DoSomething(){while(true){Debug.Log("Coroutine");yield return null;}}cs