[Unity] UniTask [ + DOTween ] (3)

suhan0304·2024년 7월 4일

유니티 - UniTask

목록 보기
3/3
post-thumbnail

간단한 에셋을 써서 아래와 같이 간단히 만들어줬다.


이제 DOTween과 UniTask를 사용하기 위해 import 해준다.

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

DOTween과 UniTask를 사용해서 Scene 애니메이션을 만들어보자.
DOTween과 UniTask를 같이 사용하기 위해선 Project Settings > Player > Script Compilation에 UNITASK_DOTWEEN_SUPPORT를 추가해줘야 한다. (수행하지 않으면 스크립트에서 오류가 발생한다.)


Animate Scene

Outer Object Animation

일단 Outer Object Animation을 먼저 해보자.

UniTask를 중간에 중지 할 수 있도록 관리하기 위해선 매개변수로 CancellationToken을 받을 필요가 있다.

public class AnimateScene : MonoBehaviour
{
    [SerializeField] private Transform[] outerObjectsTransform;

    async void Start() {
        await AnimateOuterObject(outerObjectsTransform, this.GetCancellationTokenOnDestroy());
    }

    private async UniTask AnimateOuterObject(Transform[] objectsToAnimate, CancellationToken cancellationToken) 
    {
        await UniTask.Delay(TimeSpan.FromSeconds(0.2f), cancellationToken: cancellationToken);
        for (int i = 0; i < objectsToAnimate.Length; i++) {
            objectsToAnimate[i].gameObject.SetActive(true);
            objectsToAnimate[i].DOScale(Vector3.zero, 0.5f).From().SetEase(Ease.InOutBack).WithCancellation(cancellationToken);
            await UniTask.Delay(TimeSpan.FromSeconds(0.1f), cancellationToken: cancellationToken);
        }
    }
}

From

From()이 굉장히 중요한 포인트인데, 보면 알겠지만 EndValue가 Vector.zero로 되어있다. 그렇다면 물체가 작아져서 사라지는게 아닌가? 하겠지만 From을 붙이면 현재 값이 EndValue가 되고 EndValue가 StartValue가 된다. 즉, From을 사용하면 시작하는 값을 정해줄 수 있다는 뜻이다. 따라서 실제로는 아래와 같이 작동한다.

  • DOScale(Vector3.zero, 0.5f)는 객체의 스케일을 Vector3.zero로 0.5초 동안 변경한다.
  • From()을 추가하면, 애니메이션이 Vector3.zero에서 시작하여 현재 스케일 값으로 변경됩니다. 즉, 객체가 현재 크기에서 점점 작아졌다가 다시 현재 크기로 돌아오는 애니메이션을 수행하게 된다.

CancellationToken

CancellationToken은 비동기 작업을 취소할 수 있는 방법을 제공한다. 이를 사용하면 작업이 더 이상 필요 없거나 중단되어야 할 경우에 효율적으로 작업을 중단할 수 있다.

위 코드처럼 Delay나 DOScale에 CancellationToken을 연결되어 있는 상태에서 아래와 같은 코드를 실행하면 그 때 중지된다.

cancellationTokenSource.Cancel()

GetCancellationTokenOnDestroy는 UniTask에서 제공하는 유틸리티 메서드로, Unity의 MonoBehaviour 클래스와 함께 사용될 때, 해당 객체가 파괴될 때 자동으로 취소 신호를 보내는 CancellationToken을 생성하는 데 사용된다. 이 메서드는 객체의 생명 주기와 관련된 비동기 작업을 관리하는 데 유용하다.

이 코드를 아래와 같이 개선할 수 있다.

private async UniTask AnimateOuterObject(Transform[] objectsToAnimate, CancellationToken cancellationToken) 
{
    await UniTask.Delay(TimeSpan.FromSeconds(0.2f), cancellationToken: cancellationToken);

    UniTask[] tasks = new UniTask[objectsToAnimate.Length];
    for (int i = 0; i < objectsToAnimate.Length; i++)
    {
        tasks[i] = AnimateDelay(objectsToAnimate, i, cancellationToken, i * 0.1f);
    }
    await UniTask.WhenAll(tasks);
}

private async UniTask AnimateDelay(Transform[] objectsToAnimate, int i, CancellationToken cancellationToken, float time)
{
    await UniTask.Delay(TimeSpan.FromSeconds(time), cancellationToken: cancellationToken);
    objectsToAnimate[i].gameObject.SetActive(true);
    objectsToAnimate[i].DOScale(Vector3.zero, 0.5f).From().SetEase(Ease.InOutBack).WithCancellation(cancellationToken);
}

WhenAll

위와 같이 UniTask.WhenAll(tasks)을 사용하여 작성하면 tasks 배열에 있는 모든 UniTask가 완료될 때까지 대기한다. 이를 이용해 모든 애니메이션이 완료될 때까지 기다리기 위해 사용한다.

