NPC와 대화하는 시스템은 게임의 재미와 몰입감을 높여줄 뿐 아니라, 게임 내의 이야기 진행과 플레이어의 선택에 따라 행동이 결정되는 등 게임 플레이의 다양성을 높여줍니다. 대화 시스템을 구현하고 이를 정리하였습니다.

 

💭 서론

  • 작성한 대화 시스템은 여러 기능을 포함하여 게임을 모두 만든 후에 정리하기위한 목적으로 글을 작성합니다.
  • 하나의 큰 기능을 여러가지로 나누어 작성하였습니다.

 

💬 목차

  • 총 세개의 글로 이루어져 있습니다.
  • 시스템 자체는 하나의 스크립트 파일이지만, 분량을 고려하여 세 개로 나누었습니다.

[📌현재 글] 1. [유니티] 대화 시스템(1) - Overlay Canvas

2. [유니티] 대화 시스템(2) - 선택지

3. [유니티] 대화 시스템(3) - 말풍선

 

📺 예시

  • 특정 조건(영상에서는 NPC에게 상호작용)시 대사를 출력합니다.
  • 대사를 모두 출력한 후 일정 시간이 지나면 서서히 사라지는것을 볼 수 있습니다.

 

✅ 구현(스크립트 작성)

  • 이번 글에서는 오버레이(전역 캔버스) 에서 사용하는 대화 시스템을 다룹니다.
  • 전역 캔버스에 글을 보여주고, 일정 시간이 지나면 사라지는 기능을 정리하였습니다.

 

· QuoteManager.cs

  • 아래 작성된 스크립트는 현재 글의 주제 기능 뿐만아니라 앞으로 다룰 모든 기능이 포함됩니다.
QuoteManager.cs 전체보기
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Text;

/// <summary>
/// 대사 시스템
/// </summary>
public class QuoteManager : Singleton<QuoteManager>
{
    private static bool mIsOverlayQuoteEnable = false;

    /// <summary>
    /// 오버레이 대사가 활성화 되어있는가?
    /// </summary>
    /// <value></value>
    public static bool IsOverlayQuoteEnable
    {
        get
        {
            return mIsOverlayQuoteEnable;
        }
    }

    private static bool mIsSelectQuoteEnable = false;

    /// <summary>
    /// 현재 선택형 대사가 활성화 되어있는가?
    /// </summary>
    /// <value></value>
    public static bool IsSelectQuoteEnable
    {
        get
        {
            return mIsSelectQuoteEnable;
        }
    }

    private readonly static float _MULTIPLY_LIGHT_CHAR = 1.0f;
    private readonly static float _MULTIPLY_MIDDLE_CHAR = 3.0f;
    private readonly static float _MULTIPLY_HEAVY_CHAR = 10.0f;
    private readonly static float _QUOTE_SELECTOR_PARENT_FADE_DURATION = 0.5f;
    

    [Header("스크립터블 오브젝트에서 읽은 데이터 파일")]
    [Header("대사")][SerializeField] QuoteDataReader mReadedQuoteData;
    [Header("선택형 대사")][SerializeField] SelectQuoteDataReader mReadedSelectQuoteData;
    private Dictionary<int, QuoteData> mQuoteDataDictionary = new Dictionary<int, QuoteData>(); //대사를 저장해 둘 딕셔너리
    private Dictionary<int, SelectQuoteData> mSelectQuoteDataDictionary = new Dictionary<int, SelectQuoteData>(); //대사를 저장해 둘 딕셔너리

    // Global Quote
    [Space(30)]
    [Header("Overlay Quote UI 설정")]
    [Header("오버레이 대사 출력 UI")]
    [SerializeField] private QuoteTextGUI mOverlayTextGui;

    // WorldSpace Quote
    [Space(30)]
    [Header("WorldSpace Quote 설정")]
    [Header("월드 대사 출력 UI 프리팹")]    
    [SerializeField] private GameObject mWorldBubblePrefab;

    // Quote Selector
    [Space(30)]
    [Header("Quote Select 설정")]
    [Space(10)]
    [Header("인스턴스 할 QupteSelector 오브젝트")]
    [SerializeField] private GameObject mQuoteSelectorGo;

