프로젝트/프로젝트 A

# 015 Development (with. feature additions)

효따 2025. 8. 20. 01:38

안녕하세요. 😁

 


 

이번 작업은 개선 및 수정해야 할 사항들을 간략하게나마 Develop 하는 시간을 갖겠습니다.

 

 

사실 오늘은 Enumy에 관련하여 넥서스에서 생성, 목적지로 이동, 공격하는 포탑 등에 대한 작업을 하려고 했었는데요

 

 

 

우선적으로 꼭 fix를 해야 하는 곳도 보이고,

 

게임 플레이상 문제는 없지만 사용자 입장으로 추가 및 개선되어야 할 부분들도 보였습니다.

 

 

 

우선 현재의 파악한 부분들에 대해 빠르게 적어보겠습니다.

 

 


 

1. 아이템을 이동할 때 다른 Slot에 아이템이 있으면 덮어쓰는 현상입니다.

 

 

2. 상점에서 아이템을 구매 및 판매할 때 현재 상태에 대한 메시지가 없다는 점입니다.

예) 왜 구매를 못하는지 상태 알림 메시지

 

 

3. 스킬 사용 후 애니메이션 진행 되는 상태에서 플레이어가 이동하는 문제

 

 


 

 

1. 먼저 아이템을 덮어쓰는 현상 개선은 어제 만든 InventoryItemDown 스크립트에서 작업했습니다.

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;
        }
    }

}

 

현재는 OnDrop() 이 실행되면, 단순히 해당 슬롯 오브젝트를 부모로 만들며 포지션을 정해주는데요

 


 

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) return;

        Transform draggedItem = eventData.pointerDrag.transform;
        Transform originalSlot = draggedItem.GetComponent<InventoryItemDrag>().originalParent;
 
        if (transform.childCount > 0)
        {
            Transform existingItem = transform.GetChild(0);
            existingItem.SetParent(originalSlot);
            existingItem.GetComponent<RectTransform>().position = originalSlot.GetComponent<RectTransform>().position;
        }

        draggedItem.SetParent(transform);
        draggedItem.GetComponent<RectTransform>().position = rectTransform.position;
    }
}

 

draggedItem : 드래그 중인 아이템 오브젝트

originalSlot : 원래 아이템이 있던 슬롯

 

을 미리 받아와, 현재 슬롯에 자식(child)이 있으면 아이템이 있는 것으로 판단하여

원래 슬롯으로 되돌리며 위치를 맞춰주었습니다.

 

또한 드래그 아이템은 부모를 현재 슬롯으로 설정해 주는 것으로

 

아이템 스왑(swap) 기능 구현을 마쳤습니다.

 

 


 

 

2. 상점에서 구매 및 판매를 할 때 상태 메시지 팝업은 지난번 작성한

 

"MessageController" 스크립트의 재사용과

using System.Collections;
using TMPro;
using UnityEngine;

public class MessageController : MonoBehaviour
{
    [SerializeField] private float disableTime = 2.0f;
    [SerializeField] private TextMeshProUGUI systemMessage;
    private Coroutine messageCoroutine;

    public void MessageSetting(string message)
    {
        if (messageCoroutine != null)
            StopCoroutine(messageCoroutine);

        systemMessage.text = message;
        messageCoroutine = StartCoroutine(DisableTime());
    }

    private IEnumerator DisableTime()
    {
        yield return new WaitForSeconds(disableTime);
        systemMessage.text = "";
        messageCoroutine = null;
    }
}

 

어제 작성한 "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;
    [SerializeField] private MessageController messageController;
    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)
        {
            messageController.MessageSetting("Not enough gold!");
            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);
                messageController.MessageSetting("Purchase complete!");
                return;
            }
        }

        messageController.MessageSetting("Inventory is full!");
    }

    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);
                    playerStatUpdate.PlayerStatToItemDown(info.itemAtk, info.itemHp, info.itemMp);
                    Destroy(slot.GetChild(0).gameObject);
                    messageController.MessageSetting("Sale complete!");
                    return;
                }
            }
        }

        messageController.MessageSetting("No items to sell!");
    }
}

 

