[.Net Core] MMO 컨텐츠 구현(DB,대형구조,라이브) - DB 연동 - 2

Yijun Jeon·2022년 12월 3일
0
post-thumbnail

Inflearn - '[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part9: MMO 컨텐츠 구현 (DB, 대형구조, 라이브 준비)'를 스터디하며 정리한 글입니다.


Inventory

Unity 클라이언트로 인벤토리 UI 작업

UI

다른 Scene or Prefab 들과는 다르게 Stat & Inventory 창은 게임 Scene에서 계속 들고있고 껐다 켰다만 반복하도록 설정

Prefeb

  1. UI_GameScene : 인벤토리 & Stat 창의 캔버스 Prefab
    -> 연동 : UI_GameScene.cs
  2. UI_Inventory : 인벤토리 창 전체의 Prefab
    -> 연동 : UI_Inventory.cs
  3. UI_Inventory_Item : 아이템 하나의 Prefab
    -> 연동 : UI_Inventory_Item.cs
  4. UI_Stat : Stat 창 전체의 Prefab
    -> 연동 : UI_Stat.cs

연동 로직

  1. S_ItemLsit 패킷을 받아서 UI, 클라 메모리 상 아이템 정보 설정
  2. 클라 메모리의 아이템 정보로 UI Refresh
  3. 인벤토리 UI에서 아이템의 Slot 넘버에 맞게 지정

GameScene 연동

UI_GameScene.cs

public UI_Stat StatUI { get; private set; }
public UI_Inventory InvenUI { get; private set; }

public override void Init()
{
    base.Init();

    StatUI = GetComponentInChildren<UI_Stat>();
    InvenUI = GetComponentInChildren<UI_Inventory>();

    // 처음에는 꺼줌
    StatUI.gameObject.SetActive(false);
    InvenUI.gameObject.SetActive(false);
}

GameScene.cs

UI_GameScene _sceneUI;

protected override void Init()
{
    base.Init();

    SceneType = Define.Scene.Game;

    Managers.Map.LoadMap(1);

    // 빌드 화면 크기 설정
    Screen.SetResolution(640, 480, false);

    _sceneUI = Managers.UI.ShowSceneUI<UI_GameScene>();
}

클라에서 본인의 아이템 목록을 UI_Inventory.cs에 UI 상으로만 저장하는 것이 아니라 따로 클래스를 파서 동시에 관리하는 것이 좋음
-> Item.cs, InventoryManager.cs

Item.cs : 이전에 서버에서 쓰던 Item.cs코드 수정하여 사용

InventoryManager.cs : 이전에 서버에서 쓰던 Inventory.cs코드 수정하여 사용

  • 데이터 모델링 과정에서 Item 관련 새로 정의한 부분도 클라이언트에 복붙 필요
    -> Data.contents.cs & DataManager.cs

아이템 패킷 수신

PacketHandler.cs

public static void S_ItemListHandler(PacketSession session, IMessage packet)
{
    S_ItemList itemList = (S_ItemList)packet;

    // UI 설정
    UI_GameScene gameSceneUI = Managers.UI.SceneUI as UI_GameScene;
    UI_Inventory invenUI = gameSceneUI.InvenUI;

        
    Managers.Inven.Clear();
    // 메모리상 아이템 정보 적용
    foreach (ItemInfo itemInfo in  itemList.Items)
    {
        Item item = Item.MakeItem(itemInfo);
        Managers.Inven.Add(item);
    }

    // UI 에서 표시
    invenUI.gameObject.SetActive(true);
    invenUI.RefreshUI();
}

Inventory 연동

UI_Inventory.cs

// 인벤토리 내 보유 아이템 20개 목록
public List<UI_Inventory_Item> Items { get; } = new List<UI_Inventory_Item>();
public override void Init()
{
    // 초기화
    Items.Clear();
    GameObject grid = transform.Find("ItemGrid").gameObject;
    foreach (Transform child in grid.transform)
        Destroy(child.gameObject);

    for(int i=0;i<20;i++)
    {
        GameObject go = Managers.Resource.Instantiate("UI/Scene/UI_Inventory_Item", grid.transform);
        UI_Inventory_Item item = go.GetOrAddComponent<UI_Inventory_Item>();
        Items.Add(item);
    }

}