    [Header("선택형 대사를 담을 스크롤바 오브젝트의 캔버스")]
    [SerializeField] private CanvasGroup mQuoteSelectorCanvasGroup;

    [Header("선택형 대사를 인스턴스 후 위치할 트랜스폼")]
    [SerializeField] private RectTransform mQuoteSelectorPosition;

    // 기타 변수들
    private Coroutine mCoToggleQuoteSelector; //GlobalQuoteSelector 기능을 토글하는 코루틴
    private List<GameObject> mInstantiatedQuoteSelectors = new List<GameObject>(); //런타임 도중 인스턴스된 선택형대사 오브젝트 리스트
    private NPCBase mCurrentGlobalQuoteNPC; //현재 선택되어 활성화된 대사 NPC
    private Dictionary<string, GameObject> mInstatntiatedWorldQuoteBubbles = new Dictionary<string, GameObject>(); // 월드에 인스턴스된 텍스트 버블 오브젝트들

    private void Awake()
    {
        // 초기화시 전역 활성화상태 해제
        QuoteManager.mIsOverlayQuoteEnable = false;
        QuoteManager.mIsSelectQuoteEnable = false;
    }

    private void Start()
    {
        //최초로 데이터 로드
        LoadData();
    }

    /// <summary>
    /// 대사 데이터를 모두 읽어 초기화
    /// </summary>
    private void LoadData()
    {
        //대사
        //비워주기(언어 설정)
        mQuoteDataDictionary.Clear();

        //각각 딕셔너리에 담아준다.
        foreach (QuoteData data in mReadedQuoteData.DataList)
        {
            mQuoteDataDictionary.Add(data.id, data);
        }

        //선택형 대사
        //비워주기(언어 설정)
        mSelectQuoteDataDictionary.Clear();

        //각각 딕셔너리에 담아준다.
        foreach (SelectQuoteData data in mReadedSelectQuoteData.DataList)
        {
            mSelectQuoteDataDictionary.Add(data.id, data);
        }
    }

    #region 일반 대사 기능

    //////////////////////////////////////////////////////////////
    ////////////                                    //////////////   
    ////////////           일반 대사 기능           //////////////
    ////////////                                    //////////////
    //////////////////////////////////////////////////////////////

    /// <summary>
    /// 대사 텍스트를 찾음
    /// </summary>
    /// <param name="quoteID">대사 ID</param>
    /// <returns></returns>
    private string GetQuote(int quoteID)
    {
        return mQuoteDataDictionary[quoteID].quoteText;
    }

    /// <summary>
    /// Overlay 캔버스에서 대사를 출력
    /// </summary>
    /// <param name="quoteID">대사 Id</param>
    /// <param name="caller">호출자</param>
    /// <param name="disableDelay">모든 출력이 끝나고 비활성 대기시간</param>
    /// <param name="delayPerChar">글자별 딜레이 시간</param>
    /// <param name="fadeDuration">비활성 대기시간이 지난 후 사라지는 페이드 시간</param>
    public float DisplayQuoteOverlay(int quoteID, NPCBase? caller, float disableDelay = 3.0f, float delayPerChar = 0.05f, float fadeDuration = 0.5f)
    {
        //선택형 대사들이 이미 있다면, 제거
        ClearQuoteSelectors();

        // 라벨 데이터 가져오기
        QuoteData currentQuoteData = mQuoteDataDictionary[quoteID];

        // 이름 라벨 변경
        mOverlayTextGui.SetCallerNameLabel(currentQuoteData.quoteNPC);

        // 텍스트 가져오기
        string quoteText = GetQuote(quoteID);

        // 라벨에 텍스트 쓰기
        if (mOverlayTextGui.DisplayQuoteCoroutine != null) { StopCoroutine(mOverlayTextGui.DisplayQuoteCoroutine); }
        mOverlayTextGui.DisplayQuoteCoroutine = StartCoroutine(CoDisplayQuote(mOverlayTextGui, quoteText, quoteID, false, caller, disableDelay, delayPerChar, fadeDuration));

        // 현재 대사 매니저는 대사를 출력중임
        QuoteManager.mIsOverlayQuoteEnable = true;

        // 리턴
        return GetQuoteLifetime(quoteText, disableDelay, delayPerChar, fadeDuration);
    }

