https://github.com/Cysharp/UniTask
왜 UniTask를 사용해야 할까?
1. 코루틴으로 관리하기에는 try-catch로 예외처리를 할 수가 없다.
2. 코루틴은 return 타입이 없어 실행 결과 리턴에 대한 부분을 콜백으로 처리 해야한다. (콜백지옥의 유발)
3. async 에서의 Task 리턴 자체도 가비지에 부담이 되어 잦은 호출을 할 수 없다. 하지만 UniTask를 쓰면 해결된다.
4. 전체적으로 콜백 없이 코드를 작성하는 경우 코드가 선형적으로 순서적으로 실행되기 때문에 코드가 훨씬 규칙성있고 깔끔하다.
5. 사용자에따라 유니티의 Editor Tool 작성시에 EditorCoroutines등을 사용할때도 코드 관리가 유용함.
UniTask 개발자는 UniTask를 이렇게 요약 설명 하고있다.
- - UniTask<t>는 struct base다. 즉, heap에 할당되지 않으므로 zero allocation이라는 말이 된다.(C#의 기본 Task는 class로 되어있습니다. 개발자는, 기본 비동기 Task보다 UniTask가 더 가볍다고 설명한다.)
- - 유니티의 AsyncOperations (어드레서블 async같은) 작업이나 코루틴 작업을 await으로 대기할 수 있다.
- - 유니티 메인 쓰레드(Player Loop) 기반으로 작업하여 코루틴을 대체할 수 있다. (UniTask.Yield, UniTask.Delay, UniTask.DelayFrame)
- - 모노비헤이어 메시지, 이벤트, ugui의 메세지 이벤트를 대기하거나 비동기 열거가 가능하다.
- - 유니티 메인 쓰레드(플레이어 루프)에서 실행되므로 WebGL/WASM 등에서도 사용할 수 있다.
- - Asynchronous LINQ, with Channel and Async ReactiveProperty
- - 메모리 누수를 방지하기 위한 task tracker를 제공한다.
- - 기존 C#의 Task/ValueTask/IValueTaskSource와 호환성이 뛰어나다.
기본적인 사용방법
UniTask 사용법은 기존 async Task 사용법과 상당히 유사하다. 결국 비동기 작업으로 처리하고 싶은 경우 async 키워드와 UniTask 리턴타입을 함께 사용해서 함수를 작성하면 된다.
Start문의 Function().Forget()은, 어쩔 수 없이 await을 사용하지 못하는 경우나, 일부러 사용하지 않을 때 발생하는 IDE의 경고 메세지를 무시하기 위한것이므로 사실 작성하지 않아도 된다. Forget() 으로만 실행해도 되지만 IDE 메세지가 거슬리는 경우 사용하자.
코드에서는 async 키워드를 쓰되 void타입의 함수는 UniTaskVoid를 사용하고, return 타입이 있는경우 UniTask<T>를 쓰고있다. 위와 같이 코드를 작성한 후 실행을 해보면 비동기 메세지가 계획한 시간 스케줄에 따라 잘 실행 되었음을 확인할 수 있다.
실행순서 : Hello 출력 -> 5초뒤 float value 리턴 메세지 출력 -> World 출력 -> return된 float value
public async UniTaskVoid Function()
{
Debug.Log("Hello");
await UniTask.Delay(TimeSpan.FromSeconds(1));
Debug.Log("World");
}
// return 타입이 있는경우
public async UniTask<float> FunctionFloat()
{
Debug.Log("I Will Return Float Value! Please Wait 5 Second");
await UniTask.Delay(TimeSpan.FromSeconds(5));
return 1.0f;
}
public async UniTask Start()
{
Function().Forget();
var f = await FunctionFloat();
Debug.Log("Value => " + f);
}
간단한 Web Request를 통한 사용 예시
웹서버와 통신하여 유저의 레벨을 가져오는 코드다. 보통은 이런 코드는 코루틴으로 작성다. 코루틴으로 작성하지 않으면 게임이 통신시간이 길어질수록 멈추기 때문이다. 이 때, 코루틴으로는 리턴타입을 받지 못해서 생기는 콜백 사용이 발생한다. 이런 구조가 꼬리를 물고 물면, 콜백지옥이 발생한다. 특히 네트워크 통신을 하는 경우 이런 패턴을 흔히 볼 수 있다.
IEnumerator GetUserLevel(string userName , System.Action<int> callback)
{
//try catch 예외 불가
var req = UnityWebRequest.Get("https://.../user/" + userName);
yield return req.SendWebRequest();
callback(int.Parse(req.downloadHandler.text));
}
void Logic()
{
StartCoroutine(GetUserLevel("userName", (level) =>
{
Debug.Log(level);
}));
}
하지만 UniTask를 사용하면 아래와같이 코드를 작성할 수 있다.
async UniTask<int> GetUserLevelAsync(string userName)
{
try
{
var req = UnityWebRequest.Get("https://.../user/" + userName);
var res = await req.SendWebRequest(); // Unity의 Async Operation 이라 await 가능하다.
var responseString = res.downloadHandler.text;
return int.Parse(responseString);
}
catch(Exception e)
{
Debug.LogError(e);
return -1;
}
}
async void Logic()
{
var level = await GetUserLevelAsync("user1");
Debug.Log(level);
}
UI 연출 등 코루틴으로 하던 작업을 unitask로 대체 (아래 예시에는 DoTween을 사용)
위와같은 연출에 대한 코드는 아래와 같다.
UniTask는 DoTween에 대한 await 기능또한 제공한다. (opem upm으로 dg tween을 설치하고, UNITASK_DOTWEEN_SUPPORT 를 활성화 한 경우)
이와 같은 경우에 선택적으로 await을 통해 대기할 연출과, 아닌 연출을 설정할 수 있다. 예를 들면 pressAnyKeyText 에 대한 연출에는 await을 사용하지 않았기 때문에 다음 트윈 코드 실행에 영향을 주지 않는다.
async UniTask StartTitleAnimation()
{
// 초기화면 대기
await UniTask.Delay(1500);
// 텍스트 상태 초기화
titleText.rectTransform.anchoredPosition = new Vector2(0, titleText.rectTransform.sizeDelta.y/2);
titleText.text = "Loading...";
titleText.gameObject.SetActive(true);
await titleText.rectTransform.DOAnchorPosY(-330f, 0.5f).SetEase(Ease.OutCirc);
await UniTask.Delay(700);
// 텍스트 상태 변경
titleText.text = "The Black";
titleText.fontSize = 100;
titleText.transform.localScale = new Vector3(2, 2, 2);
// 텍스트 커짐->작아짐
await titleText.transform.DOScale(1, 0.5f).SetEase(Ease.OutBack);
// pressAnyKey는 대기하지 않고 싶기 때문에 await 없이 진행
pressAnyKeyText.transform.localScale = new Vector3(2,2,2);
pressAnyKeyText.gameObject.SetActive(true);
pressAnyKeyText.transform.DOScale(1, 0.35f).SetEase(Ease.OutBack);
// 트윈 무한 반복
await titleText.transform.DOScale(1.1f, 2).SetLoops(-1, LoopType.Yoyo).SetEase(Ease.InOutQuad);
}
void Start()
{
StartTitleAnimation().Forget();
}
네트워크 통신에서의 사용
아래는 유니티의 web request를 간단하게 표현한 코드다. OnClickGameStart 함수를 호출했을때 지정한 api 서버로 요청을 보낼 수 있으며, 역시 코루틴을 사용하지 않기때문에 코드가 더욱 깔끔하고 yield를 사용하지 않으므로 try-catch 또한 선택적으로 적용 가능하다.
public async UniTask<T> Get<T>(string url)
{
var request = UnityWebRequest.Get(url);
await request.SendWebRequest();
if (request.isNetworkError || request.isHttpError)
{
Debug.LogError(request.error);
return default;
}
return JsonUtility.FromJson<T>(request.downloadHandler.text);
}
public async UniTaskVoid OnClickGameStart()
{
var result = await Get<string>("https://your-server.com/api/game/start");
// ... processing
}
그 외 유니티의 async operation 기능이나 코루틴 코드 대체목적으로 사용
두 같은 함수가 있지만, UniTask 버전은 호출과 내부 구현이 간단하다. 반면, 코루틴 버전은 yield 키워드로 대기해야만하고, 대기해서 전달받은 결과를 한번 더 파싱해줘야 하며, 호출에도 StartCoroutine 함수가 필요하다.
public async UniTaskVoid LoadObject()
{
var obj = await Resources.LoadAsync<GameObject>("...") as GameObject;
obj.transform.position = Vector3.zero;
}
IEnumerator LoadObject2()
{
var resReq = Resources.LoadAsync<GameObject>("");
yield return resReq;
var obj = resReq.asset as GameObject;
obj.transform.position = Vector3.zero;
}
비동기의 Cancellation에 대한 처리방법 (주의사항)
코루틴을 사용할때에도, 코루틴을 언젠가 취소시켜야 하는 작업, 혹은 특정 상황에 취소해야하는 작업으로 생각하고 함수를 작성했다면 코루틴을 StopCoroutine을 통해 멈추거나 오브젝트를 삭제하는 방법으로 코루틴을 중단시키기 때문에 C#의 비동기 프로그래밍에서도 Cancellation 처리는 중요하다. 코루틴과 같은경우 오브젝트가 삭제되거나 disable 되면 알아서 코루틴이 중단되지만 UniTask에서는 직접 Cancellation 관리를 해줘야하기 때문이다.
이제는 while문을 돌려보자. 플레이어 루프안에서 특정 작업을 무한 반복시킬 수 있는 비동기 함수를 작성했다. 헷갈리는 분들을 위해 코루틴 버전의 예시도 포함되어 있다. 비동기 함수와 코루틴 함수는 같은 동작을한다.
public IEnumerator CoroutineVer()
{
while (true)
{
yield return null;
Debug.Log(Time.realtimeSinceStartup);
}
}
public async UniTaskVoid WhileTest()
{
while (true)
{
await UniTask.Yield();
Debug.Log(Time.realtimeSinceStartup);
}
}
public async UniTask Start()
{
WhileTest();
}
코드를 보면 무슨 차이인지 모르실 수 있겠지만, 당연히 호출 방법에도 차이가 있고 (StartCoroutine), 위에서 계속 누차 말씀드렸듯, 비동기 방식에서는 try,catch, 그리고 대리자 없이 리턴또한 가능한다. 단 코드에서 불필요하게 표현하지 않았을 뿐이다. 그리고 위 코드를 실행하면 아래처럼 유니티 로그를 확인할 수 있다. Time.realtimeSinceStartup의 값을 계속 출력하고 있다.
여기서 주의해야 하는 점이 무엇일까? 이 플레이어 루프에서 무한 반복되는 비동기 함수는 제 프로젝트에서 AsyncObject라는 오브젝트에 UniTaskTest라는 스크립트를 붙여서 작동하고 있다. 그런데 Aynsc Object를 삭제하면 어떻게 될까? 과연 비동기 함수의 호출이 오브젝트와 함께 멈추게 될까?
아니다. 계속 비동기 함수가 호출된다. 분명히 오브젝트를 삭제했는데 말이다. 오브젝트가 삭제되어도 비동기 함수는 계속 실행되고 있으므로 CancellationToken으로 이 오브젝트를 멈춰주어야 한다. (위에서 설명했듯 코루틴처럼 UniTask의 비동기 작업은 자동 관리 되지 않는다.)
Async Object를 삭제해도 계속 호출된다
UniTask Tracker (Window 탭에서 찾을 수 있음) 를 열어서 확인을 해보면 비동기 함수가 끝나고 있지 않다는걸 확인할 수 있다. (여담으로, 이 도구를 사용하면 테스크가 종료되지 않아 생기는 메모리 누수등을 방지할 수 있다.)
이를 해결하려면 CancellationToken 으로 Cancel 해주어야한다. UniTask는 CancellationToken 관리 하기가 매우 편리하게 되어있는데, 기존 비동기로 이 작업을 수행하려면 일일히 취소 토큰을 생성하고 캐싱하고 관리해주어야 하지만 UniTask에서는 그럴필요가 없다. 단지 아래 코드처럼 UniTask 작업에 UniTask에서 제공하는 OnDestroy 시점에 Cancel이 호출되는 토큰을 넣기만 하면 된다.
public async UniTaskVoid WhileTest()
{
while (true)
{
await UniTask.Yield(cancellationToken:this.GetCancellationTokenOnDestroy());
Debug.Log(Time.realtimeSinceStartup);
}
}
아래 함수는 UniTask에서 Object의 Destroy 시점에 자동으로 Cancel이 호출되는 Cancellation 토큰을 가져오는 함수다. 이를 UniTask.Yield의 cancellation token에 적용했다. 이후에는 오브젝트를 삭제하면 정상적으로 작업이 멈춘다.
this.GetCancellationTokenOnDestroy();
그럼 아래처럼 코드를 바꿔서 다시 테스트
public async UniTaskVoid WhileTest()
{
while (true)
{
await UniTask.Yield(cancellationToken:this.GetCancellationTokenOnDestroy());
Debug.Log(Time.realtimeSinceStartup);
}
}
public async UniTask Start()
{
WhileTest();
}
private void OnDestroy()
{
Debug.Log("오브젝트를 삭제했습니다. 더이상 비동기가 실행되면 안됩니다.");
}
이후 오브젝트를 삭제하면 비동기 함수가 멈추게 된다. 이 작업이 중요한 이유는, destroy도 destroy지만 씬 변경시에도 비동기 함수가 살아있을 수 있다. 그래서 반드시 Cancellation 을 해줘야 하는 경우라면 이 작업을 해줘야한다.
'게임엔진 > Unity' 카테고리의 다른 글
[Unity] Visual Studio에서 디버깅 연결이 되지 않는 경우 (0) | 2023.09.18 |
---|---|
[Unity] Resources.LoadAll로 리소스 파일 전체 불러오기 (0) | 2023.09.15 |
[Unity] Coroutine 과 UniTask 비교 예제 (0) | 2023.09.13 |
[Unity] Mathf (0) | 2023.09.11 |
[Unity] Vector2 (0) | 2023.09.11 |