안녕하세요.😁
이번에는 아이템에 관련된 작업을 하기로 마음먹었습니다.
작업 계획은 다음과 같습니다.
1. 인벤토리에서 아이템 이동.
2. 상점 UI 만들기.
3. 아이템 구매 및 판매
먼저 인벤토리에서 아이템 이동 부분인 "InventoryItemDrag" 스크립트입니다.
using UnityEngine;
using UnityEngine.EventSystems;
public class InventoryItemDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
private Transform dragCanvas;
[SerializeField] private RectTransform rectTransform;
[SerializeField] private CanvasGroup canvasGroup;
private Transform originalParent;
void Start()
{
dragCanvas = transform.root;
}
public void OnBeginDrag(PointerEventData eventData)
{
originalParent = transform.parent;
transform.SetParent(dragCanvas);
transform.SetAsLastSibling();
canvasGroup.blocksRaycasts = false;
transform.localScale = new Vector3(1.2f, 1.2f, 1.2f);
}
public void OnDrag(PointerEventData eventData)
{
rectTransform.position = eventData.position;
}
public void OnEndDrag(PointerEventData eventData)
{
if (transform.parent == dragCanvas)
{
transform.SetParent(originalParent);
rectTransform.position = originalParent.GetComponent<RectTransform>().position;
}
canvasGroup.blocksRaycasts = true;
transform.localScale = Vector3.one;
}
}
유니티에서 UI 아이템을 마우스로 클릭하고 드래그할 수 있게 만드는 스크립트입니다.
public class InventoryItemDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
이벤트 시스템의 IBeginDragHandler, IDragHandler, IEndDragHandler을 사용하였습니다.
OnBeginDrag : 드래그 시작
OnDrag : 드래그 중
OnEndDrag : 드래그 종료
private Transform dragCanvas;
[SerializeField] private RectTransform rectTransform;
[SerializeField] private CanvasGroup canvasGroup;
private Transform originalParent;
주요 멤버 변수입니다.
dragCanvas : 드래그 중일 때 UI의 레이아웃에 영향을 받지 않고 움직이기 위한 Canvas입니다.
rectTransform : UI 위치를 쉽게 제어하기 위함입니다.
canvasGroup : 드래그 중 Raycast 차단을 하기 위함입니다.
originalParent : 드래그가 끝나면 원래 슬롯으로 돌아가기 위함입니다.
void Start()
{
dragCanvas = transform.root;
}
root를 사용하여 Hierarchy에서 최상위 부모(Canvas)를 자동으로 저장하였습니다.
인스펙터 필드에 연결을 못해준(?) 이유는
해당 오브젝트는 프리팹으로 관리를 할 것이기 때문입니다.
public void OnBeginDrag(PointerEventData eventData)
{
originalParent = transform.parent;
transform.SetParent(dragCanvas);
transform.SetAsLastSibling();
canvasGroup.blocksRaycasts = false;
transform.localScale = new Vector3(1.2f, 1.2f, 1.2f);
}
드래그 시작 (OnBeginDrag)입니다.
먼저 해당 아이템의 부모 위치를 가져와줍니다.
또한 UI 레이아웃의 영향 없이 자유롭기 위해 현재 아이템의 부모를 Canvas로 지정해 줍니다.
또한 blocksRaycasts = false를 통해 드래그 중 아이템이 다른 UI 이벤트를 막지 않도록 해주었으며
선택된 아이템의 효과를 주기 위하여 Vector3(1.2f, 1.2f, 1.2f)로 시각적인 표현을 해주었습니다.
public void OnDrag(PointerEventData eventData)
{
rectTransform.position = eventData.position;
}
드래그 중 (OnDrag)입니다.
드래그 중일 때는 마우스 커서 위치에 아이템이 따라 이동되게 해 주었습니다.
public void OnEndDrag(PointerEventData eventData)
{
if (transform.parent == dragCanvas)
{
transform.SetParent(originalParent);
rectTransform.position = originalParent.GetComponent<RectTransform>().position;
}
canvasGroup.blocksRaycasts = true;
transform.localScale = Vector3.one;
}
드래그 종료 (OnEndDrag)입니다.
원래 슬롯으로 복귀를 해주었으며 Raycast 재활성화 및 시각적 표시 원상태로 돌려주었습니다.
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class InventoryItemDown : MonoBehaviour, IPointerEnterHandler, IDropHandler, IPointerExitHandler
{
[SerializeField] private Image slotImage;
[SerializeField] private RectTransform rectTransform;
public void OnPointerEnter(PointerEventData eventData)
{
slotImage.color = Color.blue;
}
public void OnPointerExit(PointerEventData eventData)
{
slotImage.color = Color.white;
}
public void OnDrop(PointerEventData eventData)
{
if (eventData.pointerDrag != null)
{
eventData.pointerDrag.transform.SetParent(transform);
eventData.pointerDrag.GetComponent<RectTransform>().position = rectTransform.position;
}
}
}
InventoryItemDown 스크립트입니다.
해당 스크립트는 아이템이 담길 부모 오브젝트에 부착이 됩니다.
public class InventoryItemDown : MonoBehaviour, IPointerEnterHandler, IDropHandler, IPointerExitHandler
IPointerEnterHandler : 마우스가 슬롯 위로 올라왔을 때 호출됩니다.
IDropHandler : 마우스가 슬롯에서 벗어났을 때 호출됩니다.
IPointerExitHandler : 드래그한 아이템을 슬롯에 놓았을 때 호출됩니다.
[SerializeField] private Image slotImage;
[SerializeField] private RectTransform rectTransform;
변수 선언부입니다.
slotImage : 슬롯 배경 이미지입니다.
rectTransform : 슬롯 위치를 가져오기 위해 사용합니다.
public void OnPointerEnter(PointerEventData eventData)
{
slotImage.color = Color.blue;
}
public void OnPointerExit(PointerEventData eventData)
{
slotImage.color = Color.white;
}
마우스가 슬롯 위로 올라왔을 때와 벗어났을 때
시각적인 표현을 하기 위하여 color를 지정해 주었습니다.
public void OnDrop(PointerEventData eventData)
{
if (eventData.pointerDrag != null)
{
eventData.pointerDrag.transform.SetParent(transform);
eventData.pointerDrag.GetComponent<RectTransform>().position = rectTransform.position;
}
}
아이템을 슬롯에 놓았을 때입니다.
eventData.pointerDrag : 드래그 중인 아이템이며
현재 슬롯의 자식으로 이동하게 해 주었습니다.
또한 RectTransform.position을 이용하여 슬롯 중앙에 위치하도록 해주었습니다.
만일 드래그한 객체가 없을 경우 조건문에 진입하지 않습니다.
다음은 상점 UI를 배치해 보았습니다.
이 상점 UI는 NPC를 클릭하면 팝업이 될 예정입니다.
이래봬도 NPC
상점 UI를 팝업 되게 하기 위해 NpcStoreClickEvent 스크립트를 만들어 주었습니다.
using UnityEngine;
public class NpcStoreClickEvent : MonoBehaviour
{
[SerializeField] private GameObject storePanel;
[SerializeField] private Camera mainCamera;
[SerializeField] private Animator storeNpcAnimator;
void Update()
{
if (Input.GetMouseButtonDown(1))
{
if (storePanel.activeSelf == true)
{
return;
}
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, 100f, LayerMask.GetMask("NpcStore")))
{
storeNpcAnimator.SetTrigger("IsClick");
storePanel.SetActive(true);
}
}
}
}
storePanel : 클릭 시 활성화할 상점 UI입니다.
mainCamera : 마우스 클릭 위치를 월드 좌표로 변환할 때 사용됩니다.
storeNpcAnimator : NPC 클릭 시 애니메이션을 재생하기 위함입니다.
Input.GetMouseButtonDown(1)을 사용하여 마우스 우클릭을 감지하며
마우스 위치에서 레이(ray)를 쏴서 충돌을 검사합니다.
또한 충돌이 감지되면
애니메이션 전이 조건인 파라미터 "IsClick"을 발동시켜 주며
SetActive(true)를 사용하여 상점 UI를 화면에 표시되게 해 주었습니다.
다음은 StoreController 스크립트입니다.
using UnityEngine;
public class StoreController : MonoBehaviour
{
[SerializeField] private Transform[] slots;
[SerializeField] private GameObject[] itemPrefabs;
[SerializeField] private PlayerStatUpdate playerStatUpdate;
[SerializeField] private PlayerStats playerStats;
[SerializeField] private GetGoldText getGoldText;
public void OnBuyButtonClicked(string itemID)
{
int itemPlace = 0;
float itemAtk = 0;
float itemHp = 0;
float itemMp = 0;
GameObject selectedPrefab = null;
foreach (GameObject prefab in itemPrefabs)
{
ItemInfomation info = prefab.GetComponent<ItemInfomation>();
if (info != null && info.itemID == itemID)
{
itemPlace = info.itemPrice;
itemAtk = info.itemAtk;
itemHp = info.itemHp;
itemMp = info.itemMp;
selectedPrefab = prefab;
break;
}
}
if (playerStats.playerGold - itemPlace < 0)
{
Debug.Log("돈이 없음");
return;
}
else
{
getGoldText.BuyItemGetGold(itemPlace);
}
foreach (Transform slot in slots)
{
if (slot.childCount == 0)
{
GameObject newItem = Instantiate(selectedPrefab, slot);
newItem.transform.localPosition = Vector3.zero;
playerStatUpdate.PlayerStatToItemUp(itemAtk, itemHp, itemMp);
Debug.Log("구매 완료 " + itemID);
return;
}
}
Debug.Log("슬롯이 가득참");
}
public void OnSellButtonClicked(string itemID)
{
for (int i = slots.Length - 1; i >= 0; i--)
{
Transform slot = slots[i];
if (slot.childCount > 0)
{
ItemInfomation info = slot.GetChild(0).GetComponent<ItemInfomation>();
if (info != null && info.itemID == itemID)
{
getGoldText.SellItemGetGold(info.itemAmount);
print("info.itemAtk " + info.itemAtk);
playerStatUpdate.PlayerStatToItemDown(info.itemAtk, info.itemHp, info.itemMp);
Destroy(slot.GetChild(0).gameObject);
Debug.Log("판매 완료 " + itemID);
return;
}
}
}
Debug.Log("판매할 아이템 없음");
}
}
Buy와 Sell 버튼을 각 두 함수로 나누어서
(OnBuyButtonClicked / OnSellButtonClicked) 작업을 했습니다.
[SerializeField] private Transform[] slots;
[SerializeField] private GameObject[] itemPrefabs;
[SerializeField] private PlayerStatUpdate playerStatUpdate;
[SerializeField] private PlayerStats playerStats;
[SerializeField] private GetGoldText getGoldText;
먼저 변수 선언부입니다.
slots : 아이템을 배치할 슬롯들입니다.
itemPrefabs : 구매 가능한 아이템 프리팹입니다.
playerStatUpdate : 아이템 효과를 플레이어 스텟에 적용하기 위함입니다.
playerStats : 현재 플레이어의 골드 및 정보를 가져오기 위함입니다.
getGoldText : 골드 차감 및 증가 처리를 하기 위함입니다.
public void OnBuyButtonClicked(string itemID)
{
int itemPlace = 0;
float itemAtk = 0;
float itemHp = 0;
float itemMp = 0;
GameObject selectedPrefab = null;
foreach (GameObject prefab in itemPrefabs)
{
ItemInfomation info = prefab.GetComponent<ItemInfomation>();
if (info != null && info.itemID == itemID)
{
itemPlace = info.itemPrice;
itemAtk = info.itemAtk;
itemHp = info.itemHp;
itemMp = info.itemMp;
selectedPrefab = prefab;
break;
}
}
if (playerStats.playerGold - itemPlace < 0)
{
Debug.Log("돈이 없음");
return;
}
else
{
getGoldText.BuyItemGetGold(itemPlace);
}
foreach (Transform slot in slots)
{
if (slot.childCount == 0)
{
GameObject newItem = Instantiate(selectedPrefab, slot);
newItem.transform.localPosition = Vector3.zero;
playerStatUpdate.PlayerStatToItemUp(itemAtk, itemHp, itemMp);
Debug.Log("구매 완료 " + itemID);
return;
}
}
Debug.Log("슬롯이 가득참");
}
먼저 아이템 ID로 프리팹을 찾습니다.
ItemInfomation은 아이템의 기본 정보가 담겨있는 스크립트입니다.
if (playerStats.playerGold - itemPlace < 0)
{
Debug.Log("돈이 없음");
return;
}
else
{
getGoldText.BuyItemGetGold(itemPlace);
}
다음 조건문은 골드가 부족하거나 부족하지 않을 때를 판단합니다.
골드가 만일 부족하다면 현재 return을 해주는데
이곳에서 메시지나 혹은 골드가 부족하다는 시각적 효과를 줄 것입니다.
foreach (Transform slot in slots)
{
if (slot.childCount == 0)
{
GameObject newItem = Instantiate(selectedPrefab, slot);
newItem.transform.localPosition = Vector3.zero;
playerStatUpdate.PlayerStatToItemUp(itemAtk, itemHp, itemMp);
Debug.Log("구매 완료 " + itemID);
return;
}
}
Debug.Log("슬롯이 가득참");
다음으로 빈 슬롯을 찾아 아이템을 배치해 줍니다.
해당 아이템을 slot에 위치에 생성되게 해 주었고,
PlayerStatToItemUp() 함수는 플레이어의 정보를 업데이트해 주는 함수인데
아래에서 다시 살펴보겠습니다.
또한 아무런 로직들이 실행되지 않았다면, 슬롯이 가득 찬 것으로 간주하여
Debug.Log를 통해 테스트를 진행해 보았습니다,
다음은 판매 처리 하는 OnSellButtonClicked 함수입니다.
public void OnSellButtonClicked(string itemID)
{
for (int i = slots.Length - 1; i >= 0; i--)
{
Transform slot = slots[i];
if (slot.childCount > 0)
{
ItemInfomation info = slot.GetChild(0).GetComponent<ItemInfomation>();
if (info != null && info.itemID == itemID)
{
getGoldText.SellItemGetGold(info.itemAmount);
print("info.itemAtk " + info.itemAtk);
playerStatUpdate.PlayerStatToItemDown(info.itemAtk, info.itemHp, info.itemMp);
Destroy(slot.GetChild(0).gameObject);
Debug.Log("판매 완료 " + itemID);
return;
}
}
}
Debug.Log("판매할 아이템 없음");
}
for (int i = slots.Length - 1; i >= 0; i--)
앞에서부터 슬롯을 검사하는 것이 아닌 뒤에서부터 슬롯을 검사하도록 해주었습니다.
Transform slot = slots[i];
if (slot.childCount > 0)
{
ItemInfomation info = slot.GetChild(0).GetComponent<ItemInfomation>();
if (info != null && info.itemID == itemID)
{
getGoldText.SellItemGetGold(info.itemAmount);
print("info.itemAtk " + info.itemAtk);
playerStatUpdate.PlayerStatToItemDown(info.itemAtk, info.itemHp, info.itemMp);
Destroy(slot.GetChild(0).gameObject);
Debug.Log("판매 완료 " + itemID);
return;
}
}
또한 해당 Slot의 childCount가 0보다 크다면 아이템이 있는 것으로 간주를 하며
해당 아이템과 아이템이 갖고 있는 정보인 itemId를 확인하여 판매하도록 하였습니다.
public void OnSellButtonClicked(string itemID)
{
for (int i = slots.Length - 1; i >= 0; i--)
{
Transform slot = slots[i];
if (slot.childCount > 0)
{
ItemInfomation info = slot.GetChild(0).GetComponent<ItemInfomation>();
if (info != null && info.itemID == itemID)
{
getGoldText.SellItemGetGold(info.itemAmount);
print("info.itemAtk " + info.itemAtk);
playerStatUpdate.PlayerStatToItemDown(info.itemAtk, info.itemHp, info.itemMp);
Destroy(slot.GetChild(0).gameObject);
Debug.Log("판매 완료 " + itemID);
return;
}
}
}
Debug.Log("판매할 아이템 없음");
}
마지막으로 위 로직이 실행돼서 return이 된 게 아니라면, 판매할 아이템이 없는 것으로 간주하고
Debug.Log("")로 출력되게 해 주었습니다.
이 부분도 마찬가지로 메시지나 혹은 시각적인 표현을 해줘야 할 것 같습니다.
using System;
using System.Collections;
using TMPro;
using UnityEngine;
public class GetGoldText : MonoBehaviour
{
[SerializeField] private PlayerStats playerStats;
[SerializeField] private GameObject[] goldTextObjects;
[SerializeField] private TextMeshProUGUI[] goldTextObjectTexts;
[SerializeField] private TextMeshProUGUI currentGold;
[SerializeField] private float disableTime = 1f;
public void GetGold(string goldText)
{
for (int i = 0; i < goldTextObjects.Length; i++)
{
if (goldTextObjects[i].activeSelf == false)
{
goldTextObjects[i].SetActive(true);
goldTextObjectTexts[i].text = $"+G {goldText}";
currentGold.text = (int.Parse(currentGold.text) + int.Parse(goldText)).ToString();
playerStats.playerGold = int.Parse(currentGold.text);
StartCoroutine(getTextDisable(goldTextObjects[i]));
break;
}
}
}
private IEnumerator getTextDisable(GameObject goldText)
{
yield return new WaitForSeconds(disableTime);
goldText.SetActive(false);
}
public void BuyItemGetGold(int place)
{
playerStats.playerGold -= place;
currentGold.text = playerStats.playerGold.ToString();
}
public void SellItemGetGold(int amount)
{
playerStats.playerGold += amount;
currentGold.text = playerStats.playerGold.ToString();
}
}
먼저 GetGoldText 스크립트입니다.
아이템 구매 및 판매를 할 때 실행되는 함수인데요
단순히 playerStats.playerGold 에 place 및 amount를 더해주거나 빼주었으며
text에도 동기화시켜 주었습니다.
storeController 스크립트 만으로도 충분히 구현은 가능했지만
관리를 위해서라도 이것저것 참조하는 것보단 관련된 변수만 참조하고
해당 필요한 함수만 호출하는 방식으로 작업했습니다.
using UnityEngine;
public class PlayerStatUpdate : MonoBehaviour
{
[SerializeField] private PlayerStats playerStats;
public void PlayerStatToItemUp(float atk, float hp, float mp)
{
playerStats.playerDamage += atk;
print("playerStats.playerDamage " + playerStats.playerDamage);
playerStats.playerMaxHp += hp;
playerStats.playerMaxMp += mp;
}
public void PlayerStatToItemDown(float atk, float hp, float mp)
{
playerStats.playerDamage -= atk;
print("playerStats.playerDamage " + playerStats.playerDamage);
playerStats.playerMaxHp -= hp;
playerStats.playerMaxMp -= mp;
}
}
다음은 플레이어 정보가 업데이트되는 함수를 담은 스크립트 "PlayerStatUpdate"입니다.
현재는 공격력, 체력, 마나 3가지만 갖고 있기에 세 가지 정보만 업데이트되게 해 주었습니다.
예) 무기 : Atk +5 정보를 갖고 있다면,
PlayerStats엔 Atk +5 , Hp + 0, Mp + 0 이 됩니다.
감사합니다. 😁