유니티 RPG - 49. 인벤토리 데이터 저장 및 로드
Intro
이번 포스팅에서는 플레이어의 인벤토리 데이터를 저장하는것을 구현해보았습니다.
우선 인벤토리 데이터 저장과 로드를 어디에서 수행하는것이 좋을 지 고민해보았습니다.
- DataManager
- DataManager 클래스에서 인벤토리 데이터를 관리하는것이 가장 좋겠지만, 그렇게된다면 DataManager 와 Inventory 클래스의 결합도가
- Inventory
- Inventory 클래스가 무거워진다는 단점이 있습니다.
- 새로운 클래스
- 인벤토리 데이터를 로드/저장 하는 새로운 클래스를 만드는것은 불필요한 작업이 많이 늘어날것같다고 생각했습니다.
따라서 Inventory 클래스 내부에서 인벤토리 데이터 로드/세이브 기능을 추가하기로 결정하였습니다.
InventoryData.json
플레이어의 인벤토리 데이터를 저장하기위해서, 배열형태의 json 구조를 설계하였습니다.
몇가지 방법으로는
- 아이템이 있는 슬롯만 저장 - 아이템 슬롯의 갯수가 유동적일경우
- 모든 슬롯의 정보를 저장 - 고정된 아이템 슬롯 갯수
저는 두번째 방법을 택하였습니다.
{
"itemList": [
{
"slotIndex": 0,
"itemId": 0,
"amount": 0,
"isAccessibleSlot": false
},
{
"slotIndex": 1,
"itemId": 0,
"amount": 0,
"isAccessibleSlot": false
},
{
"slotIndex": 2,
"itemId": 0,
"amount": 0,
"isAccessibleSlot": false
},
{
"slotIndex": 3,
"itemId": 0,
"amount": 0,
"isAccessibleSlot": false
},
{
"slotIndex": 4,
"itemId": 0,
"amount": 0,
"isAccessibleSlot": false
},
{
"slotIndex": 5,
"itemId": 0,
"amount": 0,
"isAccessibleSlot": false
},
{
"slotIndex": 6,
"itemId": 0,
"amount": 0,
"isAccessibleSlot": false
},
//...
]
}
- 저장되어야할 정보는 4가지입니다.
- 슬롯의 인덱스, 아이템 ID, 아이템 갯수, 활성화여부
- 총 36개의 슬롯이 있으므로 36개의 정보가 필요합니다.
Inventory 클래스
따로 DTO 클래스를 만들지 않고, Inventory 클래스에 추가하였습니다.
/*
Inventory
- 인벤토리의 실질적인 내부 로직
- 아이템 추가, 아이템 사용, 아이템 삭제, 아이템 이동
- 인벤토리 데이터 Save & Load
*/
[System.Serializable]
public class ItemSlotData
{
public int slotIndex;
public int itemId;
public int amount;
public bool isAccessibleSlot;
}
[System.Serializable]
public class InventoryDataList
{
public List<ItemSlotData> itemList;
}
public class Inventory : MonoBehaviour
{
[SerializeField] Item[] items;
public ItemData[] itemDataArray;
// 인벤토리 데이터 로드
private void LoadInventoryData()
{
string path = Path.Combine(Application.persistentDataPath, "InventoryData.json");
if(File.Exists(path))
{
string jsonData = File.ReadAllText(path);
InventoryDataList dataList = JsonUtility.FromJson<InventoryDataList>(jsonData);
foreach(var data in dataList.itemList)
{
itemDataArray[data.slotIndex] = ItemTypeById(data.itemId);
// 해당 슬롯인덱스에 저장된 아이템이 없을 때
if (itemDataArray[data.slotIndex] == null)
{
continue;
}
// 아이템 생성 후 해당 슬롯에 직접 배치
Item item = itemDataArray[data.slotIndex].CreateItem();
if (item is CountableItem ci)
ci.SetAmount(data.amount);
AddItemAt(data.slotIndex, item);
}
}
// 아이템 타입별 반환
ItemData ItemTypeById(int id)
{
if (id > 10000 && id < 20000)
{
PortionItemData temp = DataManager.Instance.GetPortionDataById(id);
return temp;
}
else if (id > 20000 && id < 30000)
{
ArmorItemData temp = DataManager.Instance.GetArmorDataById(id);
return temp;
}
else if (id > 30000 && id < 40000)
{
WeaponItemData temp = DataManager.Instance.GetWeaponDataById(id);
return temp;
}
else
return null;
}
}
// 인벤토리 데이터 저장
private void SaveInventoryData()
{
InventoryDataList saveData = new InventoryDataList();
saveData.itemList = new List<ItemSlotData>();
// 모든 슬롯을 돌며
for(int i = 0; i<items.Length; i++)
{
ItemSlotData slotData = new ItemSlotData();
slotData.slotIndex = i;
// 아이템이 있는경우(id, amount 저장)
if(items[i] != null)
{
slotData.itemId = items[i].Data.ID;
if (items[i] is CountableItem ci)
slotData.amount = ci.Amount;
else
slotData.amount = 1;
}
// 아이템이 없는경우
else
{
slotData.itemId = 0;
slotData.amount = 0;
}
saveData.itemList.Add(slotData);
}
string jsonData = JsonUtility.ToJson(saveData, true);
string path = Path.Combine(Application.persistentDataPath, "InventoryData.json");
File.WriteAllText(path, jsonData);
Debug.Log("인벤토리 저장 완료");
}
// 특정 슬롯에 특정 아이템 추가
public void AddItemAt(int index, Item item)
{
if(index < 0 || index >= maxCapacity)
{
Debug.LogWarning("슬롯 인덱스 범위 초과" + index);
return;
}
items[index] = item;
UpdateSlot(index);
}
}
- 데이터의 직렬/역직렬화를 위해 ItemSlotData, InventoryDataList 클래스가 추가되었습니다.
- DTO 및 Data 클래스를 대신합니다.
- LoadInventoryData() : Json 데이터를 불러옵니다.
- 데이터를 불러옴과 동시에 인벤토리에 반영합니다.
- SaveInventoryData() : 현재 인벤토리 정보를 저장합니다.
- 모든 슬롯을 순회하여 현재 상태를 저장합니다.
- AddItemAt() : 불러온 데이터를 인벤토리에 추가하기위한 함수입니다.
- 데이터를 불러오게되면 특정 슬롯에 특정갯수만큼만 아이템을 추가하면되기때문에
- 기존의 AddItem() 의 로직이 쓸모없을뿐더러, AddItem() 함수 내부에서 로직의 중복으로인해 다른 결과를 얻을 수 있습니다.
- 따라서 AddItemAt() 함수를 새롭게 만들어 불러온 데이터를 반영하도록 하였습니다.
UpdateSlot() 함수 내부에서 SaveInventoryData() 호출하여,
인벤토리 내부의 정보가 업데이트될 때마다 인벤토리 데이터가 저장되도록 하였습니다.
버그 수정
플레이어가 장착한 아이템을 표시하는 Profile UI를 만드는 과정에서,
Inventory 클래스의 Use() 함수 내부로직을 수정했습니다.
이때 예외를 처리하지않아 소모성 아이템 사용시 갯수에 상관없이 전부 소모되던 현상이 있었습니다.
// 해당 슬롯 인덱스의 아이템 사용
public void Use(int index)
{
if (!IsValidIndex(index)) return;
if (items[index] == null) return;
// 1. 소모 아이템일 때
if(items[index] is IUsableItem usable)
{
// 사용에 성공했을 때
if(usable.Use())
{
// 수량이 있는 아이템의 경우
if(items[index] is CountableItem ci)
{
if(ci.IsEmpty)
{
Remove(index); // 수량이 다 떨어졌을경우 제거
}
}
// 일반 아이템은 바로 제거
else
{
Remove(index);
}
}
}
// 2. 장비 아이템일 때
else if(items[index] is IEquipableItem)
{
// 2.1. 무기 아이템일때
if(items[index] is WeaponItem curWeapon)
{
// 장착중인 아이템이 있으면 해제
if (equipmentUI.slotUIList[0].HasItem)
{
// 장착중인 아이템
WeaponItem prevItem = (WeaponItem)equipmentUI.items[0];
// 인벤토리에 아이템 추가
AddItem(prevItem.Data);
// 캐릭터 정보창 슬롯의 아이콘 제거
equipmentUI.slotUIList[0].RemoveItemIcon();
// 장착 해제
prevItem.Unequip();
}
curWeapon.Equip();
equipmentUI.SetItemIcon(curWeapon, curWeapon.WeaponData.Type, curWeapon.Data.ItemIcon);
}
// 2.2 방어구 아이템일때
else if(items[index] is ArmorItem curArmor)
{
// 장착중인 아이템이 있으면 해제
if(equipmentUI.slotUIList[TypeToIndex(curArmor)].HasItem)
{
// 장착중인 아이템
ArmorItem prevItem = (ArmorItem)equipmentUI.items[TypeToIndex(curArmor)];
// 인벤토리에 아이템 추가
AddItem(prevItem.Data);
// 캐릭터 정보창 슬롯의 아이콘 제거
equipmentUI.slotUIList[TypeToIndex(curArmor)].RemoveItemIcon();
// 장착 해제
prevItem.Unequip();
}
curArmor.Equip();
equipmentUI.SetItemIcon(curArmor, curArmor.ArmorData.SubType, curArmor.Data.ItemIcon);
}
// 방어구 타입별 인덱스
int TypeToIndex(ArmorItem curItem)
{
int typeIndex;
switch (curItem.ArmorData.SubType)
{
case "Shoes":
typeIndex = 1;
return typeIndex;
case "Gloves":
typeIndex = 2;
return typeIndex;
case "Top":
typeIndex = 3;
return typeIndex;
default:
return 0;
}
}
Remove(index);
}
UpdateSlot(index);
}
- 각 아이템별로 아이템을 제거하는 로직으로 수정하였습니다.
댓글남기기