유니티 텍스트 타자 타이핑 효과 제작하기

유니티 텍스트를 사용하다 보면 여러가지 효과를 적용해야 하는 경우가 생깁니다. 특히 대화 시스템을 제작할 때 이러한 효과를 만들어야 하는 경우가 많이 생기죠. 그리고 이러한 효과 중 가장 많이 기본적으로 사용되는 효과가 바로 타자기, 타이핑 효과입니다. 오늘은 이 타이핑 효과를 만드는 방법에 대해 알아보도록 하겠습니다.

유니티 텍스트 타자 타이핑 효과 제작하는 방법

유니티 텍스트 타자 타이핑 효과 적용을 위한 씬 구성하기

코딩을 시작하기에 앞서 타자 타이핑 효과를 적용하기 위한 씬을 하나 생성하여 구성해보도록 하겠습니다.

먼저 테스트 용으로 생성된 프로젝트의 샘플 씬의 카메라 설정을 변경해주도록 하겠습니다. 이는 글자가 더 선명하고 잘 보이게 하기 위함입니다.

씬에 있는 카메라를 선택하신 뒤 우측의 “Environment”“Background Type” 의 설정을 “Solid Color” 로 변경해 주신 뒤 하단의 “Background” 의 색상 파레트에서 검정색을 선택해주세요.

이후 텍스트 오브젝트를 추가해주어야 합니다. 씬에 우측릭 이후 “GameObject -> UI -> Text-TextMeshPro” 를 선택하여 텍스트 메쉬 프로 오브젝트를 추가해주세요.

텍스트 메쉬 프로를 추가하면 텍스트 메쉬 프로 에셋을 임포트 할 것인지 묻는 창이 나타나게 되는데 임포트를 진행하여 주시면 됩니다.

이후 적절한 위치로 텍스트 객체 앵커와 포지션을 설정하여 주세요.

저는 앵커를 좌우로 설정하고 좌우 여백을 0으로 설정하여 위처럼 배치가 진행되었습니다.

유니티 텍스트 타자 타이핑 효과 코드 만들기

먼저 유니티나 코딩할 때 사용하시는 에디터에서 스크립트를 생성해주세요. 스크립트는 반드시 MonoBehaviour을 상속 받아야 합니다.

using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.Events;

namespace Runtime.DialogSystem
{
    public class TextAnimator : MonoBehaviour
    {
        public bool IsAnimationEnd { get; private set; } = true;
        public bool IsPause = false;
        //프린트 속도
        [SerializeField] private float _printSpeed = 0.1f;
        //각 이벤트 당 실행될 유니티 이벤트
        [SerializeField] private UnityEvent _onTextTypingStart;
        [SerializeField] private UnityEvent _onTextTyped;
        [SerializeField] private UnityEvent _onTextTypingEnd;
        //타겟 텍스트 오브젝트
        private TextMeshProUGUI _targetText;
        //출력 목표 문자열
        private string _targetString = "";
        private IEnumerator _textAnimationCoroutine;
    }
}

먼저 필요한 변수들을 추가해 주었습니다.

IsAnimationEnd 변수의 경우 Boolean 타입으로 텍스트 출력 애니메이션이 끝났는지 판별하기 위해 사용합니다. Get/private Set을 추가하는 것으로 외부에서는 읽기만 가능하고 쓰는 것은 내부에서만 가능하도록 만들어 줬습니다.

IsPause 변수 또한 Boolean으로 정지 여부를 표시하도록 하면서 외부에서 이를 조정할 수 있도록 public으로 설정하였습니다.

이후 프린트 속도를 조정할 수 있는 _printSpeed 변수와 인스펙터 상에서 이벤트를 추가하여 말풍선 표시나 텍스트 출력 소리 등을 재생할 수 있도록 UnityEvent들을 추가해 주었습니다.

이후 출력 대상 텍스트 메쉬 프로를 저장하는 _targetText와 출력해야 하는 문자열을 저장하는 _targetString 그리고 출력 코루틴을 저장하는 _textAnimationCoroutine 변수들을 추가해주었습니다.

private void Awake() {
    _targetText = GetComponent<TextMeshProUGUI>();
}

다음으로 Awake 함수에서 _targetText에 TextMeshProUGUI객체를 불러와 할당해주었습니다.

