본문 바로가기

개발/Unity

[Unity] Merge-Is-Mine 범용적인 팝업시스템 구성

선요약

Registry 패턴을 응용하고, 구현하여

기존 팝업시스템의 데이터구성을 최적화한다. O(n^2) -> O(n)

 

팝업을 하나의 변수내에서 관리하되, 입력받는 데이터를

자동으로 알맞은 팝업에 할당하여 구성하는 시스템을 구현한다


구성목표

 

위와같이 여러 팝업이 뜨는 것을 구현해야할 필요가 있었다.

 

이때, 각 팝업당 필요한 정보가 각각 다른데, 팝업당 각각의 inspector property로 집어넣게되면

팝업하나가 늘어날때마다, 관리해야할 변수가 하나씩 더 늘어나고,

각각 팝업을 관리해야하여 유지보수에 매우 불편함이 발생하게 된다.

 

또한 리스트로 한번에 관리한다고 해도, 팝업데이터구성에 

데이터자료 개수 n, 그에따른 팝업 개수 n으로 했을때,

각 데이터별로 리스트를 모두 순회하여 판별해줘야하기 때문에 O(n^2)의 복잡도를 가진다. 

 

 

따라서 하나의 리스트로 관리하고싶어 해당 시스템 구현을 생각하게 되었다.

 

1. 다양한 팝업을 하나의 리스트로관리

2. 팝업데이터를 구성 시, 자동으로 입력데이터에 맞는 팝업을 찾아서 구성, 이때 복잡도를 최대한 줄이기

 


 

기초 구조 구현

/// <summary>
    /// 팝업시스템의 공통 함수 인터페이스
    /// </summary>
    public interface IPopup
    {
        //공통함수 목록
        void Init();
        void Open();
        void Close();
    }
    

    public interface IPopup<in T> : IPopup
    {
        void Apply(T value);
    }

 

우선 다음과같이 팝업에 기본으로 들어가야 하는 함수와, 

적용할 인자가 팝업마다 각각 다른 Apply함수를 generic을 이용하여 구성해주었다.

 

 

    /// <summary>
    /// 팝업 기본자료형
    /// </summary>
    /// <typeparam name="T"></typeparam>

    public abstract class Popup<T> : MonoBehaviour, IPopup<T>
    {
        /// <summary>
        /// popup의 recttraunsform
        /// </summary>
        protected RectTransform rectTransform;


        /// <summary>
        /// 최초 초기화함수 상위 controller의 init에서 호출해주기
        /// </summary>
        public virtual void Init()
        {
            if (rectTransform == null) { rectTransform = gameObject.GetComponent<RectTransform>(); }
            rectTransform.localScale = Vector2.zero;
        }

        /// <summary>
        /// 팝업에 데이터 적용할 함수
        /// </summary>
        /// <param name="value">적용할 데이터 자료형</param>
        public abstract void Apply(T value);

        /// <summary>
        /// 팝업 열기, y축확장 방식으로 열음
        /// </summary>
        public virtual void Open()
        {
            UiUtil.OnOffPopup(rectTransform, 0f, 0.5f, true);
        }

        /// <summary>
        /// 팝업 닫기, y축축소 방식으로 닫음
        /// </summary>
        public virtual void Close()
        {
            UiUtil.OnOffPopup(rectTransform, 0f, 1f, false);
        }
    }

 

이후 그 인터페이스를 상속받은 Popup<T> 추상 클래스를 구현해주었으며,
Open이나, Close등 공용 연출로 쓰일 함수는 이 부분에서 구현을 따로 진행해주었다.

UiUtil인 static class에 공용연출 함수를 구현하고, 이를 호출하는 방식으로 구현을 진행했다.

 

 

우선 위 구조만 완성되면, Init, Open, Close 함수는 하나의 리스트에서 관리가 가능해진다.

 

다만, Apply<T> 함수의 경우는 하나의 리스트에서 관리가 불가능하고, 구현된 클래스는 generic이라서 class 자체로는 한번에 관리가 불가능한데 이를 가능하기 위해서 어떻게 해줄 수 있을까?

 

 


팝업 관리를 위한 Registry패턴

 

위에서 구현한 팝업을 하나의 List로 관리하고, 이 리스트에 값을 줄 시 알맞은 자료형 팝업에 데이터가 들어가도록 구현하려면

 

어떤 class의 모든 서브 class를 보고싶을 때, 사용하는 패턴인 Registry패턴을 통해서 구현해줄 수 있을것이다.

다만 이때, key는 보통 임의의 string을 지정해줄 수 있지만, 하나의 팝업타입만 구성하도록, 이를

<IPopup이 받을 수 있는 데이터 타입(T), IPopup> 의 형태로 약간 변형하여 저장해줄 것이다.

 

