프로젝트 UI 관리하기

Tom·2024년 9월 29일
0
post-custom-banner

일반적인 프로그램이라면, UI는 없어선 안될 요소다. UI가 없으면 사용자가 뭘 할 수 있는데? 터미널 열어서 코드 칠꺼임?
유저는 UI를 통해 직관적이고 편리하게 시스템을 동작시키고, 개발자는 그 UI가 의도에 맞게 동작할 수 있도록 피땀눈물을 흘리면 된다. 공격버튼을 누르면 공격하고, 스킬버튼을 누르면 스킬이 나가는 건 모두 기능과 UI의 버튼이 연결되어 있기 때문. 누가 연결하냐고요? 흠.

  버튼이 한 두개가 아니어요

유명한 온라인 게임을 생각해보면, 진짜 UI요소가 한 두개가 아니다. 말 그대로 진짜 한 두개가 아님. 메뉴, 인벤토리, 옵션으로 이동시켜주는 버튼도 있고, 설정창에서 음량을 조절하는 슬라이더도 있고, 음소거, 진동 on/off를 설정하게 도와주는 토글도 있고, 게임 내내 화면에서 체력이나 쿨타임등 플레이의 중요한 정보를 알려주는 HUD도 있다. 적을 때리면 나오는 숫자도 UI다!
프로젝트를 진행하다 보면 찍어내야 할 UI가 엄청나게 많다는 이야기고, 그래서 일관적이지만 유연한 UI 관리 시스템이 있으면 좋겠다! 라고 생각했다.

UI와 데이터를 분리하자

화면에 띄워지는 UI 역시 데이터덩어리다. 이를 UI와 분리하고, 필요한 UI의 외형만 프리팹으로 관리한다면 상황에 따라 사용 시에 다른 데이터로 채워서 사용할 수 있겠다.

public class BaseUIData
{

}

public class BaseUI : MonoBehaviour
{
	public virtual void Init()
    {
    	//~초기화시에 필요한 내용 ~
    }
    
    public virtual void SetInfo(BaseUIData data)
    {
    	//필요한 데이터를 받아와서 세팅해준다
    }
}

UI와 UIData를 분리하여 관리한다면, 언제든 필요할 때 SetInfo() 메서드를 통해 내부 데이터를 정리해줄 수 있다. 상황에 따라 필요한 UI를 그때그때 만들지 않아도 된다는 점.

public enum UIDataType
{
	Alert, Confirm
}
public class UIManager // 싱글톤으로 구현하자.
{
    private BaseUI frontUI;
    private Dictionary<UIDataType, BaseUIData> uiDataPool = new Dictionary<UIDataType, BaseUIData>();
    private Dictionary<System.Type, GameObject> openUIPool = new Dictionary<System.Type, GameObject>();
    private Dictionary<System.Type, GameObject> closedUIPool = new Dictionary<System.Type, GameObject>();
    
    public void OpenUI(BaseUIData data)
    {
    	// 매개변수로 받아온 BaseUIData를 기반으로 UI를 열어주는 메서드
        // + openUIPool에 넣어주는 코드
        // + frontUI를 최신화해주는 코드
    }
    
    public void CloseUI(BaseUI ui)
    {
    	// openUI풀에서 받아온 BaseUI를 지워주고 CloseUI로 이동시켜주는 코드
        // + UI 비활성화 해주기
    }

	public BaseUI GetFrontUI()
    {
       return frontUI; // 최상단 UI 가져오기
    }
}

매니저에선 현재 최상단에 위치한 frontUI, 자주 사용되는 UI 데이터를 캐싱해놓을 uiDataPool, 현재 켜져있는 UI를 관리하는 openUIPool, 닫혀진 UI를 관리할 closedUIPool을 통해 런타임 로드를 최소화하는 방법을 사용했다. 아래에 이어서 프리팹으로 저장된 UI 오브젝트를 불러오는 코드, 또 비활성화 하는 코드를 작성하면 됨. 불러와서 켜줄 땐 openUIPool에 넣어주고, 비활성화할 땐 closedUIPool로 이동시켜주면 된다.

어떻게 써요?

유저에게 퀘스트를 수락 할건지, 안할건지를 물어보는 UI 팝업이 필요하다고 가정해보자. 필요한 데이터는 "정말 수락하시겠습니까?" 라고 물어보는 내용과 각 버튼을 누르면 실행될 로직, 그리고 버튼에 위에 올라갈 텍스트가 되겠다.

