ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Coroutine 완벽하게 이해하기
    Unity, C#/Coroutine 2019. 2. 15. 19:48

     

     

     

    자꾸 잊어 먹어서 제 나름대로 정리해 놓고 있습니다.

    늦은 나이에 시작해서 많이 부족합니다.

    틀린 부분이 있으면 댓글로 가르쳐 주시면 바로 수정하겠습니다.

     

    현재는 아이군님의 블로그 내용이 보고 공부하면서 정리했습니다.제가 완전히 이해할 때까지 계속 추가/정리 해놓을 생각입니다.

     

    (아이군의 블로그를 참고 : http://theeye.pe.kr/archives/2725)

     


    기본적인 Coroutine 예제


    예제1) 1초 동안 이미지가 자연스럽게 사라지도록 하는 코드

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
        public 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(양보)의 관계

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
        void 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을 구현해 보겠습니다. 
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    void 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을 사용해서 작성하면 이렇습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
        void 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 열거자를 넘겨서 실행되도록 합니다. 

    추가적인 오버헤드가 전혀 없는 방법입니다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        void Start()
        {
            StartCoroutine(WaitAndPrint(5.0f));
        }
     
        IEnumerator WaitAndPrint(float waitTime)
        {
            yield return new WaitForSeconds(waitTime);
     
            // waitTime 뒤에 실행됩니다.
            Debug.Log("Done. " + Time.time);
        }
    cs
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        IEnumerator 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);

     

    문자열 형태의 코루틴 함수 이름으로도 호출하는 것이 가능합니다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
        IEnumerator 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을 수행하는 데에 오버헤드가 크고 파라미터를

    한개밖에 넘길 수 없다는 제약사항이 있습니다. 물론 배열을 넘기는 것도 가능합니다.

     

    파라미터로 배열을 넘긴 예

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        void 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가 될때까지 계속 기다립니다. 아래는 예제 코드입니다. 

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
        public 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

     

    여기서 사용되는 식은 람다 표기법이 사용됩니다. 

     

    람다표기법

    1
    2
    Func<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가 될때까지 기다립니다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
        public 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); 
    코루틴 내부에서 또다른 코루틴을 호출할 수 있습니다. 물론 그 코루틴이 완료될 때까지 기다리게
    됩니다. 의존성 있는 여러 작업을 수행하는데에 유리하게 사용 될 수 있습니다. 
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
        void 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을 실행할 때 넘겨주었던 코루틴 함수의 열거자를 파라미터로 사용하여 
    그것을 중단시키는 방법입니다. 다음과 같이 사용 가능합니다.
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
        private 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) : 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간, 메모리 등을 말한다.)
    다음과 같이 멤버 변수 없이도 간편하게 사용할 수 있습니다. 
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
        void 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(클래스라고 이해하시면 될 것 같습니다.)에서 실행한 모든 코루틴을 한번에 
    종료시키는 함수입니다. 
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
        void Start()
        {
            StartCoroutine("DoSomething");
            Debug.Log("StopAllCoroutines()");
            StopAllCoroutines();
        }
        public IEnumerator DoSomething()
        {
            while(true)
            {
                Debug.Log("Coroutine");
                yield return null;
            }
        }
    cs

     

     

     
     

    댓글

Designed by Tistory.