/// <summary>
/// 텍스트 애니메이션을 실행합니다.
/// </summary>
/// <param name="paragraph">텍스트 애니메이션을 실행할 문자열</param>
public void ShowText(string paragraph) {
    //객체 비활성화 상태라면 실행하지 않습니다.
    if(!gameObject.activeSelf) return;
    //텍스트 애니메이션을 중지합니다.
    if (_textAnimationCoroutine != null) {
        StopCoroutine(_textAnimationCoroutine);
    }
    //텍스트를 초기화합니다.
    _targetText.text = string.Empty;
    _targetString = paragraph;
    //텍스트 애니메이션을 실행합니다.
    _textAnimationCoroutine = PlayTextAnimation();
    StartCoroutine(_textAnimationCoroutine);
}

애니메이션을 시작하는 함수입니다. 먼저 객체의 활성화 여부를 판별하여 준 뒤 객체가 활성화 되어 있다면 현재 실행중인 코루틴이 있는지 먼저 판별합니다. 실행중인 코루틴이 있다면 해당 코루틴을 종료시킨 후 텍스트 객체의 텍스트를 비우고 _targetString에 출력 대상이 되는 문자열을 넣어줍니다.

이후 코루틴 변수에 새로운 코루틴 함수를 할당해준 뒤 코루틴을 시작해줍니다. 코루틴 함수는 아래와 같이 되어있습니다.

/// <summary>
/// 텍스트 애니메이션 코루틴 함수입니다.
/// </summary>
private IEnumerator PlayTextAnimation()
{
    //텍스트를 초기화하고 애니메이션을 시작합니다.
    IsAnimationEnd = false;
    //시작시 유니티 이벤트 실행
    _onTextTypingStart?.Invoke();
    //텍스트를 한 글자씩 출력합니다.
    while (_targetText.text.Length < _targetString.Length) {
        //일시정지 상태라면 다음 글자를 출력하지 않습니다.
        if (IsPause) {
            yield return new WaitForSecondsRealtime(_printSpeed);
            continue;
        }
        //다음 글자가 태그라면 태그를 모두 출력합니다.
        string nextContent = _targetString[_targetText.text.Length].ToString();
        if (nextContent == "<") {
            while (nextContent != ">") {
                _targetText.text += _targetString[_targetText.text.Length];
                nextContent = _targetString[_targetText.text.Length].ToString();
            }
        }
        //다음 글자를 출력합니다.
        _targetText.text += nextContent;
        //문자 출력 중 이벤트
        _onTextTyped?.Invoke();
        //프린트 속도만큼 대기합니다.
        yield return new WaitForSecondsRealtime(_printSpeed);
    }
    //애니메이션이 끝났음을 알립니다.
    IsAnimationEnd = true;
    //출력 종료 시 이벤트
    _onTextTypingEnd?.Invoke();
    _textAnimationCoroutine = null;
}

코루틴이 실행되면 먼저 IsAnimationEnd 변수를 false로 설정한 뒤 애니메이션 시작 유니티 이벤트를 실행시켜줍니다. 이후 _targetText에 저장된 문자열의 길이가 _targetString의 길이와 같아질 때까지 문자를 출력합니다.

이때 만약 일시정지 상태라면 아무런 처리 없이 반복문을 반복하고 일시정지 상태가 아니라면 nextContent 지역변수를 설정하여 다음 출력될 문자열을 준비합니다. 이와 같이 nextContent 지역변수를 이용하는 이유는 텍스트 메쉬 프로의 RichText기능의 태그를 이용할 때 태그 내용이 표시되지 않게 하기 위함입니다.

nextContent에 _targetString에서 다음 문자를 추출하여 넣어 준 뒤 해당 문자가 태그의 시작 부분인 “<“라면 태그의 끝 부분인 “>”을 만날 때까지 nextContent에 문자를 계속 붙여줍니다. 해당 처리가 완료되었다면 해당 문자열을 _targetText.text에 더해주고 문자 출력 중 이벤트를 실행해준 뒤 위 과정을 반복합니다.

출력이 완료되었다면 IsAnimationEnd를 True로 설정해주고 출력 종료 시 이벤트를 실행하여 마무리 지어줍니다.

/// <summary>
/// 텍스트 애니메이션을 스킵합니다.
/// 만약 일시정지 상태라면 스킵하지 않습니다.
/// </summary>
public void Skip() {
    //객체 비활성화 상태라면 스킵하지 않습니다.
    if(!gameObject.activeSelf) return;
    //일시정지 상태라면 스킵하지 않습니다.
    if(IsPause) return;
    _targetText.text = _targetString;
}

