<Button 작동 안되는 버그>

플레이어와 몬스터, 그리고 게임 종료 화면 UI 부분을 합치면서 Button이 눌리지 않는 버그가 발생했다. 스크립트와 오브젝트 전부 다 연결이 잘 되어 있는데 Debug를 찍어보아도 콘솔창에 로그가 찍히지 않았다.

왜 그런가 전 작업물과 비교해보니 EventSystem이 없어서 그런 것이었다.

 

EventSystem은 한 Scene에 하나만 생성되는 input 기반의 오브젝트에 이벤트를 보내는 시스템으로 Canvas를 생성할 때 Scene에 같이 생성되며, 삭제 했을 때 다시 Canvas를 생성하면 따로 자동으로 다시 생성되지 않는다.

 

Button이 작동되지 않았던 이유는 하이어라키에 있는 모든 오브젝트들을 Prefabs로 빼서 관리하는 과정에 실수로 삭제한 것 같다.생성은 간단하다. UI - Event System을 누르면 해결된다.

 

 

<Best Time 나타내기>

합치면서 Best Time을 저장하는 기능을 구현하지 않았다는 사실을 깨달았다.

Best Time은 게임이 재시작되거나 꺼져도 저장되고 남아있어야하는 기능으로, 구현 방법은 다양하다. 예전에 강의에서 배웠던 PlayerPrefs라는 기능을 사용하여 구현하기로 했다.

PlayerPrefs는 데이터를 저장하거나 꺼낼 때 항상 key, value 페어로 저장한다. SetFloat, SetInt는 숫자, SetString은 문자열을 저장한다. PlayerPrefs.HasKey()는 데이터를 저장했는지 확인하는 메서드로, bool값을 반환시킨다.

 

최고 점수( Best Time) 역시 타이머 형식으로 저장할 것이기 때문에 메서드로 만들어준다.

타이머 형식을 정해놨기 때문에 그냥 현재 점수와 최고 점수를 나눠서 만들었다.

private string UpdateTime()
{
    // 타이머 표시
    time += Time.deltaTime;
    int hour = (int)time / 3600;
    int min = (int)time % 3600 / 60;
    int sec = (int)time % 3600 % 60;
    timeTxt.text = string.Format("{0:D2}:{1:D2}:{2:D2}", hour, min, sec);
    return timeTxt.text;
}

private string BestTimeScore(float bestScore)
{
    // 타이머 표시
    float time = bestScore;
    int hour = (int)time / 3600;
    int min = (int)time % 3600 / 60;
    int sec = (int)time % 3600 % 60;
    timeTxt.text = string.Format("{0:D2}:{1:D2}:{2:D2}", hour, min, sec);
    return timeTxt.text;
}

 

현재 점수가 최고 점수보다 높다면 갱신한다.

public TMP_Text bestScoreTxt;

public void GameOver()
{
    isRunning = false; // 시간 멈추기
    Time.timeScale = 0f;
    endPanel.SetActive(true);

    // 게임오버 시간이 현재 시간에 뜨도록
    currentScoreTxt.text = timeTxt.text;

    if (PlayerPrefs.HasKey("bestScore") == false)
    {
        PlayerPrefs.SetFloat("bestScore", time);
    }
    else
    {
        if (PlayerPrefs.GetFloat("bestScore") < time)
        {
            PlayerPrefs.SetFloat("bestScore", time);
        }
    }
    float bestScore = PlayerPrefs.GetFloat("bestScore");

    bestScoreTxt.text = BestTimeScore(bestScore);
}

 

 

완벽히 이해하지 못하고 사용해서 자료형을 변형시킨다던지 등 상황에서 맞게 유동적으로 사용하지 못한게 아쉽다. 그리고 주말에 싱글톤과 싱글톤의 객체지향적이지 못하다는 단점을 보완한 Service Locator Pattern에 대해 공부해보아야겠다.

오늘은 저번에 이어서 Timer 추가와 게임 종료 화면을 만들었다.

 

<UI> - Timer

저번 스크럼 때 회의 결과, GameManager 스크립트는 각각의 이름으로 만든 뒤, 나중에 한 번에 합치기로 했다.

위 스크립트를 GameManager 오브젝트에 컴포넌트하고 timeTxt 변수에 Text 오브젝트를 할당한다.