public void RefreshUI()
{
    // 메모리에 있는 아이템 목록 들고옴
    List<Item> items = Managers.Inven.Items.Values.ToList();
    // Slot 넘버 순으로 정렬
    items.Sort((left, right) => { return left.Slot - right.Slot; });

    foreach(Item item in items)
    {
        if (item.Slot < 0 || item.Slot >= 20)
            continue;

        Items[item.Slot].SetItem(item.TemplateId, item.Count);
    }
}
  • 각 슬롯에 들어간 아이템들의 이미지들 맞게 설정 필요
    -> 기존 ItemData.json에서 iconPath 로 이미지 경로 추가로 전달

아이템 연동

UI_Inventory_Item.cs

[SerializeField]
Image _icon;

public override void Init()
{}

// 이미지를 해당 아이템 이미지로 변경
public void SetItem(int templateId, int count)
{
    Data.ItemData itemData = null;
    Managers.Data.ItemDict.TryGetValue(templateId, out itemData);

    Sprite icon = Managers.Resource.Load<Sprite>(itemData.iconPath);
    _icon.sprite = icon;
}


Reward

몬스터가 죽었을 때 아이템을 드랍하거나 플레이어의 인벤토리에 자동으로 들어가는 보상 시스템 작업

  • 간단하게 후자로 설정
  • 몬스터도 데이터로 관리될 필요 있어짐

서버

  • 서버 작업을 할 때는 기획자와의 협업을 위해 데이터 시트까지 설계할 수 있는 능력이 필요

패킷 설계

몬스터 처리 보상으로 새로운 아이템이 생겼다는 것을 클라이언트에게 알려줘야 함

Protocol.proto

enum MsgId {
  ...
  S_ADD_ITEM = 17;
}

message S_AddItem{
	repeated ItemInfo items = 1;
}

데이터 모델링

Data.Contents.cs

#region Monster

[Serializable]
public class RewardData
{
    // 100분율 확률
    public int probability;
    public int itemId;
    public int count;
}

[Serializable]
public class MonsterData
{
    public int id;
    public string name;
    // totalExp는 처치 시 주는 경험치
    public StatInfo stat;
    public List<RewardData> rewards;
    //public string prefabpath;
}
    
[Serializable]
public class MonsterLoader : ILoader<int, MonsterData>
{
    public List<MonsterData> monsters = new List<MonsterData>();

    public Dictionary<int, MonsterData> MakeDict()
    {
        Dictionary<int, MonsterData> dict = new Dictionary<int, MonsterData>();
        foreach (MonsterData monster in monsters)
        {
            dict.Add(monster.id, monster);
        }
        return dict;
    }
}
#endregion

-> DataMananger.cs Dict, LoadData() 생성

보상 처리 - DB

DB로 아이템 생성 요청을 보내고 DB에서 성공적으로 저장이 되면 콜백으로 받아서 다시 진행

화살의 경우 ObjectType이 Player가 아니므로 보상 처리가 되지 않을 수 있음

  • 추후 펫이 생겨서 펫이 막타를 친다면 같은 상황 발생
    -> 일괄적으로 가상함수로 Owner를 받아와서 처리
GameObject.cs
public virtual GameObject GetOnwer()
{
    return this;
}
->
Arrow.cs
public override GameObject GetOnwer()
{
    return Owner;
}

Inventory.cs

public int? GetEmptySlot()
{
    for (int slot = 0; slot < 20; slot++)
    {
        Item item = _items.Values.FirstOrDefault(i => i.Slot == slot);
        // 빈 슬롯
        if (item == null)
            return slot;
    }
    // 슬롯이 다 참
    return null;
}

DbTransaction.cs

public static void RewardPlayer(Player player, RewardData rewardData, GameRoom room)
{
    if (player == null || rewardData == null || room == null)
        return;

    // TODO : 살짝 문제가 있긴 함
    // 아이템 슬롯 할당
    int? slot = player.Inven.GetEmptySlot();
    if (slot == null)
        return;

    ItemDb itemDb = new ItemDb()
    {
        TemplateId = rewardData.itemId,
        Count = rewardData.count,
        Slot = slot.Value,
        OwnerDbId = player.PlayerDbId
    };

    // You
    Instance.Push(() =>
    {
        using (AppDbContext db = new AppDbContext())
        {
            // DB에 아이템 정보 추가
            db.Items.Add(itemDb);
            bool success = db.SaveChangesEx();
            if (success)
            {
                // Me
                // 플레이어 인벤토리에 아이템 추가
                room.Push(() =>
                {
                    Item newItem = Item.MakeItem(itemDb);
                    player.Inven.Add(newItem);

                    // 클라이언트에게 알림
                    {
                        S_AddItem itemPacket = new S_AddItem();
                        ItemInfo itemInfo = new ItemInfo();
                        itemInfo.MergeFrom(newItem.Info);
                        itemPacket.Items.Add(itemInfo);

                        player.Session.Send(itemPacket);
                    }
                });
            }
        }
    });
}

