본문 바로가기
스파르타 내배캠

스파르타 내배캠 Unity 3기 10일차

by LemongO 2024. 1. 5.

흐아ㅏㅏㅏㅏㅏㅁ

 

 

 

오늘은 어제에 이어서 해보자.

 

어제 구현한 기능은 Player기본 정보에 대한 세이브 기능이다.

 

즉, Player가 가지고 있는 아이템에 대한 정보는 저장이 되지 않았기에 이 부분을 해보고자 한다.

 


 

전체적인 아이템 구매, 장착의 흐름을 먼저 알아보자.

 

 

그리고 아이템을 구매 시 

아이템 구매 - 플레이어의 보유 아이템 리스트에 추가

아이템 장착 - 플레이어의 장착 아이템 딕셔너리 Type (Weapon등) 키에 Item 값으로 추가

이것이 기본적인 흐름이다.

 

Player 클래스에 해당 List와 Dictionary 가 있지만 Player 클래스를 저장 시 이 둘은 저장되지 않는다.

Json을 이용한 저장에 Type은 지원하지 않기 때문.

 

 

그렇기 때문에 저장이 가능한 타입을 이용해야한다.

이 때 Item 클래스에는 다양한 속성이 있는데

그 중 ID 라는 아이템당 고유한 번호를 가지는 int 타입의 속성이 있다.

 

 

 

internal class Item
{
    // 고유번호 ID
    public int      ID              { get; protected set; }        
    public string   Name            { get; protected set; }
    public string   AdditionalATK   { get; protected set; }
    public string   AdditionalDEF   { get; protected set; }        
    public string   AbilityName     { get; }
    public string   Desc            { get; }        
    public int      Price           { get; }
    public bool     IsEquip         { get; protected set; } = false;
    public bool     IsBuy           { get; protected set; } = false;

    public Item(int id, string name, string abilityName, string desc, int price)
    {
        ID = id;
        Name = name;
        AbilityName = abilityName;
        Desc = desc;
        Price = price;            
    }
    
    // 기타 메서드
}

 

이 Item의 ID 를 이용해 새로운 List와 Dictionary 를 생성 후 여기에 ID를 추가해 주도록한다.

 

// 런타임에서 가지고 있을 아이템 정보들
public Dictionary<Type, Item>   equippedItems       = new Dictionary<Type, Item>();        
public List<Item>               items               = new List<Item>();

// 저장시 가지고 있을 아이템 ID
public Dictionary<string, int>  equippedItemIndex   = new Dictionary<string, int>();
public List<int>                hasItems            = new List<int>();

구매 시 (판매 기능은 코드생략)
public void BuyOrSell(int price, Item item, bool isSell = false)
{            
    Gold += price;
    // 구매 시
    if (!isSell)
    {
        // 불러오기 시 hasItems에 포함된 ID인지 체크없이 Add 하게되면 무한루프에 빠지게된다.
        if (!hasItems.Contains(item.ID))
            // 아이템의 ID를 리스트에 추가
            hasItems.Add(item.ID);
        // 플레이어 아이템 리스트에 추가
        items.Add(item);
    }
    // 판매
}

장착 시
private void EquipItem(Item item)
{
    // 아이템 타입의 item 변수가 세부타입 Weapon/Armor/Amulet 에 따라 보정수치 증가
    switch (item)
    {
        // 보정
    }

    // 불러오기 시 장착중인 아이템이 저장 되어있으면 키 중복으로 에러발생.
    if (!equippedItemIndex.ContainsKey(item.GetType().Name))
        // 장착중인 아이템 인덱스 딕셔너리에 ID 추가 (예 : 키 = "Weapon", 값 = 1)
        equippedItemIndex.Add(item.GetType().Name, item.ID);
    equippedItems[item.GetType()] = item;            
}

 

BuyOrSell 메서드의 hasItems.Add / items.Add 를 통해 

구매한 아이템의 ID를 리스트에 추가 (저장용)

구매한 아이템을 Item 리스트에 추가 (런타임)

 

EquipItem 메서드의 equippedItemIndex.Add / equippedItems[Item.GetType()] 을 통해

장착 아이템타입명("Weapon" 등) 키에 해당 아이템의 ID를 값으로 추가 (저장용)

장착 아이템타입(Weapon 등) 키에 해당 아이템을 값으로 추가 (런타임)

 


아이템 ID 리스트 / 장착 아이템 딕셔너리 저장

 

이 후 게임 종료 시

플레이어 데이터 + 아이템 ID List<int> + 장착 아이템 Dictionary<string, int> 를 저장한다.

 