    /// <summary>
    /// 말풍선 형태의 대사를 출력
    /// </summary>
    /// <param name="keyName">말풍선 오브젝트를 재사용하기위한 고유 이름(key)</param>
    /// <param name="forceText">id기반이 아닌 문자열 기반으로 대사를 출력</param>
    /// <param name="isDestroyWhenDisable">비활성화시 오브젝트 자체를 파괴하는가</param>
    /// <param name="parentTransform">말풍선을 위치시킬 트랜스폼</param>
    /// <param name="offset">말풍선의 위치 오프셋</param>
    /// <param name="disableDelay">모든 출력이 끝나고 비활성 대기시간</param>
    /// <param name="delayPerChar">글자별 딜레이 시간</param>
    /// <param name="fadeDuration">비활성 대기시간이 지난 후 사라지는 페이드 시간</param>
    public void DisplayQuoteBubble(string keyName, string forceText, bool isDestroyWhenDisable, Transform parentTransform, Vector3 offset, float disableDelay = 3.0f, float delayPerChar = 0.05f, float fadeDuration = 0.5f)
    {   
        GameObject? go = null;
        mInstatntiatedWorldQuoteBubbles.TryGetValue(keyName, out go);

        // keyName에 해당하는 프리팹이 없으면? 인스턴스
        if (go == null)
        {
            go = Instantiate(mWorldBubblePrefab, Vector3.zero, Quaternion.identity, parentTransform);
            mInstatntiatedWorldQuoteBubbles.Add(keyName, go);
        }

        QuoteTextGUI textGUI = go.GetComponent<QuoteTextGUI>();
        textGUI.SetKeyName(keyName);

        // 위치 변경
        go.transform.localPosition = Vector3.zero;
        go.transform.GetChild(0).localPosition = offset;

        // 라벨에 텍스트 쓰기
        if (textGUI.DisplayQuoteCoroutine != null) 
            StopCoroutine(textGUI.DisplayQuoteCoroutine);
        textGUI.DisplayQuoteCoroutine = StartCoroutine(CoDisplayQuote(textGUI, forceText, -1, isDestroyWhenDisable, null, disableDelay, delayPerChar, fadeDuration));
    }

    /// <summary>
    /// 말풍선 형태의 대사를 출력
    /// </summary>
    /// <param name="keyName">말풍선 오브젝트를 재사용하기위한 고유 이름(key)</param>
    /// <param name="quoteID">대사 Id</param>
    /// <param name="isDestroyWhenDisable">비활성화시 오브젝트 자체를 파괴하는가</param>
    /// <param name="parentTransform">말풍선을 위치시킬 트랜스폼</param>
    /// <param name="offset">말풍선의 위치 오프셋</param>
    /// <param name="disableDelay">모든 출력이 끝나고 비활성 대기시간</param>
    /// <param name="delayPerChar">글자별 딜레이 시간</param>
    /// <param name="fadeDuration">비활성 대기시간이 지난 후 사라지는 페이드 시간</param>
    public void DisplayQuoteBubble(string keyName, int quoteID, bool isDestroyWhenDisable, Transform parentTransform, Vector3 offset, float disableDelay = 3.0f, float delayPerChar = 0.05f, float fadeDuration = 0.5f)
    {
        DisplayQuoteBubble(keyName, GetQuote(quoteID), isActiveAndEnabled, parentTransform, offset, disableDelay, delayPerChar, fadeDuration);
    }

    #endregion

    #region 선택형 대사 기능

    //////////////////////////////////////////////////////////////
    ////////////                                    //////////////   
    ////////////         선택형 대사 기능           //////////////
    ////////////                                    //////////////
    //////////////////////////////////////////////////////////////

