개발/개발일지

Tales Saga Chronicle Blast 개발일지 2 - 인트로 화면, 메인 화면

메피카타츠 2023. 1. 12. 00:38

오늘 목표한 내용은 인트로 화면에 추가할 것을 찾고, 메인 화면에 사용할 그림을 찾고, 게임 타이틀 만드는 방법을 알아내는 것이었다. 다행히도 셋 다 해냈다!

 

1. 인트로 화면에 추가한 것

먼저 인트로 화면에는, 블루 아카이브 공식 커뮤니티에 아로나 SD 움짤이 있어서 이걸 활용하기로 했다.

이미지를 프레임 단위로 쪼개서 Animation을 활용해 움직이게 만들었다.

이후 아로나를 중앙 상단에, 안내 텍스트를 중앙 하단에 배치했다.

연출은 단순히 투명도가 0이되는 것이다보니 혹시 백그라운드에서 애니메이션이 메모리를 잡아먹을까봐 투명도가 0이 된 이후에는 active가 false가 되도록 해줬다.

 

 

2. 메인 화면에 사용할 그림

메인 화면에 사용할 그림은 게임개발부 단편 애니메이션인 beautiful day dreamer의 엔딩에 등장하는 일러스트를 활용하기로 했다. 원래는 저기에 크레딧이 나오는데, 타이틀이랑 메뉴가 저쪽에 있으면 적합할 것 같다고 느꼈다. 그래서 캐릭터 부분의 누끼를 땄다. 배경은 저 파란색 배경을 활용할까 싶었는데, 원래는 뒤에서 튀어나오는 오브젝트들이 있지만 저 부분까지 그림을 따와서 만들기는 너무 어려울 것 같았다. 그러다보니 배경이 너무 단조로워져서 게임개발부 부실 배경을 활용하기로 했다.

beautiful day dreamer의 엔딩에서 캐릭터들이 위아래로 움직이는 연출이 있어서 애니메이션으로 해당 부분도 간단히 적용했다.

 

 

 

3. 게임 타이틀

무료 온라인 게임 로고 만들기 | Adobe Express

 

무료 온라인 게임 로고 만들기 | Adobe Express

Adobe Express로 쉽게 게임 로고를 만들 수 있습니다. 게임 로고 작성기로 멋진 게임 로고를 만들어 다운로드하고 모든 플랫폼에 공유하세요.

www.adobe.com

게임 타이틀 제작 관련해서 검색해보니 처음 나온 게 이 사이트였다. 요즘 굉장히 시대가 좋아진 것 같다. 무료로 이런 것도 만들어준다니 놀라울 일이다. 원하는 로고와 키워드, 아이콘을 고르면 자동으로 로고를 생성해준다.

그렇게 해서 나온 게 이 두 개다. Blast는 따로 만드는 게 좋아보여서 따로 만들었다. 그럴듯해 보이긴 했는데...

아쉽게도 게임의 느낌보다는 상표의 느낌이 강했다. 직접 게임에 넣어보니 많이 아쉬운 느낌이 들었다.

그래서 포토샵을 활용해서 글자를 꾸미는 방법을 유튜브에서 찾아봤다.

아래 사진은 수정 이전인데 색을 빨강+파랑으로 그라데이션을 줬었다.

조금 아쉬운 것 같긴 했지만 이렇게 적용을 했는데, 친구한테 보여주니 보노보노PPT에 나올 것 같은 글씨체라면서 글씨체를 바꾸라고 했다. 아, 나의 미적 감각이여.

찾다보니 훨씬 게임개발부스러운 좋은 글씨체가 보여서 설정도 조금 만져서 훨씬 좋은 느낌을 만들어냈다.

역시 사람은 피드백을 받아야 발전한다.

Blast 로고는 Fade In 뿐만 아니라 폭발하는 것처럼 Scale을 0에서부터 키우는 애니메이션도 추가했다.

 

 

+ 4. 메뉴