static void SavePlayer(Player player, string saveFolderName)
{
    string playerDataName       = "PlayerData.json";
    string playerItemsName      = "PlayerItems.json";
    string equippedItemsName    = "EquippedItems.json";

    string playerDataPath       = Path.Combine(saveFolderName, playerDataName);
    string playerItemsPath      = Path.Combine(saveFolderName, playerItemsName);
    string equippedItemsPath    = Path.Combine(saveFolderName, equippedItemsName);

    // 플레이어 정보 저장
    JsonSerializerOptions options = new JsonSerializerOptions() { WriteIndented = true};
    string playerSaveData = JsonSerializer.Serialize(player, options);
    playerSaveData = Regex.Unescape(playerSaveData);
    File.WriteAllText(playerDataPath, playerSaveData);

    // 플레이어 보유 장비 정보 저장
    string playerItemsSaveData = JsonSerializer.Serialize(player.hasItems, options);
    playerItemsSaveData = Regex.Unescape(playerItemsSaveData);
    File.WriteAllText(playerItemsPath, playerItemsSaveData);

    // 플레이어 장착 장비 정보 저장
    string equippedItemsSaveData = JsonSerializer.Serialize(player.equippedItemIndex, options);
    equippedItemsSaveData = Regex.Unescape(equippedItemsSaveData);
    File.WriteAllText(equippedItemsPath, equippedItemsSaveData);

    Console.WriteLine("데이터가 저장되었습니다.");
}

 

7개의 구매한 아이템, 3종의 장착한 아이템
PlayerItems/EquippedItems 저장파일의 추가
7개의 서로다른 ID를 가진 아이템, 3종의 장착한 아이템 타입명과 ID

 

정상적으로 ID 값들이 저장되었다.

이제 불러오기를 할 차례다.

 


아이템 ID 리스트 / 장착 아이템 딕셔너리 불러오기

 

불러오기 시 절차는

1. 플레이어 데이터 로드 (Player 생성 = Loaded Player)

2. ItemList 로드 (List<int>)

3. EquippedItemList 로드 (Dictionary<string, int>)

 

LoadedPlayer 의 필드인 hasItems / equippedItemIndex 에 접근해

해당 필드에 2, 3에서 로드된 정보를 그대로 덮어씌운다.

 

static Player LoadPlayer(string saveFolderName, Shop shop)
{
    // 불러올 파일은 세 가지.
    // 1. 플레이어 Status를 담은 파일
    // 2. 플레이어가 보유중인 Item 의 ID를 담은 파일
    // 3. 플레이어가 장착중인 Item 의 ID를 담은 파일
    string playerDataName       = "PlayerData.json";
    string playerItemsName      = "PlayerItems.json";
    string equippedItemsName    = "EquippedItems.json";

    // 각각의 경로.
    string playerDataPath       = Path.Combine(saveFolderName, playerDataName);
    string playerItemsPath      = Path.Combine(saveFolderName, playerItemsName);
    string equippedItemsPath    = Path.Combine(saveFolderName, equippedItemsName);

    // 경로에 모든 파일들이 다 있을 때 불러오기. (하나라도 없으면 파일이 손상된 것으로 간주, 플레이어를 새로 생성한다.)
    if (File.Exists(playerDataPath) && File.Exists(playerItemsPath) && File.Exists(equippedItemsPath))
    {
        Console.WriteLine("데이터가 존재합니다.");
        // 플레이어 스테이터스
        string playerStatusData     = File.ReadAllText(playerDataPath);
        // 플레이어 보유장비
        string playerItemsIndex     = File.ReadAllText(playerItemsPath);
        // 플레이어 장착장비
        string playerEquippedIndex  = File.ReadAllText(equippedItemsPath);

        // 플레이어 스테이터스 파일을 역직렬화를 통해 Player 타입으로 반환.
        Player? loadedPlayer            = JsonSerializer.Deserialize<Player>(playerStatusData);
        // 불러온 플레이어는 스테이터스 정보만 가지고 있으므로 나머지 정보를 할당.
        // 1. 보유중인 Item ID 리스트를 같은 방법으로 List<int> 타입으로 반환.
        // 2. 장착중인 Item Type의 string, ID 딕셔너리를 같은 방법으로 Dictionary<string, int> 타입으로 반환.
        loadedPlayer.hasItems           = JsonSerializer.Deserialize<List<int>>(playerItemsIndex);
        loadedPlayer.equippedItemIndex  = JsonSerializer.Deserialize<Dictionary<string, int>>(playerEquippedIndex);
        // 불러온 정보들을 가지고 보유 및 장착중인 아이템을 불러오기.
        LoadPlayerItemsInfo(loadedPlayer, shop);

        return loadedPlayer;
    }
    else
    {
        Console.WriteLine("데이터가 없습니다.");
        return new Player(1, "르탄이", Define.PlayerClass.Worrior, 10, 5, 100, 100, 50000);
    }                
}

 

 