    /// <summary>
    /// 선택형 대사 텍스트를 찾음
    /// </summary>
    /// <param name="quoteID">대사 ID</param>
    /// <returns></returns>
    private string GetSelectQuote(int quoteID)
    {
        return mSelectQuoteDataDictionary[quoteID].quoteText;
    }

    /// <summary>
    /// 선택형 대사를 추가한다.
    /// </summary>
    /// <param name="quoteID">선택형 대사의 ID</param>
    public void AddSelectQuote(int quoteID)
    {
        GlobalQuoteSelector newQuoteSel = Instantiate(mQuoteSelectorGo, Vector3.zero, Quaternion.identity, mQuoteSelectorPosition).GetComponent<GlobalQuoteSelector>();
        mInstantiatedQuoteSelectors.Add(newQuoteSel.gameObject);
        newQuoteSel.InitText(GetSelectQuote(quoteID), quoteID);
    }

    /// <summary>
    /// 현재 인스턴스된 모든 선택형 대사들을 제거한다.
    /// </summary>
    private void ClearQuoteSelectors()
    {
        foreach (GameObject selectorGo in mInstantiatedQuoteSelectors)
            Destroy(selectorGo);

        mInstantiatedQuoteSelectors.Clear();
    }

    public void SelectQuote(int id)
    {
        //선택형 대사를 선택했을때, NPC가 있으면(등록되어있으면) 해당 NPC에게 보냄
        if (mCurrentGlobalQuoteNPC != null) { MessageDispatcher.Instance.DispatchMessage(0, "", mCurrentGlobalQuoteNPC.EntityName, "QuoteSelected", id); }

        //선택형 대사 토글 해제
        ToggleQuoteSelector(false);

        //선택형 대사 활성화 플래그 해제
        mIsSelectQuoteEnable = false;

        //커서 잠그기
        UtilityManager.TryLockCursor();
    }

    /// <summary>
    /// 선택형 대사창을 토글한다.
    /// </summary>
    /// <param name="isEnable">활성화 할것인가?</param>
    /// <param name="caller">해당 선택창에서 대사를 선택하면 메시지를 누구에게 보낼것인가?</param>
    public void ToggleQuoteSelector(bool isEnable, NPCBase caller = null)
    {
        //caller가 할당되었다면 이 선택형 대사는 선택의 결과를 전달하기위해 저장
        //null인경우 사용하지 않거나, 대사를 선택하여 isEnable = false로 만들며 null로 설정
        mCurrentGlobalQuoteNPC = caller;

        if (mCoToggleQuoteSelector != null) { StopCoroutine(mCoToggleQuoteSelector); }
        mCoToggleQuoteSelector = StartCoroutine(CoToggleQuoteSelector(isEnable));
    }

    private IEnumerator CoToggleQuoteSelector(bool isEnable)
    {
        if (isEnable)
        {
            //오브젝트 활성화
            mQuoteSelectorCanvasGroup.gameObject.SetActive(true);

            //선택형 대사 활성화
            mIsSelectQuoteEnable = true;

            //커서 잠금해제
            UtilityManager.UnlockCursor();
        }

        float process = 0f;
        float currentAlpha = mQuoteSelectorCanvasGroup.alpha;

        while (process < 1f)
        {
            process += Time.deltaTime / _QUOTE_SELECTOR_PARENT_FADE_DURATION;
            mQuoteSelectorCanvasGroup.alpha = Mathf.Lerp(currentAlpha, isEnable ? 1.0f : 0f, process);

            yield return null;
        }

        if (!isEnable)
        {
            ClearQuoteSelectors();
            mQuoteSelectorCanvasGroup.gameObject.SetActive(false);
        }
    }

    #endregion

    #region 공통 기능 

