목록

코루틴 고찰(Coroutine Consideration)

unity에서는 코루틴을 IEnumerator 로 구현하는데, 먼저 IEnumerable 에 대해서 알아봅시다.

Index

1. IEnumerable?
2. 왜 사용하는가?
3. 유니티에서의 커스텀 코루틴


IEnumerable?

이 콜렉션은 foreachr구문 등에서 개체를 한개한개 넘겨주는 일을 한다. (※ C++에서 STL의 iterator와 비슷?) 콜렉션을 하나하나 넘겨주려면 넘겨주는 쪽이 몇번쨰인지 기억해야 하는데, 이 떄 IEnumerator을 생성해서 해결한다.

즉, IEnumerator는

  • 지금 몇번쨰 까지 읽었는지(state)를 기억한다.
  • MoveNext()호출 하면 다음 순번으로 이동.
  • Current를 요구할 때 해당 순번의 개체를 리턴.

그러면 이런 구조로 볼 수 있겠다. IEnumerable에서 GetEnumerator()호출하면 -> IEnumerator는 Current(), MoveNext(), Reset()등 현재 객체 접근하는 구조. (※ IEnumerator가 더 iterator와 비슷하네 IEnumerable는 컬렉션 관리 같다) Enuerable 인터페이스에는 foreach 구문에서 필요한 멤버들을 약속한 IEnumerator 개체를 반환하는 GetEnumerator 메서드를 제공하고 있다.

그런데 이게 코루틴이랑 무슨관계?? -> C#은 코드를 멈출 수 있다..?? -> 진짜.

public static System.Collections.IEnumerable SomeNumbers()
{
  yield return 3;
  yield return 5;
  yield return 8;
}

이 메서드로 리턴 받은 IEnumerable을 foreach로 돌려보면 3,5,8을 순서대로 리턴 하는데.. why?

  • IEnumerable, IEnumerator클래스는 컴파일러가 코드를 보고 자동으로 만들어준다.(사용자 정의 컬렉션)
  • IEnumrator가 가지는 state의 초기 값은 -1이다.
  • 자동 생성된 IEnumerator에서 MoveNext() 실행하면 yield return 3; 이전까지 실행하고 state를 0으로 옮긴다.
  • state가 0일 때 Current를 읽으면 3를 리턴한다.
  • 마지막 부분(8)에서 MoveNext()실행 시 false를 리턴한다.

컴파일러가 다해준다?

  • 컴파일러가 코드를 yield return 기준으로 나눠서 클래스를 짜준다.
  • 그러므로 메서드가 부분부분 나눠서 실행

즉, IEnumerator메서드에서 IEnumerator가 자동생성 되 리턴된 IEnumerator를 받아서 MoveNext()를 실행시키면 코드 앞 부분이 실행된다.

yield return new WaitForSeconds(1);

Current()에서 WaitForSeconds를 리턴 했다면??

  • Update()에서 1초가 지났는지 매프레임 마다 체크.
  • 만약 1초가 지났다면 IEnumerator의 MoveNext()를 호출 한다.
  • 코드 다음 부분이 실행된다.

IEnumerable -> IEnumerator -> 반복기

※ 참고로 반복기란 반복기는 컬렉션에 대해 사용자 지정 반복을 수행하는 데 사용됩니다. 반복기는 메서드 또는 get 접근자일 수 있습니다. 반복기는 yield return 문을 사용하여 한 번에 하나씩 컬렉션의 각 요소를 반환합니다. 클라이언트 코드에서 foreach 문 또는 LINQ 쿼리를 사용하여 반복기를 이용

※ 참고로 C#에서 컬렉션이란 대부분의 응용 프로그램의 경우 관련 개체의 그룹을 만들고 관리하려고 합니다. 개체를 그룹화하는 방법에는 개체 배열을 만들거나 개체 컬렉션을 만드는 두 가지가 있다.(list, IEtorable등이 컬력션에 해당, 배열은 제외) 컬렉션은 클래스이므로 해당 컬렉션에 요소를 추가하려면 먼저 클래스 인스턴스를 선언해야 합니다.(new 키워드) 나중에 컬렉션에 대해 자세히 정리하자. collection 참고 사이트


왜 사용하는가?

