이번 포스팅은 Npc를 만들고 Npc와 상호작용을 했을 때, 상점을 열어보겠다.
상점을 연 후에 마우스로 더블클릭하면 아이템을 구매하거나 판매할 수 있게 해보자.
서버 작업은 사실 별게 없고 기존에 만들어둔 것과 비슷한 맥락이다.
NPC 상호작용
npc는 다음과 같은 캐릭터를 이용하겠다.
이 npc에 충돌은 불가한 콜라이더를 추가해주고 플레이어가 상호작용 범위에 도달했을 때, space바를 누르면 npc 상점이 열리도록 해보자.
밑은 MyPlayerController의 일부이다.
public float interactionCooldown = 1.5f;
private float lastInteractionTime = -1f;
public void OnTriggerStay(Collider other)
{
if (other == null) return;
if (!other.gameObject.CompareTag("NPC")) return;
float currentTime = Time.time;
if (Input.GetKeyDown(KeyCode.Space) && Managers.Object.MyPlayer.State == CreatureState.Idle)
{
if (currentTime - lastInteractionTime < interactionCooldown)
return;
NPCController npcController = other.gameObject.GetComponent<NPCController>();
lastInteractionTime = currentTime;
if(npcController != null && NpcTrigger == false)
{
npcController.OpenNpc();
}
else if(npcController != null && NpcTrigger == true)
{
npcController.CloseNpc();
}
}
}
NpcController는 각종 Npc들을 다루기 위해 만들어졌으며 위의 Npc는 NpcController를 상속한 PotionShopNpc를 이용한다.
상점 열고 닫기
PotionShopNpc을 살펴보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PotionShopNPC : NPCController
{
public Animator animator;
int prevNum = -1;
private void Start()
{
SetAnim();
}
public void SetAnim()
{
int animNum = -1;
do
{
animNum = Random.Range(0, 4);
} while (prevNum == animNum);
prevNum = animNum;
animator.SetInteger("CurAnim", animNum);
}
public override void OpenNpc()
{
base.OpenNpc();
}
public override void CloseNpc()
{
base.CloseNpc();
transform.rotation = Quaternion.Euler(0, 180, 0);
}
public override void CameraSetting()
{
Vector3 cameraPos = transform.position + new Vector3(1, 0.9f, -2.2f);
Vector3 rotate = new Vector3(0, 0, 0);
Camera.main.GetComponent<CameraController>().PlayerToNpcMove(cameraPos, rotate, gameObject);
}
public override void OpenNpcUI()
{
base.OpenNpcUI();
}
public override void CloseNpcUI()
{
base.CloseNpcUI();
}
}
using DG.Tweening;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NPCController : BaseController
{
public GameObject NameTag;
[SerializeField]
int templateId = 1;
public virtual void OpenNpc()
{
NpcOpenTrigger();
CameraSetting();
OpenNpcUI();
}
public virtual void CloseNpc()
{
CloseNpcUI();
NpcCloseTrigger();
}
public virtual void NpcOpenTrigger()
{
Camera.main.GetComponent<CameraController>().NpcTrigger = true;
Managers.Object.MyPlayer.NpcTrigger = true;
Managers.Object.MyPlayer.Body.SetActive(false);
(Managers.UI.SceneUI as UI_GameScene).NpcTrigger = true;
(Managers.UI.SceneUI as UI_GameScene).CloseAllUI();
(Managers.UI.SceneUI as UI_GameScene).CloseInfoAndSlot();
NameTag.SetActive(false);
}
public virtual void CameraSetting()
{
}
public virtual void OpenNpcUI()
{
Managers.UI.ShowPopupUI<UI_NpcSell_Popup>().Setting(templateId);
}
public virtual void CloseNpcUI()
{
Managers.UI.ClosePopupUI();
}
public virtual void NpcCloseTrigger()
{
Camera.main.GetComponent<CameraController>().NpcToPlayerMove();
Managers.Object.MyPlayer.NpcTrigger = false;
Managers.Object.MyPlayer.Body.SetActive(true);
(Managers.UI.SceneUI as UI_GameScene).NpcTrigger = false;
(Managers.UI.SceneUI as UI_GameScene).OpenInfoAndSlot();
NameTag.SetActive(true);
}
}
이렇게 구성이 되어있다.
중요한점은 상점이 열릴때 현재 열려있는 창들을 전부 닫고 카메라를 Npc 앞으로 이동한다는 점이다. 그러한 효과로 상점이 열리는데 역동감을 주었다. 그리고 중요한 UI가 열리게 된다.
상점 UI
using Data;
using Google.Protobuf.Protocol;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class UI_NpcSell_Popup : UI_Popup
{
int templateId;
NpcData npcData;
enum GameObjects
{
NpcSellContent,
PlayerSellContent
}
enum Texts
{
CoinText
}
public List<UI_NpcSellInfo> npcSellItems = new List<UI_NpcSellInfo>();
public List<UI_PlayerSellInfo> playerSellItems = new List<UI_PlayerSellInfo>();
public override void Init()
{
base.Init();
BindText(typeof(Texts));
BindObject(typeof(GameObjects));
if (Managers.Data.NpcDict.TryGetValue(templateId, out npcData) == false)
return;
RefreshUI();
}
public void Setting(int templateId)
{
this.templateId = templateId;
if (Managers.Data.NpcDict.TryGetValue(templateId, out npcData) == false)
return;
RefreshUI();
}
public void RefreshUI()
{
if (npcData == null)
return;
npcSellItems.Clear();
playerSellItems.Clear();
foreach (Transform child in GetObject((int)GameObjects.NpcSellContent).transform)
Destroy(child.gameObject);
foreach (Transform child in GetObject((int)GameObjects.PlayerSellContent).transform)
Destroy(child.gameObject);
foreach (NpcSellList item in npcData.npcSellLists)
{
GameObject go = Managers.Resource.Instantiate("UI/SubItem/UI_NpcSellInfo", GetObject((int)GameObjects.NpcSellContent).transform);
go.GetComponent<UI_NpcSellInfo>().Setting(item.TemplateId);
}
List<Item> items = Managers.Inven.Items.Values.ToList();
items.Sort((left, right) => { return left.Slot - right.Slot; });
foreach (Item item in items)
{
GameObject go = Managers.Resource.Instantiate("UI/SubItem/UI_PlayerSellInfo", GetObject((int)GameObjects.PlayerSellContent).transform);
go.GetComponent<UI_PlayerSellInfo>().Setting(item.TemplateId, item.ItemDbId);
}
GetText((int)Texts.CoinText).text = Managers.Inven.Money.ToString();
}
public override void InfoRemove()
{
foreach (var item in npcSellItems)
{
item.RemoveInfo();
}
foreach (var item in playerSellItems)
{
item.RemoveInfo();
}
}
}
UI에서는 팔고있는 아이템의 목록을 Json 파일로부터 가져와 전부 띄우게 된다. 한쪽에는 상점에서 파는 물건 한쪽은 플레이어의 인벤을 띄운다.
using Data;
using Google.Protobuf.Protocol;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UI_NpcSellInfo : UI_Base
{
public int templateId;
public int itemDbId;
ItemData itemData;
GameObject description;
bool satisfiedClass = false;
bool satisfiedLevel = false;
enum Images
{
IconImage
}
enum Texts
{
ItemNameText,
SellCoinText
}
public override void Init()
{
BindImage(typeof(Images));
BindText(typeof(Texts));
}
public void Setting(int templateId)
{
this.templateId = templateId;
if (Managers.Data.ItemDict.TryGetValue(templateId, out itemData) == false) return;
RefreshUI();
}
public void RefreshUI()
{
GetImage((int)Images.IconImage).sprite = Managers.Resource.Load<Sprite>(itemData.iconPath);
GetText((int)Texts.ItemNameText).text = itemData.name;
GetText((int)Texts.SellCoinText).text = itemData.sellGold + " 골드";
if (itemData.itemType == ItemType.Consumable)
{
ConsumableData cmData = (ConsumableData)itemData;
if (cmData.maxCount > 1)
{
// 몇개를 구매하시겠습니까? 출력 팝업
GetImage((int)Images.IconImage).gameObject.BindEvent((e) =>
{
if (e.clickCount < 2) return;
Managers.UI.ShowPopupUI<UI_SelectConfirm_Popup>().Setting("몇개를 구매하시겠습니까?", true, (count) =>
{
if (count <= 0) return;
int canMoney = Managers.Inven.Money - (itemData.sellGold * count);
if (canMoney < 0)
{
Managers.UI.ShowPopupUI<UI_Confirm_Popup>().Setting("골드가 부족합니다.");
return;
}
C_AddItem addItemPacket = new C_AddItem();
addItemPacket.TemplateId = itemData.id;
addItemPacket.Count = count;
addItemPacket.IsBuy = true;
Managers.Network.Send(addItemPacket);
});
});
}
else
{
// 정말 구매하시겠습니까? 출력 팝업
GetImage((int)Images.IconImage).gameObject.BindEvent((e) =>
{
if (e.clickCount < 2) return;
Managers.UI.ShowPopupUI<UI_SelectConfirm_Popup>().Setting("정말 구매하시겠습니까?", false, (count) =>
{
if (count != -1) return;
int canMoney = Managers.Inven.Money - (itemData.sellGold * 1);
if(canMoney < 0)
{
Managers.UI.ShowPopupUI<UI_Confirm_Popup>().Setting("골드가 부족합니다.");
return;
}
C_AddItem addItemPacket = new C_AddItem();
addItemPacket.TemplateId = itemData.id;
addItemPacket.Count = 1;
addItemPacket.IsBuy = true;
Managers.Network.Send(addItemPacket);
});
});
}
}
else
{
if (itemData.itemType == ItemType.Weapon)
{
WeaponData wp = (WeaponData)itemData;
if (wp.requirementLevel > Managers.Object.MyPlayer.Stat.Level)
satisfiedLevel = false;
else satisfiedLevel = true;
if (wp.requirementClass.Equals(Util.ChagneClassType((ClassTypes)Managers.Object.MyPlayer.ClassType)))
satisfiedClass = true;
else satisfiedClass = false;
}
else
{
ArmorData ar = (ArmorData)itemData;
if (ar.requirementLevel > Managers.Object.MyPlayer.Stat.Level)
satisfiedLevel = false;
else satisfiedLevel = true;
if (ar.requirementClass.Equals(Util.ChagneClassType((ClassTypes)Managers.Object.MyPlayer.ClassType)))
satisfiedClass = true;
else satisfiedClass = false;
}
// 정말 구매하시겠습니까? 출력 팝업
GetImage((int)Images.IconImage).gameObject.BindEvent((e) =>
{
if (e.clickCount < 2) return;
Managers.UI.ShowPopupUI<UI_SelectConfirm_Popup>().Setting("정말 구매하시겠습니까?", false, (count) =>
{
if (count != -1) return;
int canMoney = Managers.Inven.Money - (itemData.sellGold * 1);
if (canMoney < 0)
{
Managers.UI.ShowPopupUI<UI_Confirm_Popup>().Setting("골드가 부족합니다.");
return;
}
C_AddItem addItemPacket = new C_AddItem();
addItemPacket.TemplateId = itemData.id;
addItemPacket.Count = 1;
addItemPacket.IsBuy = true;
Managers.Network.Send(addItemPacket);
});
});
}
GetImage((int)Images.IconImage).gameObject.BindEvent((e) =>
{
if (itemData == null) return;
description = Managers.Resource.Instantiate("UI/SubItem/UI_ItemInfoCanvas");
description.GetComponent<UI_ItemInfoCanvas>().Setting(itemData, satisfiedClass, satisfiedLevel);
}, Define.UIEvent.PointerEnter);
GetImage((int)Images.IconImage).gameObject.BindEvent((e) =>
{
if (itemData == null) return;
if (description != null)
Managers.Resource.Destroy(description);
}, Define.UIEvent.PointerExit);
}
public void RemoveInfo()
{
if (description != null)
Managers.Resource.Destroy(description);
}
}
판매 물품 또는 인벤 물품은 위의 코드를 하나하나 전부 가지게 되는데 1차적으로 물건을 판매하거나 구매할때 조건을 검사하는 역할을 한다.
만약 클라쪽에서 조건을 만족하면 서버쪽으로 구매 요청(판매 요청)을 보내게 된다.
당연히 서버쪽에서 클라에서 보낸 데이터를 신뢰해도 될지 검증한다.
상점 서버(사실상 아이템 획득)
상점에서 아이템을 구매하는 건 사실상 아이템을 획득하는 것과 동일한 작업이다. 단지 골드가 빠져나간다는 점이 유일한 차이점이다.
그래서 패킷을 보냈을 때 아이템 획득이지만 골드 차감여부를 확인해 골드를 차감하기만 하면 된다.
당연히 상점에 물건을 파는 것도 같은 작업이다.
public void HandleAddItem(Player player, C_AddItem addItemPacket)
{
if (player == null) return;
ItemData itemData;
if (DataManager.ItemDict.TryGetValue(addItemPacket.TemplateId, out itemData) == false) return;
if(itemData.itemType == ItemType.Consumable)
{
if (addItemPacket.IsBuy)
{
int minusMoney = addItemPacket.Count * itemData.sellGold;
if (minusMoney > player.Inven.Money) return;
RewardData rewardData = new RewardData();
rewardData.itemId = addItemPacket.TemplateId;
rewardData.count = addItemPacket.Count;
DbTransaction.GetConsumableItemPlayer(player, rewardData, this, minusMoney: minusMoney);
}
else
{
RewardData rewardData = new RewardData();
rewardData.itemId = addItemPacket.TemplateId;
rewardData.count = addItemPacket.Count;
DbTransaction.GetConsumableItemPlayer(player, rewardData, this);
}
}
else
{
if (addItemPacket.IsBuy)
{
int minusMoney = addItemPacket.Count * itemData.sellGold;
if (minusMoney > player.Inven.Money) return;
RewardData rewardData = new RewardData();
rewardData.itemId = addItemPacket.TemplateId;
rewardData.count = addItemPacket.Count;
DbTransaction.GetItemPlayer(player, rewardData, this, minusMoney: minusMoney);
}
else
{
RewardData rewardData = new RewardData();
rewardData.itemId = addItemPacket.TemplateId;
rewardData.count = addItemPacket.Count;
DbTransaction.GetItemPlayer(player, rewardData, this);
}
}
}
public void HandleRemoveItem(Player player, C_RemoveItem removeItemPacket)
{
if (player == null) return;
ItemData itemData;
if (DataManager.ItemDict.TryGetValue(removeItemPacket.TemplateId, out itemData) == false) return;
Item item = player.Inven.Get(removeItemPacket.ItemDbId);
if (item == null) return;
if (item.Count - removeItemPacket.Count < 0) return;
if(removeItemPacket.IsSell)
DbTransaction.RemoveItem(player, this, removeItemPacket, plusMoney:(itemData.sellGold /2) * removeItemPacket.Count);
else
DbTransaction.RemoveItem(player, this, removeItemPacket);
}
예전에 있던 DbTranscation 함수를 재 활용하였다.
결과
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 3D] 보스 원정대 꾸리기 + 보스 컷신 (3) | 2024.07.11 |
---|---|
[Unity 3D] 클라이언트, 서버 최적화 (0) | 2024.07.04 |
[Unity 3D] 아이템 인벤창에서 옮기기 (0) | 2024.06.21 |
[Unity 3D] UI 이미지 드래그 그리고 퀵슬롯 등록하기 (0) | 2024.06.21 |
[Unity 3D] 스탯 포인트 사용하기 (0) | 2024.06.11 |