보상 처리

Monster.cs

public int TemplateId { get; private set; }
 
public void Init(int templateId)
{
    TemplateId = templateId;

    MonsterData monsterData = null;
    DataManager.MonsterDict.TryGetValue(templateId, out monsterData);

    Stat.MergeFrom(monsterData.stat);
    Stat.Hp = monsterData.stat.MaxHp;
    State = CreatureState.Idle;
}

public override void OnDead(GameObject attacker)
{
    base.OnDead(attacker);

    // 플레이어가 죽였을 때만 생성
    GameObject owner = attacker.GetOnwer();
    if(owner.ObjectType == GameObjectType.Player)
    {
        RewardData rewardData = GetRandomReward();
        if(rewardData != null)
        {
            Player player = (Player)owner;

            DbTransaction.RewardPlayer(player, rewardData, Room);
        }
    }
}

// 랜덤으로 하나의 아이템 보상
RewardData GetRandomReward()
{
    MonsterData monsterData = null;
    DataManager.MonsterDict.TryGetValue(TemplateId, out monsterData);

    // 0~100
    int rand = new Random().Next(0, 101);

    int sum = 0;
    foreach(RewardData rewardData in monsterData.rewards)
    {
        // 확률의 합이 rand를 넘기는 순간 그 아이템 반환
        sum += rewardData.probability;
        if(rand <= sum)
        {
            return rewardData;
        }
    }
    return null;
}

클라이언트

데이터 모델링

MonsterData.Json

{
  "monsters": [
    {
      "id": "1",
      "name": "도플갱어",
      "stat": {
        "level": "1",
        "maxHp": "200",
        "attack": "20",
        "speed": "10.0",
        "totalExp": "10"
      },
      // 보상목록
      "rewards": [
        {
          "probability": "10",
          "itemId": "1",
          "count": "1"
        },
        {
          "probability": "10",
          "itemId": "2",
          "count": "1"
        },
        {
          "probability": "10",
          "itemId": "100",
          "count": "1"
        },
        {
          "probability": "10",
          "itemId": "101",
          "count": "1"
        },
        {
          "probability": "10",
          "itemId": "200",
          "count": "5"
        }
      ]
    }
  ]
} 
  • 서버에서 정의한 몬스터 데이터 클래스 복붙
  • but, 리워드에 대한 정보는 클라에서 들고 있으면 안됨

Data.Contents.cs

[Serializable]
public class MonsterData
{
    public int id;
    public string name;
    // totalExp는 처치 시 주는 경험치
    public StatInfo stat;
    // 클라에서 들고 있으면 안되는 정보
    // public List<RewardData> rewards;
    public string prefabpath;
}

[Serializable]
public class MonsterLoader : ILoader<int, MonsterData>
{
    public List<MonsterData> monsters = new List<MonsterData>();

    public Dictionary<int, MonsterData> MakeDict()
    {
        Dictionary<int, MonsterData> dict = new Dictionary<int, MonsterData>();
        foreach (MonsterData monster in monsters)
        {
            dict.Add(monster.id, monster);
        }
        return dict;
    }
}

아이템 획득 수신

  • 가존 S_ItemListHandler와 거의 동일

PacketHandler.cs

public static void S_AddItemHandler(PacketSession session, IMessage packet)
{
    S_AddItem itemList = (S_AddItem)packet;

    // UI 설정
    UI_GameScene gameSceneUI = Managers.UI.SceneUI as UI_GameScene;
    UI_Inventory invenUI = gameSceneUI.InvenUI;

    // 메모리상 아이템 정보 적용
    foreach (ItemInfo itemInfo in itemList.Items)
    {
        Item item = Item.MakeItem(itemInfo);
        Managers.Inven.Add(item);
    }

    Debug.Log("아이템을 획득했습니다!");
}

