2024.12.12(목)
오늘 오전에 팀원분들과 데이터 테이블 변경점에 대한 회의를 진행하고, 오후에 작업을 진행했다.
오늘 작업한 내용은 ScrollView와 Content의 자동화...라고 할 수 있겠다.
이번 글에서는 scrollView_OwnedKnights라는 ScrollView를 이용해 예제를 진행하겠다.
먼저 UIContent.cs라는 코드를 작성하였다.
이 코드는 ScrollView의 Content로 활용되는 Prefab에 부착될 스크립트의 부모이다.
자체적으로 바로 쓸 수 있는 클래스는 아니기 때문에, 추상클래스로 작성하였다.
UIContent의 제네릭으로 IData 타입의 데이터를 가져오는 SetData
메서드를 만들었다. 이 메서드를 다른 곳에서 호출하면, 이 Content에 해당 데이터가 들어오게 된다.
KnightDefault는 현재 IData를 상속받아 Knight의 데이터를 저장하는 데이터 형식이다.
데이터의 양은 많지만, 참조 형식의 복사를 통해 데이터를 가져오게 되서 깊은 복사가 일어나지 않기 때문에, 메모리적으로 큰 문제는 없을 듯하다.
using UnityEngine;
public abstract class UIContent<T> : MonoBehaviour where T : IData
{
protected string dataID;
protected T data;
// 상속받는 content에서 override해서
// content의 ui들에 데이터 삽입해서 사용
public virtual void SetData(T inputData)
{
data = inputData;
dataID = data.ID;
}
}
public interface IData
{
string ID { get; }
}
[Serializable]
public class KnightDefault : IData
{
public string ID { get; set; }
public string NameEn { get; set; }
public string NameKr { get; set; }
public string Description { get; set; }
public int Rank { get; set; }
public string Prefeb { get; set; }
public string Thumbnail { get; set; }
public float AttackPower { get; set; }
public float AttackSpeed { get; set; }
public float Health { get; set; }
public float Defense { get; set; }
public float MoveSpeed { get; set; }
public float AttackRange { get; set; }
public int Leadership { get; set; }
public int Intelligence { get; set; }
public int Loyalty { get; set; }
public int DownPayment { get; set; }
public int Salary { get; set; }
public int TrainingLevel { get; set; }
public int Experience { get; set; }
public float TrainingWeighted { get; set; }
public float ExperienceWeighted { get; set; }
}
틀로 만들어 놓은 Content의 구현 부분이다.
UIContent를 상속받아 제네릭으로 Content의 데이터 형식에 맞게 클래스를 보내준다.
그리고, [SerializeField]
로 선언된 변수들을 인스펙터 창에서 연결을 해 주고, SetData
메서드를 override
및 base.Setdata
를 실행하여, Content의 데이터들을 넣어준다.
이렇게 하면, ScrollView에서 각 Content를 생성하고, Content 별로 SetData
만 실행해주면 알아서 해당 데이터가 적용된 Content를 만들 수 있게 된다.
Content 구현 클래스의 이름은, 현재, UI의 스크립트와 Hirerachy창에 있는 오브젝트의 이름이 통일되게 작성해주고 있기 때문에, 앞 부분이 소문자가 되었다.
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class btn_OwnedKnight : UIContent<KnightDefault>
{
[SerializeField] private Image thumbnailKnight;
[SerializeField] private TMP_Text txtKnightName;
public void OnClickBtn()
{
UIManager.Instance.Show<UI_PopupKnightInfo>();
}
public override void SetData(KnightDefault inputData)
{
base.SetData(inputData);
//썸네일은 아직 파일이 없기도 하고,
//경로로 저장된 파일을 로드해오는 과정이 필요하기 때문에, 아직 구체적으로 적지 않았다.
//thumbnailKnight = data.Thumbnail;
txtKnightName.text = data.NameKr;
}
}
다음은 ScrollView 스크립트의 틀이다.
UIContent와 마찬가지로 추상클래스로 작성하였다.
SetScrollView
메서드를 통해, ScrollView를 통해 보여줄 dataList를 가져오고, 이 데이터를 이용해서, 지정된 uiContentParent 오브젝트에 uiContentPrefab의 형태의 Content들을 생성하고 데이터를 세팅해준다.
이 때, uiContentPool 이라는 리스트를 만들어서 생성한 UIContent 인스턴스들과 연결하고 관리할 수 있게 했다.
SetActive
true, false를 통해 자체 풀링 시스템을 만들어서 dataList의 데이터가 삭제되거나 추가되었을 때, 넘치는 인스턴스들을 SetActive
true로 만들어서 꺼내 쓸 수 있다.
ClearScrollView
메서드를 이용해, 모든 UIContent를 비활성화하고, dataList를 초기화할 수 있다. 그런데, 이 메서드를 쓸 일이 있을까...? 싶다.
using System.Collections.Generic;
using UnityEngine;
public abstract class UIScrollView<T> : MonoBehaviour where T : IData
{
protected List<T> dataList;
[SerializeField] protected Transform uiContentParent;
[SerializeField] protected UIContent<T> uiContentPrefab;
// 생성된 UIContent 인스턴스 목록 (Pooling): +생성된 UIContent제어
private List<UIContent<T>> uiContentPool = new List<UIContent<T>>();
/// <summary>
/// ScrollView를 설정합니다.
/// </summary>
public void SetScrollView(List<T> newDataList)
{
// 데이터 리스트 업데이트
dataList = newDataList;
// 기존 UIContent 재사용 또는 초기화
for (int i = 0; i < dataList.Count; i++)
{
UIContent<T> uiContent;
// Pool에 이미 생성된 UIContent가 있으면 재사용
if (i < uiContentPool.Count)
{
uiContent = uiContentPool[i];
uiContent.gameObject.SetActive(true); // 활성화
}
else
{
// Pool에 없는 경우 새로 생성
uiContent = Instantiate(uiContentPrefab, uiContentParent);
uiContentPool.Add(uiContent);
}
// 데이터 설정
uiContent.SetData(dataList[i]);
}
// 사용되지 않는 Pool 객체 비활성화
for (int i = dataList.Count; i < uiContentPool.Count; i++)
{
uiContentPool[i].gameObject.SetActive(false); // 비활성화
}
}
/// <summary>
/// ScrollView를 초기화하여 모든 데이터를 제거합니다.
/// </summary>
public void ClearScrollView()
{
// Pool에 있는 모든 UIContent 비활성화
foreach (var uiContent in uiContentPool)
{
uiContent.gameObject.SetActive(false);
}
// 데이터 리스트 초기화
dataList.Clear();
}
}
아직은 특별히 ScrollView만의 로직이 따로 필요 없다.
StarteySettingSystem과 UI_StrategySetting 스크립트를 이용해, 데이터를 리스트에 빼고 넣는 로직이 작성될 것이고, 여기 ScrollView에서는 SetScrollView
메서드를 이용해 데이터를 넣는 것만 하면 된다.
ScrollView 클래스의 이름은 Content 구현 클래스와 마찬가지의 이유로 다음과 같이 작성되었다.
public class scrollView_OwnedKnights : UIScrollView<KnightDefault>
{
}
UI_StrategySetting 이라는 UI에 scrollView_OwnedKnights 가 존재하기 때문에 UI_StrategySetting 에서 ScrollView에 데이터를 집어넣는 작업을 수행할 것이다.
여기선, 다음 작업 이외의 것들은 신경쓰지 않아도 된다.
앞서 만든 scrollView_OwnedKnights라는 ScrollView 구현 클래스를 [SerializeField]를 이용해 인스펙터 창으로 연결한다.
그리고, SetKnightData
라는 메서드를 만들어서 해당 ScrollView에 데이터를 넣어준다. 이 메서드에선 아까 만든 SetScrollVeiw
라는 메서드만 호출해주면 된다.
이제, 마지막으로 해당 메서드를 실제 데이터가 있는 곳인 StrategySettingSystem 에서 호출해주면 된다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UI_StrategySetting : MonoBehaviour
{
[SerializeField] private GameObject mapInspectionCamera;
[SerializeField] private GameObject strategySettingCamera;
[SerializeField] private GameObject uiMapInspection;
[SerializeField] private GameObject uiStrategySetting;
[SerializeField] private GameObject uiPopupBattleStart;
Camera cameraMapInspectionCamera;
[SerializeField] scrollView_OwnedKnights scrollViewOwnedKnights;
private void Start()
{
cameraMapInspectionCamera = mapInspectionCamera.GetComponent<Camera>();
}
public void OnClickBattleStart()
{
uiPopupBattleStart.SetActive(true);
//UIManager.Instance.Show<UI_PopupBattleStart>();
}
public void OnClickReturnToMapInspection()
{
mapInspectionCamera.SetActive(true);
strategySettingCamera.SetActive(false);
uiMapInspection.SetActive(true);
uiStrategySetting.SetActive(false);
DragAndDropManager.Instance.SetWorldDragCamera(cameraMapInspectionCamera);
}
public void SetKnightData(List<KnightDefault> knightDataLists)
{
scrollViewOwnedKnights.SetScrollView(knightDataLists);
}
}
다음은 StrategySettingSystem이다. 여기서, 전략 설정에 대한 메서드가 진행될 것이다.
일단은, ScrollView가 잘 작동하는 지 확인하기 위해, UI_StrategySetting 오브젝트를 public으로 선언해 인스펙터 창으로 연결해 주었다. (좀 더 괜찮은 방법이 있을 것 같은데 찾아볼 예정이다.)
그리고, 맨 아래의 Start
부분에서 UI_StrategySetting 스크립트에서 작성한 SetKnightData를 호출해서 데이터를 넘겨주었다.
using System.Collections.Generic;
using System.Text;
using UnityEngine;
// 전략 설정 시스템
public class StrategySettingSystem : MonoBehaviour
{
DomainArmy domainArmy;
// 전략리스트
List<ArmyStrategy> strategyList;
// 부대임무리스트
List<UnitDivisionRole> unitDivisionRoleDataList;
// 영주
ArmyLord domainArmyLord;
// 기사리스트
List<ArmyKnight> knightList;
// 병종리스트
List<ArmyUnitType> unitTypeList;
Strategy currentStrategy;
// 임시로 ui와 연결
public UI_StrategySetting uI_StrategySetting;
public ArmyLord DomainArmyLord
{
get { return domainArmyLord; }
private set { domainArmyLord = value; }
}
public List<ArmyKnight> KnightList
{
get { return knightList; }
private set { knightList = value; }
}
public List<ArmyUnitType> UnitTypeList
{
get { return unitTypeList; }
private set { unitTypeList = value; }
}
public List<ArmyStrategy> StrategyList
{
get { return strategyList; }
private set { strategyList = value; }
}
public Strategy CurrentStrategy
{
get { return currentStrategy; }
private set { currentStrategy = value; }
}
private void Awake()
{
if (TryGetComponent<DomainArmy>(out domainArmy))
{
Debug.Log($"영지 -> 전략설정 {typeof(DomainArmy).Name} 로드 성공!");
}
else
{
Debug.LogError($"영지 -> 전략설정 {typeof(DomainArmy).Name} 로드 실패!");
}
}
// DomainArmy의 데이터를 참조해서 설정
public void SetStrategySettingSystemData()
{
domainArmyLord = domainArmy.DomainArmyLord;
strategyList = domainArmy.StrategyList;
unitDivisionRoleDataList = domainArmy.UnitDivisionRoleDataList;
knightList = domainArmy.KnightList;
unitTypeList = domainArmy.UnitTypeList;
}
// 데이터가 잘 들어갔는지 확인
public void LogStrategySettingSystemData()
{
StringBuilder sb = new StringBuilder();
sb.Append("로드: ");
sb.Append(domainArmyLord.LordData.Name);
sb.Append("\n");
sb.Append("전략 시스템's \n전략 목록: ");
for (int i = 0; i < strategyList.Count; i++)
{
sb.Append(strategyList[i].StrategyData.Name);
sb.Append(", ");
}
sb.Append("\n");
sb.Append("부대 임무 목록: ");
for (int i = 0; i < unitDivisionRoleDataList.Count; i++)
{
sb.Append(unitDivisionRoleDataList[i].Name);
sb.Append(", ");
}
sb.Append("\n");
sb.Append("기사 목록: ");
for (int i = 0; i < knightList.Count; i++)
{
sb.Append(knightList[i].KnightData.NameKr);
sb.Append(", ");
}
sb.Append("\n");
sb.Append("병종 목록: ");
for (int i = 0; i < unitTypeList.Count; i++)
{
sb.Append(unitTypeList[i].UnitTypeData.NameKr);
sb.Append(", ");
}
sb.Append("\n");
Debug.Log(sb);
}
// UI의 반응과 연계하여 전략 설정 및 저장
private void Start()
{
List<KnightDefault> knightDataList = new List<KnightDefault>();
for (int i = 0; i < knightList.Count; i++)
{
knightDataList.Add(knightList[i].KnightData);
}
uI_StrategySetting.SetKnightData(knightDataList);
}
}
위의 과정을 거치면 다음과 같이 ScrollView에 데이터가 들어간 Content들이 들어가는 것을 볼 수 있다.
오늘은 ScrollView와 Content의 틀을 만들어서 데이터 리스트를 넣어주면, 자동으로 ScrollView가 완성되게 만들어 보았다.
앞으로도 자주 쓰이게 될 ScrollView를 이제 어느정도 쉽게 쓸 수 있을 것 같다.
이상, 오늘의 TIL 끝..!
이렇게 좋은 정보를 주시다니 감사합니다!!