    private IEnumerator CoDisplayQuote(QuoteTextGUI textGUI, string quoteText, int quoteID, bool isDestroyWhenDisable, NPCBase? caller, float disableDelay, float delayPerChar, float fadeDuration)
    {
        // 텍스트 라벨 활성화
        textGUI.ToggleLabel(true, fadeDuration, isDestroyWhenDisable);

        //NPC 'caller'은 현재 대사를 출력중
        if (caller is not null) { caller.IsQuotePlaying = true; }

        StringBuilder builder = new StringBuilder();

        WaitForSeconds lightDelay = new WaitForSeconds(delayPerChar * _MULTIPLY_LIGHT_CHAR);
        WaitForSeconds middleDelay = new WaitForSeconds(delayPerChar * _MULTIPLY_MIDDLE_CHAR);
        WaitForSeconds heavyDelay = new WaitForSeconds(delayPerChar * _MULTIPLY_HEAVY_CHAR);

        //대사 텍스트 전체 검사
        for (int i = 0; i < quoteText.Length; ++i)
        {
            //RichText
            if (quoteText[i] == '<')
            {
                for (; i < quoteText.Length; ++i)
                {
                    builder.Append(quoteText[i]);

                    if (quoteText[i] == '>') { ++i; break; }
                }
            }

            //현재 텍스트 삽입
            builder.Append(quoteText[i]);

            if (quoteText[i] == ' ' || quoteText[i] == '.' || quoteText[i] == '!' || quoteText[i] == '?')
            {
                yield return middleDelay;
            }
            else if (quoteText[i] == '\n')
            {
                yield return heavyDelay;
            }
            else
            {
                yield return lightDelay;
            }

            //텍스트 업데이트
            textGUI.UpdateText(builder.ToString());
        }

        // NPC 'caller'가 있는경우, NPC 'caller'에게 메시지 출력이 끝났다고 알린다.
        if (caller is not null && quoteID != -1)
        {
            // 메시지 전송
            MessageDispatcher.Instance.DispatchMessage(0, "", caller.EntityName, "WriteQuoteEnd", quoteID);

            // NPC 'caller'은 현재 대사를 출력을 마침
            caller.IsQuotePlaying = false;
        }

        // 현재 대사 출력이 끝남
        QuoteManager.mIsOverlayQuoteEnable = false;

        // 비활성화 시간을 대기하고 비활성화 호출
        yield return new WaitForSeconds(disableDelay);
        textGUI.ToggleLabel(false, fadeDuration, isDestroyWhenDisable);

        if(textGUI.IsOverlayType)
            mIsOverlayQuoteEnable = false;
    }

    private float GetQuoteLifetime(string quoteText, float disableDelay, float delayPerChar, float fadeDuration)
    {
        float lifeTime = disableDelay + fadeDuration;

        float lightDelay = delayPerChar * _MULTIPLY_LIGHT_CHAR;
        float middleDelay = delayPerChar * _MULTIPLY_MIDDLE_CHAR;
        float heavyDelay = delayPerChar * _MULTIPLY_HEAVY_CHAR;

        //대사 텍스트 전체 검사
        for (int i = 0; i < quoteText.Length; ++i)
        {
            //RichText는 스킵
            if (quoteText[i] == '<')
            {
                for (; i < quoteText.Length; ++i)
                {
                    if (quoteText[i] == '>') { ++i; break; }
                }
            }

            // 각 영역별로 시간 추가
            if (quoteText[i] == ' ' || quoteText[i] == '.' || quoteText[i] == '!' || quoteText[i] == '?')
            {
                lifeTime += middleDelay;
            }
            else if (quoteText[i] == '\n')
            {
                lifeTime += heavyDelay;
            }
            else
            {
                lifeTime += lightDelay;
            }
        }

        return lifeTime;
    }

    public bool TryRemoveBubbleFromDictionary(string keyName)
    {
        return mInstatntiatedWorldQuoteBubbles.Remove(keyName);
    }

    #endregion
}

 

· 오버레이 캔버스

private static bool mIsOverlayQuoteEnable = false;

/// <summary>
/// 오버레이 대사가 활성화 되어있는가?
/// </summary>
/// <value></value>
public static bool IsOverlayQuoteEnable
{
    get
    {
        return mIsOverlayQuoteEnable;
    }
}
  • 현재 오버레이 대사가 출력중인지 확인하기위한 변수입니다.

 

