스터디

유니티 확장 UI - 무한 스크롤

장꼬미 2021. 7. 15. 08:08

 게임을 만들다 보면 무한한 아이템을 담은 인벤토리등의 스크롤을 구현해야 할 일이 많다.

 

 무한이 아니더라도 모바일 환경에서 200개가 넘는 동적 오브젝트를 실시간으로 생성하며 관리하는 것은 리소스의 낭비로 이어지고 부하를 초래하여 플레이 경험과 질적 하락을 일으키게 된다.

 

 이를 위해 이런 다수의 아이템 목록을 UI등에 구현할때 마치 오브젝트 풀과 같이 한정된 갯수의 오브젝트를 생성한후 이를 돌려쓰는 방식을 많이 사용한다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class makeList : MonoBehaviour
{
    [SerializeField] RectTransform _rtParent = null; //생성될 오브젝트가 배치될 부모 Rect
    [SerializeField] GameObject _gbjSlot = null; //동적 생성할 프리팹
    [SerializeField] ScrollRect _scroll = null; //스크롤뷰 갱신 확인용


    public int _slotCount = 9; //생성할 논리적 오브젝트 수
    public int _makeCount = 10; //물리적으로 생성하여 사용할 오브젝트 풀 갯수
    
    int _index = 0; //scrollView에서 최상단 오브젝트의 논리적 번호
    RectTransform _slotRect = null; //전역으로 사용할 rect사이즈
    GameObject[] _slots = null; //오브젝트 풀


    private void Awake()
    {
        _scroll.onValueChanged.AddListener(OnScrollChange);
    }

    private void Start()
    {
        _slotRect = _gbjSlot.GetComponent<RectTransform>();

        _rtParent.sizeDelta = new Vector2(_rtParent.sizeDelta.x, _slotRect.sizeDelta.y * _slotCount);
        _slots = new GameObject[_slotCount];

        for(var i = 0; i < _makeCount; ++i)
        {
            GameObject gbj = Instantiate(_gbjSlot, _rtParent);
            gbj.name = string.Format("item {0}",i);
            gbj.transform.localPosition = new Vector2( gbj.transform.localPosition.x,-_slotRect.sizeDelta.y * i);
            Refresh(gbj,i);
            _slots[i] = gbj;
        }
    }

    void OnScrollChange(Vector2 vec)
    {
        //index 가져오기
        //위 아래 옮겨주기
        int index = getCurrentIndex();
        GameObject objIndex = _slots[index];
        if(objIndex == null)
        {
            int next = index + _makeCount;
            if (next > _slotCount - 1)
                return;
            else
            {
                GameObject objNow = _slots[next];
                if(objNow != null)
                {
                    _slots[next] = objIndex;
                    _slots[index] = objNow;
                    Refresh(_slots[index], index);
                }
            }
        }
        else
        {
            if(index > 0)
            {
                GameObject obj = _slots[index - 1];
                if (obj == null)
                    return;
                int next = index - 1 + _makeCount;
                if (next > _slotCount - 1)
                    return;
                else
                {
                    GameObject objNow = _slots[next];
                    if(objNow == null)
                    {
                        _slots[next] = obj;
                        _slots[index - 1] = objNow;
                        Refresh(_slots[next], next);
                    }
                }
            }
        }
    }

    void Refresh(GameObject obj, int indexRefresh)
    {
        obj.transform.name = string.Format("item {0}",indexRefresh);
        Vector3 vec = getLocationAppear(obj.transform.localPosition, indexRefresh);
        obj.transform.localPosition = vec;
    }

    Vector3 getLocationAppear(Vector2 initVec, int locaiton)
    {
        Vector3 vec = initVec;
        vec = new Vector3(vec.x,-( _slotRect.sizeDelta.y * locaiton), 0);
        return vec;
    }

    int getCurrentIndex()
    {
        int index = (int)(_rtParent.anchoredPosition.y/ _slotRect.sizeDelta.y);
        if (index < 0)
            index = 0;
        if (index > _slotCount - 1)
            index = _slotCount - 1;
        return index;
    }


    //빠른 이동으로 인한 버그 픽스가 필요함
}

(*필드 변수는 주석을 참고하자)

 

 전체 코드이며 코드는 크게 1. 오브젝트 풀 초기화, 2. 인덱스/위치 연산, 3. 갱신 세부분으로 나뉜다.

 1. 오브젝트 풀 초기화 Start()

 : MonoBehaviour 라이프 사이클의 이벤트 함수인 Start()에서 오브젝트 풀을 원하는 갯수만큼 동적으로 생성한 오브젝트로 초기화 해준다. 현재의 코드에선 Vertical 형 스크롤뷰만 지원하고 있고 위치를 생성한 이후 현재는 의미없지만 Refresh함수를 한번 더 거쳐 위치를 다시 초기화 하고 있다.

 

 2. 인덱스/위치 연산 getCurrentIndex(), GetLocationAppear()

 : 오브젝트가 배치되는 오브젝트의 SizeDelta와 부모 Rect의 anchoredPosition을 통해 현재최상단 아이템의 논리적 인덱스를 가져와서 이를 기반해 생성/이동할 오브젝트의 위치를 연산한다.

 

 3. 갱신 onScrollChange()

 : 위의 인덱스 확인을 통해 해당 인덱스의 오브젝트를 오브젝트 풀에서 확인하고 없을 경우 마지막 오브젝트로 교체하고 위치를 초기화 한다.

 인덱스보다 작은 오브젝트가 존재하고 다음 생성할 오브젝트의 인덱스가 논리상 오브젝트 갯수보다 커지면 이미 지나간 최상단 이전의 오브젝트로 갱신하고 위치를 초기화 한다.

 

 

 해당 코드는 업데이트를 기반하여 실행되기 때문에 한번의 업데이트때 두개이상의 오브젝트가 체크되지 못하여 이빨이 빠지는등의 버그가 있을 수 있다. 또 생성 부분에 추후 활용하기 위한 불필요한 코드도 존재한다. 무엇보다 그리드형 UI를 표시하기 위하여 추가 작업이 필요하다.