본문 바로가기
게임 프로그래밍/유니티 활용

Coroutine 과 UniTask 비교 예제

by 레오란다 2023. 1. 21.
반응형

UniTask 는 async 와 await 를 유니티에서 더 사용하기 편리하도록 만든 통합 패키지입니다. UniTask 는 Coroutine 에 비해 메모리 사용량, 성능, try-catch 사용 가능 및 return 을 사용할 수 있어서  Coroutine 대신 UniTask 를 이용하도록 권장하고 있는 상황입니다.

 

이 글에서는 같은 기능을 Coroutine 과 UniTask 를 이용해 각각 구현할 때 어떤 차이가 있는지에 대해서 알아보도록 하겠습니다.


 

설치

UniTask 는 기본으로 지원되는 기능이 아니기 때문에 Package Manager 를 이용해 설치해 줘야 합니다.

 

Package Manager
Package Manager - URL

Package Manager 를 실행하고 위와 같이 [Add package from git URL...] 을 선택하고 나타나는 주소입력 창에 아래의 주소를 입력하고 [Add] 버튼을 클릭합니다.

 

https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask

 

Package Manager - 2
URL 추가


 

예제

코루틴과 UniTask 를 비교하기 위해 다음과 같은 프로그램을 만들어 보도록 하겠습니다.

 

● 마우스 왼쪽 버튼을 클릭하면 0.5 초마다 sphere 생성

 오른쪽 버튼을 클릭하면 종료

 종료 후 생성된 sphere 수를 생성자 id 와 함께 출력

 

 

async void Update()
{
  if (Input.GetMouseButtonDown(0))
  {
     if (Input.GetKey(KeyCode.LeftControl))
        await SphereGenByUniTask();
     else
        SphereGenByCoroutine();
  }
  else if (Input.GetMouseButtonDown(1))
  {
     if (Input.GetKey(KeyCode.LeftControl))
     {
        cts.Cancel();
        cts.Dispose();
     }
     else
        stopGen = true;
  }
}

유니티 스크립트를 생성하면 자동으로 생성되는 Update 함수 앞에 async keyword 가 있습니다. async 가 있으면 Update 함수가 비동기 함수로 동작해서 await 키워드를 사용할 수 있게 됩니다.


 

코루틴 이용

void SphereGenByCoroutine()
{
  stopGen = false;
  StartCoroutine(SC_SphereGen(++coroutine_spawn_id, (r) => {
     print("SpawnerID: " + r.id.ToString() + " / Spawn Count: " + r.count.ToString());
  }));

  print("SphereGenByCoroutine Done");
}

코루틴을 이용하면 코루틴의 결과는 System.Action 과 Lambda Expression (람다식) 을 이용해 처리해야 합니다. 

코드의 실행 순서는

  1. stopGen
  2. StartCoroutin(...)
  3. print("SphereGenByCoroutine done")
  4. 함수 종료
  5. stopGen이 true가 된 후에 람다식 -> print("SpawnerID: " ...);

이렇게 됩니다.

 

IEnumerator SC_SphereGen(int id, System.Action<SpawnInfo> done)
{
  int spawned = 0;
  float time = 0f;
  while(true)
  {
     Vector3 p = UnityEngine.Random.insideUnitSphere * 5;

     Vector3 pos = new Vector3(p.x, p.y, p.z);
     var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
     spawned++;

     sphere.transform.position = pos;

     time = 0f;

     while (time < 0.5f && stopGen == false)
     {
        time += Time.deltaTime;
        yield return null;
     }

     if (stopGen == true) break;
  }

  done.Invoke(new SpawnInfo() { id = id, count = spawned });
}

stopGen 이 false 인 동안에는 0.5 초마다 sphere 를 생성하는 코루틴 함수입니다. 여기서 0.5 초를 기다리는 코드를 yield return new WaitForSeconds(0.5f) 를 사용하지 않은 이유는 이 코드를 사용하면 중간에 취소시킬 방법이 없어 무조건 0.5초를 기다려야하기 때문입니다.

 

코루틴 함수가 종료될 때 인자로 받은 done 을 이용해 결과를 리턴합니다. 


 

UniTask 이용

async UniTask SphereGenByUniTask()
{
  if (cts.IsCancellationRequested)
     cts = new CancellationTokenSource();
  var r = await SpawnSphere(++unitask_spawn_id);
  print("SpawnerID: " + r.id.ToString() + " / Spawn Count: " + r.count.ToString());
  print("SphereGenByUniTask Done");
}

UniTask 를 이용해 코루틴 함수를 대체하는 코드입니다. 코루틴과 다르게 await SpawnSphere() 함수가 종료될 때까지 코드가 다음으로 진행되지 않습니다. 코드는 처음부터 순차적으로 진행됩니다.

 

async UniTask<SpawnInfo> SpawnSphere(int id)
{
  int spawned = 0;

  try
  {
     while(true)
     {
        Vector3 p = UnityEngine.Random.insideUnitSphere * 5;

        Vector3 pos = new Vector3(p.x, p.y, p.z);
        var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        spawned++;            

        sphere.transform.position = pos;

        await UniTask.Delay(500, cancellationToken: cts.Token);            
        if (cts.IsCancellationRequested) break;
     }
  }
  catch (Exception e) { }

  return new SpawnInfo() { id = id, count = spawned };
}

코루틴 함수에서는 0.5 초를 기다리다가 중간에 cancel 하는 것을 구현하기 위해 while 문을 이용했지만 UniTask 는 await UniTask.Dely(...) 에 cancellationToken 을 이용하는 것으로 간단하게 구현할 수 있습니다. 그리고 코루틴과 다르게 return 도 사용할 수 있습니다.


 

결론

처음에 언급했듯이 많은 실험에 의해서 UniTask 가 코루틴에 비해 메모리 사용과 성능면에서 더 좋은 결과를 내고 있다는 것은 입증된 사실입니다. 아래의 링크를 참조하시면 도움이 될 것 같습니다.

 

8 Reasons to Ditch Coroutine for Async - Prog.World

Introduction When it comes to asynchronous operations in Unity, the first thing that comes to mind is coroutine. And this is not surprising, since most of the examples on the network are implemented through them. But few people know that Unity has supporte

prog.world

 

 

위의 예제에서 보았듯이 코루틴에서 UniTask 를 이용한 방식으로 구현하는 것도 크게 어렵지 않기 때문에 가급적 UniTask 로 대체하는 게 좋아 보입니다. 아래는 코루틴에서 사용하는 yield return... 을 대체하는 UniTask 함수들입니다.

yield return new WaitForSeconds/WaitForSecondsRealtime await UniTask.Delay
yield return null await UniTask.Yield
await UniTask.NextFrame
yield return WaitForEndOfFrame await UniTask.WaitForEndOfFrame
new WaitForFixedUpdate await UniTask.WaitForFixedUpdate
yield return WaitUntil await UniTask.WaitUntil

이 외에도 UniTask 는 더 많은 기능을 제공하고 있습니다.

 

다만 코루틴은 StartCoroutine 호출 후 바로 다음 코드가 진행되기 때문에 로직에 따라 이 방식이 더 유리한 경우도 있을 수 있습니다. 메모리와 성능상의 문제가 크게 발생하지 않는다면 자신이 만든 로직에 따라 두 방식을 선택할 수 있는 유연성도 필요할 것입니다.

 

다음의 unitypackage 는 예제의 전체 코드입니다. 

 

UniTaskExample.unitypackage
0.00MB

 

반응형

댓글