private readonly static float _MULTIPLY_LIGHT_CHAR = 1.0f;
private readonly static float _MULTIPLY_MIDDLE_CHAR = 3.0f;
private readonly static float _MULTIPLY_HEAVY_CHAR = 10.0f;
private readonly static float _QUOTE_SELECTOR_PARENT_FADE_DURATION = 0.5f;
  • 대사에 사용될 글자를 한번에 보여주는것이 아닌 글자를 서서히 보여주기위한 딜레이 시간 오프셋입니다.
  • 글자의 특징에 따라 딜레이 시간이 다릅니다. 예를 들어 '\n'과 같은 개행문자는 HEAVY_CHAR로 계산됩니다.

 

[Header("스크립터블 오브젝트에서 읽은 데이터 파일")]
[Header("대사")][SerializeField] QuoteDataReader mReadedQuoteData;
private Dictionary<int, QuoteData> mQuoteDataDictionary = new Dictionary<int, QuoteData>(); //대사를 저장해 둘 딕셔너리

 

// Global Quote
[Space(30)]
[Header("Overlay Quote UI 설정")]
[Header("오버레이 대사 출력 UI")]
[SerializeField] private QuoteTextGUI mOverlayTextGui;
  • 오버레이에 사용할 대사 출력 컴포넌트를 레퍼런스합니다.
  • QuoteTextGUI 클래스는 잠시 후에 다루겠습니다.

 

/// <summary>
/// 대사 데이터를 모두 읽어 초기화
/// </summary>
private void LoadData()
{
    //대사
    //비워주기(언어 설정)
    mQuoteDataDictionary.Clear();

    //각각 딕셔너리에 담아준다.
    foreach (QuoteData data in mReadedQuoteData.DataList)
    {
        mQuoteDataDictionary.Add(data.id, data);
    }

    ...
}
  • 스크립터블 오브젝트로부터 대사 데이터를 모두 읽어 딕셔너리에 저장합니다.
  • 런타임도중 특정 대사를 id를 통해 접근하는데, 빠르게 접근하기위해 사용합니다.

 

/// <summary>
/// 대사 텍스트를 찾음
/// </summary>
/// <param name="quoteID">대사 ID</param>
/// <returns></returns>
private string GetQuote(int quoteID)
{
    return mQuoteDataDictionary[quoteID].quoteText;
}
  • 딕셔너리로부터 대사를 문자열로 얻습니다.

 

/// <summary>
/// Overlay 캔버스에서 대사를 출력
/// </summary>
/// <param name="quoteID">대사 Id</param>
/// <param name="caller">호출자</param>
/// <param name="disableDelay">모든 출력이 끝나고 비활성 대기시간</param>
/// <param name="delayPerChar">글자별 딜레이 시간</param>
/// <param name="fadeDuration">비활성 대기시간이 지난 후 사라지는 페이드 시간</param>
public float DisplayQuoteOverlay(int quoteID, NPCBase? caller, float disableDelay = 3.0f, float delayPerChar = 0.05f, float fadeDuration = 0.5f)
{
    //선택형 대사들이 이미 있다면, 제거
    ClearQuoteSelectors();

    // 라벨 데이터 가져오기
    QuoteData currentQuoteData = mQuoteDataDictionary[quoteID];

    // 이름 라벨 변경
    mOverlayTextGui.SetCallerNameLabel(currentQuoteData.quoteNPC);

    // 텍스트 가져오기
    string quoteText = GetQuote(quoteID);

    // 라벨에 텍스트 쓰기
    if (mOverlayTextGui.DisplayQuoteCoroutine != null) { StopCoroutine(mOverlayTextGui.DisplayQuoteCoroutine); }
    mOverlayTextGui.DisplayQuoteCoroutine = StartCoroutine(CoDisplayQuote(mOverlayTextGui, quoteText, quoteID, false, caller, disableDelay, delayPerChar, fadeDuration));

    // 현재 대사 매니저는 대사를 출력중임
    QuoteManager.mIsOverlayQuoteEnable = true;

    // 리턴
    return GetQuoteLifetime(quoteText, disableDelay, delayPerChar, fadeDuration);
}
  • 오버레이 캔버스에 대사를 출력합니다.
  • quoteID를 기반으로 대사 텍스트를 가져오고, 기타 설정을 마친 후 코루틴을 사용하여 문자를 차례대로 출력합니다.

 

