본문 바로가기
[Unity]

[Unity] 코루틴을 대체하자(UniTask)

by 김기승 2024. 5. 15.

코루틴(Coroutine)은 일반 함수와 달리

여러 프레임에 걸쳐 실행될 수 있는 특별한 함수이다.

※ 유니티 개발을 하면서 반드시 쓰게 된다.

 

하지만, 이렇게나 강력한 코루틴에도 단점이 있다.

일단, 리턴(Return) 값이 없어서 따로 콜백 처리를 해줘야 한다.

또한, Try-Catch 예외처리를 못하고,
StartCoroutine와 YieldInstruction에서 가비지가 빈번하게 생성된다.

만약, 오브젝트가 비활성화되어 있으면 실행되지도 않는다.

 

이러한 단점들을 극복한 UniTask를 소개해보겠다.

UniTask는 기존 C# Task에서 파생되어 만들어졌다.

Task와는 다르게 단일 스레드인 유니티에 최적화되어 있다.

큰 특징들을 아래에 정리해보았다.

 

① 리턴 값 존재

② Try-Catch 가능

③ Zero Allocator

④ WebGL 지원

⑤ 오브젝트 활성화 여부와 무관


패키지 설치

UniTask는 유니티에서 공식적으로 제공하는 라이브러리는 아니다.

아래의 링크에서 다운받도록 하자.

https://github.com/Cysharp/UniTask

 

GitHub - Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity.

Provides an efficient allocation free async/await integration for Unity. - Cysharp/UniTask

github.com

 

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

Git URL을 직접 Package Manager에 추가해주거나,

 

Releases에서 Unity Package를 다운받으면 된다.

 

만약, 패키지를 설치했는데 namespace를 못 찾는 경우에는

Preferences > External Tools > Regenerate project files 버튼을 클릭해주자.

 

이제부터 코루틴UniTask 문법을 비교해보도록 하자.


① 실행하기

using UnityEngine;
using System.Collections;

namespace coroutine.start
{
    public class CoroutineClass : MonoBehaviour
    {
        private void Start()
        {
            StartCoroutine(myCoroutine());
        }
        private IEnumerator myCoroutine()
        {
            while (true)
            {
                Debug.Log($"{nameof(Coroutine)} | 반복");
                yield return null;
            }
        }
    }
}

StartCoroutine 함수를 통해 코루틴을 시작할 수 있다.

Debug문을 매 프레임마다 출력하는 코드이다.

 

using UnityEngine;
using Cysharp.Threading.Tasks;

namespace unitask.start
{
    public class UniTaskClass : MonoBehaviour
    {
        private void Start()
        {
            myUniTask().Forget();
        }
        private async UniTaskVoid myUniTask()
        {
            while (true)
            {
                Debug.Log($"{nameof(UniTask)} | 반복");
                await UniTask.Yield();
            }
        }
    }
}

UniTask 코드 역시, Debug문을 매 프레임마다 출력한다.

 

UniTask는 async로 정의된 함수를 실행하는 것으로부터 시작한다.

myUniTask 함수 내부의 await 키워드로 비동기적 대기를 할 수 있다.(= yield return)

UniTask.Yield() 또는 UniTask.NextFrame()yield return null과 동일하다.

myUnityTask의 반환 값인 UniTaskVoid(=UniTask)는 따로 반환 값을 정의하지 않은 타입이다.

 

※ 단순히, Start문에서 myUniTask() 라고 선언해도 되지만,

반환 값을 사용하지 않기에, myUniTask().Forget() 으로 선언했다.


② 지연시키기

using UnityEngine;
using System.Collections;

namespace coroutine.delay
{
    public class CoroutineClass : MonoBehaviour
    {
        private void Start()
        {
            StartCoroutine(myCoroutine());
        }
        private IEnumerator myCoroutine()
        {
            yield return new WaitForSeconds(1f);
            Debug.Log($"{nameof(Coroutine)} | 1초 대기완료");
        }
    }
}