모든 정보를 불러온 플레이어의 정보에 할당했다.

이제 실제 런타임에 쓰일 아이템 리스트와 장착 아이템 리스트도 갱신해주자.

 

static void LoadPlayerItemsInfo(Player player, Shop shop)
{
    // 이중 for문으로 보유중인 Item의 ID가 상점에 진열된 Item의 ID와 일치하는지 검사.
    // 일치하면 해당 아이템을 구매상태로 전환.
    for (int i = 0; i < shop.items.Length; i++)
    {
        for (int j = 0; j < player.hasItems.Count; j++)
        {
            if (shop.items[i].ID == player.hasItems[j])
                shop.Restore(player, shop.items[i]);
        }
    }

    string weaponKey = Define.ItemType.Weapon.ToString();
    string armorKey  = Define.ItemType.Armor.ToString();
    string amuletKey = Define.ItemType.Amulet.ToString();

    // 플레이어가 가지고 있는 아이템 리스트를 루프.
    // 1. 장착 아이템 Dictionary에 각 Type 명의 키가 존재하고
    // 2. 플레이어의 아이템리스트[i] 가 해당 Type 이고
    // 3. 플레이어의 아이템리스트[i] 의 ID가 Load된 장착중인 Type의 아이템 ID와 같으면
    // 4. 해당 아이템을 장착
    for (int i = 0; i < player.items.Count; i++)
    {
        if (player.equippedItemIndex.ContainsKey(weaponKey) && player.items[i] is Weapon)
            if (player.items[i].ID == player.equippedItemIndex[weaponKey])
                player.EquipOrUnequipItem(player.items[i]);

        if (player.equippedItemIndex.ContainsKey(armorKey) && player.items[i] is Armor)
            if (player.items[i].ID == player.equippedItemIndex[armorKey])
                player.EquipOrUnequipItem(player.items[i]);

        if (player.equippedItemIndex.ContainsKey(amuletKey) && player.items[i] is Amulet)
            if (player.items[i].ID == player.equippedItemIndex[amuletKey])
                player.EquipOrUnequipItem(player.items[i]);
    }
}

 

아이템 리스트 갱신은 shop.Restore() 메서드로

장착 아이템 갱신은 player.EquipOrUnequipItem() 메서드로 갱신해준다.

 

Restore() 메서드는 아이템 가격을 지불하지 않는 형태로

EquipOrUnequipItem() 메서드는 Equip() 메서드의 분기점으로

플레이어의 구매, 장착 코드와 같은 동작을 한다

구매 시 (판매 기능은 코드생략)
public void BuyOrSell(int price, Item item, bool isSell = false)
{            
    Gold += price;
    // 구매 시
    if (!isSell)
    {
        // 불러오기 시 hasItems에 포함된 ID인지 체크없이 Add 하게되면 무한루프에 빠지게된다.
        if (!hasItems.Contains(item.ID))
            // 아이템의 ID를 리스트에 추가
            hasItems.Add(item.ID);
        // 플레이어 아이템 리스트에 추가
        items.Add(item);
    }
    // 판매
}

장착 시
private void EquipItem(Item item)
{
    // 아이템 타입의 item 변수가 세부타입 Weapon/Armor/Amulet 에 따라 보정수치 증가
    switch (item)
    {
        // 보정
    }

    // 불러오기 시 장착중인 아이템이 저장 되어있으면 키 중복으로 에러발생.
    if (!equippedItemIndex.ContainsKey(item.GetType().Name))
        // 장착중인 아이템 인덱스 딕셔너리에 ID 추가 (예 : 키 = "Weapon", 값 = 1)
        equippedItemIndex.Add(item.GetType().Name, item.ID);
    equippedItems[item.GetType()] = item;            
}

 

hasItems와 equippedItemIndex 필드는 이미 Load 과정에서 정보가 들어가 있으므로

if 문을 통해 생략하도록 조건을 걸어두고, items 와 equippedItems에 Load된 Item ID와 일치하는 아이템을 

각각 구매 / 장착 하게되면 Load된 Player의 생성이 완료된다.