유니티를 공부하던 중, 알아야 하지만 이해하기 어려운 개념을 접했다. 바로 Coroutine. 일반적인 사용은 다음과 같다.
using UnityEngine;
using System.Collections;
public class CoroutinesExample : MonoBehaviour
{
public float smoothing = 1f;
public Transform target;
void Start ()
{
StartCoroutine(MyCoroutine(target));
}
IEnumerator MyCoroutine (Transform target)
{
while(Vector3.Distance(transform.position, target.position) > 0.05f)
{
transform.position = Vector3.Lerp(transform.position, target.position, smoothing * Time.deltaTime);
yield return null;
}
print("Reached the target.");
yield return new WaitForSeconds(3f);
print("MyCoroutine is now finished.");
}
}
위 코드는 유니티 공식 사이트에서 Script 설명중 Coroutine에 대한 설명에서 따온 것이다. StartCoroutine 호출이 보이고, MyCoroutine은 IEnumerator 반환값을 갖는다. 함수안에는 yield return null;, yield return new WaitForSeconds(3f) 와같이 사용한다. 첫 감상은 “이게 다 대체 뭐야?”
일단, 키워드들을 추출해 봤다. StartCoroutine, IEnumerator, yield. 호출하는 형태나, yield 키워드등을 봐서는 쓰레드인가 생각이 들겠지만, 일단 쓰레드는 아니라고 한다. 문제 소지가 많은 멀티 쓰레드를 쓰지 않고, 게임 엔진 자체에서 콜백 형식으로 유사하게 구현해 놓은 것 같다. 제대로 이해하기 위해선, IEnumerator부터 하나씩 파볼 필요가 있다.
IEnumerator
IEnumerator부터 정말 최고의 설명을 해놓은 블로그가 있다. 일단, IEnumerator를 찾아보면, C# 다큐먼트쪽에서 이 인터페이스에 관한 설명을 찾을 수 있다. 설명을 보면, 그냥 나열자(Enumerator) 인터페이스이다. Collection에서 현재 Element를 돌려주는 Current 속성과 다음 Element로 하나 전진키시는 MoveNext(), 처음 위치로 초기화 시키는 Reset() 메소드를 갖고 있다. 일종의 이터레이터처럼 사용하는 것으로 별 생각없이 사용하던 foreach문이 이를 이용하는 것 같다.
리턴값이 IEnumerator 인데, 반환값들이 이 인터페이스를 구현하고 있나? 라는 의문이 들거다. 자세한건 yield에 대해 알아보고 얘기하자.
yield
쓰레드라면, 우선순위를 양보하는 의미의 yield. 이것도 일단, C#레퍼런스를 살펴보자.
The return type must be IEnumerable, IEnumerable<T>, IEnumerator, or IEnumerator<T>.
…
You use a
yield return
statement to return each element one at a time.You consume an iterator method by using a foreach statement or LINQ query. Each iteration of the
foreach
loop calls the iterator method. When ayield return
statement is reached in the iterator method,expression
is returned, and the current location in code is retained. Execution is restarted from that location the next time that the iterator function is called.You can use a
yield break
statement to end the iteration.
설명을 읽어보면, IEnumerable<T>, IEnumerator 리턴타입으로 정의한 메소드는 iterator 메소드가 된다고 한다. iterator 메소드가 호출될 때, yield return <expression> 을 만나면 <expression>을 리턴하고, 그 위치가 저장된다고 한다. 그리고 다음 호출 시, 저장된 위치부터 시작된다는 얘기. 만약, yield break;를 사용하면 이터레이션이 끝난다고한다.
MyCoroutine의 이해
몇가지 언어들을 사용해 왔지만, 처음 접하는 개념이다. 그래도, IEnumerator와 yield를 알고 나니, 처음에 보인 Unity 코드에서 MyCoroutine 메소드에 대해 이해가 된다.
IEnumerator 부분에서 가졌던 의문을 풀어보자. 앞서 yield 항목을 읽어보면, IEnumerator 리턴값으로 정의한 메소드는 iterator 메소드가 된다. 메소드 자체에 IEnumerator 인터페이스를 사용할 수 있다는 얘기다. 처음 unity 코드로 돌아가면, MyCoroutine()을 가지고 다음처럼 사용할 수 있다는 얘기가 된다.
IEnumerator enumerator = MyCoroutine(transform); System.Object object = enumerator.Current; enumerator.MoveNext();
StartCoroutine()의 이해
이제 StartCoroutine()을 이해해보자. 앞선 내용으로 추측이 가능하다. iterator 메소드인 MyCoroutine()을 리턴값이 false일 때까지 반복적으로 호출하며, MyCoroutine()은 차례대로 객체를 리턴해주게 된다. StartCoroutine()은 IEnumerator.Currnt로 현재 리턴된 객체를 얻어와 어떤 객체인지에 따라 적절한 처리를 해주게 된다. 관련 내용은 앞서 언급한 블로그에 잘 나와있다. 그렇다면, 리턴받아 처리하는 객체가 한정되어 있다는 추측이 가능하다. 문서를 찾아보면, YieldInstruction 을 상속한 객체들을 사용할 수 있는 것 같다. WaitForEndFrame, WaitForFixedUpdate, WaitForSeconds, WaitForSecondsRealtime 등이 있다.
주워모은 지식을 종합해서 처음의 유니티 코드로 돌아가보면, yield return null 이 보인다. null 리턴에 대해선 언급한적이 없는데, 다음 Update()를 기다려서 Update()가 실행된 후, 이어서 진행된다고 한다. while루프 조건을 빠져나갔을 때, yield return new WaitForSeconds(3f); 가 불리는게 보인다. 3초 후에 코루틴이 다시 불린다는 얘기가 된다. 즉, 3초후에 “MyCoroutine is now finished.” 문구가 찍히게 될 것이다.
그 다음은?
너무 새로운 내용이라서 간략하게 StartCoroutine() 관련 내용을 살펴봤다. 실제 게임제작 코드들에서 이 내용이 이해를 돕기를 바란다. 나도 공부중이지만, 아마도 거의 필수적으로 사용하는게 아닐까 생각이 된다. 그럼 이만.