2019. 7. 26. 17:10ㆍ무한 스크롤 뷰
항목은 스크롤 뷰에 보여지는 객체이며 게임에서 사용되는 데이터를 토대로 구현됩니다.
항목을 구현하기 위한 데이터
항목을 구현하기 위해 필요한 데이터들을 내부 데이터와 외부 데이터로 구분하여 구현합니다.
내부 데이터 |
외부 데이터 |
|
게임 데이터 |
정보 데이터 |
게임 데이터 |
테이블로 구성된 변하지 않는 데이터를 의미하며 값 자체가 변경이 되서는 안됩니다. 항목에 대한 기본 정보를 보여줄 때 사용됩니다. |
|
정보 데이터 |
유저의 행위를 통해 변화된 데이터를 저장하는 데이터입니다. 항목을 강화하면 강화된 수치, 레벨 수치 등 게임을 종료해도 그 값이 초기화가 되는 것이 아니고 계속해서 유지되어야하는 데이터를 의미합니다. |
|
외부 데이터 |
유저의 요청에 의해 항목을 변경하는 데이터를 의미합니다. 예를 들면 항목을 클릭하였을 때 해당 항목에 클릭 상태에 대한 이미지를 표시해야되는 경우를 의미합니다. |
항목 구현하기
항목에 대한 테이블 만들기
항목을 표시하려면 게임 데이터를 만들어야합니다. 이 게임 데이터는 텍스트, XML, Json 등으로 구현할 수 있습니다. 여기서는 Unity에서 제공하는 JsonUtility를 바탕으로 데이터를 가져오기 위해 Json 데이터를 사용하며 테이블은 엑셀을 사용합니다. 엑셀을 Json으로 변환하는 툴은 밑의 사이트에서 제공하는 것을 사용했습니다.
coolengineer/excel2json
Excel to JSON converter. Structured data (e.g. game design) from non-technician can be easily converted to JSON file for programmer. - coolengineer/excel2json
github.com
항목을 표시하기 위해서는 제작 의도에 따라서 다르지만 간단하게 살펴보면 다음과 같습니다.
[항목을 보여주기 위한 이미지 이름, 항목의 등급, 항목의 레벨]
index |
아틀라스 이름 |
이미지 이름 |
기본 등급 |
기본 레벨 |
|
1 |
atlas_name |
image_1 |
1 |
1 |
|
2 |
atlas_name |
image_2 |
2 |
10 |
이미지 이름의 경우 텍스처 아틀라스를 사용하면 이미지의 확장을 고려해서 아틀라스 이름을 지정해둡니다. 이렇게 구현된 테이블을 Json으로 변환하면 게임 데이터로 활용할 수 있습니다.
항목에 대한 세분화의 문제점
항목을 클래스로 구현할 때 각 항목의 대표하는 이름을 클래스로 구현하는 경우가 있습니다. 예를 들면 무기와 방어구에 관한 데이터가 있으면 다음과 같이 구현할 수 있습니다.
[System.Serializable]
public class DataWeapon
{
}
|
[System.Serializable]
public class DataClothes
{
}
|
하지만 이런 식으로 세분화할 경우 유지 보수측면에서 상당히 어려움이 생깁니다. 데이터를 세분화하는 것뿐만 아니라 이 데이터를 토대로 구현하는 각 스크립트도 세분화를 시켜야합니다. 다음 표를 참고해주세요.
종류 |
데이터 클래스 |
데이터 클래스를 토대로 항목을 표시하는 클래스 |
무기 |
DataWeapon |
UIWeaponItem |
방어구 |
DataClothes |
UIClothesItem |
예시를 들기 위해서 2개만 사용했지만 밑의 그림과 같이 6개만 되어도 12개의 스크립트 혹은 그 이상을 관리해야됩니다.(데이터를 정보화하는 클래스가 추가되면 최소 6 + 6 + 6 = 18개가 됩니다. 으악...)
스크립트 개수뿐만 아니라 다음과 같은 문제점이 발생합니다.
공통되는 기능들을 별도로 관리를 해야되는데 수정을 해야될 경우 개수만큼 모두 수정해야합니다. |
||
항목에 대한 탭을 추가해달라는 요청이 들어오면 스크립트를 추가하고 공통된 기능과 이 스크립트만의 기능을 복합해서 구현해야합니다. |
||
항목을 일괄적으로 볼 수 있게 해달라는 요청이 들어올 경우 세분화된 구조로 구현할 수가 없기 때문에 따로 작업을 해야합니다. |
밑의 이미지처럼 무기, 방어구, 악세사리 등 해당 항목들을 일괄로 표시해야하는 경우도 있습니다.
일괄 표시할 때 유의사항
일괄 표시가 언급이 되어서 간단하게 유의사항을 언급하고자 합니다.
정렬 |
일괄 처리를 할 때에도 정렬을 해달라는 요청이 있을 경우 반드시 정렬을 할 수 있는 공통되는 요소를 생각해야합니다. 무기를 우선적으로 보여준다던지, 방어구를 우선적으로 보여준다던지 등과 같이 구분할 수 있는 요소가 있어야됩니다. |
||
필터 |
정렬과 마찬가지로 공통되는 요소가 존재해야합니다. 무기만 표시한다던지, 방어구만 표시한다던지 등과 같이 구분할 수 있는 요소가 있어야합니다. |
공통 데이터 클래스
항목의 세분화는 문제점이 많습니다. 따라서 공통 클래스를 구현하여 관리하도록 합니다.
[System.Serializable]
public class DataEquipItem
{
public int Index;
public string AtlasName;
public string Thumb;
public string Grade;
public int Level;
}
|
public enum eEquipPartType : byte
{
Weapon,
Clothes,
};
|
이처럼 작성하면 항목의 종류와 상관없이 하나의 스크립트로 관리할 수 있습니다. 장비의 종류에 따라서 별도로 처리하고 싶은 경우 분기문을 사용하거나 파생시킬 클래스를 작성하여 가상함수를 제작하여 처리하면 됩니다.
데이터 클래스는 만들었는데 종류를 구분할 수 있는 방법이 없는데 어떻게 구분하나요?
항목들을 구축한 테이블을 분할해서 관리하기 때문에 데이터 클래스 안에 별도의 구분자를 구현하지 않았습니다. 자세한 것은 바로 밑의 내용을 참고해주세요.
데이터 클래스를 불러오는 관리자 클래스를 구현
데이터 클래스는 하나이지만 테이블은 무기 테이블, 방어구 테이블 등 가급적이면 분할해서 관리하는 것이 편리합니다.
공통되는 요소가 있다고 해서 테이블도 합쳐서 사용해버리면 다음과 같은 문제점들이 발생합니다.
외적인 문제 |
||
새로운 항목이 추가되면 삽입되는 항목을 기준으로 밀어야합니다. |
||
항목을 삭제할 경우 해당 항목을 기준으로 땡겨야합니다. |
||
특정 항목을 찾아야할 때 테이블에서 기능을 제공하더라도 여러 가지 번거로운 점이 있습니다. |
내적인 문제 |
||
테이블을 하나로 관리하기 때문에 항목의 종류에 대한 리스트를 가져올 경우 검색 사간이 오래 걸립니다. 항목이 200개고 종류가 6개이면 1200번을 검색합니다. 또한 항목이 다른 것에 대한 예외 처리 로직을 별도로 추가해야합니다. |
||
반복문을 사용하여 특정 아이템들을 가져올 경우 찾는 아이템들이 뒤쪽에 나열되어 있을 경우 검색양이 증가합니다. 예를 들어 가져올 아이템이 3개인데 990, 991, 992 행에 존재하고 정렬을 하지 않았을 경우 총 990 + 991 + 992 = 2973번을 검색하게 됩니다. |
위와 문제점을 고려하여 데이터를 관리하는 클래스를 구현하면 다음과 같습니다.
public class TableManager : Singleton<TableManager>
{
void Awake()
{
LoadData();
}
public void LoadData()
{
Converter ct = new Converter();
ct.ImportData(ref mWeaponDatas, "Data/WeaponTable");
ct.ImportData(ref mClothesDatas, "Data/ClothesTable");
}
public DataEquipItem GetData(int id, eEquipPartType type)
{
return GameUtil.SBinarySearch(GetData(type), id);
}
public DataEquipItem[] GetData(eEquipPartType type)
{
DataEquipItem[] datas = null;
switch (type)
{
datas = mWeaponDatas;
break;
case eEquipPartType.Clothes:
datas = mClothesDatas;
break;
}
return datas;
}
private DataEquipItem[] mWeaponDatas;
private DataEquipItem[] mClothesDatas;
}
|
코드를 보면 무기 테이블(WeaponDatas), 방어구 테이블(ClothesDatas)로 분리하여 데이터들을 가져옵니다. Converter는 JsonUtlity를 사용하여 테이블(Json 형식의 데이터)를 게임 데이터로 변환해주는 클래스입니다. 이 클래스를 통하여 받을 배열 변수와 경로를 설정하면 적절하게 변환해서 데이터들을 채워줍니다.
정보화 클래스
정보화 클래스는 게임 데이터를 내포하여 게임 데이터에 토대로 변하는 데이터들을 저장하는 클래스입니다. 예를 들어 무기에 대한 기본 정보가 있고 이 무기에 대한 강화 수치가 있습니다.
무기에 대한 기본 정보 |
변하지 않는 데이터로써 게임 데이터로 볼 수 있습니다. |
|
무기에 대한 강화 수치 |
유저의 행위에 의해 변한 데이터이고 게임이 종료되어도 유지되어야합니다. |
이를 클래스로 구현하면 다음과 같이 구현할 수 있습니다.
[System.Serializable]
public class EquipInfo
{
public EquipInfo(int id, eEquipPartType type)
{
ID = id;
Type = type;
}
public DataEquipItem Data
{
get
{
if (mData == null)
{
mData = TableManager.Instance.GetData(Type, ID);
}
return mData;
}
}
public int ID
{
get
{
return mID;
}
set
{
mID = value;
}
}
public eEquipPartType Type
{
get
{
return mType;
}
set
{
mType = value;
}
}
private int mID;
private eEquipPartType mType;
private DataEquipItem mData;
}
|
정보(Info) 클래스는 저장되는 클래스입니다. 이 클래스에서 레벨이나 강화 수치, 경험치, 등급 등 필요한 요소를 추가하면 됩니다. 단 데이터 클래스는 클래스 자체를 저장하는 것이 아니고 데이터 클래스의 해당 ID를 저장하여 데이터를 불러오는 방식을 사용해야하며 데이터의 종류를 가져오기 위해 eEquipPartType를 저장합니다.
정보 클래스를 만들었으므로 정보 클래스를 관리하는 클래스를 구현합니다.
using System.Collections.Generic;
[System.Serializable]
public class UserInfo
{
public void Initialize()
{
InitEquipItems();
}
private void InitEquipItems()
{
for (int i = 0; i < 42; ++i)
{
}
for (int i = 0; i < 39; ++i)
{
}
}
public List<EquipInfo> Weapons
{
get
{
return mWeapons;
}
}
public List<EquipInfo> Clothes
{
get
{
return mClothes;
}
}
public List<EquipInfo> mWeapons;
public List<EquipInfo> mClothes;
}
|
UserInfo는 유저에 의해 행위가 일어날 만한 정보를 모두 관리하는 클래스입니다. 이 클래스도 역시 저장되는 클래스입니다. 게임을 시작할 때 저장된 데이터를 가지고 와서 클래스의 틀에 맞게 변환하여 설정합니다.
위에는 테스트를 위해서 소지한 항목들을 모두 생성합니다. 특정 항목에 대한 초기화가 필요할 경우 다음과 같이 InitEquipItems를 수정하면 됩니다.
private void InitEquipItems()
{
Weapons.Add(new EquipInfo(1, eEquipPartType.Weapon));
Weapons.Add(new EquipInfo(11, eEquipPartType.Weapon));
Clothes.Add(new EquipInfo(1, eEquipPartType.Clothes));
Clothes.Add(new EquipInfo(11, eEquipPartType.Clothes));
}
|
UserInfo를 관리하는 클래스
using System.Collections.Generic;
public class UserManager : Singleton<UserManager>
{
public UserManager() : base()
{
if (mUserInfo == null)
{
mUserInfo = new UserInfo();
mUserInfo.Initialize();
}
}
public List<EquipInfo> Weapons
{
get
{
return mUserInfo.Weapons;
}
}
public List<EquipInfo> Clothes
{
get
{
return mUserInfo.Clothes;
}
}
private readonly UserInfo mUserInfo;
}
|
항목에 대한 프리팹
사용하고자 하는 항목을 다음과 같이 프리팹으로 만듭니다
using UnityEngine;
using System.Collections.Generic;
public class UIEquipItem : UIEndlessItem
{
public static void SSetItems(List<UIEquipInfo> items)
{
sItems = items;
}
private void OnPress(bool isDown)
{
}
public override void Initialize(int index, EndlessToken token)
{
base.Initialize(index, token);
}
public override void _Update(int rowIndex, EndlessToken token)
{
base._Update(rowIndex, token);
Draw(token);
}
private void Draw(EndlessToken token)
{
if (ItemIndex < sItems.Count)
{
Active(true);
m_ItemNumber.text = ItemIndex.ToString("0000");
m_ItemID.text = sItems[ItemIndex].ID.ToString("0000");
m_Icon.spriteName = sItems[ItemIndex].Data.Thumb;
switch (sItems[ItemIndex].Type)
{
case eEquipPartType.Weapon:
m_Icon.color = Color.red;
break;
case eEquipPartType.Clothes:
m_Icon.color = Color.blue;
break;
}
}
else
{
Active(false);
}
}
private static List<UIEquipInfo> sItems = null;
public UILabel m_ItemID;
public UILabel m_ItemNumber;
public UISprite m_Icon;
}
|
주의사항
UserInfo 안에 항목 리스트를 선언했는데 어디까지나 테스트를 목적으로 선언한 것이며 실제로 이렇게 사용할 경우 효율적이지 못합니다. 항목을 저장할 때 UserInfo를 저장하는데 저장하지 않아도 되는 정보들까지 같이 저장합니다. 이 경우 UserInfo의 클래스가 크면 클 수록, 읽고/쓰기가 빈번할 수록 그만큼 지연 시간이 발생합니다. 최적화를 생각한다면 항목 리스트 관리하는 전용 클래스가 만들어서 관리하는 것이 좋습니다. 이 부분에 대한 구조 설계는 필자도 고민 중이며 정리가 되는데로 올리겠습니다.