프로젝트/프로젝트 A

#014 Inventory (With. Store)

효따 2025. 8. 18. 18:42

안녕하세요.😁

 

이번에는 아이템에 관련된 작업을 하기로 마음먹었습니다.

 

 

작업 계획은 다음과 같습니다.

 

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 이 됩니다.

 


 

 

 

 

감사합니다. 😁

 

 

'프로젝트 > 프로젝트 A' 카테고리의 다른 글

# 016 Enemy Spawn & movement  (1) 2025.08.21
# 015 Development (with. feature additions)  (1) 2025.08.20
# 013 Player Skill Point (with. Event Trigger)  (1) 2025.08.17
# 012 Player Exp (with. Get Gold)  (5) 2025.08.17
#011 Minimap  (3) 2025.08.16