게임 로고 이미지

UI는 어느 게임이든 들어가는 중요한 부분 중 하나입니다. 그중 팝업UI는 가장 많이 쓰이기 때문에 쓰기 쉽도록 시스템을 설계 해 놓지 않으면 팝업을 생성하고 사용하는데 시간을 많이 사용하기도 합니다. 이번 시간에는 팝업을 쉽게 관리하는 방법을 알아보겠습니다.


[팝업UI란?]

화면 전체를 가리는 것이 아닌 일부만 가리는 UI를 뜻합니다. 예시로는 장비 아이템의 설명 UI, 메시지 팝업, 랭킹을 보여주는 랭킹 팝업 등이 있습니다.

[필요성]

그렇다면 왜 화면 전체를 가리는 UI와 구분을 지을까요? 용도가 다르기 때문 입니다. 보통 팝업은 화면 전체를 가린 후 한번 더 UI를 띄워야 하는 경우에 많이 사용 됩니다. 장비 인벤토리를 띄운 후 장비에 마우스를 대면 장비에 대한 설명이 필요한 UI를 띄우는 것처럼 말이죠.

[구현 코드]

using UnityEngine;
using UnityEngine.UI;

public class Popup : MonoBehaviour
{
    public Animator openAnimator;
    public Animator closeAnimator;

    private void Start()
    {
        gameObject.SetActive(false);
    }

    public void Open(System.Action onOpened = null)
    {
        if (openAnimator != null)
        {
            gameObject.SetActive(true);
            openAnimator.SetTrigger("Open");
            StartCoroutine(WaitForAnimation(openAnimator, onOpened));
        }
        else
        {
            onOpened?.Invoke();
        }
    }

    public void Close(System.Action onClosed = null)
    {
        if (closeAnimator != null)
        {
            closeAnimator.SetTrigger("Close");
            StartCoroutine(WaitForAnimation(closeAnimator, () =>
            {
                onClosed?.Invoke();
                Destroy(gameObject);
            }));
        }
        else
        {
            onClosed?.Invoke();
            Destroy(gameObject);
        }
    }

    // 애니메이션이 끝난 후 콜백 처리 코루틴
    private IEnumerator WaitForAnimation(Animator animator, System.Action callback)
    {
        // 애니메이션 상태가 "Base Layer.Idle"이 될 때까지 루프
        while (!animator.GetCurrentAnimatorStateInfo(0).IsName("Base Layer.Idle"))
        {
            yield return null;
        }

        callback?.Invoke();
    }

    // UI 업데이트 호출 메서드
    protected virtual void UpdateUI()
    {
        
    }
}

팝업의 기본이 되는 클래스 입니다. 코루틴을 사용하여 애니메이션이 끝나는 시점에 콜백을 호출하게 돼 있습니다. 팝업의 경우 열거나 닫을 때 특정한 처리를 필요하는 경우가 많기 때문에 콜백을 추가 한 것 입니다.

UpdateUI메서드는 UI를 변경할 필요가 있을 때 사용하면 됩니다. 이 메서드가 필요한 이유는 데이터가 변경 될 경우나 모든 UI를 갱신 해야할 경우 현재 활성화 된 팝업의 UpdateUI함수만 호출 해주면 편리하게 갱신이 가능하기 때문입니다.

// 팝업을 관리하는 클래스
public class PopupManager : MonoBehaviour
{
    // 싱글톤 패턴을 사용한 인스턴스 접근
    private static PopupManager _instance;
    public static PopupManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindObjectOfType<PopupManager>();
                if (_instance == null)
                {
                    GameObject managerObject = new GameObject("PopupManager");
                    _instance = managerObject.AddComponent<PopupManager>();
                }
            }
            return _instance;
        }
    }

    // 팝업 프리팹을 저장하는 딕셔너리
    private Dictionary<string, GameObject> popupPrefabs = new Dictionary<string, GameObject>();

    // 열린 팝업을 저장하는 딕셔너리
    private Dictionary<string, Popup> popups = new Dictionary<string, Popup>();

    public void OpenPopup(string popupName)
    {
        // 팝업 프리팹이 딕셔너리에 없다면 로드
        if (!popupPrefabs.ContainsKey(popupName))
        {
            GameObject prefab = Resources.Load<GameObject>(popupName);
            if (prefab != null)
            {
                popupPrefabs[popupName] = prefab;
            }
            else
            {
                Debug.LogError("Popup prefab not found: " + popupName);
                return;
            }
        }

        // 이미 열린 팝업이 없다면 생성
        if (!popups.ContainsKey(popupName))
        {
            GameObject popupObject = Instantiate(popupPrefabs[popupName]);
            var popupComponent = popupObject.GetComponent<Popup>();
            if (popupComponent != null)
            {
                popups[popupName] = popupComponent;
            }
            else
            {
                Debug.LogError("Popup component not found on prefab: " + popupName);
                return;
            }
        }

        // 생성된 팝업 열기
        popups[popupName].Open();
    }

    public void ClosePopup(string popupName)
    {
        // 딕셔너리에 해당 팝업이 있다면 닫기
        if (popups.TryGetValue(popupName, out var popup))
        {
            popups.Remove(popupName);
            popup. Close();
        }
    }

    // 팝업 이름을 이용하여 특정 팝업을 가져오는 함수
    public Popup GetPopup(string popupName)
    {
        if (popups.TryGetValue(popupName, out var popup))
        {
            return popup;
        }
        return null;
    }
}

