Reward
우리가 지금까진 한 방법은 테스트 하기 위해 프로그램이 시작 될 때 아이템을 강제적으로 플레이어에게 넣어두는 형식으로 했다.
이런 방법말고 이제 몬스터를 죽이면 아이템을 획득하게 해보자.
그렇다면 어떤 것이 추가되어야할까
일단 서버에서 OnDead를 몬스터에 추가해서 작업을 해야할 것이다.
또한 몬스터에서 드랍되는 아이템 등을 관리하기 위해서는 데이터 시트까지 추가적으로 관리를 해야만 한다.
그러기 위해 Data.Content에 Monster를 추가해보자.
#region Monster
[Serializable]
public class RewardData
{
public int probability; // 100분율
public int itemId;
public int count;
}
[Serializable]
public class MonsterData
{
public int id;
public string name;
public StatInfo Stat;
public List<RewardData> rewards;
}
[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
이제 OnDead를 하기전에 Reward 아이템을 뱉는 함수인 GetRandomReward를 작성해보자.
RewardData GetRandomReward()
{
MonsterData monsterData = null;
DataManager.MonsterDict.TryGetValue(TemplateId, out monsterData);
int rand = new Random().Next(0, 101);
int sum = 0;
foreach(RewardData rewardData in monsterData.rewards)
{
sum += rewardData.probability;
if(rand <= sum)
{
return rewardData;
}
}
return null;
}
public override void OnDead(GameObject attacker)
{
base.OnDead(attacker);
GameObject owner = attacker.GetOwner();
if (owner.ObjectType == GameObjectType.Player)
{
RewardData rewardData = GetRandomReward();
if (rewardData != null)
{
Player player = (Player)owner;
DbTransaction.RewardPlayer(player, rewardData, Room);
}
}
}
DB 쓰레드에게 떠 넘겨준후 여기서 데이터를 추가하고 빈 슬롯을 찾아온다.
허나 이 타이밍에 슬롯이 가득 차는 경우 문제가 발생할 수 있다.
public static void RewardPlayer(Player player, RewardData rewardData, GameRoom room)
{
if (player == null || rewardData == null || room == null)
return;
// TODO : 살짝 문제가 있긴 하다... with 멀티쓰레드
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.Items.Add(itemDb);
bool success = db.SaveChangesEx();
if (success)
{
// Me
room.Push(() =>
{
Item newItem = Item.MakeItem(itemDb);
player.Inven.Add(newItem);
// Client Noti
{
S_AddItem itemPacket = new S_AddItem();
ItemInfo itemInfo = new ItemInfo();
itemInfo.MergeFrom(newItem.Info);
itemPacket.Items.Add(itemInfo);
player.Session.Send(itemPacket);
}
});
}
}
});
}
아이템 착용
이제 인벤토리에 아이템이 쌓이게 되고 그 아이템을 가지고 실제로 착용해 스텟에 적용해보도록 하자.
패킷은 다음과 같이 만들어준다.
message C_EquipItem {
int32 itemDbId = 1;
bool equipped = 2;
}
message S_EquipItem {
int32 itemDbId = 1;
bool equipped = 2;
}
message S_ChangeStat {
StatInfo statInfo = 1;
}
아이템을 착용 했는지 여부는 equipped를 통해 확인할 수 있다.
플레이어가 아이템을 착용하겠다는 패킷이 오면 다음과 같이 작동하게 한다.
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);
}
이제 HandleEquipITem 함수를 room 쓰레드에 넘겨주어 처리하게 하면 된다.
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 equipOkItem = new S_EquipItem();
equipOkItem.ItemDbId = equipPacket.ItemDbId;
equipOkItem.Equipped = equipPacket.Equipped;
player.Session.Send(equipOkItem);
}
그리고 이제 DB에 노티를 해서 DB를 수정하면 된다.
public static void EquipItemNoti(Player player, Item item)
{
if (player == null || item == null)
return;
ItemDb itemDb = new ItemDb()
{
ItemDbId = item.ItemDbId,
Equipped = item.Equipped
};
Instance.Push(() =>
{
using (AppDbContext db = new AppDbContext())
{
db.Entry(itemDb).State = EntityState.Unchanged;
db.Entry(itemDb).Property(nameof(itemDb.Equipped)).IsModified = true;
bool success = db.SaveChangesEx();
if (!success)
{
// 실패했다면 유저를 Kick
}
}
});
}
이제 클라이언트가 패킷을 받아서 아이템을 착용하는 것을 해보자.
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_GameScene gameSceneUI = Managers.UI.SceneUI as UI_GameScene;
UI_Inventory invenUI = gameSceneUI.InvenUI;
invenUI.RefreshUI();
}
Inventory에서 아이템을 클릭하면 착용하고 다시 클릭하면 벗게끔 해보자.
Frame을 만들어서 아이템을 착용할 때 Frame을 껐다 켰다 해보자.
_icon.gameObject.BindEvent((e) =>
{
Debug.Log("Click Item");
C_EquipItem equipPacket = new C_EquipItem();
equipPacket.ItemDbId = ItemDbID;
equipPacket.Equipped = !Equipped;
Managers.Network.Send(equipPacket);
});
아이템을 인벤토리에서 Refresh할 때 SetItem을 호출하는데 이때 Frame을 껏다 켰다 할 수있게 한다.
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;
_frame.gameObject.SetActive(Equipped);
}
아이템 중복 해결
지금은 아이템을 착용할 때 부위에 대한 값을 정해두지 않아서 무기를 여러개 착용한다던가 방어구를 여러개 착용 가능하다. 이것을 수정해보자.
클라이언트쪽에서 착용할 수 있는지 검사하고 서버에게 전송할지 서버에서 체크하고 클라에게 통보할지는 취향차이기는 하다. 다만 서버쪽에서 검사하는게 해킹등을 방어하긴 유리하다.
그래서 서버에서 아이템을 착용하는 부분을 다음과 같이 바꾼다.
public void HandleEquipItem(Player player, C_EquipItem equipPacket)
{
if (player == null)
return;
Item item = player.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 = player.Inven.Find(
i => i.Equipped && i.ItemType == ItemType.Weapon);
}
else if(item.ItemType == ItemType.Armor)
{
ArmorType armorType = ((Armor)item).ArmorType;
unequipItem = player.Inven.Find(
i => i.Equipped && i.ItemType == ItemType.Armor
&& ((Armor)i).ArmorType == armorType);
}
if(unequipItem != null)
{
// 메모리 선적용
unequipItem.Equipped = false;
// DB에 Noti
DbTransaction.EquipItemNoti(player, unequipItem);
// 클라에 통보
S_EquipItem equipOkItem = new S_EquipItem();
equipOkItem.ItemDbId = unequipItem.ItemDbId;
equipOkItem.Equipped = unequipItem.Equipped;
player.Session.Send(equipOkItem);
}
}
{
// 메모리 선적용
item.Equipped = equipPacket.Equipped;
// DB에 Noti
DbTransaction.EquipItemNoti(player, item);
// 클라에 통보
S_EquipItem equipOkItem = new S_EquipItem();
equipOkItem.ItemDbId = equipPacket.ItemDbId;
equipOkItem.Equipped = equipPacket.Equipped;
player.Session.Send(equipOkItem);
}
}
이제 무기를 착용했을 때 스텟을 수정하게 해보자.
우리가 StatInfo에서 attack 값을 관리하고 있엇는데 무기를 장착했을 때 attack 값을 직접적으로 상승시켜줬을 경우 나중에 다른 무기로 교체를 하면 이전 무기의 상승값을 계속 고려해야한다.
그러기에 무기에대한 상승이나 스킬에 대한 영향 등은 따로 고려를 하는 편이 좋다.
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;
}
}
}
스텟창
클라이언트에서 확인할 수 있게 스텟창을 만들어보자.
프리팹을 수정해준뒤 코드를 작성해보자.
using Data;
using Google.Protobuf.Protocol;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UI_Stat : UI_Base
{
bool _init = false;
enum Images
{
Slot_Helmet,
Slot_Armor,
Slot_Boots,
Slot_Weapon,
Slot_Shield
}
enum Texts
{
NameText,
AttackValueText,
DefenceValueText
}
public override void Init()
{
Bind<Image>(typeof(Images));
Bind<Text>(typeof(Texts));
_init = true;
RefreshUI();
}
public void RefreshUI()
{
if (_init == false)
return;
Get<Image>((int)Images.Slot_Helmet).enabled = false;
Get<Image>((int)Images.Slot_Boots).enabled = false;
Get<Image>((int)Images.Slot_Armor).enabled = false;
Get<Image>((int)Images.Slot_Weapon).enabled = false;
Get<Image>((int)Images.Slot_Shield).enabled = false;
foreach (var 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;
}
}
}
MyPlayerController player = Managers.Object.MyPlayer;
player.RefreshAdditionalStat();
int totalDamage = player.WeaponDamage + player.Stat.Attack;
Get<Text>((int)Texts.NameText).text = player.name;
Get<Text>((int)Texts.AttackValueText).text = $"{totalDamage} + ({player.WeaponDamage})";
Get<Text>((int)Texts.DefenceValueText).text = $"{player.ArmorDefence}";
}
}
정상적으로 작동하는 모습을 볼 수 있다.
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 3D] Title과 Login 구현 (UI 작업, 서버 연동) (0) | 2024.04.09 |
---|---|
[Unity 2D] 대형 구조 관리 - 서버 구조 개선 작업 (0) | 2024.04.02 |
[Unity 2D] Db 연동 - Item과 Inventory (0) | 2024.03.27 |
[Unity 2D] DB 연동 - Player, HP db 연동 (0) | 2024.03.26 |
[Unity 2D] 서버 구조 변경 - Command 패턴과 Job (0) | 2024.03.14 |