다음으로 추가해줄 내용은 스킵 기능 입니다. 먼저 객체가 비활성화 상태거나 일시 정지 상태라면 그냥 넘어가고 아니라면 _targetText.text에 _targetString을 넣어 주는 것으로 스킵을 진행할 수 있습니다. 이렇게 하면 코루틴 반복문 또한 문자열 길이가 같아지기 때문에 종료되게 됩니다.

/// <summary>
/// 객체 활성화 시 텍스트 애니메이션을 다시 시작합니다.
/// </summary>
private void OnEnable()
{
    //텍스트 애니메이션을 다시 시작합니다.
    if (_textAnimationCoroutine != null) {
        StartCoroutine(_textAnimationCoroutine);
    }
}
/// <summary>
/// 객체 비활성화 시 텍스트 애니메이션을 중지합니다.
/// </summary>
private void OnDisable()
{
    //텍스트 애니메이션을 중지합니다.
    if (_textAnimationCoroutine != null) {
        StopCoroutine(_textAnimationCoroutine);
    }
}

마지막으로 객체가 활성화/비활성화 될 때 코루틴을 시작/중지 시켜주도록 하였습니다.

위 코드를 모두 통합하여 제작된 코드는 아래와 같습니다.

using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.Events;

namespace Runtime.DialogSystem
{
    public class TextAnimator : MonoBehaviour
    {
        public bool IsAnimationEnd { get; private set; } = true;
        public bool IsPause = false;
        //프린트 속도
        [SerializeField] private float _printSpeed = 0.1f;
        //각 이벤트 당 실행될 유니티 이벤트
        [SerializeField] private UnityEvent _onTextTypingStart;
        [SerializeField] private UnityEvent _onTextTyped;
        [SerializeField] private UnityEvent _onTextTypingEnd;
        //타겟 텍스트 오브젝트
        private TextMeshProUGUI _targetText;
        //출력 목표 문자열
        private string _targetString = "";
        private IEnumerator _textAnimationCoroutine;
        private void Awake() {
            _targetText = GetComponent<TextMeshProUGUI>();
        }
        /// <summary>
        /// 텍스트 애니메이션을 실행합니다.
        /// </summary>
        /// <param name="paragraph">텍스트 애니메이션을 실행할 문자열</param>
        public void ShowText(string paragraph) {
            //객체 비활성화 상태라면 실행하지 않습니다.
            if(!gameObject.activeSelf) return;
            //텍스트 애니메이션을 중지합니다.
            if (_textAnimationCoroutine != null) {
                StopCoroutine(_textAnimationCoroutine);
            }
            //텍스트를 초기화합니다.
            _targetText.text = string.Empty;
            _targetString = paragraph;
            //텍스트 애니메이션을 실행합니다.
            _textAnimationCoroutine = PlayTextAnimation();
            StartCoroutine(_textAnimationCoroutine);
        }
        /// <summary>
        /// 텍스트 애니메이션을 스킵합니다.
        /// 만약 일시정지 상태라면 스킵하지 않습니다.
        /// </summary>
        public void Skip() {
            //객체 비활성화 상태라면 스킵하지 않습니다.
            if(!gameObject.activeSelf) return;
            //일시정지 상태라면 스킵하지 않습니다.
            if(IsPause) return;
            _targetText.text = _targetString;
        }
        /// <summary>
        /// 텍스트 애니메이션 코루틴 함수입니다.
        /// </summary>
        private IEnumerator PlayTextAnimation()
        {
            //텍스트를 초기화하고 애니메이션을 시작합니다.
            IsAnimationEnd = false;
            _onTextTypingStart?.Invoke();
            //텍스트를 한 글자씩 출력합니다.
            while (_targetText.text.Length < _targetString.Length) {
                //일시정지 상태라면 다음 글자를 출력하지 않습니다.
                if (IsPause) {
                    yield return new WaitForSecondsRealtime(_printSpeed);
                    continue;
                }
                //다음 글자가 태그라면 태그를 모두 출력합니다.
                string nextContent = _targetString[_targetText.text.Length].ToString();
                if (nextContent == "<") {
                    while (nextContent != ">") {
                        _targetText.text += _targetString[_targetText.text.Length];
                        nextContent = _targetString[_targetText.text.Length].ToString();
                    }
                }
                //다음 글자를 출력합니다.
                _targetText.text += nextContent;
                _onTextTyped?.Invoke();
                //프린트 속도만큼 대기합니다.
                yield return new WaitForSecondsRealtime(_printSpeed);
            }
            //애니메이션이 끝났음을 알립니다.
            _textAnimationCoroutine = null;
            IsAnimationEnd = true;
            _onTextTypingEnd?.Invoke();
        }
        /// <summary>
        /// 객체 활성화 시 텍스트 애니메이션을 다시 시작합니다.
        /// </summary>
        private void OnEnable()
        {
            //텍스트 애니메이션을 다시 시작합니다.
            if (_textAnimationCoroutine != null) {
                StartCoroutine(_textAnimationCoroutine);
            }
        }
        /// <summary>
        /// 객체 비활성화 시 텍스트 애니메이션을 중지합니다.
        /// </summary>
        private void OnDisable()
        {
            //텍스트 애니메이션을 중지합니다.
            if (_textAnimationCoroutine != null) {
                StopCoroutine(_textAnimationCoroutine);
            }
        }
    }
}

