[Unity] Coin Collection Animation [DOTween + UniTask]

suhan0304·2024년 7월 2일

유니티 - DOTween

목록 보기
5/5
post-thumbnail

저번에 UI Tool Kit 상에서 Coin Effect를 구현해 보았는데, 이번엔 UI Tool Kit을 사용하지 않고 DOTween과 Unitask를 사용해서 다른 방식으로 구현해보자.


일단 전반적인 순서를 구별하면

  1. 랜덤한 위치로 코인들을 생성한다.
  • 이번에는 파티클 시스템을 사용하지 않는다.
  1. 코인들을 Coin Label로 움직인다.

  2. 코인이 올라가는 애니메이션을 재생한다.

위와 같은 단계로 구현해보자.


코인 생성

이번에는 저번과 달리 파티클 시스템이 아니라 게임 오브젝트를 Instantiate해서 코인을 생성할 예정이다. 따라서 Spawn되는 location도 랜덤하게 지정해준다.

일단 동전 프리팹을 하나 만들자. (UI 상의 image로 만들어준다.)


ColletingCoin.cs

void Start()
{
    // Spanw Coins

    for (int i = 0; i < cointAmount; i++) {
        GameObject coinInstance = Instantiate(coinPrefab, coinParent);
        coinInstance.transform.position = spawnLocation.position;
    }
}

간단히 생성해주는 코드만 작성해주고 인스펙터로 연결해준다. 테스트용으로 실행해보면 동전이 잘 생성된다.


생성 위치를 좀 랜덤하게 해주도록 하자.

void Start()
{
    // Spanw some Coins

    for (int i = 0; i < cointAmount; i++) {
        GameObject coinInstance = Instantiate(coinPrefab, coinParent);
        float xPosition = spawnLocation.position.x + Random.Range(minX, maxX);
        float yPosition = spawnLocation.position.x + Random.Range(minX, maxX);
        coinInstance.transform.position = new Vector3(xPosition, yPosition);
    }
}

범위를 적절히 주고 실행하면


void Start()
{
    CollectCoins();
}

[Button()]
private void CollectCoins()
{
    //0. Reset
    for(int i = 0; i < coins.Count; i++) 
    {
        Destroy(coins[i]);
    }
    coins.Clear();
    
    // 1. Spanw some Coins
    for (int i = 0; i < cointAmount; i++)
    {
        GameObject coinInstance = Instantiate(coinPrefab, coinParent);
        float xPosition = spawnLocation.position.x + Random.Range(minX, maxX);
        float yPosition = spawnLocation.position.x + Random.Range(minX, maxX);
        
        coinInstance.transform.position = new Vector3(xPosition, yPosition);
        coins.Add(coinInstance);
    }
}

이제 버튼을 누르면 coins에 coin들이 추가됐다가 다시 누르면 또 파괴되고 새로운 coin들이 추가된다.

[Button]을 통해 인스펙터에 바로 버튼으로 메소드를 실행할 수 있는데 이는, 유니티의 Odin Asset에서 제공하는 기능으로 Odin에 대한 설명은 추후에 작성할 예정이다.

코인 이동

이제 도착지점을 만들어보자. 간단한 이미지를 하나 만들고 거기로 움직이도록 DOTween의 DOMove를 사용한다. (UniTask는 추후에 한 번 다룰 예정이다.) 오브젝트 별로도 시간 차이를 주기위해 await을 반복문 내에서도 실행했다.

await UniTask.Delay(TimeSpan.FromSeconds(1f));
// 2. Move all the coins to the coin Image
for(int i = 0; i < coins.Count; i++) 
{
    coins[i].transform.DOMove(endPosition.position, duration);
    await UniTask.Delay(TimeSpan.FromSeconds(.05f));
}

Coin이 모일 때마다 Coin Image가 Punch 되도록 아래와 같이 작성해준다.

[Button()]
private void ReactToCollectionCoin() {
    endPosition.DOPunchScale(new Vector3(1.5f, 1.5f, 0), 0.1f);
}

UniTask 정리

동전의 DOMove가 끝나고 ReactToCollectionCoin가 실행되도록 해보자.

[Button()]
private async void CollectCoins()
{
	//(생략)

    // 2. Move all the coins to the coin Image
    await MoveCoinsTask();
}