인벤토리 창 컨트롤

  • I키를 누르면 인벤토리 키고 끌 수 있도록 처리

MyPlayerController.cs

protected override void UpdateController()
{
    GetUIKeyInput();
	...
}

void GetUIKeyInput()
{
    if (Input.GetKeyDown(KeyCode.I))
    {
        UI_GameScene gameSceneUI = Managers.UI.SceneUI as UI_GameScene;
        UI_Inventory invenUI = gameSceneUI.InvenUI;

        // 인벤토리가 켜져 있음
        if(invenUI.gameObject.activeSelf)
        {
            invenUI.gameObject.SetActive(false);
        }
        // 인벤토리가 꺼져 있음
        else
        {
            invenUI.gameObject.SetActive(true);
            invenUI.RefreshUI();
        }
    }
}


아이템 착용

플레이어가 보유중인 아이템을 착용하거나 탈착할 수 있도록 작업

서버

패킷 설계

Protocol.proto

enum MsgId {
  ...
  C_EQUIP_ITEM = 18;
  S_EQUIP_ITEM = 19;
  S_CHANGE_STAT = 20;
}
...
message C_EquipItem{
	int32 itemDbId = 1;
	bool equipped = 2;
}

message S_EquipItem{
	int32 itemDbId = 1;
	bool equipped = 2;
}

message S_ChangeStat{
	StatInfo statInfo = 1;
}

DB

DataModel.cs

[Table("Item")]
public class ItemDb
{
    ...
    public bool Equipped { get; set; } = false;

    [ForeignKey("Owner")]
    public int? OwnerDbId { get; set; }
    public PlayerDb Owner { get; set; }
}

기존 DbTransaction 클래스를 partial를 이용해서 용도별 파일 구분

서버에서 처리하는 데이터의 성격에 따라 메모리 or DB 중 어느 순서로 저장할 지 달라짐

  • 아이템 획득 관련 부분처럼 DB-메모리 의 처리는 속도가 느릴 수 밖에 없음
  1. itemDbId와 같이 반드시 DB에 적용돼야 처리할 수 있는 부분 : 선 DB, 후 메모리

    • Me -> You -> Me 방식으로 처리하는 용도
    • DbTransaction.cs
  2. 아이템 착용과 같이 중요하지 않고 메모리에서 먼저 처리를 해도 상관 없는 부분 : 선 메모리, 후 DB

    • 주고 받는 것 없이 알려주기만 하는 용도
    • DbTransaction_Noti.cs

DbTransaction_Noti.cs

public static void EquipItemNoti(Player player, Item item)
{
    if (player == null || item == null)
        return;

    // disconnected 상태
    ItemDb itemDb = new ItemDb()
    {
        ItemDbId = item.ItemDbId,
        Equipped = item.Equipped
    };

    Instance.Push(() =>
    {
        using (AppDbContext db = new AppDbContext())
        {
            db.Entry(itemDb).State = EntityState.Unchanged;
            // Equipped만 변경됨
            db.Entry(itemDb).Property(nameof(ItemDb.Equipped)).IsModified = true;

            bool success = db.SaveChangesEx();
            if(!success) 
            {
                // 실패했으면 Kick
            }
        }
    });
}

장착 수신

PacketHandler.cs

public static void C_EquipItemHandler(PacketSession session, IMessage packet)
{
	C_EquipItem equipPacket = (C_EquipItem)packet;
	ClientSession clientSession = (ClientSession)session;

	// 게임룸에 들어가야 장착이 가능하기 때문에 룸에서 처리
	Player player = clientSession.MyPlayer;
	if (player == null)
		return;

	GameRoom room = player.Room;
	if (room == null)
		return;

	room.Push(room.HandleEquipItem, player, equipPacket);
}

기존 GameRoom 클래스를 partial를 이용해서 용도별 파일 구분

  1. 인게임 관련 : GameRoom.cs
  2. 전투(이동, 스킬) 관련 : GameRoom_Battle.cs
  3. 아이템 관련 : GameRoom_Item.cs

GameRoom_Item.cs

