이번 포스팅은 몬스터가 사망했을 때, 일정확률로 아이템을 드랍하고 플레이어가 주워 인벤에 넣는 과정을 설명한다.
흐름
전체적은 플로우는 다음과 같다.
- 몬스터가 공격을 받았을 때(서버에서 처리), Hp가 0 이하인지 체크한다.
- Hp가 0 이하라면 몬스터마다 가지고 있는 드랍 정보 중 하나를 랜덤하게 가져온다.(null 포함)
- null이 아닐 경우 ObjectManager에 가져온 DropItem을 추가한다.
- DropItem을 필드에 Spawn한다. Spawn은 Player나 Monster와 같이 동일하게 진행된다.
- Spawn 패킷을 받은 모든 클라이언트는 받은 위치에 DropItem prefab을 생성한다.
- 플레이어의 콜라이더가 DropItem의 콜라이더하고 트리거 중일 때, z 키를 누르면 획득 패킷을 서버로 보낸다.
- 서버에서 획득 요청 패킷이 오면 실제로 플레이어와 아이템의 위치차이를 추적하고 정상적으로 획득했다면 DB에 저장하고 Inven 메모리에 추가한다. 그리고 획득했다고 Noti하는 패킷을 모든 클라에게 전송한다.
- Noti 패킷이 온 클라이언트는 획득을 요청한 클라이언트의 애니메이션을 재생한다.
이제 이것을 실제로 진행해보자.
몬스터 사망 처리
몬스터의 사망 처리는 간단하다. OnDamage 함수가 호출 됐을 때, Hp가 0보다 작은지만 확인하면 처리할 수 있다.
public virtual void OnDamaged(GameObject attacker, int damage)
{
if (Room == null)
return;
damage = Math.Max(damage - Stat.Defense, 0);
Stat.Hp = Math.Max(Stat.Hp - damage, 0);
S_ChangeHp changePacket = new S_ChangeHp();
changePacket.ObjectId = Id;
changePacket.Hp = Stat.Hp;
changePacket.ChangeHp = damage;
changePacket.IsHeal = false;
Room.Broadcast(changePacket);
//Room.Broadcast(CellPos, changePacket);
Console.WriteLine(attacker +"에 의한 HP 감소");
if (Stat.Hp <= 0)
{
State = CreatureState.Dead;
OnDead(attacker);
}
}
이제 OnDead를 살펴보자.
public override void OnDead(GameObject attacker)
{
if (_job != null)
{
_job.Cancel = true;
_job = null;
}
base.OnDead(attacker);
ItemDrop(attacker);
}
public void ItemDrop(GameObject attacker)
{
GameObject owner = attacker.GetOwner();
if (owner.ObjectType == GameObjectType.Player)
{
RewardData rewardData = GetRandomReward();
if (rewardData != null)
{
//DbTransaction.RewardPlayer(player, rewardData, Room);
// 아이템 드랍
ItemData data = null;
DropItem item = ObjectManager.Instance.Add<DropItem>();
if (rewardData.itemId != 1000)
{
if (DataManager.ItemDict.TryGetValue(rewardData.itemId, out data) == false) return;
item.Info.Name = data.name;
}
else
{
item.Info.Name = "Coin";
}
item.Owner = owner;
item._rewardData = rewardData;
item.PosInfo.Pos = PosInfo.Pos;
item.PosInfo.Pos.PosY = PosInfo.Pos.PosY + 0.5f;
Room.EnterGame(item);
}
}
}
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;
}
ItemDrop 함수를 통해 item을 생성한다. RewardData는 내가 따로 만든 클래스이며, 이에 대한 정보를 채우는건 Monster의 정보를 담은 MonsterData.json 파일에 동시에 저장되어 있다.
아이템을 생성했으니 이제 자동으로 Spawn이 될것이다.(EnterGame)
아이템 생성
아이템 Spawn 패킷이 오게된다면 클라에서는 이렇게 처리한다.
else if (objectType == GameObjectType.Dropitem)
{
GameObject go = Managers.Resource.Instantiate($"Item/DropItem/{info.Name}");
go.name = info.Name;
_objects.Add(info.ObjectId, go);
DropItem dropItem = go.GetComponent<DropItem>();
dropItem.Id = info.ObjectId;
dropItem.itemName = info.Name;
dropItem.SetPos(info.PosInfo.Pos);
}
아이템을 위치에 맞게 생성하고 이름에 따라 Prefab을 다르게 생성하게된다.
아이템 줍기
public void OnTriggerStay(Collider other)
{
if(other.gameObject == Managers.Object.MyPlayer.gameObject)
{
if (Input.GetKeyDown(KeyCode.Z) && Managers.Object.MyPlayer.State == CreatureState.Idle)
{
Managers.Object.MyPlayer.State = CreatureState.Wait;
C_GetDropItem getDropItemPacket = new C_GetDropItem();
getDropItemPacket.DropItemId = Id;
Managers.Network.Send(getDropItemPacket);
}
}
}
아이템 줍기는 다음과 같이 구성되어 있다. 이 코드는 DropItem안에 들어가 있고 플레이어가 z키를 누르면 아이템 획득 패킷을 서버에 보낸다.
받은 서버는 다음과 같이 동작한다.
public void GetDropItem(Player player, C_GetDropItem dropItemPacket)
{
if (player == null) return;
DropItem dropItem = null;
if (_dropItem.TryGetValue(dropItemPacket.DropItemId, out dropItem) == false) return;
if (dropItem.Owner != null && dropItem.Owner != player) return;
// 템 자석핵 견제
Vector3 dropItemPos = Utils.PositionsToVector3(dropItem.Pos);
Vector3 playerPos = Utils.PositionsToVector3(player.Pos);
if(Vector3.Distance(dropItemPos, playerPos) > 5f)
{
S_Banish banPacket = new S_Banish();
player.Session.Send(banPacket);
}
S_GetDropItemMotion motionPacket = new S_GetDropItemMotion();
motionPacket.ObjectId = player.Id;
motionPacket.ItemId = dropItem.Id;
Broadcast(motionPacket);
DbTransaction.GetItemPlayer(player, dropItem._rewardData, this);
}
에코 서버처럼 다시 패킷을 돌려줌과 동시에 템 자석핵 등을 체크 후 DbTranscation에 일을 넘겨준다. 이는 DB 쓰레드와 Room 쓰레드가 따로 존재하기 때문이다.(DB 저장하는건 다른거에 비해 매우 느리기 때문)
public static void GetItemPlayer(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
};
PlayerDb playerDb = new PlayerDb();
if (itemDb.TemplateId == 1000)
{
playerDb.PlayerDbId = player.PlayerDbId;
playerDb.Money = player.Inven.Money + rewardData.count;
}
// You
Instance.Push(() =>
{
using (AppDbContext db = new AppDbContext())
{
if(itemDb.TemplateId == 1000)
{
db.Entry(playerDb).State = EntityState.Unchanged;
db.Entry(playerDb).Property(nameof(PlayerDb.Money)).IsModified = true;
bool success = db.SaveChangesEx();
if (success)
{
room.Push(() =>
{
if (itemDb.TemplateId == 1000)
{
player.Inven.Money += itemDb.Count;
S_AddItem itemPacket = new S_AddItem();
itemPacket.Money = itemDb.Count;
player.Session.Send(itemPacket);
}
});
}
}
else
{
db.Items.Add(itemDb);
bool success = db.SaveChangesEx();
if (success)
{
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);
}
});
}
}
}
});
}
DB에 저장될 때 골드 같은경우는 Item이 아니라 Player DB 안에 들어가 있기 때문에 따로 처리해준 것이다.
DB 저장이 끝났다면 이제 모션을 플레이어한테 보여주여야한다.
플레이어 줍기 모션 처리
public static void S_GetDropItemMotionHandler(PacketSession session, IMessage packet)
{
S_GetDropItemMotion motionPacket = (S_GetDropItemMotion)packet;
GameObject go = Managers.Object.FindById(motionPacket.ObjectId);
if (go == null)
return;
GameObject item = Managers.Object.FindById(motionPacket.ItemId);
if (item == null)
return;
PlayerController pc = go.GetComponent<PlayerController>();
if (pc == null)
return;
pc.PickUpItem();
Managers.Resource.Destroy(item);
}
아까 돌려받은 패킷을 통해 플레이어의 PickUpItem 함수를 호출하고 아이템을 없애준다.(없애는 건 클라에서 없애는 것)
public void PickUpItem()
{
State = CreatureState.Wait;
_anim.SetTrigger("PickUp");
StartCoroutine(CoWaitForSecondsToState(1.5f, CreatureState.Idle));
}
이렇게 애니메이션을 틀어주면 된다.
영상 같은 경우는 추후 추가하도록 하겠다.
이로써 간단하게 플레이어의 아이템 줍기를 처리해봤다.
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 3D] 스탯 포인트 사용하기 (0) | 2024.06.11 |
---|---|
[Unity 3D] 획득한 아이템 착용하기 (1) | 2024.06.11 |
[Unity 3D] 각종 UI 제작! Stat창, Inven창, Equip창 (0) | 2024.05.28 |
[Unity 3D] 오브젝트 전투 및 몬스터의 플레이어 추격 (0) | 2024.05.13 |
[Unity 3D] 몬스터 움직임 동기화 with Dedicated Server (0) | 2024.05.02 |