private IEnumerator CoDisplayQuote(QuoteTextGUI textGUI, string quoteText, int quoteID, bool isDestroyWhenDisable, NPCBase? caller, float disableDelay, float delayPerChar, float fadeDuration) 
{ 
	... 
}
  • DisplayQuoteOverlay를 호출하면 추가로 호출되는 코루틴으로 글자를 특정 규칙에 맞게 차례대로 출력을 해주도록 합니다.
  • 만약 특정 글자가 띄어쓰기, 마침표, 느낌표나 물음표라면 _MULTIPLY_MIDDLE_CHAR만큼 적용하여 딜레이를 겁니다.
  • 개행문자라면 _MULTIPLY_HEAVY_CHAR만큼 적용하여 딜레이를 거는 방식입니다.
  • 모든 문자를 출력했다면 disableDelay만큼 기다렸다가 fadeDuration동안 사라집니다.

 

private float GetQuoteLifetime(string quoteText, float disableDelay, float delayPerChar, float fadeDuration)
{
	...
}
  • 해당 대사가 시작부터 사라지는데까지 소요되는 시간을 계산하여 리턴하는 함수입니다.

 

· QuoteTextGUI.cs

  • 이 클래스는 대사 시스템에서 대사를 페이드 및 페이드아웃하며, 특정 기능을 수행하기위해 구현하였습니다.

 

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

public class QuoteTextGUI : MonoBehaviour
{
    [Header("대사를 한 대상의 이름표 라벨 (nullable)")]
    [SerializeField] private TextMeshProUGUI? mTextNPCLabel;

    [Header("대사를 쓸 텍스트 라벨")]
    [SerializeField] private TextMeshProUGUI mTextLabel;

    [Header("캔버스그룹")]
    [SerializeField] private CanvasGroup mCanvasGroup;

    [Header("카메라를 항상 바라보는가?")]
    [SerializeField] private bool mLookAtCamera = false;

    [field: Header("오버레이 라벨인가?")]
    [field: SerializeField] public bool IsOverlayType { private set; get; } = false;

    private bool mIsDestroyWhenDisable = false; // 비활성화시 자체적으로 파괴되는가?
    private string mKeyName;

    /// <summary>
    /// 텍스트 출력 코루틴
    /// </summary>
    public Coroutine DisplayQuoteCoroutine;

    private Coroutine mCoToggleLabel; //페이드 인, 아웃 전용 코루틴

    private void Awake()
    {
        mCanvasGroup.alpha = 0f;
        gameObject.SetActive(false);
    }

    private void Update()
    {
        if (mLookAtCamera)
            transform.LookAt(Camera.main.transform);
    }

    public void SetKeyName(string keyName)
    {
        mKeyName = keyName;
    }

    /// <summary>
    /// 라벨 초기화
    /// </summary>
    /// <param name="textNpcName"></param>
    public void SetCallerNameLabel(string textNpcName)
    {
        mTextNPCLabel.text = textNpcName;
        mTextLabel.text = "";
    }

    /// <summary>
    /// 라벨의 텍스트를 업데이트
    /// </summary>
    /// <param name="quoteText"></param>
    public void UpdateText(string quoteText)
    {
        mTextLabel.text = quoteText;
    }

    /// <summary>
    /// 라벨을 페이드 효과와 함께 토글
    /// </summary>
    /// <param name="isEnable"></param>
    /// <param name="duration"></param>
    public void ToggleLabel(bool isEnable, float duration, bool isDestroyWhenDisable)
    {
        if (isEnable)
            gameObject.SetActive(true);

        if (isEnable == false && isDestroyWhenDisable)
            mIsDestroyWhenDisable = true;

        if (mCoToggleLabel != null)
            StopCoroutine(mCoToggleLabel);
        mCoToggleLabel = StartCoroutine(CoToggleLabel(isEnable, duration));
    }