public void HandleEquipItem(Player player, C_EquipItem equipPacket)
{
    if (player == null)
        return;

    // 플레이어의 인벤토리에 해당 아이템이 있는지 확인
    Item item = player.Inven.Get(equipPacket.ItemDbId);
    if (item == null)
        return;

    // 메모리 선 적용
    item.Equipped = equipPacket.Equipped;

    // DB에 Noti
    DbTransaction.EquipItemNoti(player, item);

    // 클라에 통보
    S_EquipItem equiptOkItem = new S_EquipItem();
    equiptOkItem.ItemDbId = item.ItemDbId;
    equiptOkItem.Equipped = item.Equipped;
    player.Session.Send(equiptOkItem);
}

클라이언트

인게임 인벤토리에서 아이템을 클릭했을 때 착용 <-> 미착용 되도록 처리

  • 인벤토리 아이템 UI 이미지에 Frame 을 하나 둬서 착용시 켜줌

UI_Inventory_Item.cs

public class UI_Inventory_Item : UI_Base
{
    [SerializeField]
    Image _icon = null;

    [SerializeField]
    Image _frame = null;
    ...
}

아이템 착용 송신

  • 아이콘을 클릭 시 처리

UI_Inventory_Item.cs

public int ItemDbid { get; private set; }
public int TemplateId { get; private set; }
public int Count { get; private set; }
public bool Equipped { get; private set; }

public override void Init()
{
    // 클릭 시 이벤트 처리 Bind
    _icon.gameObject.BindEvent((e) =>
    {
        Debug.Log("Click Item");

        C_EquipItem equipPacket = new C_EquipItem();
        equipPacket.ItemDbId = ItemDbid;
        // 착용 <-> 미착용
        equipPacket.Equipped = !Equipped;

        Managers.Network.Send(equipPacket);
    });
}

// 이미지를 해당 아이템 이미지로 변경
public void SetItem(Item item)
{
    ItemDbid = item.ItemDbId;
    TemplateId = item.TemplateId;
    Count = item.Count;
    Equipped = item.Equipped;

    Data.ItemData itemData = null;
    Managers.Data.ItemDict.TryGetValue(TemplateId, out itemData);

    Sprite icon = Managers.Resource.Load<Sprite>(itemData.iconPath);
    _icon.sprite = icon;
    
    // 착용 UI 설정
    _frame.gameObject.SetActive(Equipped);
}

아이템 착용 수신

PacketHandler.cs

public static void S_EquipItemHandler(PacketSession session, IMessage packet)
{
    S_EquipItem equipItemOk = (S_EquipItem)packet;

    // 메모리에 아이템 정보 적용
    Item item = Managers.Inven.Get(equipItemOk.ItemDbId);
    if (item == null)
        return;

    item.Equipped = equipItemOk.Equipped;
    Debug.Log("아이템을 착용 변경!");

    // UI 설정
    UI_GameScene gameSceneUI = Managers.UI.SceneUI as UI_GameScene;
    UI_Inventory invenUI = gameSceneUI.InvenUI;
    invenUI.RefreshUI();
}


착용 아이템 스탯 연동

장비 종류별로 하나씩만 착용할 수 있고 아이템의 스탯이 플레이어의 스탯에 적용되도록 작업

  • 기존 GameRoom에서 관리하던 아이템 착용 처리를 Player.cs 로 옮김
  • 실제로 아이템은 플레이어만 착용하는데 GameRoom에서 처리를 해줄 필요가 없음
  • 스탯 연동도 훨씬 수월해짐

서버

종류별 착용 제한

Player.cs

public void HandleEquipItem(C_EquipItem equipPacket)
{
    // 플레이어의 인벤토리에 해당 아이템이 있는지 확인
    Item item = Inven.Get(equipPacket.ItemDbId);
    if (item == null)
        return;

    // 물약이면 장착에서 제외
    if (item.ItemType == ItemType.Consumable)
        return;

    // 착용 요청이라면, 겹치는 부위 해제
    if (equipPacket.Equipped)
    {
        Item unequipItem = null;
        if (item.ItemType == ItemType.Weapon)
        {
            unequipItem = Inven.Find(
                i => i.Equipped && i.ItemType == ItemType.Weapon);
        }
        // 방어구면 방어구 부위도 검사
        else if (item.ItemType == ItemType.Armor)
        {
            ArmorType armorType = ((Armor)item).ArmorType;
            unequipItem = Inven.Find(
                i => i.Equipped && i.ItemType == ItemType.Armor
                && ((Armor)i).ArmorType == armorType);
        }

        if (unequipItem != null)
        {
            // 메모리 선 적용
            unequipItem.Equipped = false;

            // DB에 Noti
            DbTransaction.EquipItemNoti(this, unequipItem);

            // 클라에 통보
            S_EquipItem equiptOkItem = new S_EquipItem();
            equiptOkItem.ItemDbId = unequipItem.ItemDbId;
            equiptOkItem.Equipped = unequipItem.Equipped;
            Session.Send(equiptOkItem);
        }
    }
    ...
} 

