2024. 1. 9. 17:29ㆍSKKU DT
[SKKU DT] 47일차 -유니티 공공API 데이터 활용으로 UI 만들기 - CURD, 대리자(Delegate), LinkedList, WinMerge
CRUD는 대부분의 컴퓨터 소프트웨어가 가지는 기본적인 데이터 처리 기능인 Create(생성), Read(읽기), Update(갱신), Delete(삭제)를 묶어서 일컫는 말이다. Create 패널을 하나 만들어서 InputField로 사용자
lightbakery.tistory.com
이전 글에 이어서, 셀 목록에 Object Pool을 적용하는 것을 이어서 진행한다. -Self Study
이전 Cell과 CellPool 스크립트를 갈아엎고 LegacyDataManager에서 수정할 방법을 찾아본다.
GPT를 이용하려 했지만 잘 되지 않았다....
강사님의 답
상속 구조로 받아서 쓰게 만들예정이다.
GOTableViewController 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class GOTableViewController : MonoBehaviour
{
//Cell을 생성하기 위한 cell 프리팹
[SerializeField] protected GOTableViewCell cellPrefab; //protected는 상속받은 클래스에서 사용 가능. 어차피 Serialized이기 때문에 public은 있으나 마나.
//Cell을 생성하게 될 content 객체
[SerializeField] protected Transform content;
//Queue 생성
protected Queue<GOTableViewCell> reuseQueue = new Queue<GOTableViewCell>();
//cell의 높이
private float cellHeight;
//전체 셀의 개수
private int totalRows;
//자식 클래스에서 GOTableViewController가 표시해야 할 전체 Data의 수를 반환하는 메서드
protected abstract int numberOfRows(); //추상 메서드 생성, 맨 위의 class도 추상 클래스로 변경(public abstract class GOTableViewController) 자식 클래스가 상속받아서 해당 부분을 채울 것이다. 틀만 만드는 느낌.
//스크롤 바가 보이는 부분에다 맞추면 안되고 전체의 셀에 대한 스크롤 크기를 맞춰야 한다.
protected abstract GOTableViewCell cellForRowAtIndex(int index); //추상 메서드 하나 더 생성.
protected virtual void Start() //virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
{
totalRows = this.numberOfRows();
float screenHeight = Screen.height; //화면의 높이 얻어오기. Canvas의 높이가 960으로 설정되어있다.
cellHeight = cellPrefab.GetComponent<RectTransform>().sizeDelta.y; //셀의 높이. 200으로 설정되어있다.
//표시해야 할 cell의 수 계산
int maxRows = (int)screenHeight / (int)cellHeight + 2; //줄 수이기 때문에 int로 줄의 개수를 자연수로 받는다. 한 화면에 표시되는 셀의 개수는 960/200=4.8정도 된다. 아래 가려진 셀의 개수에 여유를 주기 위해 2를 더한다.
maxRows = (maxRows > totalRows) ? totalRows : maxRows;
//전체 셀을 포함하는 높이를 가질 수 도록 content의 크기를 조정
RectTransform contentTransform = content.GetComponent<RectTransform>();
contentTransform.sizeDelta = new Vector2(0, totalRows * cellHeight);
}
}
virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
GOTableViewController 스크립트를 상속받는 자식 클래스 LegacyTableViewController 스크립트 생성
public class LegacyTableViewController : GOTableViewController
{
}
GOTableViewController를 상속받는다.
클래스 이름에 오류 표시가 뜨는데, Alt+Enter로 자동 수정을 불러오고 적용하면 추상 메서드를 가져올 수 있다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class LegacyTableViewController : GOTableViewController
{
[SerializeField] private string serviceKey; //서버 키 입력
private List<Legacy> legacyList = new List<Legacy>(); //리스트 생성
protected void Start()
{
StartCoroutine(LoadData());
}
protected override int numberOfRows()
{
return legacyList.Count; //개수 반환
}
protected override GOTableViewCell cellForRowAtIndex(int index)
{
return null;//특정한 index의 셀을 만들려고할 때 특정한 셀을 만들어서 반환한다.
}
//이전 LegacyDataManger에서 LoadData 부분을 가져온다. 더보기 버튼 관련 코드는 지운다.
IEnumerator LoadData()
{
string url = string.Format("{0}?page={1}&perPage={2}&serviceKey={3}", Constants.url, 0, 100, serviceKey);
UnityWebRequest request = new UnityWebRequest();
using (request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log(request.error);
}
else
{
string result = request.downloadHandler.text; //텍스트 형식으로 받아오기. 많이 불러왔었던 빼곡한 글들로 불러온다.
LegacyData legacyData = JsonUtility.FromJson<LegacyData>(result); //파싱에서 가져온 이름 LegacyData
Legacy[] legacyArray = legacyData.data;
for (int i = 0; i < legacyArray.Length; i++)
{
legacyList.Add(legacyArray[i]);
}
}
base.Start();
}
}
}
Content에 Vertical Layout Group과 Content Size Fitter 지우기. 스크립트로 조절할 예정
LegacyCell 프리팹을 복사해서 LegacyTableViewCell 만들기
기존에 들어있던 스크립트 지우고 LEgacyTableViewCell 추가
CellPrefab 자리에 LegacyTableViewCell 끌어다 넣기
서비스 키 넣기
Content의 높이가 셀의 개수에 맞게 엄청 길어져있으면 된다.
GOTableViewCell 스크립트를 상속받는 자식 클래스 LegacyTableViewCell 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class LegacyTableViewCell : GOTableViewCell
{
[SerializeField] TMP_Text legacyName;
[SerializeField] TMP_Text type;
[SerializeField] TMP_Text designatedDate;
}
GOTableViewController 스크립트 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class GOTableViewController : MonoBehaviour
{
//Cell을 생성하기 위한 cell 프리팹
[SerializeField] protected GOTableViewCell cellPrefab; //protected는 상속받은 클래스에서 사용 가능. 어차피 Serialized이기 때문에 public은 있으나 마나.
//Cell을 생성하게 될 content 객체
[SerializeField] protected Transform content;
//Queue 생성
protected Queue<GOTableViewCell> reuseQueue = new Queue<GOTableViewCell>();
//cell의 높이
private float cellHeight;
//전체 셀의 개수
private int totalRows;
//자식 클래스에서 GOTableViewController가 표시해야 할 전체 Data의 수를 반환하는 메서드
protected abstract int numberOfRows(); //추상 메서드 생성, 맨 위의 class도 추상 클래스로 변경(public abstract class GOTableViewController) 자식 클래스가 상속받아서 해당 부분을 채울 것이다. 틀만 만드는 느낌.
//스크롤 바가 보이는 부분에다 맞추면 안되고 전체의 셀에 대한 스크롤 크기를 맞춰야 한다.
protected abstract GOTableViewCell cellForRowAtIndex(int index); //추상 메서드 하나 더 생성.
protected virtual void Start() //virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
{
totalRows = this.numberOfRows();
float screenHeight = Screen.height; //화면의 높이 얻어오기. Canvas의 높이가 960으로 설정되어있다.
cellHeight = cellPrefab.GetComponent<RectTransform>().sizeDelta.y; //셀의 높이. 200으로 설정되어있다.
//표시해야 할 cell의 수 계산
int maxRows = (int)screenHeight / (int)cellHeight + 2; //줄 수이기 때문에 int로 줄의 개수를 자연수로 받는다. 한 화면에 표시되는 셀의 개수는 960/200=4.8정도 된다. 아래 가려진 셀의 개수에 여유를 주기 위해 2를 더한다.
maxRows = (maxRows > totalRows) ? totalRows : maxRows;
//전체 셀을 포함하는 높이를 가질 수 도록 content의 크기를 조정
RectTransform contentTransform = content.GetComponent<RectTransform>();
contentTransform.sizeDelta = new Vector2(0, totalRows * cellHeight);
}
protected GOTableViewCell dequeueReuseableCell() //재사용이 가능한 Cell을 반환해주는 메서드. 재사용 cell 혹은 mull이 반환된다.
{
if(reuseQueue.Count > 0) //쓸만한 셀이 있다면
{
GOTableViewCell cell = reuseQueue.Dequeue();
cell.gameObject.SetActive(true);
return cell;
}
else
{
return null; //재사용 가능한 셀이 있다면 Instantiate 할 수 있다고 생각할 수 있는데, 여기서 자식 형태의 cell을 만들어낼 수는 없다. 자식 쪽에서 만들어야 함. 넣었던 자식이 그대로 반환될 것.
}
}
}
LegacyTableViewController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class LegacyTableViewController : GOTableViewController
{
[SerializeField] private string serviceKey; //서버 키 입력
private List<Legacy> legacyList = new List<Legacy>(); //리스트 생성
protected void Start()
{
StartCoroutine(LoadData());
}
protected override int numberOfRows()
{
return legacyList.Count; //개수 반환
}
protected override GOTableViewCell cellForRowAtIndex(int index)
{
//특정한 index의 셀을 만들려고할 때 특정한 셀을 만들어서 반환한다.
//부모에 만들어 놓으면, 상속만 받으면 가져다 쓰기만 하면 된다. 셀을 생성 하는 건 자식이 하더라도 셀을 재사용할 수 있는 로직은 GOTableViewController에서 만든다.
LegacyTableViewCell cell = dequeueReuseableCell() as LegacyTableViewCell; //재사용 가능한 셀이 있는가 as로 변환을 시킨다. 메서드의 반환 값이 GOTableViewCell이다.
if(cell == null)
{
cell = Instantiate(cellPrefab, content) as LegacyTableViewCell; //변환
}
return null;
}
//이전 LegacyDataManger에서 LoadData 부분을 가져온다. 더보기 버튼 관련 코드는 지운다.
IEnumerator LoadData()
{
string url = string.Format("{0}?page={1}&perPage={2}&serviceKey={3}", Constants.url, 0, 100, serviceKey);
UnityWebRequest request = new UnityWebRequest();
using (request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log(request.error);
}
else
{
string result = request.downloadHandler.text; //텍스트 형식으로 받아오기. 많이 불러왔었던 빼곡한 글들로 불러온다.
LegacyData legacyData = JsonUtility.FromJson<LegacyData>(result); //파싱에서 가져온 이름 LegacyData
Legacy[] legacyArray = legacyData.data;
for (int i = 0; i < legacyArray.Length; i++)
{
legacyList.Add(legacyArray[i]);
}
}
base.Start();
}
}
}
LegacyTableViewCell 스크립트 수정 -public으로 수정. 그래야 LegacyTableViewController에서 cell.~~을 쓸 수 있다.
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class LegacyTableViewCell : GOTableViewCell
{
public TMP_Text legacyName;
public TMP_Text type;
public TMP_Text designatedDate;
}
LegacyTableViewController 스크립트 수정
cell.legacyName.text = legacyList[index].명칭;
cell.type.text = legacyList[index].종목;
cell.designatedDate.text = legacyList[index].지정일;
위의 내용 추가, 아래 전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class LegacyTableViewController : GOTableViewController
{
[SerializeField] private string serviceKey; //서버 키 입력
private List<Legacy> legacyList = new List<Legacy>(); //리스트 생성
protected void Start()
{
StartCoroutine(LoadData());
}
protected override int numberOfRows()
{
return legacyList.Count; //개수 반환
}
protected override GOTableViewCell cellForRowAtIndex(int index)
{
//특정한 index의 셀을 만들려고할 때 특정한 셀을 만들어서 반환한다.
//부모에 만들어 놓으면, 상속만 받으면 가져다 쓰기만 하면 된다. 셀을 생성 하는 건 자식이 하더라도 셀을 재사용할 수 있는 로직은 GOTableViewController에서 만든다.
LegacyTableViewCell cell = dequeueReuseableCell() as LegacyTableViewCell; //재사용 가능한 셀이 있는가 as로 변환을 시킨다. 메서드의 반환 값이 GOTableViewCell이다.
if(cell == null)
{
cell = Instantiate(cellPrefab, content) as LegacyTableViewCell; //변환
}
cell.legacyName.text = legacyList[index].명칭;
cell.type.text = legacyList[index].종목;
cell.designatedDate.text = legacyList[index].지정일;
return cell;
}
//이전 LegacyDataManger에서 LoadData 부분을 가져온다. 더보기 버튼 관련 코드는 지운다.
IEnumerator LoadData()
{
string url = string.Format("{0}?page={1}&perPage={2}&serviceKey={3}", Constants.url, 0, 100, serviceKey);
UnityWebRequest request = new UnityWebRequest();
using (request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log(request.error);
}
else
{
string result = request.downloadHandler.text; //텍스트 형식으로 받아오기. 많이 불러왔었던 빼곡한 글들로 불러온다.
LegacyData legacyData = JsonUtility.FromJson<LegacyData>(result); //파싱에서 가져온 이름 LegacyData
Legacy[] legacyArray = legacyData.data;
for (int i = 0; i < legacyArray.Length; i++)
{
legacyList.Add(legacyArray[i]);
}
}
base.Start();
}
}
}
아직 재사용 가능한 셀을 Queue에 넣는 작업은 하지 않았다.
GOTableViewController 스크립트 수정 (수정 내용은 WinMerge로 이전 것과 비교하면 편하다)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class GOTableViewController : MonoBehaviour
{
//Cell을 생성하기 위한 cell 프리팹
[SerializeField] protected GOTableViewCell cellPrefab; //protected는 상속받은 클래스에서 사용 가능. 어차피 Serialized이기 때문에 public은 있으나 마나.
//Cell을 생성하게 될 content 객체
[SerializeField] protected Transform content;
//Queue 생성
protected Queue<GOTableViewCell> reuseQueue = new Queue<GOTableViewCell>();
//cell의 높이
private float cellHeight;
//전체 셀의 개수
private int totalRows;
//자식 클래스에서 GOTableViewController가 표시해야 할 전체 Data의 수를 반환하는 메서드
protected abstract int numberOfRows(); //추상 메서드 생성, 맨 위의 class도 추상 클래스로 변경(public abstract class GOTableViewController) 자식 클래스가 상속받아서 해당 부분을 채울 것이다. 틀만 만드는 느낌.
//스크롤 바가 보이는 부분에다 맞추면 안되고 전체의 셀에 대한 스크롤 크기를 맞춰야 한다.
protected abstract GOTableViewCell cellForRowAtIndex(int index); //추상 메서드 하나 더 생성.
protected virtual void Start() //virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
{
totalRows = this.numberOfRows(); //전체 몇 개의 값이 필요한지 얻어온다.
float screenHeight = Screen.height; //화면의 높이 얻어오기. Canvas의 높이가 960으로 설정되어있다.
cellHeight = cellPrefab.GetComponent<RectTransform>().sizeDelta.y; //셀의 높이. 200으로 설정되어있다.
//표시해야 할 cell의 수 계산
int maxRows = (int)screenHeight / (int)cellHeight + 2; //줄 수이기 때문에 int로 줄의 개수를 자연수로 받는다. 한 화면에 표시되는 셀의 개수는 960/200=4.8정도 된다. 아래 가려진 셀의 개수에 여유를 주기 위해 2를 더한다.
maxRows = (maxRows > totalRows) ? totalRows : maxRows;
//전체 셀을 포함하는 높이를 가질 수 도록 content의 크기를 조정
RectTransform contentTransform = content.GetComponent<RectTransform>();
contentTransform.sizeDelta = new Vector2(0, totalRows * cellHeight);
//초기 Cell을 생성
for(int i = 0; i < maxRows; i++)
{
GOTableViewCell cell = cellForRowAtIndex(i);//인덱스만 넣으면 셀이 만들어지도록.
cell.gameObject.transform.localPosition = new Vector3(0, -i * cellHeight, 0); //첫 번째 셀 높이는 0, 두 번째 셀 높이는 -1 * 200 = -200
}
}
protected GOTableViewCell dequeueReuseableCell() //재사용이 가능한 Cell을 반환해주는 메서드. 재사용 cell 혹은 null이 반환된다.
{
if(reuseQueue.Count > 0) //쓸만한 셀이 있다면
{
GOTableViewCell cell = reuseQueue.Dequeue();
cell.gameObject.SetActive(true);
return cell;
}
else
{
return null; //재사용 가능한 셀이 있다면 Instantiate 할 수 있다고 생각할 수 있는데, 여기서 자식 형태의 cell을 만들어낼 수는 없다. 자식 쪽에서 만들어야 함. 넣었던 자식이 그대로 반환될 것.
}
}
}
아직 잘 나오지는 않는다. Content의 피봇이 왼쪽에 있다...
피봇 x 값을 0.5로 설정하니 잘 나온다.
Scroll View의 OnValueChange에 넣을 함수를 GOTableViewController에 아래에 만든다.
public void OnValueChanged(Vector2 vector)
{
Debug.Log(vector);
}
가장 아래로 내려가면 x, y가 0이 된다.
다른 값을 Debug.Log 하면 아래와 같다.
public void OnValueChanged(Vector2 vector)
{
Debug.Log($"Screen.height: {Screen.height}, Content.localPosition.y: {content.localPosition.y}");
이제 재사용 가능한 셀을 만들어야 한다. First Cell과 Last Cell의 상태를 보고 추가할지 지울지를 판단한다. LinkedList가 필요한 상황.
GOTableViewController 스크립트 수정 -LinkedList<> 생성
//Cell 간의 연결고리를 관리하기 위한 List
protected LinkedList<GOTableViewCell> cellLinkedList = new LinkedList<GOTableViewCell>();
//초기 Cell을 생성
for(int i = 0; i < maxRows; i++)
{
GOTableViewCell cell = cellForRowAtIndex(i);//인덱스만 넣으면 셀이 만들어지도록.
cell.gameObject.transform.localPosition = new Vector3(0, -i * cellHeight, 0); //첫 번째 셀 높이는 0, 두 번째 셀 높이는 -1 * 200 = -200
//LinkedList에 Last Cell로 추가
cellLinkedList.AddLast(cell);
}
LinkedList를 생성하고 AddLast로 추가된다.
전체 코
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class GOTableViewController : MonoBehaviour
{
//Cell을 생성하기 위한 cell 프리팹
[SerializeField] protected GOTableViewCell cellPrefab; //protected는 상속받은 클래스에서 사용 가능. 어차피 Serialized이기 때문에 public은 있으나 마나.
//Cell을 생성하게 될 content 객체
[SerializeField] protected Transform content;
//Cell 재사용을 위한 Queue 생성
protected Queue<GOTableViewCell> reuseQueue = new Queue<GOTableViewCell>();
//Cell 간의 연결고리를 관리하기 위한 List
protected LinkedList<GOTableViewCell> cellLinkedList = new LinkedList<GOTableViewCell>();
//cell의 높이
private float cellHeight;
//전체 셀의 개수
private int totalRows;
//자식 클래스에서 GOTableViewController가 표시해야 할 전체 Data의 수를 반환하는 메서드
protected abstract int numberOfRows(); //추상 메서드 생성, 맨 위의 class도 추상 클래스로 변경(public abstract class GOTableViewController) 자식 클래스가 상속받아서 해당 부분을 채울 것이다. 틀만 만드는 느낌.
//스크롤 바가 보이는 부분에다 맞추면 안되고 전체의 셀에 대한 스크롤 크기를 맞춰야 한다.
protected abstract GOTableViewCell cellForRowAtIndex(int index); //추상 메서드 하나 더 생성.
protected virtual void Start() //virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
{
totalRows = this.numberOfRows(); //전체 몇 개의 값이 필요한지 얻어온다.
float screenHeight = Screen.height; //화면의 높이 얻어오기. Canvas의 높이가 960으로 설정되어있다.
cellHeight = cellPrefab.GetComponent<RectTransform>().sizeDelta.y; //셀의 높이. 200으로 설정되어있다.
//표시해야 할 cell의 수 계산
int maxRows = (int)screenHeight / (int)cellHeight + 2; //줄 수이기 때문에 int로 줄의 개수를 자연수로 받는다. 한 화면에 표시되는 셀의 개수는 960/200=4.8정도 된다. 아래 가려진 셀의 개수에 여유를 주기 위해 2를 더한다.
maxRows = (maxRows > totalRows) ? totalRows : maxRows;
//전체 셀을 포함하는 높이를 가질 수 도록 content의 크기를 조정
RectTransform contentTransform = content.GetComponent<RectTransform>();
contentTransform.sizeDelta = new Vector2(0, totalRows * cellHeight);
//초기 Cell을 생성
for(int i = 0; i < maxRows; i++)
{
GOTableViewCell cell = cellForRowAtIndex(i);//인덱스만 넣으면 셀이 만들어지도록.
cell.gameObject.transform.localPosition = new Vector3(0, -i * cellHeight, 0); //첫 번째 셀 높이는 0, 두 번째 셀 높이는 -1 * 200 = -200
//LinkedList에 Last Cell로 추가
cellLinkedList.AddLast(cell);
}
}
protected GOTableViewCell dequeueReuseableCell() //재사용이 가능한 Cell을 반환해주는 메서드. 재사용 cell 혹은 null이 반환된다.
{
if(reuseQueue.Count > 0) //쓸만한 셀이 있다면
{
GOTableViewCell cell = reuseQueue.Dequeue();
cell.gameObject.SetActive(true);
return cell;
}
else
{
return null; //재사용 가능한 셀이 있다면 Instantiate 할 수 있다고 생각할 수 있는데, 여기서 자식 형태의 cell을 만들어낼 수는 없다. 자식 쪽에서 만들어야 함. 넣었던 자식이 그대로 반환될 것.
}
}
public void OnValueChanged(Vector2 vector)
{
Debug.Log($"Screen.height: {Screen.height}, Content.localPosition.y: {content.localPosition.y}");
}
}
GOTableViewCell 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GOTableViewCell : MonoBehaviour
{
public int Index;
}
Index가 추가되었다.
GOTableViewController 스크립트 수정 -OnValueChanged 함수 작성
public void OnValueChanged(Vector2 vector)
{
Debug.Log($"Screen.height: {Screen.height}, Content.localPosition.y: {content.localPosition.y}");
if (content.localPosition.y + Screen.height > cellLinkedList.Last.Value.Index * cellHeight + cellHeight) // 부등호 왼쪽은 현재 위치 + 스크린 높이, 부등호 오른쪽은 마지막 보이는 셀의 개수 * 셀의 높이 + 여유 높이 하나 추가. 스크롤을 내려서 스크린 높이가 커지면,
{
//하단에 새로운 Cell이 만들어지는 상황
//처음에 있던 Cell은 Reuse Queue에 저장
LinkedListNode<GOTableViewCell> firstCellNode = cellLinkedList.First; //첫 번째 셀은 이미 화면을 벗어나서 없을 테니까.
cellLinkedList.RemoveFirst(); //첫 번째 있던 값을 지워버리기
firstCellNode.Value.gameObject.SetActive(false); //첫 번째 있던 셀을 비활성화하기
reuseQueue.Enqueue(firstCellNode.Value); //reuseQueue에 저장하기
//하단에 새로운 Cell 생성
LinkedListNode<GOTableViewCell> lastCellNode = cellLinkedList.Last;
int currentIndex = lastCellNode.Value.Index;
GOTableViewCell cell = cellForRowAtIndex(currentIndex + 1);
cell.gameObject.transform.localPosition = new Vector3(0, -(currentIndex + 1) * cellHeight, 0); //좌표를 마지막으로 보낸다.
cellLinkedList.AddAfter(lastCellNode, cell);
cell.gameObject.transform.SetAsLastSibling(); //게임 오브젝트 순서도 마지막으로 보낸다.
}
else if (content.localPosition.y < cellLinkedList.First.Value.Index * cellHeight)
{
//상단에 새로운 Cell이 만들어지는 상황
//마지막에 있던 Cell은 Reuse Queue에 저장
LinkedListNode<GOTableViewCell> lastCellNode = cellLinkedList.Last;
cellLinkedList.RemoveLast();
lastCellNode.Value.gameObject.SetActive(false);
reuseQueue.Enqueue(lastCellNode.Value);
//상단에 새로운 Cell 생성
LinkedListNode<GOTableViewCell> firstCellNode = cellLinkedList.First;
int currentIndex = firstCellNode.Value.Index;
GOTableViewCell cell = cellForRowAtIndex(currentIndex - 1);
cell.gameObject.transform.localPosition = new Vector3(0, -(currentIndex - 1) * cellHeight, 0);
cellLinkedList.AddBefore(firstCellNode, cell);
cell.gameObject.transform.SetAsFirstSibling();
}
}
전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class GOTableViewController : MonoBehaviour
{
//Cell을 생성하기 위한 cell 프리팹
[SerializeField] protected GOTableViewCell cellPrefab; //protected는 상속받은 클래스에서 사용 가능. 어차피 Serialized이기 때문에 public은 있으나 마나.
//Cell을 생성하게 될 content 객체
[SerializeField] protected Transform content;
//Cell 재사용을 위한 Queue 생성
protected Queue<GOTableViewCell> reuseQueue = new Queue<GOTableViewCell>();
//Cell 간의 연결고리를 관리하기 위한 List
protected LinkedList<GOTableViewCell> cellLinkedList = new LinkedList<GOTableViewCell>();
//cell의 높이
private float cellHeight;
//전체 셀의 개수
private int totalRows;
//자식 클래스에서 GOTableViewController가 표시해야 할 전체 Data의 수를 반환하는 메서드
protected abstract int numberOfRows(); //추상 메서드 생성, 맨 위의 class도 추상 클래스로 변경(public abstract class GOTableViewController) 자식 클래스가 상속받아서 해당 부분을 채울 것이다. 틀만 만드는 느낌.
//스크롤 바가 보이는 부분에다 맞추면 안되고 전체의 셀에 대한 스크롤 크기를 맞춰야 한다.
protected abstract GOTableViewCell cellForRowAtIndex(int index); //추상 메서드 하나 더 생성.
protected virtual void Start() //virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
{
totalRows = this.numberOfRows(); //전체 몇 개의 값이 필요한지 얻어온다.
float screenHeight = Screen.height; //화면의 높이 얻어오기. Canvas의 높이가 960으로 설정되어있다.
cellHeight = cellPrefab.GetComponent<RectTransform>().sizeDelta.y; //셀의 높이. 200으로 설정되어있다.
//표시해야 할 cell의 수 계산
int maxRows = (int)screenHeight / (int)cellHeight + 2; //줄 수이기 때문에 int로 줄의 개수를 자연수로 받는다. 한 화면에 표시되는 셀의 개수는 960/200=4.8정도 된다. 아래 가려진 셀의 개수에 여유를 주기 위해 2를 더한다.
maxRows = (maxRows > totalRows) ? totalRows : maxRows;
//전체 셀을 포함하는 높이를 가질 수 도록 content의 크기를 조정
RectTransform contentTransform = content.GetComponent<RectTransform>();
contentTransform.sizeDelta = new Vector2(0, totalRows * cellHeight);
//초기 Cell을 생성
for(int i = 0; i < maxRows; i++)
{
GOTableViewCell cell = cellForRowAtIndex(i);//인덱스만 넣으면 셀이 만들어지도록.
cell.gameObject.transform.localPosition = new Vector3(0, -i * cellHeight, 0); //첫 번째 셀 높이는 0, 두 번째 셀 높이는 -1 * 200 = -200
//LinkedList에 Last Cell로 추가
cellLinkedList.AddLast(cell);
}
}
protected GOTableViewCell dequeueReuseableCell() //재사용이 가능한 Cell을 반환해주는 메서드. 재사용 cell 혹은 null이 반환된다.
{
if(reuseQueue.Count > 0) //쓸만한 셀이 있다면
{
GOTableViewCell cell = reuseQueue.Dequeue();
cell.gameObject.SetActive(true);
return cell;
}
else
{
return null; //재사용 가능한 셀이 있다면 Instantiate 할 수 있다고 생각할 수 있는데, 여기서 자식 형태의 cell을 만들어낼 수는 없다. 자식 쪽에서 만들어야 함. 넣었던 자식이 그대로 반환될 것.
}
}
public void OnValueChanged(Vector2 vector)
{
Debug.Log($"Screen.height: {Screen.height}, Content.localPosition.y: {content.localPosition.y}");
if (content.localPosition.y + Screen.height > cellLinkedList.Last.Value.Index * cellHeight + cellHeight) // 부등호 왼쪽은 현재 위치 + 스크린 높이, 부등호 오른쪽은 마지막 보이는 셀의 개수 * 셀의 높이 + 여유 높이 하나 추가. 스크롤을 내려서 스크린 높이가 커지면,
{
//하단에 새로운 Cell이 만들어지는 상황
//처음에 있던 Cell은 Reuse Queue에 저장
LinkedListNode<GOTableViewCell> firstCellNode = cellLinkedList.First; //첫 번째 셀은 이미 화면을 벗어나서 없을 테니까.
cellLinkedList.RemoveFirst(); //첫 번째 있던 값을 지워버리기
firstCellNode.Value.gameObject.SetActive(false); //첫 번째 있던 셀을 비활성화하기
reuseQueue.Enqueue(firstCellNode.Value); //reuseQueue에 저장하기
//하단에 새로운 Cell 생성
LinkedListNode<GOTableViewCell> lastCellNode = cellLinkedList.Last;
int currentIndex = lastCellNode.Value.Index;
GOTableViewCell cell = cellForRowAtIndex(currentIndex + 1);
cell.gameObject.transform.localPosition = new Vector3(0, -(currentIndex + 1) * cellHeight, 0); //좌표를 마지막으로 보낸다.
cellLinkedList.AddAfter(lastCellNode, cell);
cell.gameObject.transform.SetAsLastSibling(); //게임 오브젝트 순서도 마지막으로 보낸다.
}
else if (content.localPosition.y < cellLinkedList.First.Value.Index * cellHeight)
{
//상단에 새로운 Cell이 만들어지는 상황
//마지막에 있던 Cell은 Reuse Queue에 저장
LinkedListNode<GOTableViewCell> lastCellNode = cellLinkedList.Last;
cellLinkedList.RemoveLast();
lastCellNode.Value.gameObject.SetActive(false);
reuseQueue.Enqueue(lastCellNode.Value);
//상단에 새로운 Cell 생성
LinkedListNode<GOTableViewCell> firstCellNode = cellLinkedList.First;
int currentIndex = firstCellNode.Value.Index;
GOTableViewCell cell = cellForRowAtIndex(currentIndex - 1);
cell.gameObject.transform.localPosition = new Vector3(0, -(currentIndex - 1) * cellHeight, 0);
cellLinkedList.AddBefore(firstCellNode, cell);
cell.gameObject.transform.SetAsFirstSibling();
}
}
}
아직 오류가 많다.
LegacyTableViewController 스크립트에 한 줄 추가
cell.Index = index;
전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class LegacyTableViewController : GOTableViewController
{
[SerializeField] private string serviceKey; //서버 키 입력
private List<Legacy> legacyList = new List<Legacy>(); //리스트 생성
protected void Start()
{
StartCoroutine(LoadData());
}
protected override int numberOfRows()
{
return legacyList.Count; //개수 반환
}
protected override GOTableViewCell cellForRowAtIndex(int index)
{
//특정한 index의 셀을 만들려고할 때 특정한 셀을 만들어서 반환한다.
//부모에 만들어 놓으면, 상속만 받으면 가져다 쓰기만 하면 된다. 셀을 생성 하는 건 자식이 하더라도 셀을 재사용할 수 있는 로직은 GOTableViewController에서 만든다.
LegacyTableViewCell cell = dequeueReuseableCell() as LegacyTableViewCell; //재사용 가능한 셀이 있는가 as로 변환을 시킨다. 메서드의 반환 값이 GOTableViewCell이다.
if(cell == null)
{
cell = Instantiate(cellPrefab, content) as LegacyTableViewCell; //변환
}
cell.Index = index;
cell.legacyName.text = legacyList[index].명칭;
cell.type.text = legacyList[index].종목;
cell.designatedDate.text = legacyList[index].지정일;
return cell;
}
//이전 LegacyDataManger에서 LoadData 부분을 가져온다. 더보기 버튼 관련 코드는 지운다.
IEnumerator LoadData()
{
string url = string.Format("{0}?page={1}&perPage={2}&serviceKey={3}", Constants.url, 0, 100, serviceKey);
UnityWebRequest request = new UnityWebRequest();
using (request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log(request.error);
}
else
{
string result = request.downloadHandler.text; //텍스트 형식으로 받아오기. 많이 불러왔었던 빼곡한 글들로 불러온다.
LegacyData legacyData = JsonUtility.FromJson<LegacyData>(result); //파싱에서 가져온 이름 LegacyData
Legacy[] legacyArray = legacyData.data;
for (int i = 0; i < legacyArray.Length; i++)
{
legacyList.Add(legacyArray[i]);
}
}
base.Start();
}
}
}
내려갈때 계속 셀이 생성되어 잘 작동한다. 하지만 맨위/맨끝으로 가면 오류가 난다.
끝단에 가면 동작하지 않고 하나 전까지만 동작하게 조건을 추가한다.
GOTableViewController 스크립트 수정
if ((cellLinkedList.Last.Value.Index < totalRows - 1) && (content.localPosition.y + Screen.height > cellLinkedList.Last.Value.Index * cellHeight + cellHeight))
{
}
else if ((cellLinkedList.First.Value.Index > 0) && (content.localPosition.y < cellLinkedList.First.Value.Index * cellHeight))
{
}
전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class GOTableViewController : MonoBehaviour
{
//Cell을 생성하기 위한 cell 프리팹
[SerializeField] protected GOTableViewCell cellPrefab; //protected는 상속받은 클래스에서 사용 가능. 어차피 Serialized이기 때문에 public은 있으나 마나.
//Cell을 생성하게 될 content 객체
[SerializeField] protected Transform content;
//Cell 재사용을 위한 Queue 생성
protected Queue<GOTableViewCell> reuseQueue = new Queue<GOTableViewCell>();
//Cell 간의 연결고리를 관리하기 위한 List
protected LinkedList<GOTableViewCell> cellLinkedList = new LinkedList<GOTableViewCell>();
//cell의 높이
private float cellHeight;
//전체 셀의 개수
private int totalRows;
//자식 클래스에서 GOTableViewController가 표시해야 할 전체 Data의 수를 반환하는 메서드
protected abstract int numberOfRows(); //추상 메서드 생성, 맨 위의 class도 추상 클래스로 변경(public abstract class GOTableViewController) 자식 클래스가 상속받아서 해당 부분을 채울 것이다. 틀만 만드는 느낌.
//스크롤 바가 보이는 부분에다 맞추면 안되고 전체의 셀에 대한 스크롤 크기를 맞춰야 한다.
protected abstract GOTableViewCell cellForRowAtIndex(int index); //추상 메서드 하나 더 생성.
protected virtual void Start() //virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
{
totalRows = this.numberOfRows(); //전체 몇 개의 값이 필요한지 얻어온다.
float screenHeight = Screen.height; //화면의 높이 얻어오기. Canvas의 높이가 960으로 설정되어있다.
cellHeight = cellPrefab.GetComponent<RectTransform>().sizeDelta.y; //셀의 높이. 200으로 설정되어있다.
//표시해야 할 cell의 수 계산
int maxRows = (int)screenHeight / (int)cellHeight + 2; //줄 수이기 때문에 int로 줄의 개수를 자연수로 받는다. 한 화면에 표시되는 셀의 개수는 960/200=4.8정도 된다. 아래 가려진 셀의 개수에 여유를 주기 위해 2를 더한다.
maxRows = (maxRows > totalRows) ? totalRows : maxRows;
//전체 셀을 포함하는 높이를 가질 수 도록 content의 크기를 조정
RectTransform contentTransform = content.GetComponent<RectTransform>();
contentTransform.sizeDelta = new Vector2(0, totalRows * cellHeight);
//초기 Cell을 생성
for(int i = 0; i < maxRows; i++)
{
GOTableViewCell cell = cellForRowAtIndex(i);//인덱스만 넣으면 셀이 만들어지도록.
cell.gameObject.transform.localPosition = new Vector3(0, -i * cellHeight, 0); //첫 번째 셀 높이는 0, 두 번째 셀 높이는 -1 * 200 = -200
//LinkedList에 Last Cell로 추가
cellLinkedList.AddLast(cell);
}
}
protected GOTableViewCell dequeueReuseableCell() //재사용이 가능한 Cell을 반환해주는 메서드. 재사용 cell 혹은 null이 반환된다.
{
if(reuseQueue.Count > 0) //쓸만한 셀이 있다면
{
GOTableViewCell cell = reuseQueue.Dequeue();
cell.gameObject.SetActive(true);
return cell;
}
else
{
return null; //재사용 가능한 셀이 있다면 Instantiate 할 수 있다고 생각할 수 있는데, 여기서 자식 형태의 cell을 만들어낼 수는 없다. 자식 쪽에서 만들어야 함. 넣었던 자식이 그대로 반환될 것.
}
}
public void OnValueChanged(Vector2 vector)
{
Debug.Log($"Screen.height: {Screen.height}, Content.localPosition.y: {content.localPosition.y}");
if ((cellLinkedList.Last.Value.Index < totalRows - 1) && (content.localPosition.y + Screen.height > cellLinkedList.Last.Value.Index * cellHeight + cellHeight)) // 부등호 왼쪽은 현재 위치 + 스크린 높이, 부등호 오른쪽은 마지막 보이는 셀의 개수 * 셀의 높이 + 여유 높이 하나 추가. 스크롤을 내려서 스크린 높이가 커지면,
{
//하단에 새로운 Cell이 만들어지는 상황
//처음에 있던 Cell은 Reuse Queue에 저장
LinkedListNode<GOTableViewCell> firstCellNode = cellLinkedList.First; //첫 번째 셀은 이미 화면을 벗어나서 없을 테니까.
cellLinkedList.RemoveFirst(); //첫 번째 있던 값을 지워버리기
firstCellNode.Value.gameObject.SetActive(false); //첫 번째 있던 셀을 비활성화하기
reuseQueue.Enqueue(firstCellNode.Value); //reuseQueue에 저장하기
//하단에 새로운 Cell 생성
LinkedListNode<GOTableViewCell> lastCellNode = cellLinkedList.Last;
int currentIndex = lastCellNode.Value.Index;
GOTableViewCell cell = cellForRowAtIndex(currentIndex + 1);
cell.gameObject.transform.localPosition = new Vector3(0, -(currentIndex + 1) * cellHeight, 0); //좌표를 마지막으로 보낸다.
cellLinkedList.AddAfter(lastCellNode, cell);
cell.gameObject.transform.SetAsLastSibling(); //게임 오브젝트 순서도 마지막으로 보낸다.
}
else if ((cellLinkedList.First.Value.Index > 0) && (content.localPosition.y < cellLinkedList.First.Value.Index * cellHeight))
{
//상단에 새로운 Cell이 만들어지는 상황
//마지막에 있던 Cell은 Reuse Queue에 저장
LinkedListNode<GOTableViewCell> lastCellNode = cellLinkedList.Last;
cellLinkedList.RemoveLast();
lastCellNode.Value.gameObject.SetActive(false);
reuseQueue.Enqueue(lastCellNode.Value);
//상단에 새로운 Cell 생성
LinkedListNode<GOTableViewCell> firstCellNode = cellLinkedList.First;
int currentIndex = firstCellNode.Value.Index;
GOTableViewCell cell = cellForRowAtIndex(currentIndex - 1);
cell.gameObject.transform.localPosition = new Vector3(0, -(currentIndex - 1) * cellHeight, 0);
cellLinkedList.AddBefore(firstCellNode, cell);
cell.gameObject.transform.SetAsFirstSibling();
}
}
}
양 끝으로 가도 오류가 나지 않고 잘 작동한다. 굿~
Unity Version Control 유니티 버전 컨트롤 사용하기
3D URP 프로젝트 생성, Use Unity Version Control을 체크하고 location은 Japan으로 설정한다.
에셋 스토어에서 무료 에셋을 하나 받는다.
Tanks! Tutorial | 자습서 | Unity Asset Store
Get the Tanks! Tutorial package from Unity Technologies and speed up your game development process. Find this & other 자습서 options on the Unity Asset Store.
assetstore.unity.com
아래의 Plastic SCM 홈페이지 아래로 내려가서 [Download Plastic SCM] 버튼을 눌러 다운로드 한다.
Plastic SCM - The Distributed Version Control for Big Projects
Looking for Plastic SCM? Plastic SCM was acquired by Unity in 2020 and is now a part of Unity DevOps, a modular solution from Unity Gaming Services. Unity DevOps is a tool specifically tailored for the rigors of game development, and gives users access to
www.plasticscm.com
[Window] - [Unity Version Control] 창을 켜고, 창에서 로그인, Create WorkSpace를 하면 다음과 같이 추가된 에셋들을 볼 수 있다.
Tanks 폴더 하나 만들고, Scenes 폴더를 제외한 모든 요소를 Tanks 폴더에 넣는다.
이미 체크가 되어있는 요소들을 [Check in Changes] 버튼으로 Check in 한다. github의 commit과 같다.