4 분 소요

Intro

이번 포스팅에서는 상점을 만들고, 아이템을 구매하는것을 구현해보았습니다.

image

상점 UI

상점은 UI 틀을 만들고, 유니티의 Scroll View 를 이용하여 스크롤이 가능한 형태로 만들어보았습니다.

상점 NPC에 UI를 만들어 주었고, GUI는 아래와 같은 형태로 구상했습니다.

image

image


UI의 구성을 보면,

  • StoreUI
    • Header 영역 : 인벤토리, 프로필등과 같이 마우스로 드래그할 수 있는 부분입니다.
      • UI를 비활성화 시킬 수 있는 버튼이 있습니다.
    • Content 영역 : 플레이어에게 보여질 판매 아이템들이 들어갈 영역입니다.
      • Scroll View 를 생성
      • Viewport : 보여질 부분
        • Content : 판매 아이템들이 위치할 부분
          • Content 에 Vertical LayoutGroup 컴포넌트를 추가하고, 여러개의 아이템이 들어갈 수 있도록 했습니다.
          • 각 아이템 슬롯에는 판매하는 아이템의 id, 아이콘, 아이템이름, 설명, 판매가격등의 정보가 들어갑니다.
      • ScrollBar Horizontal 는 필요가 없으므로 삭제하였습니다.

StoreItemSlotUI 클래스

StoreItemSlotUI 클래스는 상점에서 판매할 각 아이템 슬롯들이 가질 아이템데이터를 관리합니다.

아이템은 ID 범위에따라 Portion(10001~19999), Armor(20001~29999), Weapon(30001~39999) 로 분류하였습니다.

using UnityEngine;
using UnityEngine.UI;

/*
                        StoreItemSlotUI

            - 상점에서 판매하는 아이템들의 정보를 가짐
                - 아이템 ID 및 ItemData
            - 아이템 ID에 따라 아이템종류를 분류하고, 판매아이템들을 세팅
            - 인벤토리와 연결하여 아이템을 구매하면 인벤토리에 아이템 생성

*/
public class StoreItemSlotUI : MonoBehaviour
{
    public int itemId;                                          // 아이템 ID
    [SerializeField] private Image  itemIcon;                   // 아이템 아이콘
    [SerializeField] private Text   itemNameText;               // 아이템 이름
    [SerializeField] private Text   itemExplanation;            // 아이템 설명
    [SerializeField] private Text   itemPrice;                  // 아이템 가격
    [SerializeField] private Button perchaseBtn;                // 구매 버튼
    [SerializeField] private Inventory inventory;               // 연결된 인벤토리

    private ArmorItemData armorItemData;
    private WeaponItemData weaponItemData;
    private PortionItemData portionItemData;
    private ItemData curItemData;                               // 현재 슬롯의 아이템 데이터

    private int price;                                          // 아이템 가격

    private void Start()
    {
        GetItemData();

        perchaseBtn.onClick.AddListener(PerchaseItem);
    }

    // 구매 버튼 이벤트
    private void PerchaseItem()
    {
        if(DataManager.Instance.GetPlayerData().Gold > price)
        {
            inventory.AddItem(curItemData);
            DataManager.Instance.GetPlayerData().UseGold(price);
        }
        else
        {
            Debug.Log("골드가 부족합니다.");
        }
    }

    // 아이템 타입별 데이터 불러오기
    private void GetItemData()
    {
        // 1. 포션아이템
        if(itemId > 10000 && itemId < 20000)
        {
            portionItemData = DataManager.Instance.GetPortionDataById(itemId);
            curItemData = portionItemData;
            SetSlotData(portionItemData);
        }
        // 2. 방어구아이템
        else if(itemId > 20000 && itemId < 30000)
        {
            armorItemData = DataManager.Instance.GetArmorDataById(itemId);
            curItemData = armorItemData;
            SetSlotData(armorItemData);
        }
        // 3. 무기아이템
        else if(itemId > 30000 && itemId < 40000)
        {
            weaponItemData = DataManager.Instance.GetWeaponDataById(itemId);
            curItemData = weaponItemData;
            SetSlotData(weaponItemData);
        }

    }

    // 아이템 슬롯 정보 채우기
    private void SetSlotData(ItemData data)
    {
        // 1. 아이콘
        ResourceManager.Instance.LoadIcon(data.ItemIcon, sprite =>
        {
            // 성공
            if (sprite != null)
            {
                itemIcon.sprite = sprite;
            }
            else
            {
                Debug.Log($"Failed to load icon for item : {data.ItemIcon}");
            }
        });

        // 2. 아이템 이름
        itemNameText.text = data.ItemName;

        // 3. 아이템 설명
        itemExplanation.text = data.ItemExplanation;

        // 4. 아이템 가격
        itemPrice.text = data.ItemPrice.ToString() + "G";
        price = data.ItemPrice;
    }
}
  • 플레이어가 충분한 골드를 가지고있다면, 골드를 차감하고 인벤토리에 아이템을 생성합니다.

NPC 수정

여러 NPC들을 구현하기위해 NPC가 가지는 공통적인 부분은 NPC 클래스에, 개별적인 부분은 NPC를 상속받은 클래스에 구현하도록 했습니다.

/*
                    NPC

        - 플레이어와 상호작용(G키)
            ㄴ BoxCollider

        - DialogueManager 에서 이 NPC의 대화를 시작할 수 있도록 데이터를 넘김

 */
public class NPC : MonoBehaviour
{
    [SerializeField] protected GameObject dialogueUI;
    
    [HideInInspector]
    public GameObject npcUI;                                     // 개별 NPC의 UI