private async UniTask MoveCoinsTask()
{
    List<UniTask> moveCoinTask = new List<UniTask>();
    for (int i = 0; i < coins.Count; i++)
    {
        moveCoinTask.Add(MoveCoinsTask(i));
        await UniTask.Delay(TimeSpan.FromSeconds(.05f));
    }
}

private async UniTask MoveCoinsTask(int i)
{
    await coins[i].transform.DOMove(endPosition.position, duration).SetEase(Ease.InBack).ToUniTask();
    ReactToCollectionCoin();
}

private async UniTask ReactToCollectionCoin() {
    if (coinReactionTween == null) {
        coinReactionTween = endPosition.DOPunchScale(new Vector3(0.5f, 0.5f, 0.5f), 0.1f).SetEase(Ease.InOutElastic);
        await coinReactionTween.ToUniTask();
        coinReactionTween = null;
    }
}

UniTask와 DOTween을 함께 사용하고 싶으면 Project Settings > Player > Scrip Compilation에 UNITASK_DOTWEEN_SUPPORT를 추가해주어야 한다.

코인 Text

텍스트를 적용하기 전에 살짝 UI를 다듬어줬다.

[SerializeField] private TextMeshProUGUI _coinText;

private int coin;

private void SetCoin(int value) {
    coin = value;
    _coinText.text = coin.ToString();
}

private async UniTask MoveCoinsTask(int i)
{
    await coins[i].transform.DOMove(endPosition.position, duration).SetEase(Ease.InBack).ToUniTask();
    await ReactToCollectionCoin();
    SetCoin(coin + 1);
}
Reset 과정에서 SetCoin(0) 해주는거 까먹지 말자.

이러면 MoveCoinTask가 실행되고 ReactToCollectionCoin()가 끝나면 SetCoin이 현재 코인 값 + 1로 실행된다.

이동이 완료된 coin은 제거하기 위해 매개변수로 coinInstance를 넘겨주도록 바꾸고 Remove 해준다.

private async UniTask MoveCoinsTask(GameObject coinInstance)
{
    await coinInstance.transform.DOMove(endPosition.position, duration).SetEase(Ease.InBack).ToUniTask();

    GameObject temp = coinInstance;
    coins.Remove(coinInstance);
    Destroy(temp);

    await ReactToCollectionCoin();
    SetCoin(coin + 1);
}

추가로 동전 생성 시에도 효과를 주기 위해 아래와 같이 수정했다.

private async void CollectCoins()
{
    //0. Reset
    SetCoin(0);
    for (int i = 0; i < coins.Count; i++)
    {
        Destroy(coins[i]);
    }
    coins.Clear();

    List<UniTask> spawnCoinTaskList = new List<UniTask>();
    // 1. Spanw the coin to a specific location with random value
    for (int i = 0; i < cointAmount; i++)
    {
        GameObject coinInstance = Instantiate(coinPrefab, coinParent);
        float xPosition = spawnLocation.position.x + UnityEngine.Random.Range(minX, maxX);
        float yPosition = spawnLocation.position.x + UnityEngine.Random.Range(minX, maxX);

        coinInstance.transform.position = new Vector3(xPosition, yPosition);
        spawnCoinTaskList.Add(coinInstance.transform.DOPunchPosition(new Vector3(0, 30, 0), Random.Range(0, 1f))
            .SetEase(Ease.InOutElastic).ToUniTask());
        coins.Add(coinInstance);
        await UniTask.Delay(TimeSpan.FromSeconds(.01f));
    }

    await UniTask.WhenAll(spawnCoinTaskList);
    await UniTask.Delay(TimeSpan.FromSeconds(1f));
    // 2. Move all the coins to the coin Image
    await MoveCoinsTask();
}

이 때 중요한 점은 DOPunch를 UniTask로 해준 후에 spawnCoinTaskList에 넣어준다. 그 이후에 UniTask.WhenAll을 이용해 모든 spawnCoinTask가 끝나는 것을 대기하도록 했다. 이런식으로 해야 생성 애니메이션이 끝나기도 전에 MoveCoinsTask로 넘어가는 것을 방지할 수 있다.