    private IEnumerator CoToggleLabel(bool isEnable, float duration)
    {
        float process = 0f;
        float currentAlpha = mCanvasGroup.alpha;

        while (process < 1f)
        {
            process += Time.deltaTime / duration;
            mCanvasGroup.alpha = Mathf.Lerp(currentAlpha, isEnable ? 1.0f : 0f, process);

            yield return null;
        }

        if (isEnable == false)
            gameObject.SetActive(false);
            
        if (mIsDestroyWhenDisable)
        {
            QuoteManager.Instance.TryRemoveBubbleFromDictionary(mKeyName);
            Destroy(gameObject);
        }
    }
}

 

[field: Header("오버레이 라벨인가?")]
[field: SerializeField] public bool IsOverlayType { private set; get; } = false;
  • 현재 이 클래스를 사용하는 대상이 오버레이 라벨인지 설정합니다.
  • 현재 글에서는 오버레이 캔버스 내용을 다루기에 이 값은 참으로 설정되어야합니다.

 

private bool mIsDestroyWhenDisable = false; // 비활성화시 자체적으로 파괴되는가?
  • 텍스트를 표시할 오브젝트가 비활성화가 되는경우 파괴할지 설정합니다.
  • 현재 글에서는 오버레이 캔버스이기때문에 비활성화시 파괴되지않고 비활성화 상태를 유지하다 대사를 다시 출력하게되면 재사용하게됩니다.

 

private void Update()
{
    if (mLookAtCamera)
        transform.LookAt(Camera.main.transform);
}
  • mLookAtCamera가 참인경우 카메라를 항상 바라보도록 합니다.
  • 이 값은 말풍선 대사일경우 참으로 설정되어 사용됩니다.

 

/// <summary>
/// 라벨의 텍스트를 업데이트
/// </summary>
/// <param name="quoteText"></param>
public void SetCallerNameLabel(string quoteText)
{
    mTextLabel.text = quoteText;
}
  • 대사를 호출하는 이름을 표시하는 라벨의 텍스트를 변경합니다.
  • 오버레이는 대사 이름을 표시하기에 이 함수를 호출하게됩니다.

 

/// <summary>
/// 라벨을 페이드 효과와 함께 토글
/// </summary>
/// <param name="isEnable"></param>
/// <param name="duration"></param>
public void ToggleLabel(bool isEnable, float duration, bool isDestroyWhenDisable)
{
    if (isEnable)
        gameObject.SetActive(true);

    if (isEnable == false && isDestroyWhenDisable)
        mIsDestroyWhenDisable = true;

    if (mCoToggleLabel != null)
        StopCoroutine(mCoToggleLabel);
    mCoToggleLabel = StartCoroutine(CoToggleLabel(isEnable, duration));
}
  • 라벨을 토글합니다.
  • 코루틴을 실행하여 알파값을 서서히 변경시킵니다.

 

private IEnumerator CoToggleLabel(bool isEnable, float duration)
{
	...
}
  • 라벨의 알파값을 토글하며 특정 조건에서는 추가 동작을 수행합니다.
  • isEnable이 false일때 mIsDestroyWhenDisable가 참이면 오브젝트 자체를 파괴하는 등 여러 기능을 함께 수행합니다.

 

✅ 사용 방법

· 캔버스 구성

  • 대사가 표현될 라벨과 이름을 표시할 라벨을 생성하고 QuoteTextGUI 컴포넌트에 알맞게 오브젝트들을 할당합니다.
  • 오버레이 캔버스는 IsOverlayType을 True로 설정합니다.

 

· 함수 호출

QuoteManager.Instance.DisplayQuoteOverlay(5, this);
  • QuoteManager의 싱글턴에 접근하여 Overlay 대사 함수를 호출합니다.

 

'unity game modules' 카테고리의 다른 글

[유니티] 대화 시스템(3) - 말풍선  (1) 2023.03.27
[유니티] 대화 시스템(2) - 선택지  (0) 2023.03.27
[유니티] 미니맵 아이콘  (0) 2023.03.25
[유니티] 미니맵  (0) 2023.03.25
[유니티] 키 설정 시스템  (1) 2023.03.25
bonnate