아이템 스탯 적용

기존에 사용하던 플레이어의 StatInfo외에 따로 관리가 필요하게 됨

플레이어의 순수 스탯 + 아이템 스탯 방식
-> 추가적으로 늘어난 스탯에 대해서는 따로 관리를 해줘야 함
-> 아이템 탈착용시 변하는 스탯은 추가 스탯으로만 관리

기존 StatInfo : 플레이어 순수 스텟
추가 스탯 - Player 에서 prop으로 따로 관리

GameObject.cs
public virtual int TotalAttack { get { return Stat.Attack; } }
public virtual int TotalDefence { get { return 0; } }
->
Player.cs
// 아이템 착용으로 인한 추가 스탯
public int WeaponDamage { get; private set; }
public int ArmorDefence { get; private set; }

// 순수 스탯 + 추가 스탯
public override int TotalAttack { get { return Stat.Attack + WeaponDamage; } }
public override int TotalDefence { get { return ArmorDefence; } }
  • 기존 OnDamaged 에서 데미지를 TotalDefence만큼 감소
  • 기존 OnDamaged 호출에서 데미지 매개변수를 TotalAttack으로 변경

Player.cs

// 플레이어의 추가 스탯 갱신
public void RefreshAdditionalStat()
{
    // 처음부터 공식을 다시 계산하는 것이 버그를 최소화 할 수 있음
    // 다소 부하는 있게 됨
    WeaponDamage = 0;
    ArmorDefence = 0;

    foreach(Item item in Inven.Items.Values)
    {
        if (item.Equipped == false)
            continue;

        switch (item.ItemType)
        {
            case ItemType.Weapon:
                WeaponDamage += ((Weapon)item).Damage;
                break;
            case ItemType.Armor:
                ArmorDefence += ((Armor)item).Defence;
                break;
        }
    }
}

생각할 점

  1. 추가 스탯 정보를 DB에 저장해야 할까?
    -> 이미 DB에 가지고 있는 정보로도 언제든지 계산이 가능하기 때문에 따로 저장 필요X

  2. 추가 스탯 정보를 클라에 패킷으로 보내줘야 할까?
    -> 서버의 계산 로직을 클라도 가지고 있다면 클라에서도 가지고 있는 정보로 계산이 가능
    -> 따로 패킷 필요X

클라이언트

MyPlayerController.cs

public int WeaponDamage { get; private set; }
public int ArmorDefence { get; private set; }

protected override void Init()
{
    base.Init();

    RefreshAdditionalStat();
}
...
public void RefreshAdditionalStat()
{
    WeaponDamage = 0;
    ArmorDefence = 0;

    foreach (Item item in Managers.Inven.Items.Values)
    {
        if (item.Equipped == false)
            continue;

        switch (item.ItemType)
        {
            case ItemType.Weapon:
                WeaponDamage += ((Weapon)item).Damage;
                break;
            case ItemType.Armor:
                ArmorDefence += ((Armor)item).Defence;
                break;
        }
    }
}

->

아이템 착용의 변동이 있을 때마다 Refresh
PacketHandler.cs

S_ItemListHandler, S_AddItemHandler, S_EquipItemHandler

if(Managers.Object.MyPlayer != null)
	Managers.Object.MyPlayer.RefreshAdditionalStat();


스탯 창

C를 누르면 플레이어의 스탯 정보를 알려주는 스탯 창이 뜨도록 only 클라이언트 작업

UI

  • StatUI.cs : 이미지, 텍스트, 스탯 연동 관리
public class UI_Stat : UI_Base
{
    enum Images
    {
        Slot_Helmet,
        Slot_Armor,
        Slot_Boots,
        Slot_Weapon,
        Slot_Shield
    }
    enum Texts
    {
        NameText,
        AttackValueText,
        DefenceValueText
    }

