본문 바로가기

내일배움캠프/본캠프 TIL

[TIL] 텍스트 떨어지는 연출 구현

이번 시간에는 텍스트를 떨어지게 하는 연출을 코드로 구현하고 로딩 화면을 구현하는 것까지 진행해보겠다.

 

 

 

1. 애니메이터 vs 스크립트

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class FailTextAnim : MonoBehaviour
{
    [SerializeField] private Text[] letters;

    // Start is called before the first frame update
    void Start()
    {
        StartCoroutine(PlayFailAnimation());
    }

    private IEnumerator PlayFailAnimation()
    {
        yield return null;
    }
}

 

떨어지는 연출을 간단하게 애니메이션으로 만들어서 작업할 수도 있지만, 연출을 하는 리소스 대상이 바뀌면 애니메이션도 그때마다 바뀌기 때문에 스크립트로 구현하고 싶었다.

 

스크립트를 통해서 평소 즐겨 쓰는 Coroutine 함수를 통해 구현하고 싶었다. 

 

 

 

2. 애니메이션 연출 구현

 

 

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting.Antlr3.Runtime;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 실패 문구를 역동적으로 애니메이션하는 클래스
/// </summary>
public class FailTextAnim : MonoBehaviour
{
    [SerializeField] private Text[] letters;
    [SerializeField] private float delay = 0.5f;
    [SerializeField] private float fadeDuration = 1.5f;
    [SerializeField] private float moveDistance = 2.0f;

    private void Start()
    {
        StartCoroutine(PlayFailAnimation());
    }

    private IEnumerator PlayFailAnimation()
    {
        yield return null;

        foreach (var letter in letters)
        {
            RectTransform rectTransform = letter.GetComponent<RectTransform>();
            Color orginColor = letter.color;

            letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, 0f);
            rectTransform.anchoredPosition += new Vector2(0f, moveDistance);
            float elapsed = 0f;
            Vector2 startPos = rectTransform.anchoredPosition;
            Vector2 endPos = startPos - new Vector2(0f, moveDistance);

            while (elapsed < fadeDuration)
            {
                elapsed += Time.deltaTime;
                float alpha = Mathf.Clamp01(elapsed / fadeDuration);

                letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, alpha);

                rectTransform.anchoredPosition = Vector2.Lerp(startPos, endPos, alpha);

                yield return null;
            }

            yield return new WaitForSeconds(delay);
        }
    }
}

 

우선 나타날때 페이드 인/아웃 하는 것처럼 투명도 조절을 하고 싶었고 위치 조정도 해야했다.

그렇기 위해서는 다음과 같은 변수가 필요하다

 

delay: 연출되는 글자들 사이 딜레이 시간

fadeDuration: 한 글자가 연출이 완료될때 까지 걸리는 시간

moveDistance: 내려오는 거리

 

RectTransform rectTransform = letter.GetComponent<RectTransform>();

 

우선 위치를 이동하기 위해 오브젝트 컴포넌트인 RectTransform을 가져왔다. RectTransform은 오브젝트의 위치, 크기, pivot 등 다양한 옵션을 프로퍼티로 가지고 있는 컴포넌트이다.

 

RectTransform Rect Transform 컴포넌트는 Transform 컴포넌트의 2D 레이아웃 버전입니다. 트랜스폼은 포인트 하나를 나타내지만, Rect Transform은 UI 요소를 안에 넣을 수 있는 사각형을 나타냅니다. 사각 트랜스폼의 상위 컴포넌트도 사각 트랜스폼인 경우, 하위 사각 트랜스폼이 상위 사각형에 대한 상대적인 포지션과 크기도 지정할 수 있습니다.

 

 

Color orginColor = letter.color;
letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, 0f);

 

투명도를 제어하기 위해 원본 Color 값을 저장하고 Color 값의 alpha 값을 0으로 초기화 하였다. 페이드 연출을 하고 싶기 때문에 RGB값은 굳이 바꾸지 않아도 되었다.

 

 

rectTransform.anchoredPosition += new Vector2(0f, moveDistance);

 

위치 이동하는 방법에는 localPosition, position 등 다양한 속성이 있지만 UI는 anchoredPosition으로 움직이는게 좋다고 GPT에게 답변을 들어서 다음과 같이 움직임을 설정하였다.

 

 

Vector2 startPos = rectTransform.anchoredPosition;
Vector2 endPos = startPos - new Vector2(0f, moveDistance);

