[ UI ]
텍스트
UI는 User Interface의 약자로, 카메라와 상관없이 게임 화면에 계속해서 보이는 정보이다.
UI는 하이어아키 빈 공간 우클릭 해서 만들 수 있다. UI > Legacy > Text를 통해 텍스트를 생성한다.
하이어아키에서 캔버스와 그 안에 텍스트가 있는 것을 볼 수 있다. 캔버스는 카메라 화면과 상관없이, 게임 화면 위에 바로 보이는 공간이다.
텍스트의 인스펙터에는 위치, 폰트 스타일, 폰트 크기, 정렬, 색상 등을 조정할 수 있다. UI에 보이게 하고 싶은 텍스트들을 복제해서 서식에 맞게 수정했다.
씬에서는 텍스트를 볼 수 있지만, 메인 카메라에서는 텍스트가 보이지 않는다.
하지만 게임 화면에서는 카메라 화면 위에 UI가 덧씌워져 텍스트가 적용된 모습을 볼 수 있다.
[ 싱글톤 ]
개념
싱글톤은 특정 클래스의 인스턴스가 전체 프로그램 내에 단 1개만 생성되는 패턴을 의미한다. 이는 어디서든 전역적으로 접근할 수 있다. 보통 게임 내 하나만 존재하는 중요한 관리 요소에 사용된다.
public class GameManager : MonoBehaviour
{
public static GameManager Instance;
private void Awake()
{
Instance = this;
}
void Start()
{
}
}
GameManager 클래스의 변수 Instance를 생성하고, Awake 메소드에서 Instance 변수에 this 키워드를 할당한다.
여기서 this는 GameManager의 객체(인스턴스)를 의미한다. 한 팀에 팀장이 2명이면 혼란이 오는 것처럼, 관리를 용이하게 하기 위해서 이 GameManager 인스턴스는 프로그램 전체에서 단 하나만 존재해야 한다. 이를 위해 Instance = this 코드로 인스턴스는 자신이라고 칭하면서 전체 프로그램 내에서 GameManager 클래스의 유일한 인스턴스가 됐다.
또한 public static 키워드로 인해, GameManager.Instance를 사용하면 어디서든 게임 매니저의 인스턴스에 접근할 수 있다.
Awake 메소드
Awake 메서드는 게임 시작 전, 게임 오브젝트와 스크립트가 초기화되기 이전에 호출된다. 주로 게임 오브젝트와 컴포넌트의 초기 설정, 다른 개체를 참조하기 위해 사용된다. 예를 들어 필요한 데이터를 프로그램 내 다른 오브젝트에서 미리 불러올 수 있다. 게임 캐릭터가 여러 개가 있다면, 각 오브젝트들의 위치 컴포넌트를 참조하여 컴포넌트 간의 상호 작용이 가능해진다.
Awake 메소드는 Start 메서드보다 항상 먼저 실행된다. 또한 해당 오브젝트가 활성화되어 있지 않더라도 실행된다는 특징이 있다. Start 메서드는Awake 메서드가 모든 오브젝트에서 호출된 후에 각 활성화된 오브젝트에서 호출된다.
[ 점수 적용 ]
점수 저장
public class GameManager : MonoBehaviour
{
int totalScore;
public void AddScore(int score)
{
totalScore += score;
Debug.Log(totalScore);
}
}
총점수를 알기 위해서 증가하는 점수 데이터가 저장될 totalScore 변수를 생성한다. AddScore 메서드는 score 매개변수를 통해 점수 데이터를 받는다. 이렇게 메서드에 전달된 score 값이 totalScore에 더해진다. 플레이어가 점수를 획득할 때마다 totalScore의 값은 계속해서 업데이트된다.정상적으로 코드가 작동되고 있는지 콘솔창에서 확인하기 위해 Debug.Log(score)를 추가했다.
충돌 시 점수 발생
Rain 오브젝트와 Rtan 오브젝트가 부딪혔을 때 점수를 발생시키기 위해, 르탄이에게도 충돌을 적용시켜야 한다. Box Collider 2D 컴포넌트를 생성하고, Edit Collider 버튼을 클릭한다. Rtan 오브젝트를 더블 클릭하면, 씬에서 오브젝트 주위로 초록색 선이 생긴 것을 볼 수 있다. 이것이 충돌 영역이다. 오브젝트의 크기에 맞게 조절해서 충돌 영역을 수정해 준다.
Ground 오브젝트에 Ground 태그를 달아줬던 것처럼, Rtan 오브젝트에도 태그를 달아준다. 이번에는 새로 생성하지 않고 기본적으로 제공되는 Player 태그를 적용한다.
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.CompareTag("Ground"))
{
Destroy(this.gameObject);
}
else if (collision.gameObject.CompareTag("Player"))
{
GameManager.Instance.AddScore(score);
Destroy(this.gameObject);
}
}
이번에는 Rain 오브젝트와 관련된 코드를 작성해야 하므로, Rain 스크립트로 이동한다.
기존에 작성했던 Ground 오브젝트와의 충돌에 대한 코드 하단에 Rtan 오브젝트와의 충돌 코드를 작성한다.
점수를 더하는 코드는 GameManager 스크립트에 있으므로, 해당 스크립트의 인스턴스와 메서드, 매개변수를 차례대로 불러온다. 그리고 위와 동일하게 Rain 오브젝트와 Rtan 오브젝트가 부딪혔을 때, Rain 오브젝트가 파괴되도록 한다.
프로그램을 실행하면 Rain 오브젝트와 Rtan 오브젝트가 충돌했을 때 콘솔창에 현재 점수가 증가하는 것을 볼 수 있다. 또한 오브젝트끼리의 충돌이 발생했을 때 Rain 오브젝트는 파괴됐다.
점수 UI 반영
이제 현재 점수를 Score 텍스트에 적용시켜야 하는데, GameManager 인스펙터에는 Text 컴포넌트가 존재하지 않는다. 그래서 Score 텍스트에서 Text 타입을 가져온다.
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
public Text totalScoreTxt;
public void AddScore(int score)
{
totalScore += score;
totalScoreTxt.text = totalScore.ToString();
Debug.Log(totalScore);
}
}
먼저 UI 기능을 사용하기 위해 using문을 활용해 UI와 관련된 패키지를 불러온다.
텍스트 타입의 totalScoreTxt 전역 변수를 생성하면, GameManager 인스펙터에 Total Score Txt가 추가된 것을 볼 수 있다. 여기에 Score 텍스트를 드래그 앤 드롭한다.
AddScore 메서드에서 totalScoreTxt 변수에 담겨있는 text 타입의 컴포넌트를 불러오고, totalScore 데이터를 할당해 준다. 주의할 점은 text는 string 타입이고 totalScore는 int 타입이기 때문에, 둘의 타입을 맞춰주기 위해. ToString()을 사용해서 형변환을 해줘야 한다.
스크립트를 저장하고 프로그램을 실행하면, Rtan 오브젝트와 Rain 오브젝트가 충돌할 때마다 점수가 올라가는 것을 확인할 수 있다.
[ 게임 종료 ]
시간 감소 UI 반영
public Text timeTxt;
float totalTime = 30.0f;
void Update()
{
totalTime -= Time.deltaTime;
timeTxt.text = totalTime.ToString("N2");
}
점수를 적용했을 때와 같이, GameManager 인스펙터에 생긴 timeTxt 변수에 Time 텍스트를 드래그 앤 드롭한다.
그리고 totalTime 변수에 제한 시간인 30초를 설정해 준다. 계속해서 변화하는 시간을 게임에 반영해야 하기 때문에, Update 메서드에 totalTime 변수에서 시간을 빼주는 코드를 작성한다. Time.deltaTime은 두 연속된 프레임 간의 시간 차이를 초 단위로 나타낸 것이다.
timeTxt 변수에서 text 타입의 컴포넌트를 가져오고, string 타입으로 변환한 totalTime 변숫값을 할당한다. 소수점 둘째 자리까지 나타내기 위해. ToString 소괄호 안에 "N2"를 작성한다.
❓ Time.deltaTime을 사용하면 왜 모든 사용자의 속도가 일정해질까?
프레임은 1초에 화면에 표시되는 횟수이다. 컴퓨터의 성능이 좋을수록 이 프레임 수는 많아진다.
1초에 20 프레임인 성능이 안 좋은 A컴퓨터와 1초에 100 프레임인 성능이 좋은 B컴퓨터가 있다고 생각해 보자.
게임에서 똑같은 캐릭터가 스타트 지점에서부터 골 지점까지 움직이려고 한다. 만약 Time.deltaTime이 없다면, A컴퓨터는 1초에 20번 밖에 움직이지 못하지만, B컴퓨터는 1초에 100번 움직일 수 있으므로 훨씬 유리할 것이다.
public float speed = 5.0f;
void Update()
{
float movement = speed * Time.deltaTime;
transform.Translate(0, 0, movement);
}
하지만 캐릭터의 스피드에 Time.deltaTime을 곱해주면, 프레임 수가 적을수록 무브먼트 값은 커지게 된다.
A컴퓨터의 Time.deltaTime은 0.05(1/20)이고, 무브먼트 값은 5 * 0.05 = 0.25이다.
B컴퓨터의 Time.deltaTime은 0.01(1/100)이고, 무브먼트 값은 5 * 0.01 = 0.05이다.
즉 A컴퓨터는 큰 보폭으로 적게 움직이는 것이다. 극단적인 예시로 1초에 1 프레임이라면, 캐릭터가 축지법을 쓰는 것처럼 보일 수 있다.
반대로 B컴퓨터는 작은 보폭으로 많이 움직이는 것이다. 평소 게임하는 것처럼 자연스러운 모습으로 생각하면 된다.
애니메이션을 생각하면 쉽게 이해할 수 있다. 1분짜리 달리기를 하는 르탄이 애니메이션을 만드려고 한다. 내가 르탄이를 많이 그릴 수록 달리기는 자연스러워지고, 적게 그릴 수록 움직임은 뚝뚝 끊기게 된다. 이처럼 총시간은 동일하지만, 장면 수가 많을수록 움직임이 자연스러워진다.
게임 종료 표시
하이어아키에서 UI 이미지와 텍스트를 생성한다. 크기, 폰트, 색상 등을 변경해서 게임 화면처럼 수정한다. 이미지와 텍스트는 동시에 등장하므로, 텍스트와 이미지를 그룹화하고 이미지의 이름을 EndPanel로 수정한다.
인스펙트에서 이름 옆에 체크박스가 활성화되어 있으면 게임 화면에 보여지고, 활성화 되어 있지 않으면 보이지 않는다. 게임이 끝났을 때에만 보이는 화면이므로 게임이 실행되는 동안은 체크박스를 비활성화한다.
public GameObject endPanel;
void Update()
{
if (totalTime > 0f)
{
totalTime -= Time.deltaTime;
}
else
{
totalTime = 0f;
Time.timeScale = 0f;
endPanel.SetActive(true);
}
timeTxt.text = totalTime.ToString("N2");
}
GameManager 인스펙터의 endPanel 변수에 EndPanel 오브젝트를 드래그 앤 드롭한다.
Update 메서드에서 조건문을 이용해 남은 시간이 0보다 크면 계속해서 시간이 줄어들게 한다. 아니라면 게임 화면에 보이는 totalTime 변수의 값을 0으로 만들고, Time.timeScale을 활용해서 게임이 멈추게 한다.
Time.timeScale은 게임 내 시간을 조절하는 변수이다. 실시간을 의미하는 1.0을 기준으로 값이 1.0 보다 크면 시간이 빨라지고, 1.0 보다 작으면 시간이 느려진다. 값이 0이 되면 시간이 멈추게 된다. 예를 들어 값이 2.0이라면 게임 내 시간은 2배속이 된다.
그리고 인자값이 true라면 게임 오브젝트를 활성화하고, false라면 비활성화하는 SetActive 메서드를 활용해서 endPanel을 활성화한다.
❓ Text? Image? GameObject? 필드 선언할 때 어떤 클래스를 사용해야 할까?
하이어아키에서 UI 카테고리를 통해 Text 또는 Image 등을 생성한다. 그렇다면 이것들은 게임 오브젝트가 아닌 걸까?
Text, Image와 같은 것들은 모두 게임 오브젝트에 있는 컴포넌트의 일종이다. 기본적으로 게임 오브젝트가 생성되고, 선택한 컴포넌트가 자동으로 포함된다. 구분하기 쉽도록 처음부터 이름을 컴포넌트 타입으로 사용하는 것이다.
예를 들어 텍스트 컴포넌트를 포함하는 Name 오브젝트와 이미지 컴포넌트를 포함하는 Picture 오브젝트가 있다. 각 컴포넌트 타입을 게임 매니저에서 참조하려고 한다.
//컴포넌트 타입 사용
public Text nameText;
void Start()
{
nameText.text = "김서영";
}
//GameObject 사용
public GameObject nameObject;
void Start()
{
Text nameText = nameObject.GetComponent<Text>();
nameText.text = "김서영";
}
둘 다 게임 오브젝트이기 때문에 GameObject 클래스로도 참조할 수 있지만, GetComponent() 함수를 이용해서 또다시 각 컴포넌트에서 가져오는 코드를 작성해야 한다. 그래서 단일 컴포넌트를 참조할 때에는 Text 클래스, Image 클래스와 같은 컴포넌트 타입을 이용하면 간단하게 참조할 수 있다.
하지만 위에서 봤던 endPanel처럼 이미지와 텍스트가 포함된 패런팅(그룹화) 오브젝트를 참조하려면 하나의 컴포넌트 타입만 가져올 수 있는 클래스를 사용하면 안 되므로, 두 타입의 컴포넌트를 모두 참조할 수 있도록 GameObject 클래스를 사용한다.
[ 다시 시작 ]
using UnityEngine.SceneManagement;
public class RetryButton : MonoBehaviour
{
public void Retry()
{
SceneManager.LoadScene("MainScene");
}
}
RetryButton 스크립트를 생성한 뒤, 씬과 관련된 내장 함수를 사용하기 위해 SceneManagement 네임스페이스를 가져온다.
Retry 메서드를 만들고 SceneManager.LoadScene() 함수를 이용해서 MainScene을 로드하는 코드를 작성한다. 해당 코드를 실행하면 현재 씬이 먼저 언로드 되며 모든 게임 오브젝트가 비활성화 된다. 그리고 인자값의 씬을 새로 로드하며 해당 씬의 게임 오브젝트가 활성화 된다.
버튼을 클릭했을 때 위 함수를 호출할 수 있도록 버튼 컴포넌트를 추가하고, On Click에서 RetryButton > Retry()를 선택한다.
private void Awake()
{
Instance = this;
Time.timeScale = 1.0f;
}
지금은 버튼을 클릭해서 다시 시작했을 때 처음 화면으로는 돌아오지만, Rain 오브젝트가 떨어지거나 시간이 흘러가지 않는다. 이를 해결하기 위해 GameManager 스크립트에서 게임 시간을 실시간으로 설정해 주는 코드를 작성한다.
버튼을 누르면 새로운 메인 씬이 로드되면서 게임 오브젝트도 다시 활성화되므로, Awake 메서드부터 실행되어 게임이 정상적으로 작동하게 된다.
[ 회고 ]
오늘은 싱글톤과 Awake 메서드를 가장 열심히 공부했다. 텍스트로 정리하고 보니 간단했지만, 이해하기까지 오랜 시간이 걸렸다. 하지만 싱글톤은 오늘 배운 수준에서 끝나지 않고 더 복잡해지기 때문에 기초를 다지는 것이 필요했고, Awake 메소드를 이해하지 못했다면 게임이 다시 실행되어 작동되는 원리를 이해하지 못했을 것이다.
그리고 UI 컴포넌트 개념에 대해 정확히 알지 않은 채로 게임을 만들다 보니, 코드는 정상 작동되지만 어떤 원리로 게임이 실행되는지 도무지 이해가 되지 않았다. UI 컴포넌트에 대해 알아보고 이를 바탕으로 필드 선언 코드 구성을 이해했다.
1주 차에서 등장하는 모든 개념들에 대해서 완벽히 숙지하려고 하니 예상보다 시간이 훨씬 많이 소요되었다. 이번 주 남은 기간 동안은 1주차 강의에서 배운 것들을 바탕으로 빠르게 게임을 만들어 보는 것을 목표로 하고 있다.
'Coding > Unity' 카테고리의 다른 글
[내일배움캠프 17일차 TIL] 게임 엔진, PPU, 계층 구조, Input.GetAxis, 직렬화 (0) | 2024.05.08 |
---|---|
[내일배움캠프 5일차 TIL] 복습, 체력바, isKinematic, isTrigger, 레벨 (1) | 2024.04.19 |
[내일배움캠프 4일차 TIL] 복습, 마우스 포인터 추적, 점수 저장 (1) | 2024.04.18 |
[내일배움캠프 2일차 TIL] 오브젝트 중력, 충돌, 파괴, 랜덤, 반복, 클래스 (0) | 2024.04.16 |
[내일배움캠프 1일차 TIL] 오브젝트, 애니메이션, 스크립트, 캐릭터 움직이기 (0) | 2024.04.15 |