구현 아이디어?
원하는 텍스트 컴포넌트에서 대사를 출력하고싶을때, 대사 전체를 한번에 보여주는것도 물론 좋지만, 읽는 속도와 비슷하게 하나씩 글자가 나타나면 더욱 좋을 것 같다는 생각이 들었다. 추가로 각 글자마다 소리를 출력하면 더욱 좋은 게임 환경을 제공할 수 있다고 생각하였다.
미리 구성된 문자열을 전달하고 해당 문자열의 각 글자를 일정한 딜레이 시간마다 한글자씩 뽑아서 텍스트 컴포넌트에 대치시키면 만들 수 있을거라 생각했다. '.' ',' 와 같은 특수문자는 우리가 글을 읽을 때 잠깐 쉬어가는 등 지연을 하게 하는데, 이러한것도 구현하여 특정한 문자가 발견되면 지연시간을 늘려서 순간동안 천천히 글자를 뽑아 출력하도록 구현하였다.
//QuotesManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json;
using TMPro;
using UnityEngine.UI;
using System.Text;
using System.Text.RegularExpressions;
public struct QuoteData
{
public int id;
public string text;
public QuoteData(int id, string text)
{
this.id = id;
this.text = text;
}
}
public enum QuoteDirection
{
NO_CHANGE,
LEFT,
RIGHT,
}
public class QuotesManager : MonoBehaviour
{
//DisplayQuote 함수의 기본 매개변수들
const float mcDefaultDuration = -1;
const float mcTextCharDelay = 0.07f;
const float mcFadeSpeedMultiply = 3.0f;
//대사 문자열의 . , ? 와 같은 특수기호마다 특정한 딜레이를 주기 위한 변수들
const float mcMiddleDelay = 2.0f;
const float mcFarDelay = 5.0f;
//기본 매개변수로 대사 호출시 사라지는 시간을 자동으로 계산하는데,
//대사 출력이 완료되면 대사 말풍선이 사라지기까지 시간을 더한다
const float mcDefaultDisappearDuration = 2.0f;
//문자열을 출력하는 텍스트 뒤에 말풍선 이미지가 딤겨져있는 오브젝트들
[SerializeField] private GameObject[] mTextBubble;
//UI가 아닌 말풍선들의 오브젝트에 붙는 캔버스 트랜스폼. 카메라를 바라보기 위해 사용한다
[SerializeField] private RectTransform[] mTextBubbleCanvas;
//다국어 지원을 위해 옵션 데이터 매니저에서 언어 옵션을 가져오기 위한 컴포넌트이다
private OptionDataManager mOptionDataManager;
//대사 텍스트가 출력되는 TMP이다. UI가 아닐 때 사용
private TextMeshPro[] mQuoteObj;
//UI에서 대사 텍스트가 출력되는 TMP
private TextMeshProUGUI[] mQuoteObj_UI;
//텍스트 뒤의 말풍선 이미지
private Image[] mBubbleImage;
//말풍선 이미지가 초기화(씬이 로드)될때 이미지 색상
private Color[] mOriginImageColor;
//대사 텍스트가 초기화(씬이 로드)될때 텍스트의 색상
private Color[] mOriginquoteColor;
//Json 으로부터 파싱한 데이터 세트를 저장하는 딕셔너리. 아이디와 대사 문자열이 저장된다
private Dictionary<int, string> mQuotesDictionary;
//대사가 출력될 때 코루틴을 관리하기 위한 볌수들
private Coroutine[,] mTextCoroutines;
//대사의 문자열에서 각 글자가 출력될 때 소리를 출력하기 위한 사운드 매니저
private SoundManager mSoundManager;
//대사 문자열의 각 문자가 출력될때마다 재생되는 사운드의 타입을 담은 배열
private int[] mQuoteBeepSoundID;
private void Update()
{
for (int i = 0; i < mTextBubbleCanvas.Length; ++i)
{
//카메라를 바라보게 하기 위한 캔버스의 말풍선 알파값이 0보다 크면(활성화 상태라면) 카메라를 바라보게 한다.
//활성화 상태가 아닌 경우에 카메라를 프레임마다 바라보게하면 자원을 낭비한다고 생각한다.
if (mTextBubbleCanvas[i] != null && mTextBubbleCanvas[i].GetChild(0).GetComponent<Image>().color.a > 0)
{
mTextBubbleCanvas[i].LookAt(Camera.main.transform.position);
}
}
}
private void Start()
{
mSoundManager = FindObjectOfType<SoundManager>();
mOptionDataManager = FindObjectOfType<OptionDataManager>();
mQuoteObj = new TextMeshPro[mTextBubble.Length];
mQuoteObj_UI = new TextMeshProUGUI[mTextBubble.Length];
mBubbleImage = new Image[mTextBubble.Length];
mOriginImageColor = new Color[mTextBubble.Length];
mOriginquoteColor = new Color[mTextBubble.Length];
mTextCoroutines = new Coroutine[mTextBubble.Length, 2];
mQuoteBeepSoundID = new int[mTextBubble.Length];
for (int i = 0; i < mTextBubble.Length; ++i)
{
if (mTextBubble[i].GetComponentInChildren<TextMeshPro>() != null)
{
//텍스트 버블의 자식 오브젝트인 TMP를 가져온다.
mQuoteObj[i] = mTextBubble[i].GetComponentInChildren<TextMeshPro>();
mOriginquoteColor[i] = mQuoteObj[i].color;
}
else
{
//UI로 되어있는경우 TMP UI로 가져온다.
mQuoteObj_UI[i] = mTextBubble[i].GetComponentInChildren<TextMeshProUGUI>();
mOriginquoteColor[i] = mQuoteObj_UI[i].color;
}
mBubbleImage[i] = mTextBubble[i].GetComponent<Image>();
mOriginImageColor[i] = mBubbleImage[i].color;
//mTextBubble 오브젝트의 이름을 추출하여 BeepSoundID를 가져온다. 총 앞에서의 두 자리를 검사한다.
string quoteNameTag = mTextBubble[i].name.Substring(0, 2);
//만약에 추출한 이름의 첫번째 문자열이 숫자가 아닌경우 BeepSound를 사용하지 않는다고 판단한다.
//예시) a7는 사용 안하고 7a는 7번째 BeepSound를 사용한다고 구현.
if (quoteNameTag[0] < '0' || quoteNameTag[0] > '9')
{
mQuoteBeepSoundID[i] = -1;
continue;
}
int.TryParse(Regex.Replace(mTextBubble[i].name.Substring(0, 2), @"\D", ""), out mQuoteBeepSoundID[i]);
}
//Json으로부터 데이터를 추출하는 작업을 거친다.
mQuotesDictionary = new Dictionary<int, string>();
//옵션 데이터 중 언어에 띠라 읽는 파일이 다르다.
if (mOptionDataManager._OptionData.languageID == 0)
{
var jsonData = Resources.Load("Data/quotes") as TextAsset;
var jList = JsonConvert.DeserializeObject<List<QuoteData>>(jsonData.text);
foreach (var data in jList)
{
this.mQuotesDictionary.Add(data.id, data.text);
}
}
else
{
var jsonData = Resources.Load("Data/quotes_ENG") as TextAsset;
var jList = JsonConvert.DeserializeObject<List<QuoteData>>(jsonData.text);
foreach (var data in jList)
{
this.mQuotesDictionary.Add(data.id, data.text);
}
}
//씬 내의 등록된 모든 텍스트를 초기화한다.
InitAll();
}
//딕셔너리로부터 데이터를 받아온다.
private string GetQuote(int quoteID)
{
if (mQuotesDictionary.TryGetValue(quoteID, out string quote))
{
return quote;
}
Debug.LogError("No Quote Data. from id: " + quoteID);
return null;
}
//모든 말풍선을 강제로 없앤다. 초기화 작업.
public void InitAll()
{
for (int i = 0; i < mTextBubble.Length; ++i)
{
DisplayQuote(i, 0, -1, 0, 9999, " ");
}
}
//대사 출력 함수를 호출할 때 지속시간을 기본 매개변수 값으로하여 호출하면 문자열을 분석하여 자동으로 지속시간을 계산한다.
private float CheckStringDuration(string quote, float textCharDelay)
{
float duration = 0;
for (int i = 0; ; ++i)
{
if (i > quote.Length - 1) break;
if (quote[i] == '<')
{
for (; i < quote.Length; ++i)
{
if (quote[i] == '>')
{
break;
}
}
}
// . , ?와 같은 문자열이 있으면 mcFarDelay배수만큼 지속시간을 추가로 연장한다.
if ( quote[i] == '.' || quote[i] == '!' || quote[i] == '?')
{
duration += textCharDelay * mcFarDelay;
}
//\n ; : ) -와 같은 문자열이 있으면 mcMiddleDelay배수만큼 지속시간을 추가로 연장한다.
else if (quote[i] == '\n' || quote[i] == ';' || quote[i] == ':' || quote[i] == ')' || quote[i] == '-' || quote[i] == ',')
{
duration += textCharDelay * mcMiddleDelay;
}
else
{
duration += textCharDelay;
}
}
//모든 문자열이 출력된 후에 mcDefaultDisappearDuration 만큼의 여유 지속시간을 추가해준다.
return duration + mcDefaultDisappearDuration;
}
//지정 타겟의 말풍선 위치를 상대좌표로 옮긴다.
public void SetTextPosition(int targetTextID, Vector3 pos)
{
mTextBubble[targetTextID].GetComponent<RectTransform>().localPosition = pos;
}
//지정 타겟의 말풍선을 현재 문자열을 유지한 상태에서 강제로 없애게 힌다.
public void ClearQuote(int targetTextID)
{
if (mTextCoroutines[targetTextID, 0] != null)
{
StopCoroutine(mTextCoroutines[targetTextID, 0]);
}
if (mTextCoroutines[targetTextID, 1] != null)
{
StopCoroutine(mTextCoroutines[targetTextID, 1]);
}
mTextCoroutines[targetTextID, 1] = StartCoroutine(QuoteDisappear(targetTextID, 0, mcFadeSpeedMultiply));
}
//지정 타겟의 말풍선을 중심으로부터 좌우 반전 시키며 좌,우측으로 배치한다.
private void SetQuotePosition(int targetTextIDId, QuoteDirection direction)
{
RectTransform originParentRect = mTextBubble[targetTextIDId].transform.parent.GetComponent<RectTransform>();
RectTransform originChildRect = mTextBubble[targetTextIDId].transform.GetChild(0).GetComponent<RectTransform>();
Vector3 originParentSize = originParentRect.localScale;
Vector3 originChildSize = originChildRect.localScale;
switch (direction)
{
case QuoteDirection.LEFT:
{
Vector3 tempParentScale = originParentSize;
tempParentScale.x = -originParentSize.x;
originParentRect.localScale = tempParentScale;
Vector3 tempChildScale = originChildSize;
tempChildScale.x = -originChildSize.x;
originChildRect.localScale = tempChildScale;
break;
}
case QuoteDirection.RIGHT:
{
Vector3 tempParentScale = originParentSize;
originParentRect.localScale = tempParentScale;
Vector3 tempChildScale = originChildSize;
originChildRect.localScale = tempChildScale;
break;
}
default:
{
break;
}
}
}
//기본이 되는 함수. 지정한 말풍선 ID에 대사를 출력하도록 한다. 리턴은 텍스트가 끝나는 시간을 나타낸다.
public float DisplayQuote(int targetTextID, int quoteID, float duration = mcDefaultDuration, float textCharDelay = mcTextCharDelay, float fadeSpeedMultiply = mcFadeSpeedMultiply, string text = "", QuoteDirection direction = QuoteDirection.NO_CHANGE)
{
if (mBubbleImage[targetTextID] == null) return 0;
mBubbleImage[targetTextID].color = Color.clear;
if (mQuoteObj[targetTextID] != null)
{
mQuoteObj[targetTextID].color = Color.clear;
mQuoteObj[targetTextID].text = "";
}
else
{
mQuoteObj_UI[targetTextID].color = Color.clear;
mQuoteObj_UI[targetTextID].text = "";
}
if (mTextCoroutines[targetTextID, 0] != null)
{
StopCoroutine(mTextCoroutines[targetTextID, 0]);
}
if (mTextCoroutines[targetTextID, 1] != null)
{
StopCoroutine(mTextCoroutines[targetTextID, 1]);
}
SetQuotePosition(targetTextID, direction);
string tempString;
if (text == "")
{
tempString = GetQuote(quoteID);
}
else
{
tempString = text;
}
mTextCoroutines[targetTextID, 0] = StartCoroutine(QuoteAppear(targetTextID, tempString, textCharDelay));
if (duration == mcDefaultDuration)
{
duration = CheckStringDuration(tempString, textCharDelay);
}
mTextCoroutines[targetTextID, 1] = StartCoroutine(QuoteDisappear(targetTextID, duration, fadeSpeedMultiply));
return duration;
}
//지정 타겟의 말풍선 ID와 대사 ID, 방향만 빠르게 호출하는 오버로딩 함수이다.
public float DisplayQuote(int targetTextID, int quoteID, QuoteDirection direction)
{
return DisplayQuote(targetTextID, quoteID, mcDefaultDuration, mcTextCharDelay, mcFadeSpeedMultiply, "", direction);
}
//지정 타겟의 말풍선 ID를 지정하고 InclusiveMinquoteID ~ InclusiveMaxquoteID 사이의 무작위 대사 ID를 출력한다.
public float DisplayQuoteRange(int targetTextID, int InclusiveMinquoteID, int InclusiveMaxquoteID, float duration = mcDefaultDuration, float textCharDelay = mcTextCharDelay, float fadeSpeedMultiply = mcFadeSpeedMultiply, string text = "", QuoteDirection direction = QuoteDirection.NO_CHANGE)
{
return DisplayQuote(targetTextID, Random.Range(InclusiveMinquoteID, InclusiveMaxquoteID + 1), duration, textCharDelay, fadeSpeedMultiply, text, direction);
}
//delayTime만큼 기다린 후 코루틴을 통해 DisplayQuote 함수를 호출한다.
public float DisplayQuoteDelay(int targetTextID, int quoteID, float delayTime, float duration = mcDefaultDuration, float textCharDelay = mcTextCharDelay, float fadeSpeedMultiply = mcFadeSpeedMultiply, string text = "", QuoteDirection direction = QuoteDirection.NO_CHANGE)
{
Framework.TransferDataFormat8<int, int, float, float, float, float, string, QuoteDirection> data = new Framework.TransferDataFormat8<int, int, float, float, float, float, string, QuoteDirection>(targetTextID, quoteID, delayTime, duration, textCharDelay, fadeSpeedMultiply, text, direction);
mTextCoroutines[targetTextID, 0] = StartCoroutine(RunDisplayQuoteDelay(targetTextID, quoteID, delayTime, duration, textCharDelay, fadeSpeedMultiply, text, direction));
string tempString;
if (text == "")
{
tempString = GetQuote(quoteID);
}
else
{
tempString = text;
}
if (duration == mcDefaultDuration)
{
duration = CheckStringDuration(tempString, textCharDelay);
}
return duration + delayTime;
}
//딜레이 시감 후에 대사 출력을 시작한다.
IEnumerator RunDisplayQuoteDelay(int targetTextID, int quoteID, float delayTime, float duration, float textCharDelay, float fadeSpeedMultiply, string text, QuoteDirection direction)
{
yield return new WaitForSeconds(delayTime);
DisplayQuote(targetTextID, quoteID, duration, textCharDelay, fadeSpeedMultiply, text, direction);
}
//대사 출력을 시작하는 코루틴이다. 말풍선을 보여지게하고 텍스트를 textCharDelay초 마다 순차적으로 나오며 기타 연산을 한다.
IEnumerator QuoteAppear(int targetTextID, string currentQuoteData, float textCharDelay)
{
WaitForSeconds delay = new WaitForSeconds(textCharDelay);
int curretIterator = 0;
StringBuilder quote = new StringBuilder();
while (true)
{
if (curretIterator > currentQuoteData.Length - 1)
{
yield break;
}
if (currentQuoteData[curretIterator] == '<')
{
while (true)
{
quote.Append(currentQuoteData[curretIterator]);
++curretIterator;
// >가 닫히지 않은 상태에서 끝까지 간 경우는 비 정상적인 대사 형태이기에 오류를 출력한다.
if (curretIterator > currentQuoteData.Length - 1)
{
Debug.LogError(currentQuoteData + "< Not Closed");
yield break;
}
if (currentQuoteData[curretIterator] == '>')
{
break;
}
}
}
quote.Append(currentQuoteData[curretIterator]);
if (mQuoteObj[targetTextID] != null)
{
mQuoteObj[targetTextID].text = quote.ToString();
}
else
{
mQuoteObj_UI[targetTextID].text = quote.ToString();
}
if (currentQuoteData[curretIterator] != ' ')
{
mSoundManager.PlayQuoteBeep(mQuoteBeepSoundID[targetTextID]);
}
//대사 문자열 중에 . ! ? ; : ) - , 이 있으면 대사 출력을 약간 지연한다.
if (currentQuoteData[curretIterator] == '.' || currentQuoteData[curretIterator] == '!' || currentQuoteData[curretIterator] == '?')
{
yield return new WaitForSeconds(textCharDelay * mcFarDelay);
}
else if (currentQuoteData[curretIterator] == '\n' || currentQuoteData[curretIterator] == ';' || currentQuoteData[curretIterator] == ':' || currentQuoteData[curretIterator] == ')' || currentQuoteData[curretIterator] == '-' || currentQuoteData[curretIterator] == ',')
{
yield return new WaitForSeconds(textCharDelay * mcMiddleDelay);
}
else
{
yield return delay;
}
++curretIterator;
}
}
//텍스트의 지속시간이 지나면 서서히 사라지게하는 코루틴이다.
IEnumerator QuoteDisappear(int targetTextID, float durationSec, float fadeSpeedMultiply)
{
mBubbleImage[targetTextID].color = mOriginImageColor[targetTextID];
if (mQuoteObj[targetTextID] != null)
{
mQuoteObj[targetTextID].color = mOriginquoteColor[targetTextID];
}
else
{
mQuoteObj_UI[targetTextID].color = mOriginquoteColor[targetTextID];
}
yield return new WaitForSeconds(durationSec);
//durationSec을 기다리던 중 Destroy로 제거되는경우 코루틴을 강제로 중지시킨다.
if (mBubbleImage[targetTextID] == null)
{
yield break;
}
Color imageColor = mBubbleImage[targetTextID].color;
Color quoteColor;
if (mQuoteObj[targetTextID] != null)
{
quoteColor = mQuoteObj[targetTextID].color;
}
else
{
quoteColor = mQuoteObj_UI[targetTextID].color;
}
float originImageAlpha = imageColor.a;
float originQuoteAlpha = imageColor.a;
while (true)
{
//실행 도중 Destroy로 제거되는경우 코루틴을 강제로 중지시킨다.
if (mBubbleImage[targetTextID] == null)
{
yield break;
}
imageColor.a = Mathf.Lerp(imageColor.a, 0f, Time.deltaTime * fadeSpeedMultiply);
quoteColor.a = Mathf.Lerp(quoteColor.a, 0f, Time.deltaTime * fadeSpeedMultiply);
mBubbleImage[targetTextID].color = imageColor;
if (mQuoteObj[targetTextID] != null)
{
mQuoteObj[targetTextID].color = quoteColor;
}
else
{
mQuoteObj_UI[targetTextID].color = quoteColor;
}
if (imageColor.a < 0.01f && quoteColor.a < 0.01f)
{
imageColor.a = 0;
quoteColor.a = 0;
mBubbleImage[targetTextID].color = imageColor;
if (mQuoteObj[targetTextID] != null)
{
mQuoteObj[targetTextID].color = quoteColor;
}
else
{
mQuoteObj_UI[targetTextID].color = quoteColor;
}
yield break;
}
yield return null;
}
}
}
|
cs |
using Newtonsoft.Json;
- 효율적인 대사관리와 다국어 지원을 위해 대사 데이터들을 Json 형태로 관리하였다.
- Unity 2021버전까지는 직접 다운로드받아 패키지에 포함시켜서 사용했는데, 2022(2022.1.8f1)에서는 Newtonsoft.Json 패키지가 내장되어있는것을 확인하였다.
using TMPro;
- 폰트지원과 더욱 깔끔한 글자를 출력하기위해 TMP를 사용하였다.
public struct QuoteData
- Json 형식의 대사 데이터에는 대사를 식별할 <int>id와 대사 문자열인 <string>text로 이루어져있다
const float mcDefaultDuration = -1;
const float mcTextCharDelay = 0.07f;
const float mcFadeSpeedMultiply = 3.0f;
- DisplayQuote(...) 함수를 출력할 때 보다 편리하게 사용하기위해 기본 매개변수를 사용한다.
- 관리하기 쉽도록 상수값으로 정의하여 사용한다.
- mcDefaultDuration은 텍스트가 나타난 후 사라지는 지속시간이다
- mcTextCharDelay은 문자열의 각 문자를 출력할 때 딜레이 시간이다.
- mcFadeSpeedMultiply은 텍스트 말풍선이 사라질 때 서서히 사라지는 속도이다.
const float mcMiddleDelay = 2.0f;
const float mcFarDelay = 5.0f;
- , . ? 와 같은 문자는 잠깐의 지연을 주는것이 바람직하다고 판단하여 사용하는 변수이다.
- 해당 문자가 식별되면 문자당 딜레이에서 2.0f배수 ,5.0f배수만큼 딜레이가 길어지도록 구현하였다.
const float mcDefaultDisappearDuration = 2.0f;
- DisplayQuote의 함수를 호출할때 기본 매개변수인 mcDefaultDuration을 사용하여 호출하면 자동으로 문자열을 분석하여 지속시간을 계산한다. 이 때, 계산된 지속시간에 공통의 지연시간을 추가하기위해 이 변수를 사용한다.
[SerializeField] private GameObject[] mTextBubble;
- 문자열을 출력하는 TMP 뒤에 말풍선 역할을 하는 오브젝트
[SerializeField] private RectTransform[] mTextBubbleCanvas;
- UI가 아닌 3D 환경에서는 카메라의 시점에따라 텍스트 말풍선이 잘못된 방향을 가리킬 수 있다.
- 이렇게 되면 글을 제대로 읽을 수 없어 불편함을 겪을 수 있는데, 이를 방지하고자 mTextBubbleCanvas에 담겨있는 RectTransform은 카메라를 정면으로 바라보도록 구현하였다.
private TextMeshPro[] mQuoteObj;
private TextMeshProUGUI[] mQuoteObj_UI;
- TMP를 사용하여 구현할 때, UI와 UI가 아닌 환경에 배치되는 오브젝트들을 하나의 스크립트로 관리하기위해 별도의 타입을 담는 배열로 구성하였다.
좌측에는 Canvas UI에 배치된 TMP는
TextMeshPro - Text (UI)로 되어있는것을 볼 수 있다.
반대로 우측에는 (UI)가 적혀있지 않다. 유니티 자체적으로 서로 다른 컴포넌트를 사용한다.
private Image[] mBubbleImage;
private Color[] mOriginImageColor;
private Color[] mOriginquoteColor;
- 말풍선이 사라질때 서서히 사라지도록 구현한다. 바로 사라져버리는것보다 서서히 사라지게 하는것이 더 자연스럽다고 판단했기 때문이다.
- mBubbleImage의 알파값을 통해 사라지게 만들며, mOriginImageColor와 mOriginquoteColor는 씬이 로드될때 초기 색상을 담아 텍스트 출력이 끝나면 상태를 되돌리기 위해 사용한다.
private Coroutine[,] mTextCoroutines;
- 텍스트를 출력하고 사라지게하는 코루틴을 관리한다.
- 텍스트가 여러개 있기 때문에 2차원 배열로 구성되어있다.
- 같은 텍스트 버블의 코루틴이 중복으로 실행되는것을 방지하고자 사용한다.
private void Start()
- 매니저들을 찾아 알맞게 멤버변수에 초기화를 하고 초기 색상값, 컴포넌트들을 찾고 적절하게 사용 준비를 완료한다.
- for (int i = 0; i < mTextBubble.Length; ++i)에서 텍스트 말풍선의 개수만큼 반복하여 각 배열에 적절히 초기화를 시킨다.
- string quoteNameTag = mTextBubble[i].name.Substring(0, 2);
- int.TryParse(Regex.Replace(mTextBubble[i].name.Substring(0, 2), @"\D", ""), out mQuoteBeepSoundID[i]);
- 텍스트 말풍선 오브젝트의 이름을 기준으로 BeepSound를 출력하도록 한다.
- 이름 문자열의 가장 앞 두 글자를 추출하고 이것을 정규식을 통해 int형으로 형변환을 하고 mQuoteBeepSoundID[]에 저장한다. Player_TextBubble의 앞 0을 추출하게되고, 사운드 매니저의 QuoteBeep에서 0번째인 PlayerBeep을 재생하게된다.
- QuoteBeep ID 하나를 위해 TextBubble에 별도의 스크립트를 작성하여 꺼내쓰기에는 적합하지 않다고 판단하여 오브젝트의 이름을 판단하는 방식으로 구현해보았다.
private void Update()
- UI가 아닌 환경에 배치된 스프라이트와 Text는 단면이기에 카메라의 시점에따라 서로 방향이 맞지 않은경우 얇게 보이거나 아예 보이지 않아 대사 전달이 불가능할 수 있다.
- 이를 방지하기위해 인스펙터에서 미리 지정하는 mTextBubbleCanvas[] 트랜스폼의 방향을 LookAt 함수를 통해 메인 카메라를 바라보게 한다
private string GetQuote(int quoteID)
- mQuotesDictionary로부터 quoteID(id)를 이용하여 quote(text)를 받아오는 함수이다. quoteID(id)에 해당하는 키가 존재하지 않은 상태에서 대사 출력을 한 경우, 이것은 잘못된것이기에 오류를 출력하기로 하였다.
private float CheckStringDuration(string quote, float textCharDelay)
- DisplayQuote()함수를 호출할 때 기본 매개변수인 mcDefaultDuration를 이용하여 함수를 호출하면 CheckStringDuration함수가 호출된다. 문자열을 분석하여 적합한 지속시간을 계산하여 리턴한다.
- . ? \n ; : 와 같은 특수문자는 각 문자의 특정 배수만큼 지속시간을 길게 한다.
public void SetTextPosition(int targetTextID, Vector3 pos)
- targetTextID의 말풍선 위치를 pos로 옮긴다.
- 특정 상황에서는 말풍선의 위치를 의도에 따라 옮길 필요가 있다고 판단하여 구현하였다.
public void ClearQuote(int targetTextID)
- targetTextID의 말풍선을 지속시간에 상관 없이 호출 즉시 사라지게 하는 함수이다.
- 특정 상황에서 말풍선을 제거해야할 필요가 있다고 판단하여 구현하였다.
- 예시로 대사가 출력되던 중 다른 씬으로 로딩하거나, 화면이 검은색이 되어야 하는.. 경우에 대사가 출력되고있으면 이상하기때문에 필요시 즉시 말풍선을 제거해야한다.
private void SetQuotePosition(int targetTextIDId, QuoteDirection direction)
- 대사가 오브젝트의 중심으로부터 출력되는 방향을 설정한다. 기본적으로는 우측으로 설정이 되어있으며, 필요시 좌측에서도 출력할 수 있도록 하는 기능을 구현하였다.
- Allignment가 Enum:Middle가 되어있으면 중심에 있기에 효과가 없으며 Enum:Right 또는 Enum:Left로 되어있어야 효과가 있다.
- originParentRect는 자기 자신의 RectTransform이고 originChildRect는 mTextBubble의 자식 오브젝트의 RectTransform이다.
- x축의 Scale값을 서로 반전시켜 좌/우 반전이 되지만 글자는 정상적으로 출력되도록 구현하였다.
public float DisplayQuote(...)
- 핵심 함수이다. 이 함수를 외부에서 호출할경우 씬에 로드되어있는 targetTextID에 quoteID에 해당하는 문자열 데이터를 출력하는 방식으로 구현하였다.
- int targetTextID 씬에 미리 로드되어있는 mTextBubble[]과 그 자식들에 대한 인덱스 번호이다.
- int quoteID Json 형태로 저장되어있는 데이터를 추출한 딕셔너리의 키값이다.
- float duration = mcDefaultDuration DisplayQuote()가 실행된 후 해당 텍스트 말풍선이 사라지기까지 기다리는 시간이다.
- float textCharDelay = mcTextCharDelay 텍스트가 출력되는 중 각 문자별로 딜레이를 결정하는 변수이다.
- float fadeSpeedMultiply = mcFadeSpeedMultiply duration이 끝나고 텍스트 말풍선이 사라질 때 서서히 사라지는 속도를 결정한다.
- string text = "" 기본 매개변수를 사용하지 않고 별도의 문자열을 인자로 넘기면 Json과 별도로 스크립트 내에서 지정한 문자열을 전달할 수 있도록 한다.
- QuoteDirection direction = QuoteDirection.NO_CHANGE SetQuotePosition()함수를 통해 문자열의 출력 방향을 결정한다.
- mBubbleImage, mQuoteObj.Color와 같은 속성을 초기화한다.
- duration = CheckStringDuration(tempString, textCharDelay);은 mcDefaultDuration 매개변수로 DisplayQuote()가 호출되면 자동으로 duration을 측정하기 위해 호출되는 함수이다.
- QuoteAppear() 코루틴을 실행하여 문자열의 각 문자들을 순차대로 나타나도록 한다.
- QuoteDisappear() 코루틴을 실행하여 duration초 후에 문자열이 사라지도록 한다.
- 리턴하는 값은 해당 대사가 종료되는 (duration)을 리턴한다. 이 값을 이용해 특정 대사가 출력된 후에 다음 이벤트를 실행하면 아주 편리해진다.
public float DisplayQuote(int targetTextID, int quoteID, QuoteDirection direction)
- 편의성을 위해 기본 매개변수들을 사용하고 방향만 설정하여 대사를 출력하게 하는 오버로딩 함수이다.
public float DisplayQuoteRange(..., int InclusiveMinquoteID, int InclusiveMaxquoteID, ...)
- Random.Range(InclusiveMinquoteID, InclusiveMaxquoteID + 1)를 이용해 Min~Max까지의 quoteID를 무작위로 호출하게 하는 추가 기능이다.
public float DisplayQuoteDelay(int targetTextID, int quoteID, float delayTime, ...)
- 코루틴의 WaitForSeconds를 이용하여 delayTime 후에 대사가 출력되도록 하는 추가 기능이다.
- 코루틴에 대사 호출에 관련된 인자들을 전달하기 위해 'TransferDataFormat8'(8개의 데이터가 들어가는 구조체)이라는 구조체를 별도로 만들어 사용하였다.
IEnumerator QuoteAppear(Framework.TransferDataFormat3<int, string, float> data)
- 대사 출력을 시작하는 코루틴이다. 대사 문자열의 문자를 순차적으로 나타나게하는데 초점을 둔 코루틴이다.
- 말풍선 ID, 대사 문자열 전체, 설정된 각 문자 딜레이시간을 받아야하기에 TransferDataFormat3 구조체를 별도로 만들어 사용하였다.
- StringBuilder를 이용하여 currentQuoteData를 순차적으로 읽어 각 문자를 StringBuilder에 넣고 TMP.text를 StringBuilder의 값을 ToString으로 형변환하여 대치시킨다.
- 여기서 핵심은 StringBuilder를 사용해야 유동적인 문자열을 효과적으로 다룰 수 있다는 점이다.
- mSoundManager.PlayQuoteBeep(mQuoteBeepSoundID[targetTextID]);를 통해 각 문자가 출력될 때 BeepSound를 출력한다.
- 특정한 문자 , . ? : 와 같은것이 식별되면 추가 딜레이를 건다.
IEnumerator QuoteDisappear(Framework.TransferDataFormat3<int, float, float> data)
- QuoteAppear와 동시에 실행되며 duration을 wait한 후 서서히 텍스트와 말풍선을 투명하게하여 사라지게 하는 코루틴이다.
- 대상 말풍선ID, duration, 서서히 사라지는 속도 세가지의 변수를 받아와야하기에 TransferDataFormat3를 매개변수로 사용하였다.
- while(true)와 Lerp를 사용해 텍스트와 말풍선 이미지의 알파값을 조절하여 투명하게 만들고 완전히 투명해지면 대사 사라짐 작업이 완료된다.
대사 출력 기능 사용 예시
QuotesManager.cs는 매니저로 구성하여 씬 내에 하나만 존재하도록 구현하였다.
텍스트를 출력하는 기본적인 구성은 UI가 아닌곳에 TMP를 출력하기 위한 캔버스, 그 하위에는 텍스트의 말풍선 그림 역할을 할 이미지를 가지는 TextBubble, 마지막으로 텍스트를 출력할 TMP로 구성되어있다.
QuotesManager의 멤버변수에 미리 TextBubble을 등록하고 DisplayQuote()함수의 targetTextID변수가 위 사진의 Element가 된다. 해당 말풍선 오브젝트에 텍스트를 출력하게한다.
mQuoteManager.DisplayQuote(0, 0, -1, .01f, 3, "...");
...
mQuoteManager.DisplayQuote(0, 20105);
...
float delayTime = mQuoteManager.DisplayQuote(1, 20009, -1);
...
mQuoteManager.DisplayQuoteDelay(0, 20410, delayTime);
|
cs |
외부 함수에서 QuotesManager.cs가 붙어있는 오브젝트의 컴포넌트를 찾고 그곳에서 DisplayQuote()함수를 실행하여 대사를 편리하게 호출한다.
'unity game modules' 카테고리의 다른 글
[유니티] 사운드를 편하게 관리하는 사운드매니저 (0) | 2022.07.19 |
---|---|
[유니티] 대사 관리 및 다국어지원 (0) | 2022.07.18 |
[유니티] 오브젝트와 UI에 gif 이미지 출력 (0) | 2022.07.17 |
[유니티] 번개 효과 (0) | 2022.07.17 |
[유니티] 충돌 연출 및 RigidBody.AddForce()의 이해 (0) | 2022.07.17 |