while (elapsed < fadeDuration)
{
	elapsed += Time.deltaTime;
	float alpha = Mathf.Clamp01(elapsed / fadeDuration);

	letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, alpha);

	rectTransform.anchoredPosition = Vector2.Lerp(startPos, endPos, alpha);

	yield return null;
}

yield return new WaitForSeconds(delay);

 

실질적인 동작을 수행하는 코드이다. while(elasped < duration) 구문은 Unity 프로젝트를 진행하면서 진행사항을 연출할 때 정말 많이 썼다.

 

 

 

Mathf.Clamp01
값을 0과 1 사이의 범위로 제한하고 값을 반환합니다.
값이 음수이면 0이 반환되고, 값이 1보다 크면 1이 반환됩니다.

 

letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, alpha);

 

Clamp 자체가 범위를 지정하여 그 범위 내에 있는 값으로 반환해주는 기능이다.

따라서 alpha 값은 0~1 중에 있기 때문에 진행사항을 0~1중으로 변환하여 오브젝트를 서서히 보이게 할 수 있다.

 

 

 

rectTransform.anchoredPosition = Vector2.Lerp(startPos, endPos, alpha);
Lerp(float a, float b, float t) 두 지점 사이를 선형 보간합니다.

 

비슷하게 Lerp(선형보간) 함수는 a가 시작점, b가 끝점이라고 하면 t의 위치에 있는 값을 얻을 수 있다.

alpha가 진행사항을 나타내므로 현재 duration 동안 오브젝트가 위치해야할 값을 반환하여 자연스러운 연출이 가능하다.

 

 

 

 

 

3. 한글자 씩 코루틴 실행

 

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting.Antlr3.Runtime;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 실패 문구를 역동적으로 애니메이션하는 클래스
/// </summary>
public class FailTextAnim : MonoBehaviour
{
    [SerializeField] private Text[] letters;
    [SerializeField] private float delay = 0.5f;
    [SerializeField] private float fadeDuration = 1.5f;
    [SerializeField] private float moveDistance = 2.0f;

    private void Start()
    {
        for (int i=0; i<letters.Length; i++)
        {
            RectTransform rectTransform = letters[i].GetComponent<RectTransform>();
            Color orginColor = letters[i].color;

            StartCoroutine(PlayFailAnimation(letters[i], delay, rectTransform, orginColor));
        }
    }

    private IEnumerator PlayFailAnimation(Text letter, float delay, RectTransform rectTransform, Color orginColor)
    {
        yield return new WaitForSeconds(delay);

        letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, 0f);
        rectTransform.anchoredPosition += new Vector2(0f, moveDistance);
        float elapsed = 0f;
        Vector2 startPos = rectTransform.anchoredPosition;
        Vector2 endPos = startPos - new Vector2(0f, moveDistance);

        while (elapsed < fadeDuration)
        {
            elapsed += Time.deltaTime;
            float alpha = Mathf.Clamp01(elapsed / fadeDuration);

            letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, alpha);

            rectTransform.anchoredPosition = Vector2.Lerp(startPos, endPos, alpha);

            yield return null;
        }
    }
}

 

코루틴에서 글자 수대로 진행하는게 아닌 글자마다 코루틴을 실행하도록 변경하였다.

이유는 초기에 선언하는 변수가 반복적으로 선언되는 것이 불필요한 작업이라고 생각해서이다.

 

 

4. 글자마다 랜덤하게 딜레이

 

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting.Antlr3.Runtime;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 실패 문구를 역동적으로 애니메이션하는 클래스
/// </summary>
public class FailTextAnim : MonoBehaviour
{
    [SerializeField] private Text[] letters;
    [SerializeField] private float baseDelay = 0.1f;
    [SerializeField] private float randomDelay = 0.05f;
    [SerializeField] private float fadeDuration = 1.5f;
    [SerializeField] private float moveDistance = 2.0f;

    private void Start()
    {
        float delay = 0f;

        for (int i=0; i<letters.Length; i++)
        {
            RectTransform rectTransform = letters[i].GetComponent<RectTransform>();
            Color orginColor = letters[i].color;

            float temp = baseDelay + Random.Range(0f, randomDelay);
            delay += temp;

            StartCoroutine(PlayFailAnimation(letters[i], i * delay, rectTransform, orginColor));
        }
    }