코루틴 활용 시 오브젝트의 property를 이용한 물체 이동이라든지, 매 프레임마다 값을 가져와 사용해야 하는 부분들을 Update함수의 의존성 없이도 물체을 움직일 수 있다는 점이다. 물론 어쩔수 없이 의존될 수도 있지만 되도록 피하는게 코드효율을 높이는데 좋고, 필요한 상황에 대해서만 값을 매 프레임마다 가져올 수 있어 되도록 코루틴으로 작성을 하자.

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

  • yield break; 코루틴을 끝낸다.

  • yield return new WaitForSecondsRealtime (float time); WaitForSeconds와 하는 역할은 동일하지만 결정적으로 다른것은, Scaled Time의 영향을 받지 않고 현실 시간 기준으로만 동작

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

  • yield return new WaitForEndOfFrame (); 하나의 프레임워 완전yield return null;히 종료될 때 호출이 됩니다. Update(), LateUpdate() 이벤트가 모두 실행되고 화면에 렌더링이 끝난 이후에 호출

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

  • yield return new WaitUntil (System.Func predicate); WaitUntil에 실행하고자 하는 식을 정의해 두면 매번 Update() 와 LateUpdate() 이벤트 사이에 호출해 보고 결과값이 true면 이후로 재진행

    IEnumerator Example() {
        Debug.Log("조건만족 까지 기다리는 중...");
        yield return new WaitUntil(() => frame >= 10);
        Debug.Log("조건만족 후");
    }
    
    void Update() {
        if (frame <= 10)
        {
            Debug.Log("Frame: " + frame);
            frame++;
        }
    }
  • yield return new WaitWhile(System.Func predicate); WaitWhile은 WaitUntil과 동일한 목적을 가지고 있지만 한가지만 다릅니다. WaitUntil은 람다식 실행 결과값이 true가 될때까지 기다린다면 WaitWhile은 false가 될때까지 기다립니다. 즉 WaitWhile은 결과가 true인 동안 계속 기다린다.

    IEnumerator Example() {
        Debug.Log("조건만족 까지 기다리는 중...");
        yield return new WaitWhile(() => frame < 10);
        Debug.Log("조건만족 후");
    }
    
    void Update() {
        if (frame <= 10)
        {
            Debug.Log("Frame: " + frame);
            frame++;
        }
    }

    10프레임에 도달하면 false가 되어서 이후 진행이 되겠네요.

  • yield return StartCoroutine (IEnumerator coroutine); 코루틴 내부에서 또다른 코루틴을 호출할 수 있습니다. 물론 그 코루틴이 완료될 때까지 기다리게 됩니다. 의존성 있는 여러작업을 수행하는데에 유리하게 사용.

    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");
    }

    출력결과

    Run TestRoutine
    Run OtherRoutine #1
    Run OtherRoutine #2
    Run OtherRoutine #3
    Finish OtherRoutine
    Finish TestRoutine
  • yield return new WWW(string) URL의 내용을 검색하기위한 작은 유틸리티 모듈이며, 호출 WWW(url)로 백그라운드에서 다운로드를 시작 속성을 검사하여 isDone다운로드가 완료되었는지 확인하거나 다운로드 객체를 생성하여 게임의 나머지 부분을 차단하지 않고 자동으로 기다릴 수 있습니다. 웹 서버에서 데이터를 가져 오려는 경우에 사용하십시오. 또한 웹에서 다운로드 한 이미지로 텍스처를 만들고 새로운 웹 플레이어 데이터 파일을 스트리밍하고로드하는 기능.

    public class ExampleClass : MonoBehaviour
    {
    public string url = "http://images.earthcam.com/ec_metros/ourcams/fridays.jpg";
    IEnumerator Start()
    {
        //스트림 관련 이므로 using쓴듯
        using (WWW www = new WWW(url))
        {
            yield return www;
            Renderer renderer = GetComponent<Renderer>();
            renderer.material.mainTexture = www.texture;
        }
    }
    }
    • tips : Note: URLs passed to WWW class must be ’%’ escaped.
    • tips : http://, https:// and file:// protocols are supported on iPhone. ftp:// protocol support is limited to anonymous downloads only. Other protocols are not supported.
    • tips : When using file protocol on Windows and Windows Store Apps for accessing local files, you have to specify file:/// (with three slashes).
  • yield return new AsyncOperation 비동기 작업이 끝날 때까지 대기 ( 씬로딩 )

유니티에서의 커스텀 코루틴

public static class MonoBehaviorExt {
    public static Coroutine<T> StartCoroutine<T>(this MonoBehaviour obj, IEnumerator coroutine) {
        Coroutine<T> coroutineObject = new Coroutine<T>();
        coroutineObejct.coroutine = obj.startCoroutine(coroutineObejct.InteranlRoutine(coroutine));
        return coroutineObject;
    }
}

public class Coroutine<T> {
    public T Value {
        get {
            if(e != null) {
                throw e;
            }
            return returnVal;
        }
    }
    private T returnVal;            //코루틴 작동 사이에 원하는 값 체크 용도.
    private Exception e;            //에러 캐치 용도.
    public Coroutine coroutine;     

    public IEnumerator InternalRoutine(IEnumerator coroutine) {
        while(true) {
            //에러 핸들링
            try {
                //다음 state로 이동.
                if(!coroutine.MoveNext()) {
                    yield break;
                }
            }
            catch(Exception e) {
                //에러 캐치
                this.e = e;
                yield break;
            }

            object yielded = coroutine.Current;
            if(yielded != null && yielded.GetType() is typeof(T)) {
                //TypeICareAbout 특정 타입 변수 저장
                returnVal = (T)yielded;
            }
            else {
                yield return coroutine.Current;
            }
        }
    }
}