5주차 팀 프로젝트: 아마도.. 궁수의 전설 (1)
저는 UI를 담당하였습니다.
요 며칠간은 UI 작업하느라 TIL에 정말 쓸 내용이 없었습니다.
유니티) 화면 해상도 대응
최근 간단한 미니 게임을 만들어 공유하였으나제가 작업한 해상도와 유저의 화면 해상도의 차이가 있어버튼이 겹치거나 이미지가 삐져나오는 등 UI 배치에 문제가 발생하였습니다. 이를 해결
ybbro.tistory.com
예전에 공부한 내용 그대로 여러 화면 해상도에 대응하기 위한 기법들을 사용하였습니다.
위 포스팅 내용 외에는 기술적인 면보다는 끈기와 눈썰미가 요구되는 작업이었습니다.
하지만 UI/UX는 게임에서 유저 플레이 체감에 중요한 요소이기에 정성을 다해 만들었습니다.
1. 전투 UI 화면 구성
1) 상단 UI
- 일시정지 버튼
- 레벨, 경험치 바
- 획득 재화 표시
2) 일시정지 UI
- 전투 일시 정지 기능
- 획득한 스킬 확인 기능
- 메인 화면으로 복귀 버튼
- 일시정지 해제(계속하기) 버튼
3) 스킬 선택(레벨업) UI
- 선택 도중 일시정지
- 레벨업 or 스테이지 클리어 상황, 3가지 스킬 중 하나 획득 가능
4) 게임오버 UI
- 메인화면으로 나가기 버튼
5) 스테이지 클리어 UI
- 메인 화면으로 나가기 버튼
- 다음 스테이지 이동 버튼
2. 실행 및 오류 수정
3. 사용 코드
팀원님들과의 원활한 협업 및 작업량이 많아질 때 제가 헤매지 않기 위해 전반적으로 주석을 상세하게 달았습니다.
코드를 거의 해석할 필요 없이 주석만 쭉 읽어도 어떤 변수, 함수, 기능인지 쉽게 파악할 수 있습니다.
1) UIManager_Battle.cs
* 특이사항
(1) 사용자가 프로그램 창 사이즈 변경 시 정해진 비율대로만 바뀌게끔
(현재 위아래 사이즈 조정에만 대응. 급한 작업이 끝나면 좌우도 대응되도록 마저 수정)
(2) UI 크기에 맞게 자동으로 버튼들의 폰트 사이즈 각각 설정 후 작은 사이즈로 통일
(UI에 통일감을 줌. 폰트 사이즈 차이로 완성도가 크게 떨어져 보이는 느낌을 받았기에 작성)
using UnityEngine;
using TMPro;
public class UIManager_Battle : SceneOnlyManager<UIManager_Battle>
{
[SerializeField] Transform Panel_Pause, Panel_LevelUp, Panel_GameOver;
// 레벨, 골드 : 게임 플레이 중 써주기 위함
// 계속하기, 나가기 버튼의 텍스트는 글자 크기를 맞춰주기 위해 가져옴
[SerializeField]
TextMeshProUGUI
gold_text,
level_text,
continue_button_text,
exit_button_text;
// 체력바
[SerializeField] RectTransform hpBar;
float targetAspectRatio; // 너비/높이로 게임 화면 비율 계산한 값
protected override void Awake()
{
// 다른 스크립트에서도 쉽게 접근 가능하게끔 인스턴스 생성
base.Awake();
// 게임 실행시의 해상도 비율을 기억
if (TryGetComponent(out UnityEngine.UI.CanvasScaler canvasScaler))
{
Screen.SetResolution((int)canvasScaler.referenceResolution.x, (int)canvasScaler.referenceResolution.y, false);
targetAspectRatio = canvasScaler.referenceResolution.x / canvasScaler.referenceResolution.y;
}
else
Debug.Log("캔버스 스케일러가 없어요");
// 일시정지 화면의 버튼 텍스트 크기 통일
Pause_Buttons_TextSize_Unify(continue_button_text, exit_button_text);
}
// RectTransform 자체가 변경되었을 때 호출 >> 빌드 후 창 크기 변경 때 호출
void OnRectTransformDimensionsChange()
{
MaintainAspectRatio();
}
/* 사용자가 프로그램 창 크기를 조절할 때 설정한 화면 비율에 맞게 늘어나거나 줄어들게끔
* 제작 사유 : 빈 부분 보이면 몰입감을 해칠 수 있음
* 호출 시점 : 창 크기가 바뀔 때마다
*
* 상하로 당기는 건 대응이 되나 좌우는 대응이 되지 않아 시도하다 꼬여서 원복... >> 생각보다 깔끔하게 되지는 않네요
* !!!!! : 당장 급하진 않으니 나중에 수정
*/
void MaintainAspectRatio()
{
// 현재 프로그램창 너비, 높이
int newWidth = Screen.width,
newHeight = Screen.height;
// 초기 화면비에 맞춰주기
newWidth = Mathf.RoundToInt(newHeight * targetAspectRatio);
// 해상도 설정
Screen.SetResolution(newWidth, newHeight, false);
}
// 일시정지 UI 활성화
public void Enable_Pause() => Panel_Pause.gameObject.SetActive(true);
// 레벨업 UI 활성화
public void Enable_LevelUp() => Panel_LevelUp.gameObject.SetActive(true);
// 게임오버/스테이지 클리어 UI 활성화
public void Enable_GameOver() => Panel_GameOver.gameObject.SetActive(true);
// 골드 획득량 표시 변화
public void SetGoldText(int gold) => gold_text.text = gold.ToString();
// exp 바 채워진 양 표시 변화
public void SetExpRatio(float ratio) => hpBar.localScale = new Vector3(ratio, 1, 1);
// 전투에서 나가기 버튼
// 전투 UI의 모든 메인 화면으로 나가기 버튼에서 해당 메서드 참조(똑같은 기능)
public void Button_Exit()
{
// !!!!! 공용으로 쓰는 씬 전환 매니저를 만들 것이라면 그걸 가져다 쓰기. 따로 없다고 하면 여기서 씬 전환해보자
// 현재 버튼에 연결은 해둔 상태
Debug.Log("나가기!");
}
// 레벨 표시 변경
public void SetLevelText(int nextLevel) => level_text.text = $"Lv. {nextLevel}";
/*
일시정지 UI의 전투 나가기, 계속하기 버튼의 안내 텍스트 사이즈 통일
테스트 결과 게임 실행 때 1번만 맞춰주면 이후에 창 크기를 줄이거나 늘려도 텍스트 사이즈가 동일함을 확인
제작 사유 : 글자 크기 차이가 나면 없어보임
*/
public void Pause_Buttons_TextSize_Unify(TextMeshProUGUI text1, TextMeshProUGUI text2)
{
// 폰트 자동 사이즈 옵션을 둘 다 켜주기
text1.enableAutoSizing = text2.enableAutoSizing = true;
// 폰트 자동 사이즈 옵션 적용 시 나가기, 계속하기 버튼의 텍스트 사이즈
float
continue_text_size = text1.fontSize,
exit_text_size = text2.fontSize;
// 텍스트 사이즈가 작은 쪽으로 통일
if (continue_text_size > exit_text_size)
{
// 자동 텍스트 사이즈 옵션 해제하고, 텍스트 사이즈 통일
text1.enableAutoSizing = false;
text1.fontSize = exit_text_size;
}
else
{
text2.enableAutoSizing = false;
text2.fontSize = continue_text_size;
}
}
protected override void OnDestroy()
{
base.OnDestroy();
}
}
2) UI_Pause
* 특이사항
팀원님들 각자 깃 브랜치에서 작업 후 합쳤기에
소통해가며 얻은 정보로 연결할 준비를 미리 해뒀다가
머지 이후 매개 변수만 기입하여 빠르게 연결
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.UI;
public class UI_Pause : MonoBehaviour
{
[SerializeField] Transform Panel_Skills;
// 스킬의 인덱스들을 모은 것
List<int> skills_index = new List<int>();
// 활성화 때 획득한 스킬들에 맞춰 스킬 아이콘을 출력하게끔
private void OnEnable()
{
// 시간 비율을 0으로 >> 게임 일시정지
Time.timeScale = 0;
// 플레이어 스킬 리스트만 가져오면 동작하게끔 만들어 둠
SkillIconsInit(SkillManager.Instance.SelectedSKills);
}
void SkillIconsInit(List<ISkill> playerSkills)
{
// 기존의 스킬 수
int previous_skillCount = skills_index.Count;
// 이전보다 많아진 스킬들에 대해
for (int i = previous_skillCount; i < playerSkills.Count; i++)
{
// 해당 위치에 추가할 스킬
int tmpSkill = playerSkills[i].Id;
// 새로 얻은 스킬을 리스트에 추가
skills_index.Add(tmpSkill);
// 새로 얻은 스킬을 표시할 비활성화 상태의 자식 오브젝트
Transform skillIconObject = Panel_Skills.GetChild(i);
// 스킬 테두리/스킬 배경/스킬 아이콘 계층 순서로 되어 있음
// 스킬 아이콘 Image 컴포넌트에 접근
if (skillIconObject.GetChild(0).GetChild(0).TryGetComponent(out Image iconImage))
{
// 해당 스킬에 알맞는 아이콘 설정
iconImage.sprite = TableManager.Instance.GetTable<SkillTable>().GetDataByID(tmpSkill).SkillIcon;
// 활성화하여 스킬 아이콘을 화면에 표시
skillIconObject.gameObject.SetActive(true);
}
}
}
// 일시정지 패널 비활성화
public void Disable_PausePanel()
{
// 시간 비율을 원 상태로 돌리고
Time.timeScale = 1;
// 패널 비활성화
gameObject.SetActive(false);
}
}
3) UI_LevelUP.cs
* 특이사항
팀원님이 작성한 코드, 구조 분석을 통한 기능 연결
기존의 약속과 차이점을 캐치하고 의사소통을 통해 빠르게 오작업 여지를 줄임
using UnityEngine;
using TMPro;
using System;
using System.Collections.Generic;
using UnityEngine.UI;
public class UI_LevelUP : MonoBehaviour
{
// 유저들에게 정보를 보여주기 위한 UI 오브젝트들
[SerializeField] SkillOption[] skillOptions;
// 스킬 선택지에 나온 스킬들의 인덱스. 선택지 3개 고정이라기에 크기 고정
int[] skillOptionIndexs = new int[3];
private void OnEnable()
{
// 레벨업 고르는 동안은 일시정지
Time.timeScale = 0;
SkillOptionInit();
}
// 레벨업 패널이 등장할 때, 레벨업 선택지 초기화
void SkillOptionInit()
{
// 랜덤하게 뽑은 3개의 스킬을 가져오게끔
HashSet<SkillData> skillDatas = new HashSet<SkillData>(SkillManager.Instance.GetSkillToSelect());
// HashSet은 인덱스가 없으므로 여기서 i를 선언하여 사용
int i = 0;
foreach (SkillData skillData in skillDatas)
{
// 각 버튼에 어떤 스킬이 들어가 있는지 인덱스 저장
skillOptionIndexs[i] = skillData.Id;
// UI 표시 정보 변경
skillOptions[i].name.text = skillData.Name;
skillOptions[i].icon.sprite = skillData.SkillIcon;
skillOptions[i].info.text = skillData.Info;
// 다음 인덱스로
i++;
}
}
public void Button_OptionSelect(int buttonIndex)
{
// 선택한 버튼과 일치하는 스킬 인덱스를 통해 플레이어 스킬 추가
SkillManager.Instance.SelectSkill(skillOptionIndexs[buttonIndex]);
// 일시정지 해제
Time.timeScale = 1;
// 스킬 선택이 끝났으니 레벨업 패널 비활성화
gameObject.SetActive(false);
}
// 스킬 선택 버튼에 스킬 정보를 표시할 UI오브젝트들
[Serializable]
public class SkillOption
{
public TextMeshProUGUI name;
public Image icon;
public TextMeshProUGUI info;
}
}