1초 대기 후, Debug를 출력하는 코루틴 코드이다.

 

using UnityEngine;
using Cysharp.Threading.Tasks;

namespace unitask.delay
{
    public class UniTaskClass : MonoBehaviour
    {
        private void Start()
        {
            myUniTask().Forget();
        }
        private async UniTaskVoid myUniTask()
        {
            await UniTask.Delay(1000);
            Debug.Log($"{nameof(UniTask)} | 1초 대기완료");
        }
    }
}

UniTask.Delay 함수는 밀리세컨드(ms)만큼 지연시킬 수 있다.


③ 값 반환하기

using UnityEngine;
using UnityEngine.Events;
using System.Collections;

namespace coroutine.returnValue
{
    public class CoroutineClass : MonoBehaviour
    {
        private IEnumerator Start()
        {
            yield return myCoroutine((value) => { Debug.Log($"{nameof(Coroutine)} | 반환 : {value}"); });
        }
        private IEnumerator myCoroutine(UnityAction<int> callback)
        {
            int value = 100;
            yield return new WaitForSeconds(1f);
            callback?.Invoke(value);
        }
    }
}

코루틴은 값을 반환하지 않기 때문에,

주로 콜백 함수를 인자로 전달해서 내부에서 호출한다.

따라서, 코루틴이 중첩되면 콜백 지옥에 빠지게 된다.

※ 필드 변수를 선언하는 방식도 있지만, 불필요한 메모리가 낭비된다.

 

using UnityEngine;
using Cysharp.Threading.Tasks;

namespace unitask.returnValue
{
    public class UniTaskClass : MonoBehaviour
    {
        private async void Start()
        {
            Debug.Log($"{nameof(UniTask)} | 반환 : {await myUniTask()}");
        }
        private async UniTask<int> myUniTask()
        {
            int value = 100;
            await UniTask.Delay(1000);
            return value;
        }
    }
}

UniTask는 일반 함수처럼 return을 사용해 값을 반환할 수 있다.

다만, 반환 값으로 UniTask<T> 형식을 사용해야한다.(이때, T는 반환하는 자료형)

예를 들어, 위 코드에서 반환 값을 받으려면 int value = await myUniTask()가 되겠다.


④ 중첩하기

using UnityEngine;
using System.Collections;

namespace coroutine.nested
{
    public class CoroutineClass : MonoBehaviour
    {
        private IEnumerator Start()
        {
            yield return myCoroutine(1f);
            yield return myCoroutine(2f);
            yield return myCoroutine(3f);
        }
        private IEnumerator myCoroutine(float duration)
        {
            yield return new WaitForSeconds(duration);
            Debug.Log($"{nameof(Coroutine)} | {duration}초 대기완료");
        }
    }
}

코루틴 내부에서 코루틴을 사용하는 코드이다.

순차적으로, 1초 > 2초 > 3초를 대기한다.

 

using UnityEngine;
using System;
using Cysharp.Threading.Tasks;

namespace unitask.nested
{
    public class UniTaskClass : MonoBehaviour
    {
        private async void Start()
        {
            await myUniTask(1f);
            await myUniTask(2f);
            await myUniTask(3f);
        }
        private async UniTask myUniTask(float duration)
        {
            await UniTask.Delay(TimeSpan.FromSeconds(duration));
            Debug.Log($"{nameof(UniTask)} | {duration}초 대기완료");
        }
    }
}

await를 여러번 사용하는 것으로 동일한 결과를 낼 수 있다.

※ TimeSpan.FromSeconds 함수로 초 단위를 밀리세컨드 단위로 변환했다.


⑤ 정지하기

using UnityEngine;
using System.Collections;

namespace coroutine.stop
{
    public class CoroutineClass : MonoBehaviour
    {
        private Coroutine _routine;

