-
(34) 연출&대사 시스템 총정리Galaxy Ball/2. 싱글플레이 - 스토리모드 2024. 7. 11. 17:59
오랜만에 글을 적어본다. 물론 글을 적고 있지 않을때에도 개발은 꾸준히 해왔으나
도저히 개발에다 글까지 동시에 적을 시간이 없어서 여태 못하다가 이제 다시 성실하게 적어보려고 한다
우선 최종적으로 완성한 연출과 대사 시스템을 총 정리해 보도록 하겠다
가장 먼저 대사 나올 LineBox. 라인 박스는 두가지의 구성요소로 이루어져 있는데
하나는 백그라운드가 될 Panel, 나머지 하나는 실질적인 대사가 적힐 Line이다
그리고 LineBox에는 ShowText라는 스크립트가 부착되어 있다.
using Newtonsoft.Json; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.Events; using System.Collections; public class TextWithDelay { public string text; public float delay; } public class Chat { public int id; public List<TextWithDelay> textWithDelay; } public class ShowText : MonoBehaviour { public TMP_Text chatting; public GameObject ChatBox; public TextAsset JsonFile; private List<Chat> chats; private int currentChatIndex = 0; private int currentTextIndex = 0; private bool isTyping = false; private Coroutine typingCoroutine; private int chatIdToDisplay = -1; public int logTextIndex = 0; // 로그용 변수 추가 public UnityEvent<int> OnChatComplete = new UnityEvent<int>(); void Awake() { ChatBox.SetActive(true); if (JsonFile != null) { try { var json = JsonFile.text; chats = JsonConvert.DeserializeObject<List<Chat>>(json); } catch (JsonReaderException e) { Debug.LogError("Failed to parse JSON: " + e.Message); } } } void OnEnable() { if (chatIdToDisplay != -1) { DisplayChatById(chatIdToDisplay); } } void Update() { if (Input.GetMouseButtonDown(0)) { if (isTyping) { StopCoroutine(typingCoroutine); chatting.text = chats[currentChatIndex].textWithDelay[currentTextIndex].text; isTyping = false; } else { currentTextIndex++; if (currentTextIndex >= chats[currentChatIndex].textWithDelay.Count) { currentTextIndex = 0; if (chatIdToDisplay == -1) { currentChatIndex++; } else { ChatBox.SetActive(false); OnChatComplete.Invoke(chats[currentChatIndex].id); chatIdToDisplay = -1; return; } } DisplayCurrentChat(); } } } private void DisplayCurrentChat() { if (currentChatIndex < chats.Count) { if (currentTextIndex < chats[currentChatIndex].textWithDelay.Count) { var textWithDelay = chats[currentChatIndex].textWithDelay[currentTextIndex]; typingCoroutine = StartCoroutine(Typing(textWithDelay.text, textWithDelay.delay)); // 로그 텍스트 인덱스 증가 logTextIndex++; } else { ChatBox.SetActive(false); OnChatComplete.Invoke(chats[currentChatIndex].id); } } else { ChatBox.SetActive(false); OnChatComplete.Invoke(chats[currentChatIndex - 1].id); } Debug.Log("텍스트 순서 : " + logTextIndex); } IEnumerator Typing(string text, float delay) { isTyping = true; chatting.text = string.Empty; for (int i = 0; i < text.Length; i++) { chatting.text += text[i]; yield return new WaitForSeconds(delay); } isTyping = false; } public void DisplayChatById(int chatId) { chatIdToDisplay = chatId; var chat = chats.Find(c => c.id == chatId); if (chat != null) { currentChatIndex = chats.IndexOf(chat); currentTextIndex = 0; DisplayCurrentChat(); } else { Debug.LogError("Chat with ID " + chatId + " not found."); } } }
여기에서는
1. json 파일을 받아 비직렬화를 한뒤 해당 chatId에 맞는 텍스트를 출력하는 역할을 한다
2. 텍스트가 출력될 때 타자기로 치는 듯한 효과를 내는 연출도 구현한다
public void DisplayChatById(int chatId)
{
chatIdToDisplay = chatId;
var chat = chats.Find(c => c.id == chatId);
if (chat != null)
{
currentChatIndex = chats.IndexOf(chat);
currentTextIndex = 0;
DisplayCurrentChat();
}
else
{
Debug.LogError("Chat with ID " + chatId + " not found.");
}
}자 그럼 누가 이 자리에 chatId를 넣어줄까
using UnityEngine; public class TextManager : MonoBehaviour { public GameObject Linebox; public void GiveMeTextId(int chatId) { if (Linebox != null) { Linebox.SetActive(true); ShowText showTextScript = Linebox.GetComponent<ShowText>(); if (showTextScript != null) { showTextScript.DisplayChatById(chatId); } else { Debug.LogError("ShowText script not found on Linebox."); } } else { Debug.LogError("Linebox is not assigned."); } } }
바로 이 TextManager라는 스크립트에서 담당한다.
정확히는 TextManager도 chatId값을 외부에서 받아온다. 그래서 메서드 이름을 헷갈리지 않도록
GiveMeTextId라고 해둔것도 있다ㅋㅋ
위 스크립트에서는 GiveMeTextId 메서드에서 chatId값을 받자마자 맨 처음 봤던 Linebox를 활성화 시킨뒤
ShowText 스크립트에 있는, 실질적으로 문장을 출력하는데 쓰이는 DisplayChatById에 chatId를 넣어준다
어떻게 보면 바로 뒤에 나올 실질적인 ChatId값을 주는 스크립트와
직접 문장을 출력하는 ShowText 스크립트의 중간다리 역할이다
자 그럼 마지막으로 실질적인 ChatId값을 주는 스크립트를 보자
using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; public class Ch1Story : MonoBehaviour { public GameObject Clearhere; private ShowText showText; private StageGameManager stageGameManager; public Camera mainCamera; public Image FadeIn; public GameObject Stage; public GameObject RemainTime; void Start() { stageGameManager = FindObjectOfType<StageGameManager>(); TextManager textManager = FindObjectOfType<TextManager>(); showText = FindObjectOfType<ShowText>(); if (showText != null) { showText.OnChatComplete.AddListener(OnChatCompleteHandler); } if (stageGameManager.StageClearID == 1) { textManager.GiveMeTextId(1); } if (stageGameManager.StageClearID == 2) { textManager.GiveMeTextId(2); } if (stageGameManager.StageClearID == 5.5) { Destroy(Stage); textManager.GiveMeTextId(3); } if(stageGameManager.StageClearID == 6) { Destroy(Stage); RemainTime.SetActive(true); } } void Update() { showText = FindObjectOfType<ShowText>(); if (showText != null && stageGameManager.StageClearID == 1) { if (showText.logTextIndex < 41) { Stage.SetActive(false); } if (showText.logTextIndex >= 42) { Stage.SetActive(true); Clearhere.gameObject.SetActive(true); } } if (showText != null && stageGameManager.StageClearID == 5.5) { if (showText.logTextIndex == 4) { StartCoroutine(IncreaseCameraSize(mainCamera, 112, 5.5f)); } if (showText.logTextIndex == 8) { ContinuousRandomMovement[] randomMovements = FindObjectsOfType<ContinuousRandomMovement>(); foreach (ContinuousRandomMovement randomMovement in randomMovements) { randomMovement.enabled = false; } } if (showText.logTextIndex == 23) { ContinuousRandomMovement[] randomMovements = FindObjectsOfType<ContinuousRandomMovement>(); foreach (ContinuousRandomMovement randomMovement in randomMovements) { randomMovement.enabled = true; } StartCoroutine(HandleCameraAndFadeIn(mainCamera, 15, 7f)); } } } private void OnChatCompleteHandler(int chatId) { if (chatId == 1) { Debug.Log("ID 1 채팅이 완료되었습니다."); } } IEnumerator IncreaseCameraSize(Camera camera, float targetSize, float duration) { float startSize = camera.orthographicSize; float timeElapsed = 0f; while (timeElapsed < duration) { camera.orthographicSize = Mathf.Lerp(startSize, targetSize, timeElapsed / duration); timeElapsed += Time.deltaTime; yield return null; } camera.orthographicSize = targetSize; } IEnumerator HandleCameraAndFadeIn(Camera camera, float targetSize, float duration) { yield return StartCoroutine(IncreaseCameraSize(camera, targetSize, duration)); RemainTime.SetActive(true); yield return new WaitForSeconds(15f); FadeIn.gameObject.SetActive(true); Color fadeColor = FadeIn.color; float fadeDuration = 2f; // 페이드 인 시간 설정 float timeElapsed = 0f; while (timeElapsed < fadeDuration) { fadeColor.a = Mathf.Lerp(0, 1, timeElapsed / fadeDuration); FadeIn.color = fadeColor; timeElapsed += Time.deltaTime; yield return null; } fadeColor.a = 1; FadeIn.color = fadeColor; yield return new WaitForSeconds(3f); stageGameManager.StageClearID += 0.5f; stageGameManager.SaveStageClearID(); SceneManager.LoadScene("Prologue 2"); } }
바로 스토리 연출과 대사를 담당하는 Ch1Story 스크립트이다
이름이 Ch1Story인 이유는 이 게임의 스토리가 챕터별로 나뉘어져 있는데,
그중 챕터1에 해당하는 연출과 대사정보를 담고 있는 정말 중요한 핵심 스크립트이다.
여기서 모든 연출, 대사가 나오는 타이밍, 대사량, 심지어 몇번째 대사에 무슨 연출이 나올지도 구현해놓았다
우선은 다른거 다 놓고 이것만 보자
if (stageGameManager.StageClearID == 1)
{
textManager.GiveMeTextId(1);
}
if (stageGameManager.StageClearID == 2)
{
textManager.GiveMeTextId(2);
}
if (stageGameManager.StageClearID == 5.5)
{
Destroy(Stage);
textManager.GiveMeTextId(3);
}만약 stageGameManager.StageClearID가 1이라면
방금 보았던 GiveMeTextId에 1의 값을 넣어준다. 그럼 이 "1"이라는 값이 TextManager에서는
chatId = 1이 되고 이게 또 ShowText로 넘어가 비직렬화한 Json 파일에서 ID = 1에 해당하는 문장을 출력하게 된다
이런식으로 상황에 따라, 정확히는 StageClearID 값에 따라 대사를 출력하는 타이밍을 조절한다
<3줄 요약>
1. Ch1Story에서 StageClearID 값에 따라 TextManager의 GiveMeTextId 메서드에게 ChatId를 넘겨줌
2. TextManager에서 값을 받으면 LineBox를 활성화한뒤 ShowText의 DisplayChatById 메서드에 chatId를 넣어준다
3. json파일을 비직렬화 한 ShowText는 건네받은 chatId값과 일치하는 ID에 해당하는 문장을 출력해줌
마지막으로 특정 대사 뒤에 특정 연출이 나오는것도 구현해보았다
void Update()
{
showText = FindObjectOfType<ShowText>();
if (showText != null && stageGameManager.StageClearID == 1)
{
if (showText.logTextIndex < 41)
{
Stage.SetActive(false);
}
if (showText.logTextIndex >= 42)
{
Stage.SetActive(true);
Clearhere.gameObject.SetActive(true);
}
}이걸로 예를 들어보겠다. 당연히 Update 안에 넣어주었고,
만약 ShowText가 활성화 된 상태에서 StageClearID가 1인 조건 속에
만약 showText.logTextIndex 가 41 이하라면 Stage 오브젝트를 쭉 비활성화하다가
42와 같거나 넘어서는 순간 Stage를 활성화한뒤, Clearhere이라는 오브젝트도 함께 활성화해주는 연출이다
여기서 showText.logTextIndex란, 대사의 순서이다
[ { "id":1, "textWithDelay":[ { "text":"안녕?", "delay":0.05 }, { "text":"난 이 게임을 만든 개발자야", "delay":0.05 }, { "text":"......", "delay":0.12 }, { "text":"...왜? 생각한거랑 좀 달라?", "delay":0.05 }, {
만약 이렇게 json파일이 있다고 치면, "...왜? 생각한거랑 좀 달라?" 에 해당하는 showText.logTextIndex는 4번인것이다
이름만 봐도 짐작이 가겠지만 정확히 말해 logTextIndex는 int 타입 로그형 변수이다
private void DisplayCurrentChat()
{
if (currentChatIndex < chats.Count)
{
if (currentTextIndex < chats[currentChatIndex].textWithDelay.Count)
{
var textWithDelay = chats[currentChatIndex].textWithDelay[currentTextIndex];
typingCoroutine = StartCoroutine(Typing(textWithDelay.text, textWithDelay.delay));
// 로그 텍스트 인덱스 증가
logTextIndex++;
}
else
{
ChatBox.SetActive(false);
OnChatComplete.Invoke(chats[currentChatIndex].id);
}
}
else
{
ChatBox.SetActive(false);
OnChatComplete.Invoke(chats[currentChatIndex - 1].id);
}
Debug.Log("텍스트 순서 : " + logTextIndex);
}실제로 ShowText에서 동작하는 logTextIndex 변수이다. 대사가 하나 넘어갈때마다 1이 더해지는것을 확인할 수 있다
즉, 현재 출력중인 대사의 순서를 showText.logTextIndex라는 변수안에 넣어준뒤
이것을 이용하여 때에 따라 다른 연출을 구현하는것이다
그럼 이제 이런 연출이 가능해지는것이다
'Galaxy Ball > 2. 싱글플레이 - 스토리모드' 카테고리의 다른 글
(37) 저장 / 불러오기 구현 (0) 2024.07.28 (35) 새로운 적 유닛 정보창 (0) 2024.07.12 (33) 챕터1 스토리 (0) 2024.06.26 (32) 연출 시스템 구축 & 챕터1 맵 최종완성 (0) 2024.06.24 (31) 프롤로그 제작 #1 (0) 2024.06.19