    bool _init = false;
    public override void Init()
    {
        // 자동 바인딩
        Bind<Image>(typeof(Images)); 
        Bind<Text>(typeof(Texts));

        _init = true;
        RefreshUI();
    }
	...
}

이미지 설정

StatUI.cs

// stat 갱신
// 모두 장착 해제한 상태에서 하나하나씩 채워주는 것이 좋음
public void RefreshUI()
{
  if (_init == false)
      return;
  // 우선은 다 가림
  Get<Image>((int)Images.Slot_Helmet).enabled = false;
  Get<Image>((int)Images.Slot_Armor).enabled = false;
  Get<Image>((int)Images.Slot_Boots).enabled = false;
  Get<Image>((int)Images.Slot_Weapon).enabled = false;
  Get<Image>((int)Images.Slot_Shield).enabled = false;

  // 채워줌
  foreach (Item item in Managers.Inven.Items.Values)
  {
      if (item.Equipped == false)
          continue;

      ItemData itemData = null;
      Managers.Data.ItemDict.TryGetValue(item.TemplateId, out itemData);
      Sprite icon = Managers.Resource.Load<Sprite>(itemData.iconPath);

      if(item.ItemType == ItemType.Weapon)
      {
          Get<Image>((int)Images.Slot_Weapon).enabled = true;
          Get<Image>((int)Images.Slot_Weapon).sprite = icon;
      }
      else if (item.ItemType == ItemType.Armor)
      {
          Armor armor = ((Armor)item);
          switch(armor.ArmorType)
          {
              case ArmorType.Helmet:
                  Get<Image>((int)Images.Slot_Helmet).enabled = true;
                  Get<Image>((int)Images.Slot_Helmet).sprite = icon;
                  break;
              case ArmorType.Armor:
                  Get<Image>((int)Images.Slot_Armor).enabled = true;
                  Get<Image>((int)Images.Slot_Armor).sprite = icon;
                  break;
              case ArmorType.Boots:
                  Get<Image>((int)Images.Slot_Boots).enabled = true;
                  Get<Image>((int)Images.Slot_Boots).sprite = icon;
                  break;
          }
      }
  }
  ...
}

텍스트 설정

StatUI.cs

public void RefreshUI()
{
	...
    // Text
    MyPlayerController player = Managers.Object.MyPlayer;
    player.RefreshAdditionalStat();

    Get<Text>((int)Texts.NameText).text = player.name;

    // 공격력 -> 총 공격력(+무기 공격력) 양식
    int totalDamage = player.Stat.Attack + player.WeaponDamage;
    Get<Text>((int)Texts.AttackValueText).text = $"{totalDamage}(+{player.WeaponDamage})";
    Get<Text>((int)Texts.DefenceValueText).text = $"{player.ArmorDefence}";
}

키보드 설정

MyPlayerController.cs

void GetUIKeyInput()
{
    ...
    else if (Input.GetKeyDown(KeyCode.C))
    {
        UI_GameScene gameSceneUI = Managers.UI.SceneUI as UI_GameScene;
        UI_Stat statUI = gameSceneUI.StatUI;

        // 인벤토리가 켜져 있음
        if (statUI.gameObject.activeSelf)
        {
            statUI.gameObject.SetActive(false);
        }
        // 인벤토리가 꺼져 있음
        else
        {
            statUI.gameObject.SetActive(true);
            statUI.RefreshUI();
        }
    }
}

디버그

클라이언트만 ArmorType을 제대로 파싱하지 못하여 장비 타입이 분류가 안되는 버그가 발생하게 됨

  • JsonUtiliy에서 string 데이터인 ArmorType을 int 로 인식하기 때문
    -> 클라의 DataManager.cs의 파싱 방법을 서버와 똑같이 NewTonJson으로 바꿔주면 됨

DataManager.cs

Loader LoadJson<Loader, Key, Value>(string path) where Loader : ILoader<Key, Value>
{
	TextAsset textAsset = Managers.Resource.Load<TextAsset>($"Data/{path}");
    return JsonUtility.FromJson<Loader>(textAsset.text);
}

->

Loader LoadJson<Loader, Key, Value>(string path) where Loader : ILoader<Key, Value>
{
    TextAsset textAsset = Managers.Resource.Load<TextAsset>($"Data/{path}");
    return Newtonsoft.Json.JsonConvert.DeserializeObject<Loader>(textAsset.text);
}

0개의 댓글