    protected bool isPlayerInRange = false;                      // 플레이어가 범위안에 있는지 여부

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            isPlayerInRange = true;
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if(other.CompareTag("Player"))
        {
            isPlayerInRange = false;
        }
    }

    // NPC UI 활성/비활성화
    public void SetActiveNpcUI()
    {
        npcUI.SetActive(!npcUI.activeSelf);
        DialogueManager.Instance.isReadyToTalk = !npcUI.activeSelf;
        DialogueManager.Instance.npc = null;
    }
}
  • NPC 클래스는 이제 플레이어와의 상호작용 및 대화창 UI 활성 및 비활성화를 담당합니다.
  • NPC 클래스를 상속받아 개별 NPC를 구현합니다.

Store 클래스

public class Store : NPC
{
    [SerializeField] private DialogueDataSO dialogueData;               // 상점 NPC 대화 데이터
    [SerializeField] private GameObject storeUI;                        // 상점 UI

    private void Start()
    {
        npcUI = storeUI;
    }

    private void Update()
    {
        // 대화 시작
        if(isPlayerInRange && Input.GetKeyDown(KeyCode.G) && !dialogueUI.activeSelf)
        {
            DialogueManager.Instance.StartDialogue(dialogueData, this);
        }
    }  
}
  • Store 클래스는 상점 NPC로, 활성화 시킬 상점 UI와 상점 NPC의 대화데이터를 연결시켜주어야합니다.
  • “G” 키 입력을 통해 상호작용을 할 수 있습니다.

DialogueManager

/*
                    DialogueManager
        
        - 넘겨받은 대화 데이터를 가지고 대화를 시작

        - NPC의 대화를 "--"를 기준으로 나누어 여러페이지에 걸쳐 출력되도록함
        
        - 대화시 타이핑 효과   
 */

public class DialogueManager : Singleton<DialogueManager>
{
    [SerializeField] private GameObject dialogueUI;             // 대화창 오브젝트
    [SerializeField] private TMP_Text npcNameText;              // NPC 이름 텍스트
    [SerializeField] private TMP_Text dialogueText;             // 대화 텍스트

    private Queue<string> pages = new Queue<string>();
    private bool isTypipng = false;                             // 대화 타이핑 효과
    private float typingSpeed = 0.05f;                          // 타이핑 속도

    [HideInInspector]
    public NPC npc;                                             // 현재 대상 NPC
    [HideInInspector]                                           // 대화하기 가능여부(중복대화방지)
    public bool isReadyToTalk = true;

    private void Update()
    {
        if(dialogueUI.activeSelf && npc != null && Input.GetKeyDown(KeyCode.G))
        {
            DisplayNextPage(npc);
        }
    }

    // 대화시작
    public void StartDialogue(DialogueDataSO dialogue, NPC npcData)
    {
        if (!isReadyToTalk)
            return;

        isReadyToTalk = false;

        npc = npcData;

        dialogueUI.SetActive(true);                     // 대화창 UI 활성화

        npcNameText.text = dialogue.npcName;
        pages.Clear();                                  // 이전 대화 내용 초기화

        foreach(string sentence in dialogue.sentences)
        {
            SplitSentenceToPages(sentence);
        }

        DisplayNextPage(npc);
    }

    // 다음페이지 대화내용 출력
    public void DisplayNextPage(NPC npc)
    {
        if (isTypipng) return;

        // 더이상 출력할 내용이 없으면
        if(pages.Count == 0)
        {
            EndDialogue();                      // 대화창 비활성화 
            npc.SetActiveNpcUI();               // 현재 대화중인 NPC의 UI 출력
            return;
        }

        string page = pages.Dequeue();
        StopAllCoroutines();
        StartCoroutine(TypePage(page));
    }

    // 기호 "--"를 기준으로 대화 페이지 나누기
    private void SplitSentenceToPages(string sentence)
    {
        string[] pagesArray = sentence.Split(new string[] { "--" }, System.StringSplitOptions.RemoveEmptyEntries);

        foreach (string page in pagesArray)
        {
            pages.Enqueue(page.Trim());         // 공백 제거 후 큐에 저장
        }
    }
 
    // 타이핑 효과
    private IEnumerator TypePage(string page)
    {
        isTypipng = true;
        dialogueText.text = "";

        foreach(char letter in page.ToCharArray())
        {
            dialogueText.text += letter;
            yield return new WaitForSeconds(typingSpeed);
        }

        isTypipng = false;
    }

    // 대화창 비활성화
    private void EndDialogue()
    {
        dialogueUI.SetActive(false);
    }
}

image

  • DialogueManager 클래스는 NPC에게 넘겨받은 대화데이터를 가지고 대화를 시작
  • 대화내용을 특수기호 “–“를 기준으로 나누어 여러페이지에 걸쳐 출력되도록 하였습니다.
  • 코루틴을 사용하여 대화가 출력될때 타이핑효과를 주었습니다.

기타 수정

아이템 데이터에

아이템의 설명(ItemExplanation)과 아이템 가격(ItemPrice)필드를 추가하였습니다.

image

그에 따른 Json, ItemData 등의 파일 수정이 필요합니다.


플레이어의 데이터에 골드가 추가되었습니다.

상점에서 아이템을 구매할 때 골드가 차감되도록 하기위한 UseGold 함수를 추가하였습니다.

    // 골드 사용
    public void UseGold(int amount)
    {
        // 골드가 -이면 0으로 표기
        gold = (gold - amount) < 0 ? 0 : gold - amount;
    }

댓글남기기