🧠 오늘의 핵심 정리

  • feat: 드래그 앤 드롭(장비) 구현 시작 (인벤 슬롯 이동 + 장비 슬롯 드롭 장착)
  • ItemDropTarget 도입으로 드롭 대상(인벤/장비/퀵슬롯)을 한 스크립트로 처리
  • 인벤토리에 MoveItem() 추가 → 슬롯 순서 변경(정렬) 기반 마련
  • 장착 시 Inventory.RemoveItem() 타이밍 정리 (EquipItem 내부에서 처리)
  • feat: 대사창 추가 (E로 상호작용, 클릭으로 다음 대사, 초상화/이름 표시)
  • GameManager/UIManager도 Singleton 기반으로 정리하며 시스템 확장 준비

 

 

🖱️  드래그 앤 드롭(장비) : 인벤 → 장비 슬롯 드롭

오늘은 드디어… 제가 가장 걱정하던(?) 드래그 앤 드롭을 건드리기 시작했습니다.

우선 목표는 “인벤 슬롯에서 아이콘을 드래그해서 장비 슬롯에 드롭하면 장착되는 흐름”을 먼저 만드는 것이었습니다.

ItemSlot.cs - 드래그 인터페이스 추가

public class ItemSlot : MonoBehaviour, IPointerClickHandler,
    IBeginDragHandler, IDragHandler, IEndDragHandler
{
    // ...
}

드래그 중에는 원래 아이콘은 반투명 처리하고, 커서 따라다니는 “가짜 아이콘(DragIcon)”을 생성해서 가장 위에서 따라오게 만들었습니다.

드래그 시작/이동

public void OnBeginDrag(PointerEventData eventData)
{
    if (currentItem == null) return;

```
iconCanvasGroup.alpha = 0.4f;
iconCanvasGroup.blocksRaycasts = false;

dragIcon = new GameObject("DragIcon");
dragIcon.transform.SetParent(canvas.transform, false);
dragIcon.transform.SetAsLastSibling();

Image img = dragIcon.AddComponent<Image>();
img.sprite = icon.sprite;
img.raycastTarget = false;
```

}

public void OnDrag(PointerEventData eventData)
{
if(dragIcon != null)
dragIconRect.position = eventData.position;
}

드래그가 끝나면 Raycast로 드롭 위치를 찾아서, 거기에 ItemDropTarget이 있으면 ReceiveItem으로 넘겨 처리하도록 구성했습니다.

드래그 종료 → 드롭 타겟 탐색

public void OnEndDrag(PointerEventData eventData)
{
    iconCanvasGroup.alpha = 1f;
    iconCanvasGroup.blocksRaycasts = true;

```
if(dragIcon != null) Destroy(dragIcon);

List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, results);

foreach(var result in results)
{
    var dropTarget = result.gameObject.GetComponent<ItemDropTarget>();
    if (dropTarget != null)
    {
        dropTarget.ReceiveItem(currentItem);
        return;
    }
}
```

}

 

 

🎯  ItemDropTarget : 인벤/장비/퀵슬롯 드롭 분기

드롭 처리 쪽은 “슬롯마다 스크립트가 다르면 금방 지옥이 되겠다…” 싶어서, ItemDropTarget 하나로 인벤/장비/퀵슬롯을 구분 처리하도록 만들었습니다.

특히 장비 슬롯은 EquipSlotType이 맞는지 검사해서, 잘못된 부위에 드롭하면 바로 막도록 했습니다.

ItemDropTarget.cs - 장비 슬롯 처리

if (isEquipmentSlot)
{
    if (equipment == null) return;

```
if (droppedItem.data.EquipSlotType != equipSlotType)
{
    Debug.Log("잘못된 장비 타입");
    return;
}

bool success = equipment.EquipItem(droppedItem);
if (success)
{
    slotImage.sprite = droppedItem.data.Icon;
    slotImage.color = Color.white;
}
return;
```

}

퀵슬롯은 우선 “아이콘 표시”까지만 연결했고, 이후에는 실제로 사용/소비/무기 교체 같은 로직을 추가해야 합니다. 지금 단계에서는 드롭 대상에 따라 처리 분기가 되는 기반이 생긴 게 제일 의미 있었습니다.

 

 

📦  인벤토리 슬롯 이동 (MoveItem)

인벤토리 내부 순서를 바꾸는 기능도 같이 추가했습니다. 인벤 슬롯에 드롭하면 해당 슬롯의 sibling index를 기준으로 items 리스트 순서를 바꾸는 방식입니다.

