지우너
[Unity 3D Run] 게임오버 UI (2) 공부+약간 수정 본문
1. UI를 가장 위에 오게 하기
Render Mode를 Screen Space-Overlay로 바꾸어 게임 화면 상에서 제일 위에 나오게 만들었다.
그런데 게임뷰에서 Canvas의 외곽선이 보이는 문제가 생겼다.
✔️ https://www.inflearn.com/questions/199907/text-추가시-canvas-선이-보여요
위 링크를 보면 Gizmo의 Canvas를 체크해제하여 Scene뷰, Game뷰 모두에서 Canvas 외곽선을 표시하지 않도록 만드는 해결법이 나와있다. 이 방법이 마음에 들지 않아 Gizmo를 켠 채로 여러 해상도에서 UI가 깨지지 않게 설정을 바꾸었더니 해결이 됐다(왜 해결된 건지는 모르겠다...).
2. 여러 해상도에 UI가 대응할 수 있게 설정
Canvas오브젝트 선택 > Canvas Scaler 컴포넌트 > UI Scale Mode를 Scale With Screen Size로 설정
Match를 0.5로 설정(게임에 따라 다르겠지만, 나는 이렇게 변경했다.)
그리고 설정을 바꿔서 작아진 UI를 다시 키워줬다. 이렇게 했더니 아래와 같이 해상도에 상관없이 UI가 깨지지 않고 유지됐다.
3. 게임오버 UI 스크립트
스크립트를 어떻게 작성해야 할지 몰라 유튜브로 여러 강의 찾아서 봤다.
우선, 잘못된 점은 GameManager.cs를 Canvas에 Add Component했었다.
GameOverUI를 게임오버, 게임클리어, 게임 플레이 중 상태에 따라 껐다 켜기 위해 UI가 들어있는 Canvas에 어태치 했던 거였는데, 좋지 않은 방식이었던 것 같다. 그래서 GameManager라는 빈 오브젝트를 생성해서, 거기에 스크립트를 붙여줬다.
기타 자잘하게 알게된 팁들
+++ 앵커 프리셋 단축키 +++
Shift: 기준점(pivot)변경
Alt: 위치 변경 (stretch에 있는 것을 클릭하면 크기까지 함께 변경됨)
+++ [Header("# Game Object")] 코드 +++
유니티 - [Header] - 코딩 공부 포기하지 말고!!!
+++ text 설정 & 꼼수 +++
3D 쿼터뷰 액션게임 - UI 배치하기 [유니티 기초 강좌 B51]
폰트 크기가 Rect Transform에 설정된 Width/Height를 넘어서는 경우 텍스트가 보이지 않게 되는 현상이 있었다. Horizontal Overflow랑 Vertical Overflow를 Overflow로 설정하면 정해진 크기를 넘더라도 잘 표시된다.
그리고 UI를 만들 때는 위에 있는 2D버튼을 클릭하고 하면 편하다!(UI는 2D라서)
폰트 크기를 키우면 text가 흐리게 보이는 게 있다. scale을 조금 작게 설정하면 뚜렷한 느낌이 된다(꼼수)
참고한 사이트/링크
3D 쿼터뷰 액션게임 - UI 배치하기 [유니티 기초 강좌 B51]
https://www.inflearn.com/questions/199907/text-추가시-canvas-선이-보여요
유니티 - [Header] - 코딩 공부 포기하지 말고!!!
3D 쿼터뷰 액션게임 - UI 로직 연결하기 [유니티 기초 강좌 B53]
3D 쿼터뷰 액션게임 - 게임 완성하기 [유니티 기초 강좌 BE5]
Unity - 다른 스크립트 함수, 변수를 불러올때 (디자인 패턴 싱글톤)
GameManeger의 역할: 게임의 승패, 점수, 플레이어 턴/적 턴 등을 관리하는 스크립트
유니티 - [Header] - 코딩 공부 포기하지 말고!!!
//GameManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
public static GameManager instance;
[Header("# Game Control")]
public float gameTime;
public float maxGameTime = 2*10f;
[Header("# Player Info")]
public int health;
public int maxHealth=100;
public int level;
public int kill;
public int exp;
public int[] nextExp = {3, 5, 10, 100, 150, 210, 280, 360, 450}
[Header("# Game Object")]
public PoolManager pool;
public Player player;
void Awake()
{
instance = this;
}
void Start()
{
health=maxHealth;
}
void Update()
{
gameTime += Time.deltaTime;
if(gameTime > maxGameTime){
gameTime = maxGameTime;
}
}
public void GetExp()
{
exp++;
if (exp == nextExp[level]){
level++;
exp = 0;
}
}
}
// HUD.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // UI를 사용할 때 필요
public class HUD : MonoBehaviour
{
public enum InfoType { Exp, Level, Kill, Time, Health}
public InfoType type;
// Text와 Slider 변수 선언 및 초기화
Text myText;
Slider mySlider;
void Awake()
{
// 초기화 하는 방법은 당연히 GetComponent<>();
myText = GetComponent<Text>();
mySlider = GetComponent<Slider>();
}
void LateUpdate()
{
switch (type)
{
case InfoType.Exp:
// 슬라이더의 값은 0~1: 현재 경험치/최대 경험치
// 게임매니저 인스턴스에 경험치를 만들어줬었다.
// Slider에 HUD를 Add Component해주고 Type을 Exp로 설정해주면 된다.
// https://youtu.be/ip0xffLSWlk?t=1173
float curExp = GameManager.instance.exp;
float maxExp = GameManager.instance.nextExp[GameManager.instance.level];
mySlider.value = curExp/maxExp;
break;
case InfoType.Level:
// 문자열로 바꿔주기 위해 string의 Format함수를 사용한다.
// Format: 각 숫자 인자값을 지정된 형태의 문자열로 만들어주는 함수
// 인자 값의 문자열이 들어갈 자리를 {순번} 형태로 작성. 여기는 0번째 인자값을 넣어야 하니까 {0}
// F0, F1, F2, ...로 소수점 자리를 지정해준다. 여기서는 소수점이 필요없으니까 F0으로 한다.
myText.text = string.Format("Lv.{0:F0}", GameManager.instance.level);
break;
case InfoType.Kill:
myText.text = string.Format("{0:F0}", GameManager.instance.kill);
break;
case InfoType.Time:
// 남은 시간을 플레이어한테 보여줘야 한다.
float remainTime = GameManager.instance.maxGameTime - GameManager.instance.gameTime
// 구한 시간을 분과 초로 분리해줘야 한다. 00:00의 형식이기 때문
// Mathf.FloorToInt로 소수점 버리기
int min = Mathf.FloorToInt(remainTime/60);
int sec = Mathf.FloorToInt(remainTime%60);
// D0, D1, D2, ...: 자리수를 지정. 여기서는 00:00으로 2자리씩 보여주고 싶으니 2자리로 고정해줬다.
myText.text = string.Format("{0:D2}:{1:D2}", min, sec);
break;
case InfoType.Health:
float curHealth = GameManager.instance.health;
float maxHealth = GameManager.instance.maxHealth;
mySlider.value = health/maxHealth;
break;
}
}
}
캐릭터가 시네머신?으로 되어 있어서 정중앙에서 움직이는 것이 아님.
따라서 체력 게이지가 캐릭터를 따라가도록 코드를 짜줘야 한다.
// Follow.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Follow : MonoBehaviour
{
// UI의 경우 transform이 아니라 Rect Transform이다.
// Rect Transform의 경우 따로 변수를 만들어 줘야한다.
RectTransform rect;
void Awake()
{
//Rect Transform 초기화
rect = GetComponent<RectTransform>();
}
// 캐릭터가 물리적 이동을 FixedUpdate로 하고 있기 때문에 얘도 그렇게 만들어준다.
void FixedUpdate()
{
// 캐릭터가 올라가 있는 월드 좌표계와 UI가 표시되는 스크린 좌표계는 다르기 때문에 아래와 같이 코드를 작성하면 안 된다.
// rect.position = GameManager.instance.player.transform.position;
// WorldToScreenPoint(월드좌표): 월드 상의 오브젝트 위치를 스크린 좌표로 변환해주는 함수
// 참고! 반대로 스크린 좌표 위치를 월드 좌표로 변환해주는 함수도 있음
rect.position = Camera.main.WorldToScreenPoint(GameManager.instance.player.transform.position);
}
}
강의를 다 보고 든 생각인데 얼마 전에 본 유튜브처럼 구글 스프레드시트(.json)로 캐릭터 정보나 스테이지 정보를 관리하면 좋을 것 같다는 생각을 했다.
3D 쿼터뷰 액션게임 - UI 로직 연결하기 [유니티 기초 강좌 B53]
PlayerPrefs: 유니티에서 제공하는 간단한 저장 기능(float, int, string만 저장할 수 있음)
PlayerPrefs.SetInt / PlayerPrefs.SetFloat / PlayerPrefs.SetString
PlayerPrefs.SetInt(”Key”, Value); 의 형식으로 작성하는 것 같음
저장했던 데이터를 불러올 때는 GetInt / GetFloat / GetString을 이용한다.
GameManager 스크립트 만들기 타임라인(https://youtu.be/7B9BsVnG8D8?t=504)
필요한 모든 변수를 public으로 선언 → 좀 더 효율적으로 깔끔하게 하는 방법은 없을까 변수 너무 많고 복잡한데, 다른 사람들도 이렇게 작성하는 걸까??
// GameManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // UI를 사용할 때 필요
public class GameManager : MonoBehaviour
{
// 메뉴를 켜면 플레이어가 없어지면서 화면이 천천히 좌우로 움직이는 애니메이션 적용
public GameObject menuCam;
// 게임 플레이시 캐릭터를 따라 다니는 메인 카메라
public GameObject gameCam;
// 플레이어 스크립트로 받기
public Player player;
// 플레이어를 상대할 보스 역시 스크립트로 가져오기
public Boss boss;
public int stage;
public float playTime;
public bool isBattle; //싸우고 있는지 여부
public int enemyCntA; // 그 스테이지에 남은 몬스터A 수
public int enemyCntB; // 그 스테이지에 남은 몬스터B 수
public int enemyCntC; // 그 스테이지에 남은 몬스터C 수
// UI를 위한 변수
public GameObject menuPanel;
public Text maxScoreTxt;
public GameObject gamePanel;
// 상단의 점수, 스테이지, 플레이타임 정보
public Text scoreTxt;
public Text stageTxt;
public Text playTimeTxt;
// 하단 왼쪽 플레이어 정보
public Text playerHealthTxt; // 현재 체력
public Text playerAmmoTxt; // 가지고 있는 탄약 갯수
public Text playerCoinTxt; // 보유 코인
// 하단 가운데 장비 이미지
// 좌클릭L은 일반공격이라서 따로 작성x
public Image weapon1Img;
public Image weapon2Img;
public Image weapon3Img;
public Image weaponRImg;
// 하단 우측 남은 몬스터 수를 체크해주는 텍스트
public Text enemyATxt;
public Text enemyBTxt;
public Text enemyCTxt;
// 상단 가운데 보스 체력바는 보스가 나왔을 때만 보이도록 하기
public RectTransform bossHealthGroup;
public RectTransform bossHealthBar; // 체력바(늘었다 줄었다 해야 하니까 따로 선언)
}
이렇게 선언해주고 Hierarchy에서 게임 오브젝트들을 끌어서 넣어준다.
// GameManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // UI를 사용할 때 필요
public class GameManager : MonoBehaviour
{
// 변수 선언부 생략
void Awake()
{
maxScoreTxt.text = string.Format("{0:n0}", PlayerPrefs.GetInt("MaxScore"));
}
// 게임시작 버튼을 위한 함수 생성
public void GameStart()
{
// 메뉴 관련 오브젝트 비활성화
menuCam.SetActive(false);
menuPanel.SetActive(false);
// 게임 관련 오브젝트 활성화
gameCam.SetActive(true);
gamePanel.SetActive(true);
// 플레이어 게임 오브젝트 활성화
player.gameObject.SetActive(true);
}
}
게임 시작 버튼의 OnClick() 이벤트에 GameManager를 붙인 오브젝트를 끌어다 놓고, GameStart() 함수를 연결
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // UI를 사용할 때 필요
public class GameManager : MonoBehaviour
{
// 변수 선언부 생략
// Awake() 생략
// GameStart() 생략
void Update()
{
// 싸울 때만 플레이 타임을 더해주기
if (isBattle)
{
playTime += Time.deltaTime;
}
}
// Update에서 이미 처리된 정보를 LateUpdate에서 표시하기
void LateUpdate()
{
// score값은 player가 갖고 있다.
scoreTxt.text = string.Format("{0:n0}", player.score);
stageTxt.text = "STAGE" + stage;
// playTime은 초단위니까 초단위를 시간, 분, 초로 나눠서 텍스트에 넣기
// playTime은 float형이니까 int로 강제형변환해주기
int hour = (int)(playTime/3600);
// 분은 위에서 계산한 시간을 playTime에서 빼고 그걸 60으로 나눠야 함
int min = (int)((playTime - hour *3600)/60);
int sec = playTime % 60;
// playTimeTxt.text = string.Format("{0:00}", hour) + ":" string.Format("{0:00}", min) + ":" + string.Format("{0:00}", sec);
// 이 영상에서는 위의 주석처럼 코드를 작성했는데, 최근 영상에서 아래와 같은 형식으로 코드를 작성했던 게 생각나서 이렇게 써봤다.
playTimeTxt.text = string.Format("{0:D2}:{1:D2}:{2:D2}", hour, min, sec);
playerHealthTxt.text = player.health + " / " + player.maxHealth;
playerCointTxt.text = string.Format("{0:n0}", player.coin);
//플레이어가 갖고 있는 무기에 따라 탄약의 수를 표시하기 탄약을 사용하지 않는 무기는 '-'라고 표시
if (player.equipweapon == null) playerAmmoTxt.text = "- / " + player.ammo;
else if (player.equipweapon.type == Weapon.Type.Melee) playerAmmoTxt.text = "- / " + player.ammo;
else playerAmmoTxt.text = player.equipWeapon.curAmmo + " / " + player.ammo;
// 무기 아이콘은 보유 상황에 따라 알파값a(=투명도)만 변경
// player.hasWeapon[i]가 있으면 1, 아니면 0 -> 삼항연산자를 이용하여 표현하기
weapon1Img.color = new Color(1, 1, 1, player.hasWeapon[0] ? 1 : 0);
weapon2Img.color = new Color(1, 1, 1, player.hasWeapon[1] ? 1 : 0);
weapon3Img.color = new Color(1, 1, 1, player.hasWeapon[2] ? 1 : 0);
weaponRImg.color = new Color(1, 1, 1, player.hasGrenades>0 ? 1 : 0); // 수류탄은 0개보다 많이 갖고 있으면 1 아니면 0으로 해주기
// 적의 숫자
enemyATxt.text = enemyCntA.ToString();
enemyBTxt.text = enemyCntB.ToString();
enemyCTxt.text = enemyCntC.ToString();
// 보스의 체력 이미지의 Scale을 남은 체력 비율에 따라 변경
// boss.curHealth와 boss.maxHealth 모두 int형이기 때문에 둘 중 하나를 float으로 변경해줘야 한다.
bossHealthBar.localScale = new Vector3((float)boss.curHealth/boss.maxHealth, 1, 1);
}
}
3D 쿼터뷰 액션게임 - 게임 완성하기 [유니티 기초 강좌 BE5]
게임오버 구현하기
//Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
// 플레이어 스크립트에 게임매니저 변수 선언
public GameManager manager;
IEnumerator OnDamage(bool isBossAtk)
{
// 앞뒤 코드 생략
// !isDead를 안 넣어주면 체력이 0 이하가 되었을 때 계속 OnDie()를 호출해서 죽는 현상 발생
// 이미 죽었다면 더이상 OnDie()를 호출하지 않도록 조건을 추가한 것
if(health <=0 && !isDead)
OnDie();
}
void OnDie()
{
anim.SetTrigger("doDie");
isDead = true;
manager.GameOver();
}
}
// GameManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // UI를 사용할 때 필요
using UnityEngine.SceneManagement; // Scene 관련 함수를 사용할 때 필요
public class GameManager : MonoBehaviour
{
public GameObject overPanel;
public Text curScoreText;
// 현재 점수가 최고 점수일 경우 나타나도록 만들기
public Text bestText;
public void GameOver()
{
// UI
overPanel.SetActive(ture);
gamePanel.SetActive(false);
curScoreText.text = scoreTxt.text;
// 기존에 저장된 최고점수를 불러와서 maxScore에 저장
int maxScore = PlayerPrefs.GetInt("MaxScore");
// 지금 달성한 점수player.score가 저장된 최고점수maxScore보다 높을 경우
if(player.score > maxScore)
{
bestText.gameObject.SetActive(ture);
// 최고점수를 현재 달성한 점수player.score로 갱신하기
PlayerPrefs.SetInt("MaxScore", player.score);
}
}
// 게임 오버 후, 돌아가기 위해 재시작 함수를 생성
public void Restart()
{
SceneManager.LoadScene(0);
}
}
그리고 Restart 버튼의 OnClick()에 GameManager 오브젝트를 끌어다 놓고 Restart 함수를 설정한다.
버튼 오브젝트 > Button컴포넌트에 Navigation이 Automatic으로 설정되어 있으면 스페이스바, 탭, 엔터에 따라 버튼이 작동할 수 있다.
이를 방지하기 위해 Navigation을 None으로 바꿔주기
이미지 배치할 때 Slice-Simple → SetNativeSize → Simple-Slice로 설정 변경하기
(왜 이렇게 하는 거지?? 이렇게 바꾸면 무슨 차이가 있을까)
public과 SerializeField 선언의 차이점
Unity - 다른 스크립트 함수, 변수를 불러올때 (디자인 패턴 싱글톤)
GameManager과 같은 클래스의 인스턴스는 하나의 게임에 여러 개가 있으면 안 될 때가 있다. 이럴 때 싱글턴을 지정해줘야 하는데, static을 통해서 수행할 수 있다.
if (instance == null){
instance = this;
}
else if (instance != this){
Destroy(gameObject);
}
this와 gameObject의 차이
this는 Component. 만약 Player.cs가 있다면 this는 Player Component.
gameObject는 Player.cs라는 스크립트 속 class Player를 통해 객체화된(Instantiate) 오브젝트를 의미한다.
🤔 this는 Player.cs를 오브젝트에 넣어서 Add Component 하는 것처럼? 스크립트 자체를 가지고 올 때 this를 쓰는 건가?
게임 오브젝트는 Unity의 기초적인 오브젝트로 캐릭터, 소품, 배경을 나타낸다. 독자적으로 많은 것을 하기보다는 기능을 구현하는 컴포넌트의 컨테이너 역할을 한다.
게임 오브젝트는 항상 위치와 오리엔테이션을 나타내기 위해 Transform 컴포넌트가 부착되어 있으며, 이를 제거할 수 없다.
컴포넌트는 게임에서 오브젝트와 동작에 관한 기본 구성요소. 모든 게임 오브젝트의 작동과 관련한 부품.
'Project' 카테고리의 다른 글
[Unity 3D Run] 해상도 설정 (0) | 2023.04.08 |
---|---|
[Unity 3D Run] 게임오버/클리어 UI (3) (1) | 2023.03.25 |
[Unity 3D Run] 게임 오버 UI(1) (0) | 2023.03.16 |
[Unity 3D Run] 장애물 충돌 설정 (0) | 2023.03.13 |
[Unity 3D Run] 캐릭터 이동 코드 수정, 카메라 추적 (0) | 2023.03.12 |