<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를 빼먹어서 오류가 떴었다.

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

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

  • S.A (Starting Assignments)

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

 

  • Wire Frame

캐릭터 만들기

Asset에서 캐릭터 그림파일을 추가한 뒤, 빈 오브젝트에 이미지를 넣어준다.

 

이미지 파일의 크기를 조절하는 방법에는 여러가지 방법이 있다.

  • Main Camera - Camera Size 조절

카메라 사이즈가 커지면 캐릭터가 작아보이는 것을 이용한다.

 

  • Scale값 조절

 

  • PPU값 조절

 

 

캐릭터 이동

캐릭터 이동은 이전 글 중 <Unity> 입문 - 2D 게임 개발(2)의 입력과 캐릭터 이동 부분을 참고하자.

Legacy 형태는 짧지만 객체지향에 위배되는 코드이기 때문에 지양하는 것이 좋으며, Input System 방식의 객체지향 형식으로 코드를 쓰는 것이 좋다. 그러나 간단하게 테스트용으로 쓰이기도 하므로 알아두는 것이 좋다.

 

방 만들기

방 만들기 역시 <Unity> 입문 - 2D 게임 개발(3)의 타일맵 부분을 참고하면 된다.

캐릭터의 Rigidbody 2D와 타일의 Collider가 충돌이 일어나면 실제 물리 충돌이 일어나는데 그로 인해 캐릭터가 빙글빙글 도는 현상이 일어난다. 그것을 방지하기 위해 Rigidbody 2D - Constraints - Freeze Rotation의 Z을 체크해준다.

 

타일맵을 찍을 때 Edit - Preference - Tile Palette에서 기본툴 외의 다른 툴들을 추가해서 사용할 수 있다.

 

캐릭터 애니메이션 추가

애니메이터 창을 열어 기본 상태 애니메이션에 걷는 애니메이션을 추가해준 뒤, 서로 연결해준다.

 

Stand에서 Walk로 가는 Transition을 클릭하여 Has Exit Time 체크를 해제하고, Conditions를 추가해서 Walk를 true로 바꿔준다.

 

Walk에서 Stand로 가는 Transition 마찬가지로 Has Exit Time 체크를 해제하고, 이번엔 Walk를 false로 바꿔준다.

Has Exit Time는 종료 시점을 활성화하는 옵션으로 종료 시점의 비율만큼 애니메이션이 재생된 후에 다음 상태로 전이된다. 즉시 재생시키고 싶다면 비활성화 해야한다.

 

이제 위에서 만든 애니메이션을 관리하기 위해 스크립트를 만든다.

움직일 때 Walk로 전환 즉, 키를 입력받았을 때 전환시키려면 InputController가 필요하기 때문에 컨트롤러를 불러온다.

PlayerInputController controller;

[SerializeField] private Animator anim;

private void Awake()
{
    controller = GetComponent<PlayerInputController>();
}

void Start()
{
    controller.OnMoveEvent += Animation;
}

void Animation(Vector2 direction)
{
    anim.SetBool("walk", direction.magnitude > 0f);
}

 

 

+ Recent posts