    private IEnumerator PlayFailAnimation(Text letter, float delay, RectTransform rectTransform, Color orginColor)
    {
        yield return new WaitForSeconds(delay);

        letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, 0f);
        rectTransform.anchoredPosition += new Vector2(0f, moveDistance);
        float elapsed = 0f;
        Vector2 startPos = rectTransform.anchoredPosition;
        Vector2 endPos = startPos - new Vector2(0f, moveDistance);

        while (elapsed < fadeDuration)
        {
            elapsed += Time.deltaTime;
            float alpha = Mathf.Clamp01(elapsed / fadeDuration);

            letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, alpha);

            rectTransform.anchoredPosition = Vector2.Lerp(startPos, endPos, alpha);

            yield return null;
        }
    }
}

 

일정한 간격으로 글자가 연출되면 밋밋하니 연출 사이 딜레이 값을 랜덤하게 바꿔주었다.

 

baseDelay: 기본적으로 걸리는 베이스 시간

randomDelay: 랜덤하게 조정할 delay 값

 

 

 

 

5. 각도 회전 추가 

 

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting.Antlr3.Runtime;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 실패 문구를 역동적으로 애니메이션하는 클래스
/// </summary>
public class FailTextAnim : MonoBehaviour
{
    [SerializeField] private Text[] letters;
    [SerializeField] private float baseDelay = 0.1f;
    [SerializeField] private float randomDelay = 0.05f;
    [SerializeField] private float fadeDuration = 1.5f;
    [SerializeField] private float moveDistance = 2.0f;
    [SerializeField] private float rotationAngle = 20.0f;

    private void Start()
    {
        float delay = 0f;

        for (int i=0; i<letters.Length; i++)
        {
            RectTransform rectTransform = letters[i].GetComponent<RectTransform>();
            Color orginColor = letters[i].color;
            rectTransform.localRotation = Quaternion.identity;

            float temp = baseDelay + Random.Range(0f, randomDelay);
            delay += temp;

            StartCoroutine(PlayFailAnimation(letters[i], i * delay, rectTransform, orginColor));
        }
    }

    private IEnumerator PlayFailAnimation(Text letter, float delay, RectTransform rectTransform, Color orginColor)
    {
        yield return new WaitForSeconds(delay);

        letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, 0f);
        rectTransform.anchoredPosition += new Vector2(0f, moveDistance);
        float elapsed = 0f;
        Vector2 startPos = rectTransform.anchoredPosition;
        Vector2 endPos = startPos - new Vector2(0f, moveDistance);

        while (elapsed < fadeDuration)
        {
            elapsed += Time.deltaTime;
            float alpha = Mathf.Clamp01(elapsed / fadeDuration);

            letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, alpha);

            float rot = Mathf.Lerp(0f, rotationAngle, alpha);
            rectTransform.localRotation = Quaternion.Euler(0f, 0f, rot);

            rectTransform.anchoredPosition = Vector2.Lerp(startPos, endPos, alpha);

            yield return null;
        }
    }
}

 

 

rotationAngle: 회전할 각도

 

 

float rot = Mathf.Lerp(0f, rotationAngle, alpha);
rectTransform.localRotation = Quaternion.Euler(0f, 0f, rot);

 

오브젝트를 회전시키기 위해서는 localRotation을 값을 수정해도 되지만 Quaternion을 많이 사용한다.

 

Quaternion Unity는 Quaternion 클래스를 사용하여 게임 오브젝트의 3차원 방향을 저장하고, 이를 통해 한 방향에서 다른 방향으로의 상대 회전을 설명합니다.

 

 

 

그림으로 설명하면 위와 같다. 한 문자 씩 애니메이션을 실행하므로 delay를 통해 역동적으로 애니메이션을 연출할 수 있다.

 

 

 

6. 글자 이미지 파일로 변경

 

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting.Antlr3.Runtime;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 실패 문구를 역동적으로 애니메이션하는 클래스
/// </summary>
public class FailTextAnim : MonoBehaviour
{
    [SerializeField] private Image[] letters;
    [SerializeField] private float baseDelay = 0.1f;
    [SerializeField] private float randomDelay = 0.05f;
    [SerializeField] private float fadeDuration = 1.5f;
    [SerializeField] private float moveDistance = 2.0f;
    [SerializeField] private float rotationAngle = 20.0f;