코드가 모두 제작되었으니 이제 해당 코드를 텍스트 객체에 할당해주면 됩니다.

이제 인트펙터에서 해당 컴포넌트에 이벤트를 추가하거나 텍스트 출력 속도를 바꿔줄 수 있게 되었습니다. 이후 디버깅 용 코드를 만들어 테스트를 진행해 보았습니다. 디버깅 용 코드는 아래와 같습니다.

using Runtime.DialogSystem;
using UnityEngine;

namespace Test.DialogSystem
{
    public class TextAnimatorDebugScript : MonoBehaviour
    {
        [SerializeField] private TextAnimator _textAnimator;
        [SerializeField] private string[] _testTexts;

        private int _index = 0;
        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space)) {
                if (!_textAnimator.IsAnimationEnd) {
                    _textAnimator.Skip();
                }
                else {
                    _textAnimator.ShowText(_testTexts[_index]);
                    _index = (_index + 1) % _testTexts.Length;
                }
            }
            if (Input.GetKeyDown(KeyCode.P)) {
                _textAnimator.IsPause = !_textAnimator.IsPause;
            }
        }
    }
}

아래는 작동하는 영상입니다.

영상을 보면 글자가 하나씩 나오면서 태그도 내용 표시 없이 제대로 작동하며 스킵 기능 또한 잘 작동하는 것을 확인할 수 있습니다. 이렇게 직접 유니티 텍스트 타자 타이핑 효과를 제작해 보았습니다.

유니티 텍스트 타자 타이핑 효과 에셋 추천

위와 같이 직접 구현하는 방법도 있는 반면 유니티 에셋 스토어에 존재하는 에셋을 이용하는 방법 또한 존재합니다. 저의 경우 TextAnimator이라는 에셋을 사용하고 있습니다.

해당 에셋의 경우 텍스트 타이핑 애니메이션을 제외하고도 다양한 애니메이션을 제공하고 있습니다. 해당 애니메이션은 태그 방식으로 사용할 수 있기 때문에 문장 단위 뿐만 아니라 단어 단위, 글자 단위로도 사용할 수 있습니다. 때문에 아주 유용하게 사용하실 수 있습니다.

텍스트 애니메이터 홍보영상

위와 같은 효과들을 직접 제작할 수도 있겠지만 이는 텍스트 렌더링 관련을 수정하는 방법을 배우는 과정이 필요합니다.

매우 번거로운 과정이기도 하고 에셋의 가격이 50달러이지만 할인을 자주해서 25달러에 구매가 가능하기 때문에 효용을 따져보았을 때 에셋을 구매해 사용하는 것이 더 나은 선택지일 수도 있습니다.

이와 관련된 링크는 아래에 남겨두도록 하겠습니다. 혹시 관심 있으신 분들은 한번 구매하여 사용하시는 것도 괜찮을 것 같습니다.

Text Animator for Unity | GUI Tools | Unity Asset Store


이렇게 오늘은 유니티 텍스트 타자 타이핑 효과 제작에 관하여 알아보았습니다. 제 개인적인 생각으로 텍스트 타자 타이핑 애니메이션이 유니티에서 기초적인 코루틴을 익히고 사용하는데 좋은 방법이 아닐까 합니다. 그러니 혹시나 에셋을 이용하시려는 독자분들도 한번 제작해 보시는 것도 좋을 것 같습니다.

다른 유니티 글을 찾아보고 싶으신 분들은 아래 링크를 확인해주세요.

유니티 및 C# 튜토리얼

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