게임 시작, 옵션, 종료 버튼을 간단히 만들어서 추가했다. ButtonManager를 추가해서 OnClick만 연결해놓은 상태이고, 종료를 제외한 기능 구현은 내일 할 예정이다.

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

public class ButtonManager : MonoBehaviour
{
    public void OnClickGameStartBtn()
    {
        Debug.Log("시작하기");
        GameObject.Find("Canvas/Main/BtnTemp").GetComponent<Button>().Select();
    }
    public void OnClickOptionBtn()
    {
        Debug.Log("옵션");
        GameObject.Find("Canvas/Main/BtnTemp").GetComponent<Button>().Select();
    }
    public void OnClickGameQuitBtn()
    {
        Application.Quit();
        GameObject.Find("Canvas/Main/BtnTemp").GetComponent<Button>().Select();
    }
}

Select() 부분은 버튼을 누르면 해당 버튼이 선택된 것으로 간주되어 갖다대도 하이라이팅이 안되어서 다른 버튼을 선택하도록 했다. 근데 버튼을 눌렀다가 다른 위치로 이동한 다음에 커서를 떼면 적용되지 않아서, 마우스 커서를 뗄 때를 감지해서까지 이 부분을 적용해야하나? 하는 생각이 들었다. 어떻게 할지는 아직 고민이다.

 

using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.UIElements;
using Image = UnityEngine.UI.Image;

public class GameManager : MonoBehaviour
{
    [SerializeField] AudioManager audioManager;

    [SerializeField] private float timeMultiplier;
    [SerializeField] private TMP_Text guideText;

    private IEnumerator FadeInText(float timeSpeed, TMP_Text text)
    {
        text.color = new Color(text.color.r, text.color.g, text.color.b, 0);
        while (text.color.a < 1.0f)
        {
            text.color = new Color(text.color.r, text.color.g, text.color.b, text.color.a + (Time.deltaTime * timeSpeed));
            yield return null;
        }
        yield return new WaitForSeconds(1f);
    }

    private IEnumerator FadeOutText(float timeSpeed, TMP_Text text)
    {
        text.color = new Color(text.color.r, text.color.g, text.color.b, 1);
        while (text.color.a > 0.0f)
        {
            text.color = new Color(text.color.r, text.color.g, text.color.b, text.color.a - (Time.deltaTime * timeSpeed));
            yield return null;
        }
    }

    private IEnumerator Opening()
    {
        StartCoroutine(FadeInText(timeMultiplier, guideText));
        StartCoroutine(FadeInImage(timeMultiplier, GameObject.Find("Canvas/Main/Arona").GetComponent<Image>()));
        yield return new WaitForSeconds(4);
        StartCoroutine(FadeOutText(timeMultiplier, guideText));
        StartCoroutine(FadeOutImage(timeMultiplier, GameObject.Find("Canvas/Main/Arona").GetComponent<Image>()));
        yield return new WaitForSeconds(3);
        GameObject.Find("Canvas/Main/Arona").SetActive(false);
        audioManager.PlayBGM("Restart", 0.3f);
        StartCoroutine(FadeInImage(timeMultiplier, GameObject.Find("Canvas/Main/MainBackground").GetComponent<Image>()));
        yield return new WaitForSeconds(1);
        StartCoroutine(FadeInImage(timeMultiplier, GameObject.Find("Canvas/Main/MainCharacter").GetComponent<Image>()));
        yield return new WaitForSeconds(1);
        StartCoroutine(FadeInImage(timeMultiplier, GameObject.Find("Canvas/Main/Title1").GetComponent<Image>()));
        yield return new WaitForSeconds(1);
        audioManager.PlaySFX("Explode", 0.4f);
        StartCoroutine(FadeInImage(timeMultiplier, GameObject.Find("Canvas/Main/Title2").GetComponent<Image>()));
        GameObject.Find("Canvas/Main/Title2").GetComponent<Animator>().Play("Blast");
        yield return new WaitForSeconds(2);
        GameObject.Find("Canvas/Main/BtnStart").SetActive(true);
        StartCoroutine(FadeInText(timeMultiplier, GameObject.Find("Canvas/Main/BtnStart/Text").GetComponent<TMP_Text>()));
        yield return new WaitForSeconds(0.5f);
        GameObject.Find("Canvas/Main/BtnOption").SetActive(true);
        StartCoroutine(FadeInText(timeMultiplier, GameObject.Find("Canvas/Main/BtnOption/Text").GetComponent<TMP_Text>()));
        yield return new WaitForSeconds(0.5f);
        GameObject.Find("Canvas/Main/BtnQuit").SetActive(true);
        StartCoroutine(FadeInText(timeMultiplier, GameObject.Find("Canvas/Main/BtnQuit/Text").GetComponent<TMP_Text>()));
    }

