코루틴(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 깃허브를 참고하자.
'[Unity]' 카테고리의 다른 글
[Unity] Input System으로 입력 처리하기 (35) | 2024.06.08 |
---|---|
[Unity] 오브젝트 풀링을 간단히 구현해보자(ObjectPool) (38) | 2024.05.24 |
[Unity] 선을 그리는 UI를 만들어보자 (60) | 2024.04.28 |
[Unity] UI 모양을 내 맘대로 바꿔보자(VertexHelper) (50) | 2024.04.21 |
[Unity] 동적으로 메쉬 병합하기(CombineMeshes) (33) | 2024.03.04 |
댓글