오늘은 굉장히 많은 변경사항이 있었다.
어제 발생했던 문제는
크게 두 가지였다.
기존에 팝업 텍스트를 관리하는 방법은 Dictionary로 키와 값으로 관리했다.
private Dictionary<string, string> popupTxts = new Dictionary<string, string>();
public PopupTexts()
{
popupTxts.Add("exitConfirmation", "정말 나가시겠습니까? 현재 진행한 데이터를 모두 잃을 수 있습니다.");
}
이렇게 했을 때 단점은 "팝업텍스트에 모든걸 두면 과하지 않을까?" 라는 피드백을 튜터님께 받을 수 있었다.
또한 관리적인 측면에서도 유니티의 다양한 기능을 전혀 활용하지 않고 있었다.
여기에 다양한 해결 방법이 있었는데,
Scriptable Object 사용으로 해결하기로 했다.
우선 팝업 데이터를 담고 있는 SO를 제작했다.
팝업 데이터를 구조체로 제작한 이유는 내용이 중간에 변경되지 않기 때문이다.
만약 동적으로 내용이 바뀐다면 클래스로 제작하는 것이 좋다.
using UnityEngine;
[System.Serializable]
public struct PopupData
{
public int popupKey;
public string title;
public string message;
public string leftButtonText;
public string rightButtonText;
}
[CreateAssetMenu(fileName = "PopupSO", menuName = "Popup/PopupSO", order = 0)]
public class PopupSO : ScriptableObject
{
public PopupData[] popups;
}
UIManager에서 팝업에 대한 정보를 불러와 관리할 것이다.
SO데이터는 어드레서블 에셋과 연동했다.
public async void OpenConfirmationPopup(int popupKey, Action leftButtonAction, Action rightButtonAction)
{
await ScriptableObjectManager.Instance.LoadScriptableObject(AddressableLabel.PopupSO);
PopupSO foundPopupDataSO = null;
foreach(var so in ScriptableObjectManager.Instance.SOBaseDic[AddressableLabel.PopupSO])
{
if(so is PopupSO popupDataSO)
{
foundPopupDataSO = popupDataSO;
break;
}
}
if(foundPopupDataSO != null)
{
foreach (var popupData in foundPopupDataSO.popups)
{
if (popupData.popupKey == popupKey)
{
ConfirmationPopup popup = OpenUI<ConfirmationPopup>();
popup.SetTitle(popupData.title);
popup.SetMessage(popupData.message);
popup.SetLeftButtonText(popupData.leftButtonText);
popup.SetRightButtonText(popupData.rightButtonText);
popup.SetLeftButtonAction(leftButtonAction);
popup.SetRightButtonAction(rightButtonAction);
break;
}
}
}
else
{
Debug.LogError("팝업데이터 없음");
}
}
현재 언어의 정보를 불러와서 저장하는 역할을 한다. 이때 팝업에서 현재 Locale을 정보를 가져가면, 키값을 자유롭게 변경하게 제작했다.
public static class GlobalSettings
{
public static string CurrentLocale { get; set; }
}
void OnSelectionChanged(int index)
{
if (index >= 0 && index < LocalizationSettings.AvailableLocales.Locales.Count)
{
var locale = LocalizationSettings.AvailableLocales.Locales[index];
LocalizationSettings.SelectedLocale = locale;
GlobalSettings.CurrentLocale = locale.Identifier.Code;
int selectedIndex = LocalizationSettings.AvailableLocales.Locales.IndexOf(locale);
languageDropdown.SetValueWithoutNotify(selectedIndex);
Debug.Log($"Locale changed to: {GlobalSettings.CurrentLocale}");
}
else
{
Debug.LogWarning("Selected language index is out of range.");
}
}
해당하는 키만 불러와서, 현재 locale의 정보를 파악하고 키값만 교체해 Localization까지 적용할 수 있었다!
public void OnExitBtn()
{
int exitPopupKey = GlobalSettings.CurrentLocale == "en-US" ? 2000 : 1000;
UIManager.Instance.OpenConfirmationPopup(
exitPopupKey,
() => {
UIManager.Instance.CloseUI<ConfirmationPopup>();
},
() => {
LoadingSceneController.LoadScene("StartScene");
}
);
}
어드레서블 에셋을 프로젝트에서 왜 사용할까?
비동기적 에셋 관리의 중요성을 알게 됐다.
어드레서블 에셋을 사용하면 필요할 때만 특정 에셋을 메모리에 로드할 수 있다. 특히 게임이나 대규모 응용 프로그램에서 중요한 메모리 관리와 성능 최적화를 가능하게 한다. 불필요한 에셋이 메모리에 상주하는 것을 방지하여 전체 애플리케이션의 효율성을 높일 수 있었다.
비동기 로딩은 메인 스레드를 차단하지 않고 에셋을 로드한다. 사용자 인터페이스(UI)가 멈추거나 지연되는 것을 방지하고, 게임이나 애플리케이션의 반응성을 유지하는 데 도움이 된다.
어드레서블 시스템은 에셋을 그룹화하고, 태그를 지정하여 관리할 수 있게 한다.
개발자는 필요에 따라 다양한 에셋을 보다 쉽게 찾아서 사용할 수 있었다.
아마 대규모 프로젝트에서 특히 유용할 것 같다.
어드레서블 시스템은 로컬뿐만 아니라 네트워크를 통해 에셋을 로드할 수 있는 기능을 제공해준다.
콘텐츠의 동적 업데이트나 확장이 가능해지며, 사용자가 필요로 할 때 에셋을 다운로드할 수 있어 애플리케이션의 초기 다운로드 크기를 줄일 수 있다고 한다. 아직 빌드를 해보지 않았지만, 이러한 장점을 위해 도입했다.
스크립터블 오브젝트를 통해 데이터를 구조화하고, 어드레서블 에셋을 사용하여 이를 관리함으로써 코드의 간결성을 유지할 수 있다.
코드의 가독성을 높이고 유지보수를 용이하게 한다.
UI를 제작하다보면, 여러가지 UI 요소들이 겹쳐 표시될 때, 어떤 UI가 사용자에게 보여져야하는지 결정하는데 Sorting Order가 중요하다.
특히, 팝업이나 메시지 박스같은 정보들이 제대로 표시되지 않으면 UX가 크게 저하된다.
나 또한 Sorting Order를 프리팹 하나하나 설정했는데, 이러한 작업이 불필요하게 느껴졌고, UI를 켰다 껐다 하는 작업에서 자동으로 Sorting Order를 자동으로 관리하도록 스크립트를 제작했다.
이때 스택을 이용했는데, 스택은 LIFO(Last In, First Out, 후입선출) 구조다.
사용자가 가장 최근에 상호작용한 UI 요소에 빠르게 접근할 수 있게 해준다.
예를 들어, 여러 레벨의 메뉴가 중첩될 때, 사용자는 '뒤로' 버튼을 통해 직전의 상태로 쉽게 돌아갈 수 있다.
스택을 사용하여 소팅 오더 관리: UIManager에 UI 요소의 참조와 소팅 오더를 저장하는 스택을 추가한다.
UI 열기 시 소팅 오더 적용: 새 UI 요소를 열 때, 스택에 추가하고, 해당 UI 요소의 Canvas 소팅 오더를 설정한다.
UI 닫기 시 소팅 오더 업데이트: UI 요소를 닫을 때, 스택에서 제거하고, 스택의 최상단 UI 요소의 소팅 오더를 업데이트한다.
using UnityEngine;
public class UIElement
{
public GameObject GameObject;
public int SortingOrder;
public UIElement(GameObject gameobject, int sortingOrder)
{
GameObject = gameobject;
SortingOrder = sortingOrder;
}
}
private Stack<UIElement> uiStack = new Stack<UIElement>();
private int currentSortingOrder = 0;
public T OpenUI<T>() where T : Component
{
string prefabName = typeof(T).Name;
string path = "UI";
if (UIElements.TryGetValue(prefabName, out GameObject uiElement))
{
if (uiElement == null)
{
UIElements.Remove(prefabName);
return CreateUI<T>(path, prefabName);
}
else
{
uiElement.SetActive(true);
return uiElement.GetComponent<T>();
}
}
else
{
T newComponent = CreateUI<T>(path, prefabName);
if (newComponent != null)
{
UIElements.Add(prefabName, newComponent.gameObject);
newComponent.gameObject.SetActive(true);
}
var canvas = newComponent.GetComponent<Canvas>();
if (canvas != null)
{
canvas.sortingOrder = ++currentSortingOrder;
uiStack.Push(new UIElement(newComponent.gameObject, currentSortingOrder));
}
return newComponent;
}
}
public void CloseUI<T>() where T : Component
{
string prefabName = typeof(T).Name;
if (UIElements.TryGetValue(prefabName, out GameObject uiElement))
{
uiElement.SetActive(false);
}
else
{
Debug.LogWarning($"CloseUI 호출됨: {prefabName} 타입의 UI 요소를 찾을 수 없다.");
}
if (uiStack.Count > 0 && uiStack.Peek().GameObject == uiElement)
{
uiStack.Pop();
currentSortingOrder = uiStack.Count > 0 ? uiStack.Peek().SortingOrder : 0;
}
}
키값을 교체해주니 잘 적용되었다!
첫 번째 UI
두 번째 UI
Sort Order 값이 잘 적용됐다!