public Text timeTxt;
float time;

private void Update()
{
    time += Time.deltaTime;
    timeTxt.text = time.ToString();
}

 

그런데 할당이 안되는 문제가 발생했다. UnityEngine.UI 네임스페이스도 잘 추가 되어있고 변수명 등 오타가 나지도 않았는데 timeTxt에 Text 오브젝트가 추가가 안됐다. 계속 헤맨 결과, 알고보니 레거시 형태의 Text를 추가했기 때문이었다.

Text 대신 TextMeshPro를 사용하는 경우, 스크립트에서도 TextMeshProUGUI를 사용해야한다.

public TMP_Text timeTxt;
float time;

private void Update()
{
    time += Time.deltaTime;
    timeTxt.text = time.ToString();
}

 

이렇게 하면 0.00으로 타임 표시가 된다.

 

그러나 내가 원하는 것은 00:00 형식이었기 때문에 string.Format 메서드를 사용하여 형식이 지정된 문자열을 만든다.

private void Update()
{
    time += Time.deltaTime;
    int min = (int)time % 3600/60;
    int sec = (int)time % 3600%60;
    timeTxt.text = string.Format("{0:D2}:{1:D2}", min, sec);
}

"{0:D2}:{1:D2}"가 형식 문자열로 0, 1은 각각 몇 번째 인수인지, D2는 십진수를 의미한다.

 

시간까지 표현하고 싶다면 추가해주면 된다.

private void Update()
{
    time += Time.deltaTime;
    int hour = (int)time / 3600;
    int min = (int)time % 3600/60;
    int sec = (int)time % 3600%60;
    timeTxt.text = string.Format("{0:D2}:{1:D2}:{2:D2}", hour, min, sec);
}

 

 

<게임 종료 화면>

게임 종료 화면은 UI Panel로 만들기로 했다.

종료 화면의 내용은 버틴 시간과 다시하기, 메인으로 가는 버튼으로 구성되어있다. 일단 껍데기를 만들어준다.

 

게임 종료 화면은 게임 오버가 됐을 때 떠야하므로 꺼둔다.

 

이제 게임이 끝났을 때 화면이 나타나도록 하기 위해 GameManager를 싱글톤 처리한다.

싱글톤이란 하나의 인스턴스만 생성하고 그 인스턴스에 대해 전역적인 접근을 제공하는 것이다.

public static gameManager I;

void Awake()
{
    if (I == null)
    {
        I = this;
    }
    else
    {
        Destroy(gameObject);
    }
    // 현재 게임 오브젝트를 새로운 씬으로 이동해도 파괴되지 않도록 설정
    DontDestroyOnLoad(gameObject);
}

 

 

게임이 종료되면 시간을 멈추고 endPanel이 켜지도록 한다.

public GameObject endPanel;

public void GameOver()
{
    Time.timeScale = 0;
    endPanel.SetActive(true);
}

Time.timeScale이 0이면 시간이 멈추고 1이면 진행된다. 궁금해서 찾아보니 음수 값을 넣으면 0으로 처리되며 0.5는 2배 느려지고 2를 넣으면 2배, 3을 넣으면 3배 등 배속이 되도록 처리된다고 한다.

 

Update()와 gameOver 간의 시간차가 있기 때문에 게임오버가 되자마자 시간을 멈추기 위해 isRunning이라는 불값을 선언해주고 true로 설정한 뒤, 살아있을 때만 업데이트 되도록 한다.

private bool isRunning = true;

private void Update()
{
    if (isRunning) // 게임실행 중에 시간이 간다
    {
        // 타이머 표시
        time += Time.deltaTime;
        int hour = (int)time / 3600;
        int min = (int)time % 3600 / 60;
        int sec = (int)time % 3600 % 60;
        timeTxt.text = string.Format("{0:D2}:{1:D2}:{2:D2}", hour, min, sec);
    }
}

public void GameOver()
{
    isRunning = false; // 시간 멈추기
    Time.timeScale = 0f;
    endPanel.SetActive(true);
    
    // 게임오버 시간 = 현재 시간
	currentScoreTxt.text = timeTxt.text;
}

 

버튼을 눌렀을 때 각각 Retry, Main의 기능을 하도록 메서드를 만들어준다.