        private IEnumerator Start()
        {
            _routine = StartCoroutine(myCoroutine());
            yield return new WaitForSeconds(5f);
            StopCoroutine(_routine);
            Debug.Log($"{nameof(Coroutine)} | 정지");
        }
        private IEnumerator myCoroutine()
        {
            while (true)
            {
                Debug.Log($"{nameof(Coroutine)} | 반복");
                yield return null;
            }
        }
    }
}

코루틴은 StartCoroutine 함수가 반환하는 Coroutine 인스턴스를

StopCoroutine에 전달해 정지할 수 있다.

 

위 코드는 5초 뒤에 코루틴을 정지한다.

using UnityEngine;
using System.Threading;
using Cysharp.Threading.Tasks;

namespace unitask.stop
{
    public class UniTaskClass : MonoBehaviour
    {
        private CancellationTokenSource _token = new();

        private async void Start()
        {
            myUniTask(_token).Forget();
            await UniTask.Delay(5000);
            _token.Cancel();
            Debug.Log($"{nameof(UniTask)} | 정지");
        }
        private async UniTask myUniTask(CancellationTokenSource token)
        {
            while (true)
            {
                Debug.Log($"{nameof(UniTask)} | 반복");
                await UniTask.Yield(token.Token);
            }
        }
    }
}

UniTask는 CancellationTokenSource 인스턴스, 즉 토큰을 비동기 함수의 인자로 전달해야한다.

이때, Cancel 함수가 실행되면 await가 중단된다.

 

거의 모든 비동기 함수는 토큰을 인자로 받을 수 있다.

위 코드에서는 토큰을 myUniTask 함수에 전달했고, 또 그 토큰을 UniTask.Yield 함수에 전달했다.

※ UniTask 비동기 함수의 종류로는 Delay, Yield, NextFrame, WaitForEndOfFrame,

WaitForFixedUpdate, WaitForSeconds 등이 있다.

 

만약, 직접 토큰을 처리하고 싶으면,

        private async UniTask myUniTask(CancellationTokenSource token)
        {
            while (true)
            {
                Debug.Log($"{nameof(UniTask)} | 반복");
                await UniTask.Yield();
                if (token.IsCancellationRequested) break;
            }
        }

IsCancellationRequested 프로퍼티를 활용하면 된다.


+ WhenAll

using UnityEngine;
using Cysharp.Threading.Tasks;

namespace unitask.whenAll
{
    public class UniTaskClass : MonoBehaviour
    {
        private async void Start()
        {
            // public static UniTask WhenAll(params UniTask[] tasks)
            await UniTask.WhenAll(myUniTask(1f), myUniTask(2f), myUniTask(3f));
            Debug.Log($"{nameof(UniTask)} | 모든 대기완료 ");
        }

        private async UniTask myUniTask(float duration)
        {
            await UniTask.WaitForSeconds(duration);
            Debug.Log($"{nameof(UniTask)} | {duration}초 대기완료");
        }
    }
}

UniTask.WhenAll은 인자로 전달받은 async 함수들이 완료될 때까지 기다리는 비동기 함수이다.

 

위 코드에서는 WhenAll 함수에 세 개의 async 함수를 추가했으나,

원하는 만큼 추가할 수 있다.


+ WaitUntil

using UnityEngine;
using Cysharp.Threading.Tasks;

namespace unitask.waitUntil
{
    public class UniTaskClass : MonoBehaviour
    {
        private async void Start()
        {
            await UniTask.WaitUntil(() => { return (1 + 1) == 2; });
            Debug.Log($"{nameof(UniTask)} | 조건 일치");
        }
    }
}

UniTask.WaitUntil은 bool 타입을 반환하는 람다식(또는 일반 함수)이,

true를 반환할 때까지 기다리는 비동기 함수이다.


위에서 설명한 것들은 가장 기본적인 사용법이 되겠다.

더 자세한 내용은 UniTask 깃허브를 참고하자.

 

댓글