Inventory.cs - MoveItem()

public void MoveItem(Item item, int newIndex)
{
    if (!items.Contains(item)) return;

```
items.Remove(item);
if(newIndex >= items.Count) items.Add(item);
else items.Insert(newIndex, item);

OnInventoryChanged?.Invoke();
```

}

드롭 타겟에서 인벤 슬롯일 경우, 이렇게 newIndex를 계산해서 호출하도록 연결했습니다.

ItemDropTarget.cs - 인벤 슬롯 처리

if (isItemSlot)
{
    int newIndex = itemSlot.transform.GetSiblingIndex();
    inventory.MoveItem(droppedItem, newIndex);
    return;
}

 

 

🛠️  장착 시 인벤토리 제거 타이밍 정리

이 부분은 체감상 꽤 중요했습니다.

기존에는 “인벤에서 UseItem 호출 → EquipItem 성공하면 인벤에서 RemoveItem” 흐름도 있었는데, 드래그 앤 드롭으로 장착까지 들어오면 제거 타이밍이 애매해질 수 있더라고요.

그래서 EquipItem 내부에서 inventory.RemoveItem(item)을 호출하도록 넣어서, 장착이 성공한 순간에 “인벤에서 빠지는 책임”을 장비 시스템 쪽으로 몰아줬습니다.

Equipment.cs - 장착 성공 시 인벤에서 제거

public bool EquipItem(Item item)
{
    // ... 장착 로직
    inventory.RemoveItem(item);
    OnEquipmentChanged?.Invoke();
    return true;
}

이렇게 해두면 “어떤 방식(더블클릭/드롭)으로 장착하든” 결국 EquipItem이 통일된 진입점이 되어서 구조가 더 안정적이 될 것 같습니다.

 

 

💬  feat: 대사창(상호작용) 추가

오늘은 UI 시스템만 한 게 아니라, 게임 분위기에 중요한 대사창도 같이 붙였습니다.

E를 눌러 오브젝트와 상호작용하고, 대사창이 떠 있으면 마우스 클릭(0)으로 다음 대사를 넘기도록 구성했습니다.

PlayerInteraction.cs - E 상호작용 / 클릭으로 다음

if (!UIManager.Instance.talkPanel.activeSelf && Input.GetKeyDown(KeyCode.E) && scanObject != null)
{
    GameManager.Instance.Action(scanObject);
}
if (UIManager.Instance.talkPanel.activeSelf && Input.GetMouseButtonDown(0))
{
    GameManager.Instance.ShowNextDialogue();
}

대사 데이터는 TalkManager에서 id 기반으로 관리했고, “대사:초상화 인덱스” 형태로 split 해서 초상화도 함께 출력되도록 했습니다.

UIManager.cs - NPC면 초상화/이름 표시

public void ShowDialogue(string speakerName, ObjectData objData, string talkData)
{
    if (objData.isNpc)
    {
        string[] split = talkData.Split(':');
        talkText.text = split[0];
        portraitImg.sprite = talkManager.GetPortrait(objData.id, int.Parse(split[1]));
        portraitImg.color = Color.white;
        nameText.text = speakerName;
    }
    else
    {
        talkText.text = talkData;
        portraitImg.color = new Color(1, 1, 1, 0);
    }
}

그리고… 대사 내용에 살짝 이스터에그도 넣으셨던데(“아니 박상현 어디감??”) 이런 거 하나 들어가면 갑자기 팀 프로젝트가 살아나는 느낌이라 웃겼습니다. ㅎㅎ

 

 

 

📌 오늘의 회고

 

오늘은 시스템이 두 갈래로 크게 진행된 날이었습니다.

하나는 인벤토리/장비 드래그 앤 드롭, 다른 하나는 대사창(상호작용)이었습니다.

드래그 앤 드롭은 역시… 시작하자마자 “드롭 대상마다 규칙이 달라질 텐데 어떻게 확장하지?”라는 고민이 바로 따라오더라고요.

그래도 ItemDropTarget로 드롭 분기 기반을 먼저 만들어둔 덕분에, 다음에 퀵슬롯/드롭 아이템 창을 붙일 때도 흐름을 크게 유지하면서 확장할 수 있을 것 같습니다.

대사창도 최소 기능은 돌아가게 되었고, 이제 정말로 “스토리 좀비 게임” 느낌이 나기 시작해서 개인적으로는 꽤 만족스러운 하루였습니다.