본문 바로가기
Development/C#

멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 62일차

by Mobics 2025. 2. 25.

 

목차


    퀴즈 게임 만들기

    25.02.25

    Object Pool 패턴을 이용한 Level 팝업 만들기

    - 초기화 작업

    1. Content의 높이를 설정한다.
    2. 화면의 보이는 영역 만큼만 Cell을 추가한다.

    - Scroll 처리

    1. 화면 밖으로 나가는 Cell 제거
    2. 새롭게 등장하는 Cell 추가

    --> 노란색은 새로 추가될 Cell, 회색은 제거될 Cell

     

    >> 퀴즈 게임에 있던 Object Pool.cs 가져오기

     

    >> 빈 게임 오브젝트로 'Object Pool' 만들고 Object Pool.cs 추가 및 바인딩

     

    >> Cell Prefab에 Cell.cs 추가 및 바인딩

     

    ※ Test할 때는 Viewport의 Mask를 끄는게 결과를 보기에 좋다

     

    활동

    : Quiz Game의 Stage 팝업을 재사용 Cell 방식으로 만들기

     

    ※ 다른 수강생님의 구현 순서

    1. 행별로 어디부터 어디까지 보일지 계산
    2. 위/아래 추가인지 삭제인지 판단
    3. 행을 추가하거나 삭제할 때 3개씩 처리 --> 예외적으로 마지막 행은 꼭 3개가 아닐수도 있으니 총 개수에 맞춰서 처리

     

    지난 시간에 놓친 활동하기

    : Stage Popup Panel을 켰을 때 바로 마지막에 클리어한 Stage로 점프하도록 구현

    --> Hint : Content의 RectTransform을 어떻게 지정할 것인지? 계산해보자

    // Stage Popup Panel을 켰을 때 바로 마지막에 클리어한 Stage로 점프
    var contentPos = 340 * (lastStageIndex / 3); // Cell Size : 300, Cell Spacing : 40, 한 줄에 Cell 3개
    contentTransform.gameObject.GetComponent<RectTransform>().DOAnchorPosY(contentPos, 1f);

     

    최종 코드

    >> ScrollViewController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    
    [RequireComponent(typeof(ScrollRect))]
    [RequireComponent(typeof(RectTransform))]
    public class ScrollViewController : MonoBehaviour
    {
        [SerializeField] private float cellHeight;
        
        private ScrollRect _scrollRect; // ScrollRect는 Content도 제어 가능하다
        private RectTransform _rectTransform;
        
        private List<Item> _items;      // Cell에 표시할 Item 정보
        private LinkedList<Cell> _visibleCells = new LinkedList<Cell>();    // 화면에 표시되고 있는 Cell 정보
    
        private float _lastYValue = 1f;
        
        private void Awake()
        {
            _scrollRect = GetComponent<ScrollRect>();
            _rectTransform = GetComponent<RectTransform>();
        }
    
        private void Start()
        {
            LoadData();
        }
    
        /// <summary>
        /// 현재 보여질 Cell Index를 반환하는 Method
        /// </summary>
        /// <returns>startIndex : 가장 위에 표시될 Cell Index, endIndex : 가장 아래에 표시될 Cell Index</returns>
        private (int startIndex, int endIndex) GetVisibleIndexRange()
        {
            var visibleRect = new Rect(
                _scrollRect.content.anchoredPosition.x,
                _scrollRect.content.anchoredPosition.y,
                _rectTransform.rect.width,
                _rectTransform.rect.height);
            
            // 스크롤 위치에 따른 시작 Index 계산
            var startIndex = Mathf.FloorToInt(visibleRect.y / cellHeight);  // FloorToInt : float 값을 내림하여 int로 변환
            
            // 화면에 보이게 될 Cell 개수 계산
            int visibleCount = Mathf.CeilToInt(visibleRect.height / cellHeight); // CeilToInt : float 값을 올림하여 int로 변환
            
            // 버퍼 추가
            startIndex = Mathf.Max(0, startIndex - 1);  // startIndex가 0보다 크면 startIndex - 1, 아니면 0
            visibleCount += 2;
            
            return (startIndex, startIndex + visibleCount - 1); // Count말고 Index로 사용하기 위해 -1 
        }
    
        /// <summary>
        /// 특정 Index가 화면에 보여야 하는지 여부를 판단하는 Method
        /// </summary>
        /// <param name="index">특정 Index</param>
        /// <returns>true, false</returns>
        private bool IsVisibleIndex(int index)
        {
            var (startIndex, endIndex) = GetVisibleIndexRange();
            endIndex = Mathf.Min(endIndex, _items.Count - 1);
            return startIndex <= index && index <= endIndex;
        }
    
        /// <summary>
        /// _items에 있는 값을 Scroll View에 표시하는 함수
        /// _items에 새로운 값이 추가되거나 기존 값이 삭제되면 호출됨
        /// </summary>
        private void ReloadData()
        {
            // _visibleCells 초기화
            _visibleCells = new LinkedList<Cell>();
            
            // Content의 높이를 _item의 데이터의 수만큼 계산해서 높이를 지정
            var contentSizeDelta = _scrollRect.content.sizeDelta;
            contentSizeDelta.y = _items.Count * cellHeight;
            _scrollRect.content.sizeDelta = contentSizeDelta;
            
            // 화면에 보이는 영역에 Cell 추가
            var (startIndex, endIndex) = GetVisibleIndexRange();
            var maxEndIndex = Mathf.Min(endIndex, _items.Count - 1);
            for (int i = startIndex; i < maxEndIndex; i++)
            {
                // Cell 만들기
                var cellObject = ObjectPool.Instance.GetObject();
                var cell = cellObject.GetComponent<Cell>();
                cell.SetItem(_items[i], i);
                cell.transform.localPosition = new Vector3(0, -i * cellHeight, 0);
                
                _visibleCells.AddLast(cell);
            }
        }
    
        private void LoadData()
        {
            _items = new List<Item>()
            {
                new Item {imageFileName = "image1", title = "Title 1", subtitle = "Subtitle 1"},
                new Item {imageFileName = "image2", title = "Title 2", subtitle = "Subtitle 2"},
                new Item {imageFileName = "image3", title = "Title 3", subtitle = "Subtitle 3"},
                new Item {imageFileName = "image4", title = "Title 4", subtitle = "Subtitle 4"},
                new Item {imageFileName = "image5", title = "Title 5", subtitle = "Subtitle 5"},
                new Item {imageFileName = "image6", title = "Title 6", subtitle = "Subtitle 6"},
                new Item {imageFileName = "image7", title = "Title 7", subtitle = "Subtitle 7"},
                new Item {imageFileName = "image8", title = "Title 8", subtitle = "Subtitle 8"},
                new Item {imageFileName = "image9", title = "Title 9", subtitle = "Subtitle 9"},
                new Item {imageFileName = "image10", title = "Title 10", subtitle = "Subtitle 10"},
                new Item {imageFileName = "image11", title = "Title 11", subtitle = "Subtitle 11"},
                new Item {imageFileName = "image12", title = "Title 12", subtitle = "Subtitle 12"},
                new Item {imageFileName = "image13", title = "Title 13", subtitle = "Subtitle 13"},
                new Item {imageFileName = "image14", title = "Title 14", subtitle = "Subtitle 14"},
                new Item {imageFileName = "image15", title = "Title 15", subtitle = "Subtitle 15"},
                new Item {imageFileName = "image16", title = "Title 16", subtitle = "Subtitle 16"},
                new Item {imageFileName = "image17", title = "Title 17", subtitle = "Subtitle 17"},
                new Item {imageFileName = "image18", title = "Title 18", subtitle = "Subtitle 18"},
                new Item {imageFileName = "image19", title = "Title 19", subtitle = "Subtitle 19"},
                new Item {imageFileName = "image20", title = "Title 20", subtitle = "Subtitle 20"},
                new Item {imageFileName = "image21", title = "Title 21", subtitle = "Subtitle 21"},
                new Item {imageFileName = "image22", title = "Title 22", subtitle = "Subtitle 22"},
                new Item {imageFileName = "image23", title = "Title 23", subtitle = "Subtitle 23"},
                new Item {imageFileName = "image24", title = "Title 24", subtitle = "Subtitle 24"},
                new Item {imageFileName = "image25", title = "Title 25", subtitle = "Subtitle 25"},
                new Item {imageFileName = "image26", title = "Title 26", subtitle = "Subtitle 26"},
                new Item {imageFileName = "image27", title = "Title 27", subtitle = "Subtitle 27"},
                new Item {imageFileName = "image28", title = "Title 28", subtitle = "Subtitle 28"},
                new Item {imageFileName = "image29", title = "Title 29", subtitle = "Subtitle 29"},
                new Item {imageFileName = "image30", title = "Title 30", subtitle = "Subtitle 30"},
            };
            ReloadData();
        }
    
        #region Scroll Rect Events
    
        public void OnValueChanged(Vector2 value) // 스크롤 방향에 따라 0 ~ 1의 값이 들어온다.
        {
            if (_lastYValue < value.y)  // 위로 스크롤
            {
                // 상단에 새로운 Cell이 필요한지 확인 후 필요하면 추가
                var firstCell = _visibleCells.First.Value;
                var newFirstIndex = firstCell.Index - 1;
                
                if (IsVisibleIndex(newFirstIndex))  // newFirstIndex가 만들어야 할 Cell인지 확인
                {
                    var cell = ObjectPool.Instance.GetObject().GetComponent<Cell>();
                    cell.SetItem(_items[newFirstIndex], newFirstIndex);
                    cell.transform.localPosition = new Vector3(0, -newFirstIndex * cellHeight, 0);
                    _visibleCells.AddFirst(cell);
                }
                
                // 하단에 있는 Cell이 화면에서 벗어나면 제거
                var lastCell = _visibleCells.Last.Value;
                if (!IsVisibleIndex(lastCell.Index))
                {
                    ObjectPool.Instance.ReturnObject(lastCell.gameObject);
                    _visibleCells.RemoveLast();
                }
            }
            else  // 아래로 스크롤
            {
                // 하단에 새로운 Cell이 필요한지 확인 후 필요하면 추가
                var lastCell = _visibleCells.Last.Value;
                var newLastIndex = lastCell.Index + 1;
                
                if (IsVisibleIndex(newLastIndex))
                {
                    var cell = ObjectPool.Instance.GetObject().GetComponent<Cell>();
                    cell.SetItem(_items[newLastIndex], newLastIndex);
                    cell.transform.localPosition = new Vector3(0, -newLastIndex * cellHeight, 0);
                    _visibleCells.AddLast(cell);
                }
                
                // 상단에 있는 Cell이 화면에서 벗어나면 제거
                var firstCell = _visibleCells.First.Value;
                if (!IsVisibleIndex(firstCell.Index))
                {
                    ObjectPool.Instance.ReturnObject(firstCell.gameObject);
                    _visibleCells.RemoveFirst();
                }
            }
            
            _lastYValue = value.y;
        }
    
        #endregion
    }

     

    >> Cell.cs

    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class Cell : MonoBehaviour
    {
        [SerializeField] private Image image;
        [SerializeField] private TMP_Text titleText;
        [SerializeField] private TMP_Text subtitleText;
        
        public int Index { get; private set; }
    
        public void SetItem(Item item, int index)
        {
            //image.sprite = Resources.Load<Sprite>(item.imageFileName);
            titleText.text = item.title;
            subtitleText.text = item.subtitle;
    
            Index = index;
        }
    }

     

    >> ObjectPool.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class ObjectPool : MonoBehaviour
    {
        [SerializeField] private GameObject prefab;
        [SerializeField] private int poolSize;
        [SerializeField] private Transform parent;  // Object Pool이 생성될 Parent
    
        private Queue<GameObject> _pool;
        private static ObjectPool _instance; // Singleton을 상속받지 않고 패턴만 사용
    
        public static ObjectPool Instance
        {
            get { return _instance; }
        }
    
        private void Awake()
        {
            _instance = this;
            _pool = new Queue<GameObject>();
        }
    
        /// <summary>
        /// Object Pool에 새로운 Object 생성 Method
        /// </summary>
        private void CreateNewObject()
        {
            GameObject newObject = Instantiate(prefab, parent);
            newObject.SetActive(false);
            _pool.Enqueue(newObject);
        }
    
        /// <summary>
        /// Object Pool에 있는 Object를 반환하는 Method
        /// </summary>
        /// <returns>Object Pool에 있는 Object</returns>
        public GameObject GetObject()
        {
            if (_pool.Count == 0) CreateNewObject();
                
            GameObject obj = _pool.Dequeue();
            obj.SetActive(true);
            return obj;
        }
    
        /// <summary>
        /// 사용한 Object를 Object Pool로 되돌려 주는 Method
        /// </summary>
        /// <param name="obj">반환할 Object</param>
        public void ReturnObject(GameObject obj)
        {
            obj.SetActive(false);
            _pool.Enqueue(obj);
        }
    }

     

    >> StagePopupPanelController.cs

    : quiz-game

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using DG.Tweening;
    using UnityEngine;
    
    [RequireComponent(typeof(PopupPanelController))]
    public class StagePopupPanelController : MonoBehaviour
    {
        [SerializeField] private GameObject stageCellPrefab;
        [SerializeField] private Transform contentTransform;
        
        private void Start()
        {
            GetComponent<PopupPanelController>().SetTitleText("STAGE");
    
            var lastStageIndex = 90;         // UserInformations.LastStageIndex;
            var maxStageCount = 100;        // Constants.MAX_STAGE_COUNT;
            
            // Stage Popup Panel을 켰을 때 바로 마지막에 클리어한 Stage로 점프
            var contentPos = 340 * (lastStageIndex / 3); // Cell Size : 300, Cell Spacing : 40, 한 줄에 Cell 3개
            contentTransform.gameObject.GetComponent<RectTransform>().DOAnchorPosY(contentPos, 1f);
            
            // Stage Cell 만들기
            for (int i = 0; i < maxStageCount; i++)
            {
                GameObject stageCellObject = Instantiate(stageCellPrefab, contentTransform);
                StageCellButton stageCellButton = stageCellObject.GetComponent<StageCellButton>();
    
                if (i < lastStageIndex)
                {
                    stageCellButton.SetStageCell(i, StageCellButton.StageCellType.Clear);
                }
                else if (i == lastStageIndex)
                {
                    stageCellButton.SetStageCell(i, StageCellButton.StageCellType.Normal);
                }
                else
                {
                    stageCellButton.SetStageCell(i, StageCellButton.StageCellType.Lock);
                }
            }
        }
    }

    C# 단기 교육 보강

    14일차

    장염 이슈로 추후 작성 예정...