목차
퀴즈 게임 만들기
25.02.25
Object Pool 패턴을 이용한 Level 팝업 만들기
- 초기화 작업
- Content의 높이를 설정한다.
- 화면의 보이는 영역 만큼만 Cell을 추가한다.
- Scroll 처리
- 화면 밖으로 나가는 Cell 제거
- 새롭게 등장하는 Cell 추가
--> 노란색은 새로 추가될 Cell, 회색은 제거될 Cell
>> 퀴즈 게임에 있던 Object Pool.cs 가져오기
>> 빈 게임 오브젝트로 'Object Pool' 만들고 Object Pool.cs 추가 및 바인딩
>> Cell Prefab에 Cell.cs 추가 및 바인딩
※ Test할 때는 Viewport의 Mask를 끄는게 결과를 보기에 좋다
활동
: Quiz Game의 Stage 팝업을 재사용 Cell 방식으로 만들기
※ 다른 수강생님의 구현 순서
- 행별로 어디부터 어디까지 보일지 계산
- 위/아래 추가인지 삭제인지 판단
- 행을 추가하거나 삭제할 때 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일차
장염 이슈로 추후 작성 예정...
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 64일차 (0) | 2025.02.27 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 63일차 (0) | 2025.02.26 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 61일차 (0) | 2025.02.25 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 60일차 (0) | 2025.02.21 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 59일차 (1) | 2025.02.20 |