using UnityEngine.SceneManagement;

// retry 버튼을 누르면 현재 씬이 다시 로드되도록 한다
public void Retry()
{
    SceneManager.LoadScene("MainGame");
}

// main 버튼을 누르면 메인화면을 부른다
public void MainTitle()
{
    SceneManager.LoadScene("FirstTitle");
}

 

버튼을 눌렀을 때, Scene이 로드 되지 않고 오류가 뜬다면 File - Build Settings에서 해당 Scene이 추가 되어있는지 확인해보자.

 

확인 결과 잘 작동한다. 아직 플레이어가 게임 오버 됐을 때 부분이 없기 때문에 그 부분을 담당한 분과 합쳐서 확인해보아야 할 것 같다.

 

작업하면서 어려웠던 점은 위에서 언급했다시피 협업할 때 아직 구현이 되지 않은 부분을 임시로 돌려보려면 어떻게 처리해야하는지가 곤란했다. 그리고 TMP(TextMeshPro)를 사용할 때, 스크립트에서 레거시 Text를 처리하는 방법과 달라 자꾸 헷갈렸다. 현재 시간과 게임 오버 시간을 처리할 때도 그 점을 잊.text를 빼먹어서 오류가 떴었다.

다른 분들과 합칠 때 하이어라키를 비우지 않으면 meta 데이터로 인해 충돌이 날 가능성이 높아지기 때문에 한 명이 Scene을 미리 만들어두고, 서로 작업할 때 본인 이름파일을 생성하여 Prefabs로  인스턴스화 하기로 했다.

일단 기능 구현을 먼저 해놓고 꾸미는 것은 나중에 하기로 했다.

 

<게임 시작 화면> - StartButton 만들기

Button 컴포넌트는 Sprites에서는 작동이 안되며 UI에서만 실행되기 때문에 Image를 선택하여 버튼을 생성해준다.

StartButton script를 만들고 버튼을 눌렀을 때, MainGame씬으로 넘어가도록 한다.

public class StartScene : MonoBehaviour
{
    public void GameStart()
    {
        SceneManager.LoadScene("MainGame");
    }
}

 

 

<UI> - Timer

UI에서 Text를 추가한 뒤, GameManager에서 시간이 계속해서 흐르도록 Update에서 Time.deltaTime을 사용한다.

public Time timeTxt;
float time;

void Update()
{
    time += Time.deltaTime;
    timeTxt.text = time.ToString();
}

time GameObject 자기 자신을 time에 넣어주어 게임 화면에 시간을 띄운다. 이 부분은 GameManager를 건들기 때문에 일단 방법만 작성해놓고 스크럼 때 조율하기로 했다.

 

 

<게임 종료 화면>

게임 종료 화면 역시 UI로 만들지 Scene으로 만들지 조율이 필요하기 때문에 스크럼 때 회의 후 진행하기로 했다.

 

 

맡은 부분이 상대적으로 쉬워서 다른 분들을 돕거나 디자인을 해볼 생각이다. 개인과제 때 현재 시간 띄우기, 캐릭터 선택창, 이름 바꾸기 등 선택구현도 시도해보아서 많은 도움이 된 것 같다.

스크립트 간의 상속과 접근 제한자 등의 이해가 필요하기 때문에 주말에는 C# 문법 부분을 노션에 정리하며 다시 한 번 복습해볼 생각이다.

'부트캠프 > Study' 카테고리의 다른 글

<Unity> 숙련 - 2D 게임 개발(5)  (0) 2023.12.10
<Unity> 숙련 - 2D 게임 개발(4)  (0) 2023.12.08
벡터(Vecter)  (0) 2023.11.29
ScreenToWorldPoint와 WorldtoScreenPoint의 차이  (0) 2023.11.28
<Unity> 입문 - 2D 게임 개발(3)  (1) 2023.11.28

이번 팀과제는 추억의 게임 중 한 가지를 선택하여 현대적인 버전으로 재현하기이다.

우리팀은 닷지(Dodge) 게임을 바탕으로 공격 기능을 추가하여 Vampire Survivors 느낌으로 기획했다.

  • S.A (Starting Assignments)

현대인들을 타겟층으로 기획했으며, 우주 테마로 컨셉을 잡았다.

 

  • Wire Frame

+ Recent posts