DOPunchPosition 메서드만 사용하면 해당 애니메이션이 완료될 때까지 대기할 방법이 없다. 따라서 애니메이션이 모두 끝난 후에 수행되어야 하는 후속 작업을 실행할 수 없다.


Scripts

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using DG.Tweening;
using Sirenix.OdinInspector;
using TMPro;
using UnityEngine;
using Random = UnityEngine.Random;

public class CollectionCoin : MonoBehaviour
{
    [SerializeField] private GameObject coinPrefab;
    [SerializeField] private Transform coinParent;
    [SerializeField] private Transform spawnLocation;
    [SerializeField] private Transform endPosition;
    [SerializeField] private TextMeshProUGUI _coinText;

    [SerializeField] private float duration;

    [SerializeField] private int cointAmount;
    [SerializeField] private float minX;
    [SerializeField] private float maxX;
    [SerializeField] private float minY;
    [SerializeField] private float maxY;
    List<GameObject> coins = new List<GameObject>();

    private Tween coinReactionTween;
    private int coin = 0;

    void Start()
    {
        SetCoin(0);
    }

    [Button()]
    private async void CollectCoins()
    {
        //0. Reset
        for (int i = 0; i < coins.Count; i++)
        {
            Destroy(coins[i]);
        }
        coins.Clear();

        List<UniTask> spawnCoinTaskList = new List<UniTask>();
        // 1. Spanw the coin to a specific location with random value
        for (int i = 0; i < cointAmount; i++)
        {
            GameObject coinInstance = Instantiate(coinPrefab, coinParent);
            float xPosition = spawnLocation.position.x + Random.Range(minX, maxX);
            float yPosition = spawnLocation.position.x + Random.Range(minX, maxX);

            coinInstance.transform.position = new Vector3(xPosition, yPosition);
            spawnCoinTaskList.Add(coinInstance.transform.DOPunchPosition(new Vector3(0, 30, 0), Random.Range(0, 1f))
                .SetEase(Ease.InOutElastic).ToUniTask());
            coins.Add(coinInstance);
            await UniTask.Delay(TimeSpan.FromSeconds(.01f));
        }

        await UniTask.WhenAll(spawnCoinTaskList);

        // 2. Move all the coins to the coin Image
        await MoveCoinsTask();
    }

    private void SetCoin(int value) {
        coin = value;
        _coinText.text = coin.ToString();
    }

    private async UniTask MoveCoinsTask()
    {
        List<UniTask> moveCoinTask = new List<UniTask>();
        for (int i = 0; i < coins.Count; i++)
        {
            moveCoinTask.Add(MoveCoinsTask(coins[i]));
            await UniTask.Delay(TimeSpan.FromSeconds(.05f));
        }
    }

    private async UniTask MoveCoinsTask(GameObject coinInstance)
    {
        await coinInstance.transform.DOMove(endPosition.position, duration).SetEase(Ease.InBack).ToUniTask();

        GameObject temp = coinInstance;
        coins.Remove(coinInstance);
        Destroy(temp);

        await ReactToCollectionCoin();
        SetCoin(coin + 1);
    }

    private async UniTask ReactToCollectionCoin() {
        if (coinReactionTween == null) {
            coinReactionTween = endPosition.DOPunchScale(new Vector3(0.5f, 0.5f, 0.5f), 0.1f).SetEase(Ease.InOutElastic);
            await coinReactionTween.ToUniTask();
            coinReactionTween = null;
        }
    }
}

Points

  • UniTask가 코루틴보다 편한 게 체감이 된다. 확실히 공부해보자 (성능 면에서도 좋다고 하니까!
  • 특히나 DOTween과 UniTask를 같이 쓰니깐 엄청 편리하다!
  • Odin Asset 쓰니깐 메소드 실행이 정말 편해진다. 잘 사용해보자.
    - NaughtyAttributes도 한 번 잘 알아보자.
  • UI Tool Kit으로 구현한 것보다는 쉬운데?
    - 이거 나중에 마우스 클릭 위치에서 코인이 원형으로 생성되는 기존의 파티클 시스템 같은 효과를 줘보는 것도 좋을것 같다.
profile
Be Honest, Be Harder, Be Stronger

0개의 댓글