배운거 잊지 않도록 하는 메모 겸 튜토리얼성 정보예요.
PC에 최적화 되어있고 본인도 막 배운 정보니까 틀린 점 있으면 댓글 부탁.
1. UniTask
C#에는 기본적으로 Task async/await 비동기 코드가 존재하지만 Unity의 생명주기 기반 싱글스레드 환경에서는 적합하지 않다.
이런 유니티에서 async/await를 사용 가능하게 만들어주는 라이브러리가 UniTask이다.
2. Coroutine을 써야 하는 이유 vs UniTask를 써야하는 이유
Coroutine
- 추가 라이브러리가 설치가 필요 없다.
- 사용법이 비교적 쉬운편이며 리턴값이 필요 없고 yield return 값을 캐싱할 수 있는 간단한 비동기 작업에 적합하다.
- 컴포넌트가 비활성화되면 코루틴도 자동으로 비활성화된다.
UniTask
- 클래스가 아닌 스트럭트 기반이라 캐싱하지 않아도 가비지가 발생하지 않는다.
- CancellationTokenSource와 내장된 추가 함수를 사용하여 비동기 작업을 섬세하게 컨트롤할 수 있다.
- 필수 에셋인 DoTween과의 연계가 가능하다.
3. 사용법
유니태스크는 기본적으로 UniTask, UniTask<T>, UniTaskVoid의 반환값을 가지며 실행하는 방법은 다음과 같다.
UniTaskVoid는 await로 사용할 수 없지만 비용이 저렴하다는 장점이 있다.
UniTask().Forget()와 _ = UniTask();는 동일하게 작동하지만 Forget()을 사용하는 것을 권장한다. (댓글 참고)
4. CancellationTokenSource
기본적으로 유니태스크는 코루틴과 달리 컴포넌트가 비활성화 되어도 자동으로 취소되지 않는다.
그래서 CancellationTokenSource를 사용하여 UniTask의 비동기 작업을 제어해야 한다.
CancellationTokenSource 객체를 생성하여 await UniTask 메서드의 인수로 넣으면
cts.Cancel()을 하는 즉시 await UniTask 이후의 코드는 취소된다.
만약 이후의 코드를 실행하고 싶다면 .SuppressCancellationThrow()를 붙여줘야한다.
SuppressCancellationThrow() 뒤의 코드는 실행되었지만, 없다면 실행되지 않은 모습이다.
기타 CancellationTokenSource의 특징은 다음과 같다
- Cancel() 한 CancellationTokenSource는 원상복구가 불가능하다.
- CancellationTokenSource는 클래스 기반이므로 가비지가 생성된다.
- Dispose() 함수로 메모리에서 해제하여 가비지를 수동으로 제거할 수 있다.
위 특징들로 인해 기본적인 사용법은 사진과 같다.
CancellationTokenSource를 전역변수로 설정하여 오브젝트가 활성화 될 때 객체를 생성하고 비활성화 될 때 Cancel 및 Dispose를 하는 것이다.
5. DoTween과의 연계
유니티의 필수 에셋 DoTween과 연계도 가능하다.
Project Settings > Player > Scripting Define Symbols에
UNITASK_DOTWEEN_SUPPORT 를 입력하면 된다.
그러면 await 에 DoTween 객체를 사용가능하다.
다만 두트윈을 그냥 사용할 때에는 경고가 뜨기 때문에 _ = 으로 결과값을 버려주어야 한다.
두트윈에 .ToUniTask를 붙이면 UniTask의 특정 메소드(WhenAll, WhenAny 등)의 인수로 활용 가능하며 cancellationToken도 넣을 수 있다.
만약 두트윈과 유니태스크(또는 코루틴)를 둘 다 사용 중이라면 위쪽과 같은 코드가 만들어 질 수 있다.
하지만 두트윈이 끝나기 전에 UniTask.WaitForSeconds(10f)이 먼저 종료되어 버그가 발생할 수 있으므로
아래쪽처럼 WhenAll로 묶는 사용법을 권장한다.
작업을 취소하는 방법은 .ToUniTask로 CancellationTokenSource를 등록하는 방법 외에도
await된 Tweener를 Kill, Complete 등을 하는 방법이 있다.
위 두 코드는 결과가 같다.
개인적으론 1. 비동기 메서드의 반환값을 꼭 받고 싶을때 2. 모노비헤이비어랑 관련없는 비동기 실행이 꼭 필요할때 가 아니면 코루틴 쓰는게 낫다 봄
대기 시간이 인수값에 따라 변하거나 (코루틴도 딕셔너리 등 활용하면 메모리 낭비가 적게할 수 있지만) 두트윈과의 연계를 생각할 때에도 필요하다고 생각되네요.
나는 문법 통일하는게 좋아서 다 유니테스크로 쓰는편인디 토큰 처리가 귀찮긴 한데 그거 감내하고 쓰더라도 문법이 괜찮음
dot ween unitask 연계는 엄청좋네
근데 dotween 버그날 수 있다는 저거 그냥 먼저 실행된 tween은 이미 실행됐으니 await의 영향은 안받고, 독립적으로 되는거 아니냐 의도적으로 저렇개 써야할 일 있을것같은데
독립적으로 실행되어서 생기는 버그입니다. DoTween 지속시간과 await 대기시간을 똑같이 하더라도 틱 단위로 끝나는 시간이 어긋날 수 있더라구요. 실제로 겪은 일인데 transform.DoScale(Vector3.zero, 1f); await UniTask.WaitForSeconds(1f); transform.localScale = Vector3.one; 위 코드에서 간헐적으로 DoScale이 완료되기 전에 초기화 코드가 실행되어 최종 결과는 DoScale의 결과 값인 Vector3.zero가 되는 버그가 있었습니다.
아 그런의미구나 그럴 수 있겠네
UniTask().Forget()은 오류 사항을 무시하고 _ = UniTask();는 오류 사항을 표시한다. <- 이거 반대 아님? 오류가 exception말하는거 아닌가 레딧에도 await하거나 forget하지 않은 비동기 메소드에서 exception이 씹힌다는 글 있던데 나도 경험했고
알아본 결과 UniTask().Forget()과 _ = UniTask();는 동일하게 작동하는데 _ = UniTask();는 간헐적으로 exception을 뱉지 않는다는 보고가 있네요. (저는 20번 가량 반복해봤지만 재현 안 됨) 실제로 둘 다 exception 처리를 해봤는데 유니티 콘솔에 잘 표시되어서 더 찾아보니 'Forget() 메서드는 UniTask가 던지는 예외를 무시'라는 예외와 경고를 혼동한 정보가 문제였네요. 공식 문서를 보면 'IDE에서 경고를 띄우지 않으려면 Forget()을 붙여야 한다.'는 의미였습니다. 게시글은 수정하겠습니다.