UniTask는 유니티에서 쓰이는 비동기 방식과 코루틴 (Coroutine)을 대체하기 위해 나온
라이브러리 입니다.
Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity.
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
특징
Zero-Allocation
Struct 기반으로 제작.
Heap 공간에 새로운 메모리를 할당하지 않기 때문에 GC가 메모리를 해제하는데 쓰는 비용 최소화.
Coroutine 완전 대체
yield return null, yield return new WaitForSeconds 등 코루틴에서 쓰이는 모든 것들을 대체.
TaskTracker를 이용해 메모리 누수 방지
Task / ValueTask / IValueTaskSource와 호환
UniTask 설치
우선 유니티 상단바에서 Window - Package Manager 에 들어갑니다.
그 다음에 왼쪽 위 + 버튼을 클릭하고 Add package from git URL 을 클릭합니다.
해당 링크를 복붙해서 Add 누르면 자동으로 설치가 됩니다.
또는
Releases · Cysharp/UniTask
Provides an efficient allocation free async/await integration for Unity. - Cysharp/UniTask
github.com
해당 사이트로 들어가서 원하는 버전을 찾은 후
이런 식으로 된 unitypackage 확장명의 파일을 찾아 다운로드 하고 열면
해당 창이 나오는데 이 상태에서 Import하면 됩니다.
UniTask 사용법
기본 형태는 이런 식으로 되어 있습니다.
private async UniTask Foo()
{
await UniTask.WaitForSeconds(1);
await UniTask.Yield();
}
기존 비동기 방식과 같은 async/await 방식을 사용하고 있습니다.
UniTaskVoid, UniTask<T>
private async UniTaskVoid Foo()
{
Debug.Log("Foo");
await UniTask.Delay(TimeSpan.FromSeconds(1));
Debug.Log("Foo End");
}
UniTaskVoid는 기존 void 형을 대체하기 위한 것으로
기존 async void는 UniTask 시스템에서 지원하지 않기 때문에 UniTaskVoid를 쓴다고 합니다.
물론 async void도 사용 가능합니다.
public class UniTaskTest : MonoBehaviour
{
private async void Start()
{
var task = await Foo();
Debug.Log($"{task}");
}
private async UniTask<float> Foo()
{
await UniTask.Delay(TimeSpan.FromSeconds(1));
return Time.time;
}
}
UniTask<T>에서 T는 제네릭 형식으로 리턴하고자 하는 값의 타입을 적으면 일반 함수에서 리턴하듯이
쓸 수 있습니다.
float을 반환시키는 위에 코드에서 Delay 시간을 출력하면 아래처럼 나옵니다.
Yield, NextFrame
private async UniTask Foo()
{
// 다음 프레임까지 대기. 코루틴의 yield return null과 동일
await UniTask.NextFrame();
// 두 함수는 동일
await UniTask.Yield();
await UniTask.Yield(PlayerLoopTiming.Update);
}
인수가 있는 UniTask.Yield는 yield return null과 약간 다르다고 하는데
다음 호출 때까지 대기한다고 합니다.
예를 들어 위 코드는 매개 변수에 PlayerLoopTiming.Update 라고 적혀있는데
Update에서 호출하면 다음 프레임의 Update에서 실행됩니다.
PlayerLoopTiming.FixedUpdate라고 적으면 현재 프레임의 FixedUpdate에서 실행 한 후 다음 프레임의 FixedUpdate에서 또 실행되겠죠?
다만, PreUpdate의 경우엔 현재 프레임의 Update에서 실행된다고 합니다.
아래는 PlayerLoopTiming의 종류입니다.
public enum PlayerLoopTiming
{
Initialization = 0,
LastInitialization = 1,
EarlyUpdate = 2,
LastEarlyUpdate = 3,
FixedUpdate = 4,
LastFixedUpdate = 5,
PreUpdate = 6,
LastPreUpdate = 7,
Update = 8,
LastUpdate = 9,
PreLateUpdate = 10,
LastPreLateUpdate = 11,
PostLateUpdate = 12,
LastPostLateUpdate = 13,
#if UNITY_2020_2_OR_NEWER
TimeUpdate = 14,
LastTimeUpdate = 15,
#endif
}
DealyFrame, Delay
private async UniTask Foo()
{
// 60 프레임 (1초) 대기
await UniTask.DelayFrame(60);
// 1초 대기. 코루틴의 WaitForSeconds와 동일
await UniTask.Delay(TimeSpan.FromSeconds(1));
// 1초 대기. 코루틴의 WaitForSecondsRealtime과 동일
await UniTask.Delay(TimeSpan.FromMilliseconds(1000), ignoreTimeScale: true);
}
Delay의 두 번째 매개변수인 ignoreTimeScale가 true이면 게임 속 시간이 아닌 현실 시간을 기준으로 딜레이 시킵니다.
WaitForEndOfFrame, WaitForSeconds
// 프레임이 끝날 때까지 대기
#if UNITY_2023_1_OR_NEWER
await UniTask.WaitForEndOfFrame();
#else
await UniTask.WaitForEndOfFrame(this);
#endif
// 1초 대기
await UniTask.WaitForSeconds(1);
UniTask.WaitForEndOfFrame은 현재 프레임이 끝날 때까지 대기합니다.
Unity 버전 2023.1 이상 버전은 인수가 없어도 됩니다.
UniTask.WaitForSeconds는 말그대로 몇 초간 대기한다는 뜻의 함수입니다.
WaitUntil
private async UniTaskVoid Bar()
{
// return 값이 true가 될 때까지 대기. 코루틴의 yield return new WaitUntil(() => ...)과 같음
await UniTask.WaitUntil(() =>
{
Debug.Log("Waiting...");
// 스페이스바를 누르면 true를 반환하여 대기를 종료
return Input.GetKeyDown(KeyCode.Space);
});
Debug.Log("Space key pressed!");
}
WaitUntil은 return 값이 true가 될 때까지 대기합니다.
위 코드에선 Space를 누를 때까지 대기하라고 했지만
i > 4, i == 10 && j == 20 이런 식으로 비교식도 넣을 수 있습니다.
첫번째 인수에 매개변수 없는 함수가 들어가거나 위 코드처럼 람다식으로 간단하게 만들 수 있습니다.
WaitUntilValueChanged
public class UniTaskTest : MonoBehaviour
{
private UniTaskTest2 _test2 = new();
private void Start()
{
Foo();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
_test2.Text = "World";
}
}
private async UniTask Foo()
{
// _test2.Text가 "World"가 될 때까지 대기
await UniTask.WaitUntilValueChanged(_test2, Bar);
Debug.Log($"Hello, {_test2.Text}!");
}
private bool Bar(UniTaskTest2 test2)
{
Debug.Log(test2.Text);
return String.Equals(test2.Text, "World");
}
}
public class UniTaskTest2
{
public string Text { get; set; } = "Hello";
}
WaitUntilValueChanged는 어떤 변수 내 값이 변경될 때까지 대기하는 함수입니다.
첫번째 인수에는 클래스 참조 변수가 들어가고
두번째 인수에는 위에 코드처럼 첫번째 인수의 클래스를 매개변수로 받는 함수가 들어가거나
await UniTask.WaitUntilValueChanged(_test2, test2 => String.Equals(test2.Text, "World"));
이 코드처럼 간략하게 람다식으로 작성할 수도 있습니다.
WhenAll
public class UniTaskTest : MonoBehaviour
{
private void Start()
{
Main();
}
private async UniTaskVoid Main()
{
await UniTask.WhenAll(Foo(), Bar());
Debug.Log("둘 다 누름");
}
private async UniTask Foo()
{
await UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.A));
Debug.Log("A 누름");
}
private async UniTask Bar()
{
await UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.B));
Debug.Log("B 누름");
}
}
WhenAll은 UniTask를 반환하는 함수들이 작업을 완료할 때까지 기다리는 함수입니다.
CancellationTokenSource, CancellationToken
public class UniTaskTest : MonoBehaviour
{
private CancellationTokenSource _cts = new CancellationTokenSource();
private async void Start()
{
Foo();
}
private async UniTaskVoid Foo()
{
try
{
while (true)
{
await UniTask.Delay(TimeSpan.FromMilliseconds(100), cancellationToken: _cts.Token);
Debug.Log("Foo");
}
}
// Cancel된 경우 OperationCanceledException 발생
catch (OperationCanceledException e)
{
Debug.Log("Canceled");
throw;
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
_cts?.Cancel();
}
}
private void OnDestroy()
{
_cts?.Cancel();
_cts?.Dispose();
}
}
우선 CancellationTokenSource는 UniTask가 아닌 C#에서 기본적으로 제공하는 기능입니다.
하지만 UniTask에서 쓰는 이유는 비동기를 취소시킬 수 있기 때문입니다.
CancellationTokenSource는 CancellationToken을 발급, 취소 등 기능을 담당하고
CancellationToken은 CancellationTokenSource에서 발급된 토큰으로 작업 간에 전달하는 역할을 합니다.
여기서 중요한 것은
CancellationTokenSource은 메모리에 할당이 되기 때문에 GC의 처리 대상이 됩니다.
그래서 사용 후 Dispose로 폐기해야 GC가 메모리를 회수합니다.
Dispose | Cancel |
CancellationTokenSource를 완전 삭제 | CancellationTokenSource가 메모리에 잔류 |
삭제 시 취소 요청X | Cancel하면 취소 요청 O |
또한 Cancel을 하면
while (true)
{
if (_cts.Token.IsCancellationRequested)
{
Debug.Log("Canceled");
break;
}
await UniTask.Delay(TimeSpan.FromMilliseconds(100), cancellationToken: _cts.Token);
Debug.Log("Foo");
}
위 코드처럼 IsCancellationRequested에 걸리게 됩니다.
두번째로 중요한 점은
CancellationToken을 Cancel하면 영구적으로 Cancel 상태가 됩니다.
그래서 Token을 되살리고 싶다면
_cts.Cancel();
_cts = new CancellationTokenSource();
이 코드처럼 객체를 재생성할 수 밖에 없습니다.
SuppressCancellationThrow()
위에서 설명한 CancellationToken은 Cancel을 하면 try catch에서 catch 부분에
OperationCanceledException로 예외 처리가 납니다.
근데 try는 괜찮은데 catch로 가면 비용이 상당히 많이 듭니다.
그래서 UniTask 제작자는 이에 대한 대책을 마련했는데 그게 SuppressCancellationThrow 함수입니다.
private async UniTask Test()
{
var isCanceled = await UniTask.WaitWhile(() =>
{
Debug.Log("비동기 실행 중");
return true;
}, cancellationToken: _cts.Token).SuppressCancellationThrow();
if (isCanceled)
{
Debug.Log("비동기 정지");
}
}
사용법은 간단합니다.
걍 UniTask 함수에 SuppressCancellationThrow() 함수만 붙여주면 됩니다.
그리고 SuppressCancellationThrow 함수는 취소했는지에 대한 결과값이 반환됩니다.
단, 주의해야 할 점은
private async UniTask Test()
{
while (true)
{
Debug.Log("비동기 실행 중");
await UniTask.WaitForSeconds(1f, cancellationToken: destroyCancellationToken).SuppressCancellationThrow();
}
}
위 코드와 같은 경우인데 이 경우엔 취소했는지 반환 값은 반환하지만
전체적인 흐름의 예외 처리는 막지 못해서 OperationCanceledException 예외는 발생하게 됩니다.
destroyCancellationToken, Application.exitCancellationToken
이 Token들은 2022.2 버전 이후에 생긴 Token입니다.
CancellationToken과 다르게 직접 생성할 필요가 없습니다.
일단 destroyCancellationToken을 먼저 살펴보면
이름대로 오브젝트가 Destory 될 때 토큰이 취소됩니다.
해당 Token은 MonoBehaviour에 들어있어서 그냥 꺼내오기만 하면 됩니다.
private async UniTask Test()
{
var isCanceled = await UniTask.WaitWhile(() =>
{
Debug.Log("비동기 실행 중");
return true;
}, cancellationToken: destroyCancellationToken).SuppressCancellationThrow();
if (isCanceled)
{
Debug.Log("비동기 정지");
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Destroy(this.gameObject);
}
}
위 코드 실행 결과인데 스페이스바를 누르면 자기 자신 오브젝트가 파괴되면서
토큰도 취소되어 비동기 함수가 멈추는 걸 볼 수 있습니다.
다음은 Application.exitCancellationToken 입니다.
얘도 이름에서 알 수 있듯이 에디터에서 게임 모드를 끝내면 취소되는 토큰입니다.
이 토큰은 MonoBehaviour에 없어서 Application 안에 있는 정적 프로퍼티를 가져와서 쓰면 됩니다.
private async UniTask Test()
{
var isCanceled = await UniTask.WaitWhile(() =>
{
Debug.Log("Application.exitCancellationToken 비동기 실행 중");
return true;
}, cancellationToken: Application.exitCancellationToken).SuppressCancellationThrow();
if (isCanceled)
{
Debug.Log("Application.exitCancellationToken 취소");
}
}
위 두 개의 Token도 OperationCanceledException 예외를 발생시키기 때문에
저는 SuppressCancellationThrow을 사용했습니다.
해당 포스트는 기능 공부 후 추가 예정
'유니티 > Tutorial' 카테고리의 다른 글
Unity 디자인 패턴 Observer (0) | 2025.04.25 |
---|---|
Unity 디자인 패턴 Singleton (0) | 2025.04.07 |
Unity Compute Shader (0) | 2025.03.22 |
Unity New Input System (0) | 2025.02.19 |
Unity FSM 유한 상태 머신 (0) | 2025.01.13 |