(1). 가장 먼저 playerStats.playerGold - itemPlace 가 0보다 작다면

 

소지하고 있는 골드가 부족한 것으로 판단했습니다.

        if (playerStats.playerGold - itemPlace < 0)
        {
            messageController.MessageSetting("Not enough gold!");
            return;
        }

 

 

(2). 위 조건문에서 return이 되지 않고, slot에 빈 공간이 있다면

 

아이템 구매를 성공한 것으로 판단했습니다.

        foreach (Transform slot in slots)
        {
            if (slot.childCount == 0)
            {
                GameObject newItem = Instantiate(selectedPrefab, slot);
                newItem.transform.localPosition = Vector3.zero;
                playerStatUpdate.PlayerStatToItemUp(itemAtk, itemHp, itemMp);
                messageController.MessageSetting("Purchase complete!");
                return;
            }
        }

 

 

(3). 모든 로직에서 return이 되지 않았다면 슬롯에 아이템이 가득 찬 것으로 판단했습니다.

    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)
        {
            messageController.MessageSetting("Not enough gold!");
            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);
                messageController.MessageSetting("Purchase complete!");
                return;
            }
        }

        messageController.MessageSetting("Inventory is full!");
    }

 

 


 

다음은 sell 버튼을 클릭 시 실행되는 OnSellButtonCliked() 함수입니다.

    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);
                    playerStatUpdate.PlayerStatToItemDown(info.itemAtk, info.itemHp, info.itemMp);
                    Destroy(slot.GetChild(0).gameObject);
                    messageController.MessageSetting("Sale complete!");
                    return;
                }
            }
        }

        messageController.MessageSetting("No items to sell!");
    }

 

슬롯의 카운트가 1 이상이면 아이템이 있는 것으로 판단하여

 

해당 아이템을 판매해 주었으며

 

만일 슬롯의 카운트가 1 이상인게 없으면, 슬롯에 해당 아이템이 없는 것으로 판단했습니다.


 

3. 스킬 사용 후 애니메이션이 진행되는 상태에서 이동하는 문제입니다.

    private IEnumerator SkillEDelay(Vector3 pos)
    {
        isSkill03 = true;
        playerAnim.SetTrigger("IsSkillE");

        yield return new WaitForSeconds(0.5f);
        playerNav.Warp(pos);

        yield return new WaitForSeconds(0.7f);
        isSkill03 = false;
    }
    private IEnumerator SkillRDelay(Skill skill)
    {
        isSkill04 = true;
        playerAnim.SetTrigger("IsSkillR");

        yield return new WaitForSeconds(1.0f);
        ObjectGen(skill);

        yield return new WaitForSeconds(1.0f);
        isSkill04 = false;
    }

 

현재 isSkill03 / isSkill04가 true인 상태일 때 플레이어의 이동을 제한하고 있습니다.

 

yield return new WaitForSeconds()를 이용하여 애니메이션이 끝나는 시점을 기다리도록 해주었습니다.

 

 

작업을 진행하면서 너무 하드코딩된 시간이 아닌가?라는 생각이 들었습니다.

 

 

처음에는 현재 재생 중인 애니메이션을 가져와 시간을 제한해주려고 했는데

    AnimatorStateInfo stateInfo = playerAnim.GetCurrentAnimatorStateInfo(0);
    float clipLength = stateInfo.length;

 

이렇게 작성을 해도 애니메이션이 전이되면서 부드럽게 보정되는 시간 같은 것을 고려하면

 

또 비슷한 하드코딩이 되었습니다.

 

 

지금 현재 드는 생각은 애니메이션이 완전히 끝나는 시점을 받아와 그때 isSkill03과 isSkill04를 해결해 주면 될 것 같은데...

 

이 부분은 조금 더 고민을 해봐야 할 것 같습니다.

 

 


 

 

마지막으로 추가된 기능입니다.

 

상점 UI가 고정된 형태로 있다 보니, 뭔가 답답한 감이 없지 않아 있었고

 