따라서 해당 패턴을 구현해주면 다음과 같다.

 

    public class Registry<T>
    {
        private int size = 32;
        private readonly Dictionary<Type, T> map;

        public Registry(int rsize)
        {
            size = rsize;
            map = new Dictionary<Type, T>(size);
        }

        public void Register(T mb)
        {
            if (mb == null) return;

            var type = mb.GetType();
            var iface = type.GetInterfaces();

            //interface 검사 (generic)
            for (int i = 0; i < iface.Length; i++)
            {
                if (!typeof(T).IsAssignableFrom(iface[i])) continue;

                if (iface[i].IsGenericType)
                {
                    var args = iface[i].GetGenericArguments();
                    if (args != null && args.Length == 1)
                    {
                        // 키: 데이터 타입 (예: ScorePopupData) → 값: 인스턴스
                        map[args[0]] = mb;
                        return; // 보통 하나만 있으면 충분
                    }
                }
            }

            //class 검사 (generic)
            var bt = type;
            while (bt != null && bt != typeof(object))
            {
                if (typeof(T).IsAssignableFrom(bt) && bt.IsGenericType)
                {
                    var args = bt.GetGenericArguments();
                    if (args != null && args.Length == 1)
                    {
                        map[args[0]] = mb;
                        return;
                    }
                }
                bt = bt.BaseType;
            }

            if (typeof(T).IsAssignableFrom(type))
            {
                map[type] = mb; // 예: map[typeof(Popup2)] = instance
                return;
            }
        }

        public Type GetRegType<TData>(T instance)
        {

            if (instance == null) return null;

            foreach (var kv in map)
            {
                // 동일 인스턴스(레퍼런스) or 값 동등성(값 타입/Equals 오버라이드)
                if (ReferenceEquals(kv.Value, instance) ||
                    EqualityComparer<T>.Default.Equals(kv.Value, instance))
                {
                    return kv.Key; // 등록된 key(Type) 반환
                }
            }
            return null;
        }


        public bool TryResolve<TData>(T instance)
        => map.TryGetValue(typeof(TData), out instance);

        public bool TryResolve(Type key, T instance)
            => map.TryGetValue(key, out instance);


        /// <summary>
        /// 키: typeof(TData)로 등록된 인스턴스가 있으면 action 호출
        /// 한 타입당 1개만 매핑하는 현재 구조에 맞춘 최소 구현
        /// </summary>
        public bool GetInstance<TData>(Action<T> action)
        {
            if (action == null) return false;

            if (map.TryGetValue(typeof(TData), out var instance))
            {
                action(instance);
                return true;
            }
            return false;
        }

        public void Clear()
        {
            map.Clear();
        }


    }

 

위 레지스터는, 팝업외에 다른 자료형에도 적용가능하도록 generic으로 구성을 해주었다.

Dictonary의 빠른연산속도를 기반으로하여, 팝업의 종류가 많이 늘어나더라도 부담이 되지 않게된다.

 

사용예시를 보면 다음과 같다.

private Registry<IPopup> popupreg;
private List<IPopup> popups;
    

        public void Init()
        {
            popupreg.Clear();

            for (int i = 0; i < popups.Count; i++)
            {
                popups[i].Init();
                popupreg.Register(popups[i]);
            }

            RankPopupDatas = ScoreCalculator.Instance.LoadTop3RankPopupData();

        }
    


        /// <summary>
        /// data(ScorePopupData 또는 RankPopupData)를 처리하는 모든 팝업에 Apply(+선택 Open)
        /// </summary>
        public void ApplyDat<T>(T data, bool openAfter = false)
        {
            Debug.Log("apply");
            // 레지스트리에서 T를 키로 모든 팝업을 꺼내와 일괄 처리
            var found = popupreg.GetInstance<T>(popup =>
            {
                Debug.Log("apply2");
                ((IPopup<T>)popup).Apply(data);
                if (openAfter) popup.Open();
            });

            if (!found)
                Debug.LogWarning($"[ScorePopupController] No popups registered for {typeof(T).Name}");
        }

 

ApplyDat(
                (ScoreCalculator.Instance.ObjectCount.ToString(),
                FmtFromMilliseconds(ScoreCalculator.Instance.ActualTime)),
                true
            );
            
            
ApplyDat(scorePopupData, true);

 

 

이를통해, 각 팝업을 한번에관리해주며, 팝업데이터를 구성하는 작업도 모든 리스트를 돌지않고, 빠르게 진행할 수 있다.

Dictoinary를 기반으로 하였기때문에, O(1) 의 조회속도를 가져
데이터의 종류 n , 그에따른 팝업의 종류 n 으로

 

기존리스트로 하나씩 비교하여 진행했을 경우는

전체 데이터구성 O(n^2) 의 복잡도가 걸리지만

 

구현된 패턴으로 진행을하면

전체 데이터구성에 O(n) 으로 구성을 완료할 수 있다.