public enum ConfirmType
{ 
    YesOnly, // 수락 or 확인만 누를 수 있는 컨펌창일 경우
    YesOrNo  // 수락과 거부가 동시에 필요한 경우
}

public class ConfirmUIData : BaseUIData
{
    public ConfirmType ConfirmType;
    public string descText = "정말 수락하시겠습니까?";
    public string okBtnText = "예";
    public Action onClickOkBtn;
    public string cancelBtnText = "아뇨? 제가 미쳤습니까?";
    public Action onClickCancelBtn;
}

public class ConfirmUI : BaseUI
{
	// 각 멤버와 맞는 UI Element를 연결해준다 (인스펙터에서 해도 됨)
	public TextMeshProUGUI descText; 
    public Button okBtn;
    public Button cancelBtn;
    public TextMeshProUGUI okBtntext;
    public TextMeshProUGUI cancelBtnText;
    
	public override void SetInfo(BaseUIData _data)
    {
    	base.SetInfo(_data);
        data = _data as ConfirmUIData;
        descText.text = data.descText;
        okBtntext.text = data.okBtnText;
        onClickOkBtn = data.onClickOkBtn;
        cancelBtnText.text = data.cancelBtnText;
        onClickCancleBtn = data.onClickCancelBtn;

        okBtn.gameObject.SetActive(true); // 확인버튼은 항상 활성화
        // 거부 버튼은 ConfirmType이 YesOrNo일 경우에만 활성화
        cancelBtn.gameObject.SetActive(data.ConfirmType == ConfirmType.YesOrNo);
        
        // 리스너 달아주기
		okBtn.onClick.AddListener(OnClickOkBtn);
        cancelBtn.onClick.AddListener(OnClickCancelBtn);
    }
    
    public void OnClickOkBtn()
    {
        onClickOkBtn?.Invoke();
        onClickOkBtn = null;
        CloseUI();
    }

    public void OnClickCancelBtn()
    {
        onClickCancleBtn?.Invoke(); 
        onClickCancleBtn = null;
        CloseUI();
    }
}

자 이제 써봐야겠지?

이미지와 텍스트, 버튼 두개를 이용해서 간단한 UI 프리펩을 만들었다.

using System;
using UnityEngine;

public class Test : MonoBehaviour
{
    Action act;
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Return))
        {
            ConfirmUIData confirmUIData = new ConfirmUIData()
            {
                ConfirmType = ConfirmType.YesOrNo,
                descText = "정말 수락하시겠습니까?",
                okBtnText = "예",
                cancelBtnText = "아뇨? 제가 미쳤습니까?",
                onClickOkBtn = () => Debug.Log("정말 좋은 선택입니다"),
                onClickCancelBtn = () => Debug.Log("정말 좋은 선택입니다"),
            };
            UIManager.Instance.OpenUI<ConfirmUI>(confirmUIData);
        }
    }
}

테스트를 위해 엔터를 누르면 UI를 실행하는 코드도 작성했슴.
의도한바대로 잘 작동하는걸 확인했다.

그럼 다른 데이터를 넣어서 다른 UI 내용을 만드려면?

if (Input.GetKeyDown(KeyCode.F))
{
    ConfirmUIData confirmUIData = new ConfirmUIData()
    {
        ConfirmType = ConfirmType.YesOnly,
        descText = "왜 수락했습니까?",
        okBtnText = "뭐요",
        onClickOkBtn = () => Debug.Log("정말 좋은 선택입니다")
    };
    UIManager.Instance.OpenUI<ConfirmUI>(confirmUIData);
}

아래에 F로 작동하는 새로운 UI를 만드는 코드를 추가했다.
이것도 잘 잘동했다. 끝!

++ 새로 알아간 사실 : Canvas를 생성할 때 매번 UI에서 딸깍으로 생성해서 정확하게 알지 못했는데, Canvas는 Canvas, Canvas Scaler, Graphic Raycaster로 구성되어있다. 특히 GraphicRaycaster가 빠지면 입력 레이캐스팅을 감지 못하니 조심하도록 하자. (한 10분 고생함)

profile
여기 글은 보통 틀린 경우가 많음
post-custom-banner

0개의 댓글