이처럼 UniTask로 각각 애니메이션을 만들고 모든 애니메이션 task가 끝날 때 까지 WhenAll로 기다리는 코드로 작성하면 어떤 효과가 있을까? 기존 코드랑 특징을 비교 해보면서 장단점을 따져보자.

첫 번째 코드 :

  • 순차적으로 각 객체의 애니메이션을 실행한다. 각 객체가 활성화되고 스케일 애니메이션이 시작되기 전에 0.1초 대기한다.
  • for 루프 내에서 각 객체의 애니메이션이 끝날 때까지 0.1초씩 대기한다.

두 번째 코드 :

  • 각 객체의 애니메이션을 비동기적으로 처리한다. 각 객체의 애니메이션이 시작되기 전에 개별적으로 대기 시간을 설정하고, UniTask.WhenAll을 사용하여 모든 애니메이션이 완료될 때까지 기다린다.
  • 각 객체의 애니메이션 시작 전에 대기 시간을 설정하고, AnimateDelay 메서드에서 비동기적으로 대기한다.

첫 번째 코드의 경우 코드가 간단하고 이해하기 쉬우며 각 객체가 순차적으로 애니메이션 되어야 하는 경우 적합하게 사용할 수 있다. 하지만 많은 객체를 처리할 때 성능이 저하될 수 있다.
이에 반해 업데이트 된 두 번째 코드는 각 객체의 애니메이션을 병렬로 처리하여 더 효율적이고 유연한 애니메이션 관리를 가능하게 한다. 병렬 처리를 통해 애니메이션 실행을 최적화하고자 한다면 개선된 두 번째 코드가 더 적합하다!


Inner Object Animation

섬 위에 있는 모든 오브젝트에 대해서 실행하는 애니메이션을 작성해준다.

[SerializeField] private Transform[] innerObjectsTransform;

async void Start() {
    CancellationToken cancellationTokenOnDestroy = this.GetCancellationTokenOnDestroy();
    await AnimateOuterObject(outerObjectsTransform, cancellationTokenOnDestroy);
    await AnimateInnerObject(innerObjectsTransform, cancellationTokenOnDestroy);
}

private async UniTask AnimateInnerObject(Transform[] objectsToAnimate, CancellationToken cancellationToken) 
{
    for (int i = 0; i < objectsToAnimate.Length; i++) 
    {
        objectsToAnimate[i].gameObject.SetActive(true);
        innerObjectsTransform[i].DOScale(Vector3.zero, 0.5f).From().SetEase(Ease.InOutBack).WithCancellation(cancellationToken);
    }
}

실행해보면 Box Collider는 음수 사이즈를 지원하지 않는다고 오류가 발생한다.

왜 뜨냐면 Ease 설정 때문이다. Easing 사이트에서 Ease 그래프를 확인할 수 있는데 InOutBack의 경우 실제 변하는 값의 범위 바깥으로 나가는 것을 볼 수 있다.

그래서 Vector3.zero에서 시작하면 Box Collider의 사이즈가 음수로 내려갔다가 다시 scale이 커지다 보니 이런 오류가 발생하는 것이다. InOutBack이 아니라 OutBack으로 바꿔주면 오류가 해결된다.

현재 Outer Object의 애니메이션이 끝나기도 전에 Inner Object 애니메이션이 실행되는데 Outer Object가 실행이 되고 난 후에 Inner Object가 실행되도록 해보자.

private async UniTask AnimateOuterObject(Transform[] objectsToAnimate, CancellationToken cancelltationToken) 
{
    await UniTask.Delay(TimeSpan.FromSeconds(0.2f), cancellationToken: cancelltationToken);

    UniTask[] tasks = new UniTask[objectsToAnimate.Length];
    for (int i = 0; i < objectsToAnimate.Length; i++)
    {
        tasks[i] = AnimateDelay(objectsToAnimate, i, cancelltationToken, i * 0.1f, i == objectsToAnimate.Length - 1);
    }
    await UniTask.WhenAll(tasks);
}

private async UniTask AnimateDelay(Transform[] objectsToAnimate, int i, CancellationToken cancellationToken, float time, bool lastObject)
{
    await UniTask.Delay(TimeSpan.FromSeconds(time), cancellationToken: cancellationToken);
    objectsToAnimate[i].gameObject.SetActive(true);
    if (lastObject) {
        await objectsToAnimate[i].DOScale(Vector3.zero, 0.5f).From().SetEase(Ease.InOutBack).WithCancellation(cancellationToken);
    }
    else {
        objectsToAnimate[i].DOScale(Vector3.zero, 0.5f).From().SetEase(Ease.InOutBack).WithCancellation(cancellationToken);
    }
}

이렇게 하면 기존의 DOScale 메서드를 호출하고 결과를 기다리지 않고 다음 코드로 넘어갔던 동기적인 방식과는 다르게 마지막 객체인 경우에 DOScale을 await해준다. 그러면 마지막 오브젝트의 DOScale이 끝날 때까지 해당 라인에서 await하고 있고 그러면 await AnimateOuterObject(outerObjectsTransform, cancellationTokenOnDestroy);가 끝나지 않고 계속해서 기다리고 있기 때문에 await AnimateInnerObject(innerObjectsTransform, cancellationTokenOnDestroy);가 실행되지 않고 기다리다가, 마지막 오브젝트의 DOScale이 끝나면 실행된다.

