최종적으로 프로젝트를 합치는 과정에 게임 완성도를 높이기 위해 추가구현으로 메인 화면에 Animation을 넣고 메인화면씬과 게임플레이 씬에 Audio를 추가했다.
그리고 발표를 맡게 되어코드에 대해 전체적인 이해를 위해 다른 분들의 코드를 공부했다.
ppt를 만들고 대본을 쓰는 중인데 완성이 늦어 리허설을 못해서 걱정이 많이 된다.
일단 대본을 완성하고 내일 오전에 팀원분들과 잠깐이라도 리허설을 해봐야겠다.
이번 프로젝트를 하며 사람 간의 협업과 의사소통에 대한 많은 생각을 하게 되었고 실력에 대한 고민도 많이 들었다. 이론을 이해했다고 착각한 상태로 혼자 응용해보려니까 힘들었다. 내가 맞게 공부하고 있는지, 어느 것을 모르는 상태인지 확실히 알고 공부 방법에 대해서도 다시 생각해보아야겠다.
플레이어와 몬스터, 그리고 게임 종료 화면 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에 대해 공부해보아야겠다.
저번 스크럼 때 회의 결과, 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를 빼먹어서 오류가 떴었다.