싱글톤을 이용하여 팝업의 이름으로 접근하여 열고 닫는 것이 쉽도록 하였습니다. 또한 프리팹을 딕셔너리에 저장하는 이유는 최적화 때문입니다. 이미 로드한 프리펩을 재사용 할 수 있도록 딕셔너리에 담아 놓는 것 입니다. 가장 기본이 되는 기능들만 만들었으니 필요한 기능은 추가하여 사용하시면 되겠습니다.

[사용 예시]

메세지 팝업을 만드는 것을 가정하여 코드를 작성해 보겠습니다.

먼저, 코드를 작성하기 전에 메세지 프리팹을 만듭니다. 그 후 MessagePopup 스크립트를 제작하여 해당 프리팹에 추가합니다. 아래는 MessagePopup 스크립트 입니다.

public class MessagePopup : Popup
{
    public Text messageText;

    private string message;

    // 메세지를 설정하는 메서드
    public void SetMessage(string newMessage)
    {
        message = newMessage;
        UpdateUI(); // 데이터가 변경되면 UI를 업데이트
    }

    // UI 업데이트 메서드
    protected override void UpdateUI()
    {
        base.UpdateUI();
        messageText.text = message;
    }

    public void OnCloseButtonClick()
    {
        Close();
    }
}

Popup클래스를 상속 받아서 만들었습니다. OnCloseButtonClick을 클릭 버튼에 연결 시켜서 팝업을 닫도록 하면 됩니다. UpdateUI에는 UI를 설정해주는 코드를 작성하여 넣어 주시면 됩니다.

// MessageButton.cs
using UnityEngine;
using UnityEngine.UI;

public class MessageButton : MonoBehaviour
{
    // 연결된 버튼
    public Button button;

    private void Start()
    {
        // 버튼에 클릭 이벤트 추가
        button.onClick.AddListener(OpenMessagePopup);
    }

    private void OpenMessagePopup()
    {
        // 버튼을 눌렀을 때 MessagePopup 열기
        PopupManager.Instance.OpenPopup(StringConst.MessagePopup);
        
        // MessagePopup에 메세지 설정 (예시: "Hello, World!")
        if (PopupManager.Instance.GetPopup(StringConst.MessagePopup) is MessagePopup messagePopup)
        {
            messagePopup.SetMessage("Hello, World!");
        }
    }
}

팝업을 열기위한 버튼을 만든 클래스의 예시 입니다. 팝업을 열고 필요한 설정을 SetMessage를 통해 해주면 됩니다.

[팝업UI 구조]

크게 두 가지로 나뉩니다. 하나의 캔버스에 필요한 팝업들을 미리 생성하여 껐다 켰다 하는 방식프리팹을 만들 때 캔버스 밑에 팝업을 만들어서 팝업 생성 시 캔버스와 함께 생성하는 방법 입니다.(이럴 경우 팝업을 2개 생성 했다면 캔버스도 2개가 되는 구조 입니다.) 프로젝트 상황이나 사용자의 선호도에 따라서 사용하시면 될 것 같습니다.

[코드의 장점]

  • 가장 필요한 핵심 기능들만 작성 되어 필요한 기능을 추가하는 것이 쉽습니다.
  • 이해하기 쉬우며 UI를 만들 때 확장성이 좋습니다.
    • 전체 닫기나 전체 UI 갱신 같은 기능들을 고려하여 만든 코드 이기에 그렇습니다.
  • 싱글톤을 이용하여 팝업을 관리하기 때문에 열고 닫는 것이 쉬우며 성능을 고려하여 만들었습니다.
  • 애니메이션을 이용하여 열고 닫는 효과를 제어하기에 효과적인 협업이 가능합니다.

[코드의 단점]

  • 핵심 기능만 있기 때문에 필요한 기능은 추가 해야 합니다.
  • 1인개발 시 직접 열고 닫는 효과를 제어한다면 애니메이션을 만들어 줘야 합니다.
    • 만약 Dotween을 사용하고 싶다면 애니메이션을 플레이 하는 부분을 수정해 주면 됩니다.

[마무리]

팝업UI 시스템은 잘못 설계하게 되면 프로젝트의 생산성을 급격하게 저하 시키기 때문에 처음에 최대한 깊이 생각하셔서 코드를 구현하셔야 합니다. 규모가 있는 프로젝트 일수록 더욱 그렇습니다. 또한 상황에 따라서 중요도가 달라지기도 합니다. 극단적인 예시로 게임의 팝업이 하나만 필요하고 띄우는 횟수도 많지 않다면 설계 자체가 필요 없습니다. 하지만 팝업이 1개가 아닌 여러 개 라면 작성된 코드를 보시고 깊이 생각하셔서 자신만의 팝업UI 시스템을 만들어 보는 것을 추천 드립니다.

[같이 보면 좋은 글]

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다