    private IEnumerator FadeInImage(float timeSpeed, Image image)
    {
        image.color = new Color(image.color.r, image.color.g, image.color.b, 0);
        while (image.color.a < 1.0f)
        {
            image.color = new Color(image.color.r, image.color.g, image.color.b, image.color.a + (Time.deltaTime * timeSpeed));
            yield return null;
        }
    }
    private IEnumerator FadeOutImage(float timeSpeed, Image image)
    {
        image.color = new Color(image.color.r, image.color.g, image.color.b, 1);
        while (image.color.a > 0.0f)
        {
            image.color = new Color(image.color.r, image.color.g, image.color.b, image.color.a - (Time.deltaTime * timeSpeed));
            yield return null;
        }
    }

    void Start()
    {
        StartCoroutine(Opening());
        GameObject.Find("Canvas/Main/BtnStart").SetActive(false);
        GameObject.Find("Canvas/Main/BtnOption").SetActive(false);
        GameObject.Find("Canvas/Main/BtnQuit").SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

이외에는 코드를 조금 수정했다.

TextManager를 따로 두었는데, Coroutine을 활용하다보니 여기저기로 왔다갔다하면서 코드가 너무 난잡해져서 결국 GameManager도 다시 통합했다.

코드가 좀 지저분하게 보이는데 생각해보니 굳이 Coroutine이 아니라 Animation을 활용해서 투명도를 조절하면 됐을텐데, 라는 생각이 들었다. 그리고 Coroutine을 너무 많이 사용하는 것은 좋지 않으려나? 하는 생각도 들었다. 그런데 찾아보면 코루틴으로 페이드인 페이드아웃을 구현한 사람들이 꽤 있는데, 성능상 큰 차이는 없을지도 모르겠다.

인터넷에서 잠깐 찾아보니, Animation을 사용하거나 Coroutine을 사용하거나 큰 차이가 없다는 것 같기도 하다. Animation도 꽤 무겁다는 듯. 우선 기능을 만들어놨으니 지금은 Coroutine을 활용해야겠다.

 

결과적으로 오늘 완성된 부분은 다음과 같다.

생각했던 것보다 괜찮게 나온 것 같다. 물론 아직 만들어진 건 메인화면 밖에 없지만, 뭔가 만들어지고 있는 모습을 보니 나름 만족스럽다. 역시 게임에서는 시각적인 요소가 중요한 것 같다. 나야 고작 타이틀 하나 만드는데도 쉽지 않았는데, 배경이나 일러스트를 그리는 데에는 대체 얼마나 많은 노력과 수정들이 있었을지 생각해보면 존경스럽다. 오늘도 다시금 깨닫는다. 미적 감각이 부족한 나에게 아티스트들은 신과 같은 존재이다.

 

내일은 버튼의 기능을 구현하고 효과음을 넣어야겠다. 게임 내 스토리에서 사용되는 이미지 리소스들도 어찌저찌 따와서 적용하고 본격적으로 게임 내용을 개발할 수 있게 되면 좋을 것 같다.