    private void Start()
    {
        float delay = 0f;

        for (int i=0; i<letters.Length; i++)
        {
            RectTransform rectTransform = letters[i].GetComponent<RectTransform>();
            Color orginColor = letters[i].color;
            rectTransform.localRotation = Quaternion.identity;

            float temp = baseDelay + Random.Range(0f, randomDelay);
            delay += temp;

            StartCoroutine(PlayFailAnimation(letters[i], i * delay, rectTransform, orginColor));
        }
    }

    private IEnumerator PlayFailAnimation(Image letter, float delay, RectTransform rectTransform, Color orginColor)
    {
        yield return new WaitForSeconds(delay);

        letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, 0f);
        rectTransform.anchoredPosition += new Vector2(0f, moveDistance);
        float elapsed = 0f;
        Vector2 startPos = rectTransform.anchoredPosition;
        Vector2 endPos = startPos - new Vector2(0f, moveDistance);

        while (elapsed < fadeDuration)
        {
            elapsed += Time.deltaTime;
            float alpha = Mathf.Clamp01(elapsed / fadeDuration);

            letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, alpha);

            float rot = Mathf.Lerp(0f, rotationAngle, alpha);
            rectTransform.localRotation = Quaternion.Euler(0f, 0f, rot);

            rectTransform.anchoredPosition = Vector2.Lerp(startPos, endPos, alpha);

            yield return null;
        }
    }
}

 

이미지 파일로 리소스가 변경되어 배열 타입만 Image로 변경하였다.

다행히 Image 속성에도 Color값이 존재하기 때문에 다른 코드는 수정을 하지 않아도 괜찮았다.

 

 

 

 

7. 최종 구현

 

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting.Antlr3.Runtime;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 실패 문구를 역동적으로 애니메이션하는 클래스
/// </summary>
public class FailTextAnim : MonoBehaviour
{
    [Header("Letter Setting")]
    public Image[] letters;

    [Header("Animation Setting")]
    public float baseDelay = 0.1f;
    public float randomDelay = 0.05f;
    public float fadeDuration = 0.8f;
    public float moveDistance = 60f;
    public float rotationAngle = 10.0f;

    // Start is called before the first frame update
    void Start()
    {
        float delay = 0f;

        // 코루틴을 사용하여 한 글자 씩 내려가는 연출 구현
        for(int i=0; i<letters.Length; i++)
        {
            // 처음에는 투명하게 세팅
            Color orginColor = letters[i].color;
            letters[i].color = new Color(orginColor.r, orginColor.g, orginColor.b, 0f);
            RectTransform rectTransform = letters[i].GetComponent<RectTransform>();
            rectTransform.anchoredPosition += new Vector2(0f, moveDistance);
            rectTransform.localRotation = Quaternion.identity;

            float temp = baseDelay + Random.Range(0f, randomDelay);
            delay += temp;

            StartCoroutine(PlayFailAnimation(letters[i], i * delay, rectTransform, orginColor));
        }
    }

    private IEnumerator PlayFailAnimation(Image letter, float delay, RectTransform rectTransform, Color orginColor)
    {
        // 초반 딜레이 시작
        yield return new WaitForSeconds(delay);

        // 시간에 따른 투명도 조정
        float elapsed = 0f;
        Vector2 startPos = rectTransform.anchoredPosition;

        // 기울인 위치에 따라 위치 조정
        Vector2 rotatedOffset = Quaternion.Euler(0f, 0f, rotationAngle) * Vector2.down * moveDistance;
        Vector2 endPos = startPos + rotatedOffset;

        while (elapsed < fadeDuration)
        {
            elapsed += Time.deltaTime;
            float alpha = Mathf.Clamp01(elapsed / fadeDuration);

            // 투명도 증가
            letter.color = new Color(orginColor.r, orginColor.g, orginColor.b, alpha);

            // 글자 회전
            float rot = Mathf.Lerp(0f, rotationAngle, alpha);
            rectTransform.localRotation = Quaternion.Euler(0f, 0f, rot);

            // 위치 조정
            rectTransform.anchoredPosition = Vector2.Lerp(startPos, endPos, alpha);

            yield return null;
        }
    }
}

 

 

// 기울인 위치에 따라 위치 조정
Vector2 rotatedOffset = Quaternion.Euler(0f, 0f, rotationAngle) * Vector2.down * moveDistance;
Vector2 endPos = startPos + rotatedOffset;

 

기울인 값에 따라 위치 조정 코드를 추가하였다.