Tales Saga Chronicle Blast 개발일지 4 - 이미지 수정, 효과음, 대화, 일러스트, 애니메이션 등
벌써 12시 반이다. 이것저것 욕심이 나서 계속 구현하다보니 시간이 많이 늦어졌다. 오전엔 좀 게을리 했는데, 오후~저녁 시간 쯤 되니 많이 바빠졌다. 아무튼, 오늘의 목표는 [1. AUTO, MENU 버튼 구현]과 [2. 게임개발부 캐릭터들이 나오며 대사를 나누는 장면 개발]이었다.
버튼 구현은 못했다. 왜냐하면 AUTO는 스토리가 진행되어야 의미가 있었고, MENU 버튼은 생각해보면 블루 아카이브에서는 대화창 숨기기, 로그, 스킵 등 밖에 없었는데 내가 개발하고 있는 이 게임은 이 화면이 게임의 메인 진행 화면이기 때문에 옵션이나, 게임 종료, 필요하다면 세이브 기능까지 넣어야 할 수도 있어서 메뉴가 많이 복잡해질 것 같았다. 그래서 조금 나중에 구현하기로 했다. 2번은 어느정도 됐다. 근데 준비할 것이 상당히 많았다. 먼저 목표한 것 이외에 작업한 것을 쓰겠다.
1. 메인화면 배경 일러스트 변경
캐릭터 일러스트를 찾다가 발견한 건데, 배경화면으로 배포하는 공식 일러스트 중에 화질 높은 게임개발부 배경이 있었다. 그 일러스트로 변경했다.
2. 메인화면 폭발 효과음 변경
블루 아카이브에서는 스토리가 진행될 때 다양한 효과음들이 나온다. 이걸 찾아봤는데 안나와서 직접 녹음했다. 다행히도 배경음악이 안 나오는 때가 많아서 녹음하기 상당히 수월했다. 이 과정에서 찾은 폭발 효과음으로 변경했다.
3. 마우스 커서 변경
마우스 커서가 일반 커서다보니 뭔가 좀 어색했다. 블루 아카이브의 커서로 기본 마우스 커서를 변경했다. 알고보니 64x64사이즈에 빈 공간이 많이 있었던 것 같다. 다행히 유니티에서는 클릭하는 Spot의 좌표를 지정할 수 있는 기능이 있어서 편하게 등록했다.
스토리 진행 관련해서는 고민을 좀 많이 했다.
먼저, 스토리 내용이 길어질 것 같아 StoryManager를 만들었다. 스토리는 쓰기 편하게 하기 위해서 따로 함수를 만들어 눈에 잘 들어오고 편하게 쓸 수 있도록 만들었다. 캐릭터명, 부서명, 내용, 텍스트 속도 4개지를 입력하여 대화를 진행시킬 수 있다. 그리고 대화 기록과 불러오기를 염두에 두고 List에 각각의 정보를 저장하도록 했다.
private void AppendDialog(string characterNameText, string departmentNameText, string dialogText, float textSpeed)
{
characterNameList.Add(characterNameText);
departmentNameList.Add(departmentNameText);
dialogList.Add(dialogText);
textSpeedList.Add(textSpeed); // 텍스트 속도 : (textSpeed) 배
}
// 스토리 내용을 순서대로 저장함
private void InitializeDialog()
{
AppendDialog("???", "", "........", 0.25f); // 0
AppendDialog("???", "", "........!", 0.25f);
AppendDialog("???", "", "......님!", 0.25f);
AppendDialog("???", "", "선생님!", 1f);
AppendDialog("모모이", "게임개발부", "선생님! 왜 멍하니 있어!", 1f);
AppendDialog("미도리", "게임개발부", "저희 게임개발부가 만든, 테일즈 사가 크로니클을 플레이 해보고 싶다고 그러셨잖아요?", 1f); // 5
AppendDialog("유즈", "게임개발부", "여, 역시 <올해의 쿠소게 상>에서 1위를 한 게임은 플레이하고 싶지 않으실지도...", 1f);
AppendDialog("아리스", "게임개발부", "테일즈 사가 크로니클, 진짜 갓겜입니다.", 1f);
AppendDialog("모모이", "게임개발부", "자자, 앉아! 세팅은 벌써 끝났어!", 1f);
}
여기의 InitializeDialog() 는 Start()함수에서 불러와서 초기 데이터를 넣도록 설정했다. 지금 생각해보니 Awake가 나으려나 싶기도 하다.
private void Start()
{
ConnectGameObject(); // GameObject 연결
InitializeDialog(); // 스토리 내용 생성
}
ConnectGameObject()는 여기서 사용할 다양한 게임 오브젝트들을 등록하는 부분이다. 이것저것 사용할 GameObject들이 많다보니, transform.Find 함수를 이용해서 처음에 등록해놓기로 했다.
private void ConnectGameObject()
{
characterName = story.transform.Find("Episode/Dialog/CharacterName").gameObject;
departmentName = story.transform.Find("Episode/Dialog/DepartmentName").gameObject;
dialog = story.transform.Find("Episode/Dialog/DialogText").gameObject;
yuzu = story.transform.Find("Episode/Character/Yuzu").gameObject;
aris = story.transform.Find("Episode/Character/Aris").gameObject;
midori= story.transform.Find("Episode/Character/Midori").gameObject;
momoi = story.transform.Find("Episode/Character/Momoi").gameObject;
}
어제는 게임 시작 버튼을 눌렀을 때 GameManager에서 시작하는 동작을 전부 맡았지만, 일부 부분을 StoryManager로 넘겼다.
// 스토리에 처음 진입했을 때 작동하는 함수
public IEnumerator StoryStart()
{
story.SetActive(true);
story.transform.Find("EpisodeStart").gameObject.SetActive(true);
story.transform.Find("Episode").gameObject.SetActive(false);
StartCoroutine(gameManager.FadeOutMusic());
yield return new WaitForSeconds(6.5f);
story.transform.Find("EpisodeStart/WindowFadeOut").gameObject.SetActive(true);
yield return new WaitForSeconds(2f);
story.transform.Find("Episode").gameObject.SetActive(true);
isCanProgress = true;
StoryProgress();
}
StoryProgress() 가 실질적으로 스토리를 진행시키는 함수이다. 외부에서 버튼을 눌러 접근할 수 있도록 public으로 선언했다. 진행할 수 없을 때는 return으로 되돌려주고, 아니면 코루틴을 실행한다.
// 스토리를 진행시키는 함수
public void StoryProgress()
{
if (!isCanProgress)
{
return;
}
StartCoroutine(StoryAction(storyNum));
}
StoryAction() 코루틴은 스토리가 진행되면서 나타나는 캐릭터 움직임이나 효과음 등을 관리한다. 이런 것들이 종료되면 적절한 타이밍에 텍스트를 출력하도록 설정한다. 단, 이런 것들이 진행되는 도중에 클릭을 막아놓거나 조치를 취해놓지 않아서 현재로써는 빠르게 클릭을 하면 여러 번 실행되는 오류가 있다. 이 부분은 내일 조치를 취할 예정이다.
// 스토리 진행시 텍스트 외에 캐릭터 움직임, 효과음 등을 관리하는 파트
private IEnumerator StoryAction(int num)
{
if(num == 0)
{
audioManager.PlaySFX("Silence");
}
else if(num == 1)
{
audioManager.PlaySFX("Silence");
}
else if(num == 2)
{
audioManager.PlaySFX("Silence");
}
else if (num == 3)
{
dialog.GetComponent<TMP_Text>().fontSize = 60;
audioManager.PlaySFX("Start");
}
else if (num == 4)
{
StartCoroutine(gameManager.FadeInImage(1f, story.transform.Find("Episode/EpisodeBackground").GetComponent<Image>()));
StartCoroutine(gameManager.FadeInImage(1f, momoi.transform.Find("CharacterImage").GetComponent<Image>()));
StartCoroutine(gameManager.FadeInImage(1f, momoi.transform.Find("Halo").GetComponent<Image>()));
audioManager.PlayBGM("PixelTime");
isCanProgress = false;
yield return new WaitForSeconds(1.5f);
dialog.GetComponent<TMP_Text>().fontSize = 42.5f;
momoi.transform.Find("Talking").GetComponent<Animator>().Play("Talking");
momoi.GetComponent<Animator>().Play("Jumping");
audioManager.PlaySFX("Talking");
isCanProgress = true;
}
StoryText();
}
StoryText() 부분은 기본적으로는 텍스트를 한 글자씩 출력시키는 역할을 하지만 이미 한 글자씩 출력되고 있는 경우 해당 코루틴을 중지하고 전체 텍스트를 한 번에 출력한다.
private void StoryText()
{
if (isStoryProgressing)
{
StopCoroutine(coroutineStoryProgress);
SetTextBox(storyNum);
return;
}
coroutineStoryProgress = StartCoroutine(CoroutineSetTextBox(storyNum));
}
CoroutineSetTextBox()가 기본적으로 먼저 실행되는 함수이기 때문에, 추가적인 기능을 넣었다. 이름이 3글자일 때와 2글자일 때 캐릭터가 소속된 부서를 나타내는 글자의 위치가 다르기 때문에 캐릭터 이름의 길이가 변경되면 알맞은 위치에 보여질 수 있도록 위치를 조절한다. 아직 2~3글자의 이름을 가진 캐릭터밖에 나오지 않아서 거기까지만 설정해놓은 상태이다.
// 대화를 한 글자씩 출력해주는 함수(기본)
private IEnumerator CoroutineSetTextBox(int num)
{
if (characterNameList.Count <= num) yield break;
// 이름 길이에 따라 소속 표시 위치 변경
if (characterName.GetComponent<TMP_Text>().text.Length != characterNameList[num].Length) // 이름 길이가 변경되었을 때만 동작
{
if (characterNameList[num].Length == 2)
{
departmentName.GetComponent<RectTransform>().anchoredPosition = new Vector2(-457, departmentName.GetComponent<RectTransform>().anchoredPosition.y);
}
else if (characterNameList[num].Length == 3)
{
departmentName.GetComponent<RectTransform>().anchoredPosition = new Vector2(-405, departmentName.GetComponent<RectTransform>().anchoredPosition.y);
}
}
characterName.GetComponent<TMP_Text>().text = characterNameList[num];
departmentName.GetComponent<TMP_Text>().text = departmentNameList[num];
dialog.GetComponent<TMP_Text>().text = "";
isStoryProgressing = true;
float textSpeed = textSpeedList[num];
for (int i = 0; i < dialogList[num].Length; i++)
{
dialog.GetComponent<TMP_Text>().text += dialogList[num][i];
yield return new WaitForSeconds(0.05f / textSpeed);
}
storyNum++;
isStoryProgressing = false;
}
// 대화 나오고 있을 때 클릭하면 한 번에 출력되도록 해주는 함수
private void SetTextBox(int num)
{
if (characterNameList.Count <= num) return;
characterName.GetComponent<TMP_Text>().text = characterNameList[num];
departmentName.GetComponent<TMP_Text>().text = departmentNameList[num];
dialog.GetComponent<TMP_Text>().text = dialogList[num];
storyNum++;
isStoryProgressing = false;
}
스토리 입력과 관리가 편해진 건 좋았지만, 블루 아카이브는 스토리에 굉장히 충실한 게임이었다. 내가 스토리를 재미있게 본 이유가 있었다. 스토리 진행 도중 캐릭터들의 움직임이나, 표정 변화, 다양한 이펙트, 상황에 어울리는 효과음, 배경음 등 생각보다 신경쓴 것들이 엄청나게 많았다.
먼저 캐릭터 스탠딩 일러스트를 찾고 있었는데, 친구가 링크를 찾아서 보내줘서 받았다. 감사하다. 머리 위에 헤일로가 일체형인 일러스트였는데, 블루 아카이브 스토리에서는 헤일로가 위아래로 움직이기 때문에 헤일로를 분리해서 안보이는 부분을 그리고 헤일로가 움직이는 애니메이션과 캐릭터가 움직이는 애니메이션을 적용했다. 그리고 각각 일러스트에서도 헤일로들을 지웠다. 모모이와 미도리만 했고, 내일 유즈와 아리스를 할 예정이다. 옛날에 애니메이션을 다루는 작업도 잠시 맡았었는데, 그 경험이 떠올라서 꽤나 수월하게 할 수 있었던 것 같다. 당시엔 조금 힘들었지만 지금와서는 굉장히 도움이 됐다.
다음으로는 대화창이었다. 대화창은 배경이 위쪽이 적당히 끊기는 느낌이 있었는데, 그라데이션 툴을 사용해도 느낌이 잘 안 살았다. 몇 번 시도해보니 아래쪽은 그냥 칠하고 위쪽 부분만 그라데이션으로 하니 비슷하게 나왔다. 색감은 약간 다르지만 비슷하게 나온 것 같다. 그리고 회색바와 캐릭터명, 부서명, 대화내용 등의 텍스트 박스를 추가했다.
그 다음은 효과음이었다. 효과음은 찾아봐도 안 나와서 녹음을 했다. 28개를 녹음했는데, "이게 쓰이려나?" 싶어서 녹음하지 않은 것도 있다. 2개는 배경음악이 없는 부분을 못찾았는데, 부끄러운 감정표현의 효과음과 하트 모양의 감정표현의 효과음이다.(2장 마지막 발표 직전 아리스한테서 나옴)
마지막으로 감정 표현 이펙트다. 침묵하는 부분과 대화하는 부분을 따왔는데, !?, ?, 말하는거, 땀, 빛, 화난표시, 음표, 웃는표시, 엉킴, 하트, 부끄러움 등을 추가로 따와야한다. 작업이 생각보다 번거로우니 필요할 때 따와야겠다.
아, 그리고 인트로 부분 없이 스토리 부분을 빨리 테스트하기 위해서 GameManager에 설정을 추가해서 스토리의 원하는 부분부터 시작할 수 있도록 설정도 마쳐놨다. 이외에도 몇 가지 깨달은 점이 있는데... 그새 까먹었다. 짬이 날 때 메모를 좀 해놔야겠다.
아무튼, 결과적으로 오늘까지 완성된 부분은 이렇다.
뒤에 부분은 대사만 있어서 빠르게 넘겼다. 일러스트 화질이 약간 눈에 띄는 것 같은데 이정도면 그래도 나쁘지 않은 것 같다. 블루 아카이브 스토리 부분을 열화판이지만 만들어냈다는 점이 상당히 고무적이다. 근데 스토리를 만드는 부분에 노가다적인 부분이 좀 많아서 스토리를 최대한 간결하게 짜야할 것 같다. 블루 아카이브 시나리오 라이터 채용 부분에 "기획을 한정된 글자수 안에서 이야기로 풀어낼 수 있는 문장력" 이라는 내용이 있는 이유를 이제야 알 것 같다.
아무튼, 내일 할 일은 먼저 여러 번 클릭을 했을 떄 발생하는 오류에 대한 조치다. 이후에 진행상황을 불러올 것인지, 챕터별로 나눌 것인지를 정해야겠다. 만약 챕터별로 나누면 작업이 많이 수월해질 것 같다. 하지만 그에 따른 UI가 추가적으로 필요할 것이다. 어느 쪽이 효율적일지 저울질을 해봐야겠다. 근데 단순히 생각해봐도 아마 챕터별로 나누는 게 훨씬 편할 것 같다. ㅋㅋ. 그리고 아리스와 유즈의 헤일로 작업을 마무리하는 것과, 일부 감정 표현을 추가로 따와서 애니메이션을 적용시키는 것까지는 해야겠다. 방식이 결정되면 메뉴와 오토 기능도 추가하면 좋을 것 같다. 스토리 진행은 20번째까지는 진행이 되면 좋을 것 같다. 글을 쓰다보니 벌써 1시 20분이 되었다. 어서 자러 가야겠다.