Chapter 11. Corutine, 커스텀 코루틴, 최적화

Date:     Updated:

카테고리:

태그:

인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click

Chapter 11. Corutine

  • IEnumerator,Ienumerable, yield에 대한 이해는 링크 참고

코루틴은 하나의 프로세스를 여러 루틴들이 시간을 나눠서 사용하는 방식으로 스레드와는 다르다. 스레드는 동시에 여러 프로세스가 여러 작업을 병렬적으로 진행하는 것. 코루틴은 직렬처리이며 병렬처리처럼 보이게끔 해주는 함수이다. 유니티는 병렬적으로 함수들을 동시에 여러가지 실행하지 못한다. 한번에 함수를 하나하나 실행시켜주는 것이다.

🚀 코루틴 원리

class Test
{
    public int id = 0;
}

class CoroutineTest : IEnumerable
{
    public IEnumerator GetEnumerator() // 인터페이스 구현
    {
        yield return new Test() { id = 1 }; // Test 객체 리턴 후 다시 돌아 와서 밑에 실행
        yield return new Test() { id = 2 }; // Test 객체 리턴 후 다시 돌아 와서 밑에 실행
        yield return new Test() { id = 3 }; // Test 객체 리턴 후 다시 돌아 와서 밑에 실행
        yield return new Test() { id = 4 }; // Test 객체 리턴 후 다시 돌아 와서 밑에 실행

        yield return null; // null 리턴 후 다시 돌아오기 👉 즉 한 프레임 쉰거나 마찬가지

        yield break; // 영구 종료. 일반 return과 같음. 다시 안돌아옴.
    }
  • yield는 어떤 타입이든 다 리턴할 수 있다.
    • object, 즉 System.Object을 상속받는 모든 것을 리턴할 수 있다.
  • foreach문 돌릴 객체가 아니라면 IEnumerable 상속 받지 않고 그냥 public IEnumerator GetEnumerator() 만 만들어도 무방하다. IEnumerable로 업캐스팅 할 일 없다면!
    • 자세한 사항은 링크 참고
    • CoroutineTest 타입의 객체를 foreach문 돌릴거라서 IEnumerable를 상속받도록 한다.
public class GameScene : BaseScene
{
    protected override void Init()
    {
        // ...
        CoroutineTest test = new CoroutineTest();
        foreach(System.Object t in test)
        {
            Test value = (Test)t;
            Debug.Log(value.id);
        }
  • CoroutineTest 클래스는 IEnumerable 을 상속받고 이를 구현했기 때문에 test로 foreach 문을 돌릴 수 있게 된 것이다.
    • 이를 통해 yield의 리턴 값들이 t에 담기게 된다.


🚀 코루틴을 사용하는 이유

코루틴은 C# 차원에서 지원한다.

  1. 함수의 상태를 저장/복원 하는게 가능 👉 마치 일시정지!
    • 엄청 오래 걸리는 작업을 잠시 끊거나
    • 원하는 타이밍에 함수를 잠시 스탑했다가 복원하려는 경우
  2. 리턴은 우리가 원하는 타입으로 가능
    • 심지어 클래스 타입 리턴도 가능
    • object로 리턴하기 때문
    void GenerateItem()
    {
        // 아이템을 만들어준다.
        // DB 저장 
        // 멈춤. 기다렸다가 DB 성공 요청을 받고나서 실행!!! 
        // DB 저장 안하고 바로 다음 로직 실행하면 문제 
    }

DB에서 데이터를 가져오는 과정이 끝나지도 않았는데 다음 로직으로 넘어가면 문제가 생길 것이다. DB에서 데이터를 가져오는 성공 요청을 받고나서 다음 로직으로 가야 안전하기 때문에 DB에서 데이터를 가져오는 성공 요청을 받을 때까지 대기해야 한다. 따라서 코루틴을 사용하면 좋겠다!

    float deltaTime = 0;
    void ExplodeAfter4Seconds()
    {
        deltaTime += Time.deltaTime; // 시간 더해주고
        if (deltaTime >= 4) // 체크하고 (1000번 검사해야됨...ㅠ)
        {
            // 로직
        }
    }

코루틴을 사용하지 않는 경우

예를 들어 몬스터가 스킬 쿨타임이 4초라면 위와 같이 매프레임마다 Time.deltaTime을 더해 if (deltaTime >= 4)가 되는지를 매번 체크해서 4초가 지났을 때만 스킬을 쓰게끔 위와같이 구현할 수 있을 것이다. 작은 규모의 프로그램에선 괜찮지만 만약 이런 몬스터가 1000마리라면! 4초동안 천 번을 if (deltaTime >= 4) 체크 하게 되는 것과 같다. 스킬이 한개가 아닌 여러개라면? 더할 것이다.. 그래서 규모가 큰 게임에선 이런 방식은 낭비이며 프레임드랍이 생길 수도 있다. 유니티는 병렬적으로 함수들을 동시에 여러가지 실행하지 못한다. 한번에 함수를 하나하나 실행시켜주는 것이다. 짧은 시간안에 많은 연산을 해야하므로 프레임 드랍이 발생할 수 있는 것이다.

코루틴을 사용하는 경우 👉 코루틴을 사용하는게 더 성능이 좋고 가독성이 좋다.

참고

  1. 컴파일러가 자동으로 만들어준 IEnumerator 의 Current 값에 WaitForSeconds(seconds) 객체를 대입한다.
  2. 리턴으로 인하여 코루틴 함수가 일시 정지된다. 코루틴 함수를 호출으로 와서 리턴해줌.
  3. 유니티 엔진에서 IEnumerator 의 Current 값의 타입을 체크한다. WaitForSeconds이니 seconds 초만큼 대기한다.
  4. 4초 대기 완료하면 MoveNext()를 통해 다음 yield를 만날 때까지 다음 실행을 하기 위해 코루틴 함수 일시정지 되었던 부분으로 돌아간다. 다음 실행을 마저 한다.
public class GameScene : BaseScene
{
    Coroutine co;

    // 방법2 : 코루틴 (성능상 더 좋다)
    IEnumerator CoExplodeAfterSeconds(float seconds)
    {
        Debug.Log("Ezplode Enter");
        //WaitForSeconds 인식하고 seconds 만큼 대기하고 호출한곳 갔다가 다시 돌아옴
        yield return new WaitForSeconds(seconds);
        Debug.Log("Ezplode Execute");
        co = null;
    }
  • “Ezplode Enter” 출력
  • seconds시간만큼 대기
    • WaitForSeconds(seconds) 객체가 리턴되면서 코루틴 함수를 호출한 곳이자 리턴 받은 곳으로 돌아가 이 대기 시간동안 이 코루틴함수를 호출한 곳이 있는 메인 루틴을 실행한다. 쉬는 동안 다른 일을 하는 것이다.
      • 밑에 🚀코루틴 진행 순서 참고
  • “Ezplode Execute” 출력

co 객체는 UnityEngine.Coroutine 타입으로, 단순히 StopCoroutine의 인수로 넘겨 코루틴을 종료시키기 위해 존재하는 타입이다.

image

image

  • StartCoroutine 👉 Coroutine 타입의 객체를 리턴한다.
    • 오버로딩 매개 변수
      • IEnumerator : IEnumerator 리턴하는 코루틴 함수 리턴값
        • StartCoroutine(CoExplodeAfterSeconds(4.0f));
      • String : IEnumerator 리턴하는 코루틴 함수의 이름(문자열)
        • 함수의 인수는 0~1개밖에 못 넘긴다는 제약 사항
        • 이름으로 함수를 찾기 때문에 성능이 저하될 수 있다고 한다.
        • StartCoroutine(“CoExplodeAfterSeconds”, 4.0f);

image

  • StopCoroutine 👉 Coroutine 타입을 파라미터로 받는 오버로딩도 있다.
    • StartCoroutine을 실행시켜 리턴받은 Coroutine객체를 StopCoroutine에 넘기면 해당 코루틴 함수를 중지시킬 수 있다.
      • 이렇게 CoroutineStartCoroutine에 넘기려는 용도로밖에 사용이 안된다.
    • 만약 함수 이름 문자열로 넘겨 실행된 StartCoroutine를 중지시킬 땐 마찬가지로 StopCoroutine에도 함수 이름 문자열로 넘겨야 한다.
    IEnumerator CoStopExplode(float seconds)
    {
        Debug.Log("Stop Enter");
        //WaitForSeconds 인식하고 seconds 만큼 대기하고 호출한곳 갔다가 다시 돌아옴
        yield return new WaitForSeconds(seconds);
        Debug.Log("Stop Execute");
        if (co != null) // 무언가 실행 중이라면
        {
            StopCoroutine(co);
            co = null;
        }
    }
  • 근데 위와 같이도 사용이 가능한 것 같다.
    • Coroutine 객체 co가 null 이 아니라는 것은 현재 어떤 코루틴 함수가 실행 중이라는 뜻이나 마찬가지다.
      • if (co != null) 체크 가능.
    protected override void Init()
    {
        //...
        co = StartCoroutine("CoExplodeAfterSeconds", 4.0f);

        StopCoroutine(co);

        co = StartCoroutine("CoStopExplode", 2.0f);
    }


🚀 코루틴 진행 순서

    IEnumerator CoExplodeAfterSeconds(float seconds)
    {
        Debug.Log("Explode Enter");
        yield return new WaitForSeconds(seconds);
        Debug.Log("Explode Execute");
    }

    IEnumerator CoStopExplode(float seconds)
    {
        Debug.Log("Stop Enter");
        yield return new WaitForSeconds(seconds);
        Debug.Log("Stop Execute");
    }

    protected override void Init()
    {
        StartCoroutine(CoExplodeAfterSeconds(3.0f));
        StartCoroutine(CoStopExplode(5.0f));
        Debug.Log("A");
        Debug.Log("B");
        Debug.Log("C");
        Debug.Log("D");
        Debug.Log("E");
        Debug.Log("F");
    }

image

  1. CoExplodeAfterSeconds(3.0f) 실행
  2. “Explode Enter” 출력
  3. new WaitForSeconds(seconds) 객체를 yield return한다. 함수를 호출했던 곳으로 (Init 내부)로 돌아온다.
  4. 유니티 엔진에서 이 객체의 타입을 분석하니 3초 만큼 대기하는 것이라고 한다. 그래서 3초를 대기한다.
  5. 위 3 를 대기하는 동안 메인 루틴을 마저 실행한다. 따라서 CoStopExplode(5.0f)을 실행한다.
  6. “Stop Enter” 출력
  7. new WaitForSeconds(seconds) 객체를 yield return한다. 함수를 호출했던 곳으로 (Init 내부)로 돌아온다.
  8. 유니티 엔진에서 이 객체의 타입을 분석하니 5초 만큼 대기하는 것이라고 한다. 그래서 5초를 대기한다.
  9. 위 3 를 대기하는 동안 메인 루틴을 마저 실행한다. 따라서 “A”,”B”, … “E” 를 출력한다.
  10. 3초의 대기가 끝났다. 다시 CoExplodeAfterSeconds 함수 내부로 돌아와 마저 “Explode Execute” 출력을 실행한다.
  11. 5초의 대기가 끝났다. 다시 CoStopExplode 함수 내부로 돌아와 마저 “Stop Execute” 출력을 실행한다.


🚀 코루틴 최적화 (Coroutine Manager)

  • yield return new을 사용하면 컴파일러는 자동으로 IEnumerator 객체를 만든다.
    • 👉 가비지
  • 거기다가 StartCoroutine 까지 사용하면 Coroutine 객체를 리턴한다.
    • 👉 가비지

이렇게 yield return newStartCoroutine의 사용으로 2 개의 가비지가 생성된다. 코루틴을 정말 많이 사용한다면 가비지 양이 어마어마 할 것이다.

  • yield return new 사용은 어쩔 수 없으니 가비지 되는 Coroutine 객체 리턴을 막기 위해 StartCoroutine 사용을 막고 내가 직접 실행시키자!!!
    • 직접 IEnumerator 객체들이 모인 List를 만들고 여기서 각각의 모든 코루틴 함수들을 독립적으로 실행시키는 것이다.

블로그 코드 참고 1 👉 StartCoroutine 사용을 줄이는 코루틴 매니저

직접 CoroutineManager 같은 클래스를 만든다. 싱글톤으로 관리되는..!! 모든 코루틴을 유니티 엔진에게 넘기지 않고, 즉 StartCoroutine을 통해 넘기는게 아니라 직접 클래스를 만들어 코루틴 기능을 제작하고 모든 코루틴들을 관리하는 것이다. 가비지가 생기지 않도록! 유니티 에셋 스토어에는 코루틴을 최적화시킨 More Effective Coroutine 이라는 에셋도 존재한다.

블로그 코드 참고 2 👉 yield return new로 인한 WaitForSeondsRealTIme은 같은 YieldInstruction 객체 생성(IEnumerator 상속으로 추정된다) 가비지를 막기 위해 캐싱해놓음.

IEnumerator TickEverySecond()
{
    var wait = new WaitForSeconds(1f); // Cache
    while(true)
    {
        Debug.Log("Tick"); // 1초마다 출력될 것
        yield return wait; // Reuse
    }
}

간단하게 요런식으로 new WaitForSeconds(1f) 객체를 wait에 캐싱해둘 수 있다. 미리 wait에 보관해둔 객체를 yield return 하면 가비지가 엄청나게 줄어들 것이다. 1초에 한번씩 new WaitForSeconds(1f) 객체를 만들 것을(1분이면 60번 만드는 셈일 것) 그냥 단 한번만 만들고 wait에 보관해둬서 이거 재활용하는 것이니 얼마나 가비지가 줄어들겠는가


🚀 커스텀 코루틴

출처 및 참고

유니티에서 WaitForSeconds라던가 WaitForSeondsRealTIme이라던가 등등 원하는 동작을 기다리게 한다던가 얼마간의 시간이 지나간 후 어떤 행동이 되어야 MoveNext() 가 True가 될지 등등을 커스텀할 수 있도록 클래스를 직접 만들 수도 있다.

  • 첫 번째 방법 : CustomYieldInstruction 상속 받기
    • KeepWaiting 함수를 오버라이딩 해야 한다.
      • 계속 기다릴 것인지를 묻는 함수이므로 계속 기다려야 한다면 true를 반환하고 어떤 뭐 입력이 들어오면 더 이상 기다릴 필요가 없게 false를 반환한다던지 이런식으로 구현할 수가 있다.
  • 두 번째 방법 : IEnumerator 상속 받기
    • MoveNext로 KeepWaiting처럼 더 기다려야 하는지에 대한 여부를 코루틴에게 전달 가능
    • 코루틴이 Current을 리턴하므로 코루틴이 대기 시간을 가지는 동안 다른 동작을 처리할 수 있다.
      • 코루틴이 대기 시간 가지는 동안 코루틴을 호출했었던 메인 루틴으로 돌아가 마저 다른 일 하다가 특정 로직이 발생하면 대기를 멈추고 코루틴 함수 내부로 돌아오게끔 이런식으로 커스텀할 수 있게 됨!


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Unity Lesson 2 카테고리 내 다른 글 보러가기

댓글 남기기