await은 쉽게 말해 await뒤에 따라오는 메소드, 함수가 끝날때까지 거기 await이라는 단어 위치에서 코드 실행이 기다리고 있겠다는 뜻!


Ship Animation

[SerializeField] private Transform shipTransform;
[SerializeField] private Transform startPosition;

async void Start() {
    CancellationToken cancellationTokenOnDestroy = this.GetCancellationTokenOnDestroy();
    await AnimateOuterObject(outerObjectsTransform, cancellationTokenOnDestroy);
    await AnimateInnerObject(innerObjectsTransform, cancellationTokenOnDestroy);
    await AnimateShip(shipTransform, cancellationTokenOnDestroy);
}

private async UniTask AnimateShip(Transform ship, CancellationToken cancellationToken) {
    currentShipPosition = ship.position;
    ship.position = startPosition.position;
    ship.gameObject.SetActive(true);
    await ship.DOScale(Vector3.zero, 0.5f).From().SetEase(Ease.OutBack).WithCancellation(cancellationToken);
    await ship.DOMove(currentShipPosition, 0.7f).SetEase(Ease.OutBack).WithCancellation(cancellationToken);
}

Script

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using DG.Tweening;
using TMPro;
using Unity.VisualScripting;
using UnityEngine;

public class AnimateScene : MonoBehaviour
{
    
    [SerializeField] private Transform[] outerObjectsTransform;
    [SerializeField] private Transform[] innerObjectsTransform;
    [SerializeField] private Transform shipTransform;
    [SerializeField] private Transform startPosition;

    private Vector3 currentShipPosition;

    async void Start() {
        CancellationToken cancellationTokenOnDestroy = this.GetCancellationTokenOnDestroy();
        await AnimateOuterObject(outerObjectsTransform, cancellationTokenOnDestroy);
        await AnimateInnerObject(innerObjectsTransform, cancellationTokenOnDestroy);
        await AnimateShip(shipTransform, cancellationTokenOnDestroy);
    }

    private async UniTask AnimateOuterObject(Transform[] objectsToAnimate, CancellationToken cancelltationToken) 
    {
        await UniTask.Delay(TimeSpan.FromSeconds(0.2f), cancellationToken: cancelltationToken);

        UniTask[] tasks = new UniTask[objectsToAnimate.Length];
        for (int i = 0; i < objectsToAnimate.Length; i++)
        {
            tasks[i] = AnimateDelay(objectsToAnimate, i, cancelltationToken, i * 0.1f, i == objectsToAnimate.Length - 1);
        }
        await UniTask.WhenAll(tasks);
    }

    private async UniTask AnimateDelay(Transform[] objectsToAnimate, int i, CancellationToken cancellationToken, float time, bool lastObject)
    {
        await UniTask.Delay(TimeSpan.FromSeconds(time), cancellationToken: cancellationToken);
        objectsToAnimate[i].gameObject.SetActive(true);
        if (lastObject) {
            await objectsToAnimate[i].DOScale(Vector3.zero, 0.5f).From().SetEase(Ease.InOutBack).WithCancellation(cancellationToken);
        }
        else {
            objectsToAnimate[i].DOScale(Vector3.zero, 0.5f).From().SetEase(Ease.InOutBack).WithCancellation(cancellationToken);
        }
    }

    private async UniTask AnimateInnerObject(Transform[] objectsToAnimate, CancellationToken cancellationToken) 
    {
        for (int i = 0; i < objectsToAnimate.Length; i++) 
        {
            objectsToAnimate[i].gameObject.SetActive(true);
            innerObjectsTransform[i].DOScale(Vector3.zero, 0.5f).From().SetEase(Ease.OutBack).WithCancellation(cancellationToken);
        }
    }

    private async UniTask AnimateShip(Transform ship, CancellationToken cancellationToken) {
        currentShipPosition = ship.position;
        ship.position = startPosition.position;
        ship.gameObject.SetActive(true);
        await ship.DOScale(Vector3.zero, 0.5f).From().SetEase(Ease.OutBack).WithCancellation(cancellationToken);
        await ship.DOMove(currentShipPosition, 0.7f).SetEase(Ease.OutBack).WithCancellation(cancellationToken);
    }
}


이처럼 DOTween과 UniTask를 적절히 사용하면 애니메이션을 굉장히 쉽게 구현할 수 있다. 이번엔 애니메이션만을 간단히 구현했지만 꼭 애니메이션이 아니어도 UI, 게임 오브젝트의 이동 및 크기 조절과 같은 기능적인 부분에서도 굉장히 유용하게 사용할 수 있다.

profile
Be Honest, Be Harder, Be Stronger

0개의 댓글