자유자재로 사용자가 이동을 하게끔 하면 좋을 것 같았습니다.


 

그래서 바로 스크립트(StoreDragHandler)를 작성하기로 했습니다.

 


 

 

1. 클래스 선언부입니다.

 

public class StoreDragHandler : MonoBehaviour, IBeginDragHandler, IDragHandler

IBeginDragHandler : 드래그가 시작될 때 이벤트 처리

IDragHandler : 드래그 중일 때 이벤트 처리

 

-마우스로 드래그해서 UI 이동을 구현하는 용도입니다.-

using UnityEngine;
using UnityEngine.EventSystems;

public class StoreDragHandler : MonoBehaviour, IBeginDragHandler, IDragHandler
{
    [SerializeField] private RectTransform storePanel;
    private Vector2 offset;

    public void OnBeginDrag(PointerEventData eventData)
    {
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            storePanel.parent as RectTransform,
            eventData.position,
            eventData.pressEventCamera,
            out var localMousePos
        );

        offset = storePanel.localPosition - (Vector3)localMousePos;
    }

    public void OnDrag(PointerEventData eventData)
    {
        if (storePanel == null) return;

        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            storePanel.parent as RectTransform,
            eventData.position,
            eventData.pressEventCamera,
            out var localMousePos
        );

        storePanel.localPosition = localMousePos + offset;
    }
}

 

 

 


 

 

2. 변수 선언부입니다.

storePanel : 드래그로 움직일 상점 UI

offset : 마우스를 클릭한 위치와 패널의 좌표 차이

    [SerializeField] private RectTransform storePanel;
    private Vector2 offset;

 

 


 

 

3.  드래그 시작

ScreenPointToLocalPointInRectangle : 마우스 스크린 좌표를 UI 로컬 좌표로 변환

localMousePos : 상점 패널의 부모 기준 좌표계에서 본 마우스 위치

offset : 패널 위치 - 마우스 위치

 

클릭한 순간의 상대적인 차이를 기억하며, UI를 클릭하는 순간 UI가 마우스 위치에 맞춰 움직이게 하기 위함입니다.

    public void OnBeginDrag(PointerEventData eventData)
    {
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            storePanel.parent as RectTransform,
            eventData.position,
            eventData.pressEventCamera,
            out var localMousePos
        );

        offset = storePanel.localPosition - (Vector3)localMousePos;
    }

 

 


 

 

4. 드래그 중

마우스의 현재 좌표를 다시 localMousePos로 변환하며 

마우스 위치 + 처음에 저장한 offset 만큼 이동합니다.

 

결과적으로 드래그하는 동안 패널이 마우스의 위치를 따라오게 됩니다.

    public void OnDrag(PointerEventData eventData)
    {
        if (storePanel == null) return;

        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            storePanel.parent as RectTransform,
            eventData.position,
            eventData.pressEventCamera,
            out var localMousePos
        );

        storePanel.localPosition = localMousePos + offset;
    }

 

 


 

 

마지막으로 수정 및 추가한 기능들을 영상으로 한 번에 보며 마무리하겠습니다.

 

 


 

뭔가 하나씩 개발하는 과정도 재밌고 구현된 모습을 보는 것도 재밌는 것 같습니다.

 

이것저것 더 해보고 싶은 건 많은데요!

 

역시나 시간이 부족하다는 게.. 아쉽습니다.🤣

 

 

그래도.. 빠르게 진행되진 않아도!

 

하루하루 달라져가는 프로젝트의 개발 과정 및 결과를 선보이도록 하겠습니다.

 


 

모두 좋은 하루 되세요.

 

감사합니다.

 

근데 언제 시간이 이렇게... 하하.. 😂

 

 

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

# 017 Enemy (2) (with. Turret)  (0) 2025.08.23
# 016 Enemy Spawn & movement  (1) 2025.08.21
#014 Inventory (With. Store)  (2) 2025.08.18
# 013 Player Skill Point (with. Event Trigger)  (1) 2025.08.17
# 012 Player Exp (with. Get Gold)  (5) 2025.08.17