🛠️ 오늘 한 작업

  • 대사창 시스템 작업을 시작했습니다.
  • SO로 만든 대화 데이터를 다시 SO 기반 DB로 묶고, string key로 불러오는 구조를 만들었습니다.
  • 초기 무기 주먹 아이템 작업으로 무기를 장착하지 않았을 때 기본 주먹이 장착되도록 했습니다.
  • 기본 주먹 아이템은 인벤토리에 들어가거나 드래그/해제되지 않도록 예외 처리를 추가했습니다.
  • 장비 / 무기 내구도 다는거 작업으로 공격 시 무기 내구도, 피격 시 방어구 내구도가 감소하도록 연결했습니다.
  • 내구도가 0 이하가 된 무기나 방어구가 장착 해제되도록 수정했습니다.
  • 드랍 아이템을 인벤토리에서 바닥으로 버리는 흐름도 추가로 작업했습니다.

 

📌 오늘 계획

오늘은 대사창 작업과 장비 쪽 예외 처리를 같이 진행하려고 했습니다.

  • 대사창 만들기
  • SO 대화 데이터를 key 값으로 불러오는 구조 만들기
  • 무기 아이템을 장착하지 않았을 때 기본 주먹 아이템이 장착되도록 만들기
  • 기본 주먹 아이템이 인벤토리에 들어가지 않도록 막기
  • 무기와 장비 내구도 감소 처리하기

이전에는 대화 데이터를 인스펙터에 직접 많이 넣어두는 방식이라 관리가 불편했습니다. 이번에는 key 값으로 대화 데이터를 불러오는 방식으로 바꿔서, 특정 대화를 호출할 때 훨씬 편하게 만들고 싶었습니다.

 

💬 SO 기반 대화 데이터 구조

먼저 대화 데이터를 TalkData ScriptableObject로 만들었습니다.

대화에는 NPC 이름, 초상화, 실제 대사 배열이 필요했고, 대화가 이미 끝났는지 확인하기 위한 값도 같이 넣었습니다.

그리고 여러 대화 데이터를 key 값으로 찾기 위해 TalkDataDB를 만들었습니다. Unity 인스펙터에서 Dictionary를 바로 다루기 어렵기 때문에, 중간 데이터인 SetDialogue 리스트를 만들고 내부에서 Dictionary로 변환하는 방식으로 구현했습니다.

TalkDataDB.cs - key 기반 대화 데이터 조회

[System.Serializable]
public class SetDialogue
{
    public string key;
    public TalkData data;
}

[CreateAssetMenu(fileName = "DialogueDB", menuName = "Dialogue/Database")]
public class TalkDataDB : ScriptableObject
{
    public List<SetDialogue> dialogues = new();
    private Dictionary<string, TalkData> talkData;

    private void Build()
    {
        if (talkData != null) return;

        talkData = new Dictionary<string, TalkData>();

        foreach (var dialogue in dialogues)
        {
            if (string.IsNullOrEmpty(dialogue.key) || dialogue.data == null) continue;
            talkData[dialogue.key] = dialogue.data;
        }
    }

    public TalkData Get(string key)
    {
        Build();
        return talkData.TryGetValue(key, out var data) ? data : null;
    }
}

이 방식으로 바꾸면 대화 데이터를 필요한 곳에서 key 값으로 불러올 수 있습니다. 인스펙터에 대화 데이터를 계속 직접 끼워 넣는 것보다 훨씬 관리하기 편할 것 같습니다.

 

🗨️ DialogueManager 연결

대화 UI와 대화 데이터를 연결하기 위해 DialogueManager도 만들었습니다.

StartDialogueByKey로 key 값을 넘기면 TalkDataDB에서 해당 대화 데이터를 찾아오고, 대화가 시작되면 한 줄씩 UI에 표시하도록 했습니다.

DialogueManager.cs - key로 대화 시작

public void StartDialogueByKey(string key)
{
    if (talkDataDB == null) return;

    TalkData talk = talkDataDB.Get(key);
    if (talk == null) return;

    StartDialogue(talk);
}

public void NextDialogue()
{
    if (currentTalk == null) return;

    index++;

    if (currentTalk.dialogueLines != null && index < currentTalk.dialogueLines.Length)
    {
        ShowLine();
    }
    else
    {
        EndDialogue();
    }
}

대사 안에 \n이나 \\n이 들어간 경우 줄바꿈이 되도록 처리했고, NPC 대화인지 아닌지에 따라 이름과 초상화 표시도 다르게 처리했습니다.

아직 대화 트리나 선택지까지 만든 것은 아니지만, key로 대화를 시작하고 다음 대사로 넘기는 기본 구조는 잡았습니다.

 

👊 기본 주먹 무기 처리

무기를 장착하지 않았을 때는 기본 주먹 아이템이 장착되도록 수정했습니다.

기본 주먹은 실제 인벤토리 아이템처럼 다뤄지면 안 됩니다. 그래서 uid를 0으로 둔 특수 아이템으로 만들고, 무기 슬롯이 비어 있을 때 자동으로 장착되도록 했습니다.

Equipment.cs - 기본 주먹 무기 보장

private const int DEFAULT_WEAPON_ID = 30000;

private ItemStack CreateDefaultWeapon()
{
    return new ItemStack(
        0L,
        DEFAULT_WEAPON_ID,
        1,
        1,
        null);
}

public void EnsureDefaultWeapon()
{
    if (GetEquipped(EquipSlotType.Weapon) != null) return;

    equippedItems[EquipSlotType.Weapon] = CreateDefaultWeapon();

    if (itemDB.TryGetWeapon(DEFAULT_WEAPON_ID, out var weapon) && weapon != null)
    {
        player.AttackDamage.SetWeaponAttack(weapon.Attack);
        player.AttackDelay.SetWeaponAtkDelay(weapon.AttackSpeed);
    }

    EventManager.TriggerEvent(EventKey.EquipmentChanged);
}

또한 기본 주먹은 드래그하거나 장착 해제할 수 없도록 예외 처리를 넣었습니다. 무기 슬롯에서 아무것도 없는 상태가 되면 공격 계산이 꼬일 수 있기 때문에, 항상 기본 무기가 있다고 보는 방식으로 잡았습니다.

이 방식은 아직 특수 예외가 섞여 있긴 하지만, 플레이어가 무기를 들고 있지 않은 상태에서도 기본 공격을 유지하기에는 괜찮은 방향이라고 생각했습니다.

 

🛡️ 무기 / 방어구 내구도 감소

오늘은 무기와 방어구 내구도 감소도 연결했습니다.

무기는 공격이 실제로 몬스터에게 맞았을 때 내구도가 줄어들도록 했고, 방어구는 플레이어가 피해를 받을 때 해당 부위 장비의 내구도가 줄어들도록 했습니다.

PlayerAttack.cs - 공격 성공 시 무기 내구도 감소

if (hitMonster)
{
    MinusWeaponDurability(1);
}

private void MinusWeaponDurability(int amount)
{
    var weapon = eq.GetEquipped(EquipSlotType.Weapon);
    if (weapon == null) return;
    if (weapon.itemId == 30000 && weapon.uid == 0) return;
    if (!weapon.HasRuntime) return;

    weapon.runtime.durability -= amount;

    if (weapon.runtime.durability <= 0)
    {
        eq.DestroyEquipped(EquipSlotType.Weapon);
        return;
    }

    EventManager.TriggerEvent(EventKey.EquipmentChanged);
}

기본 주먹은 내구도가 줄어들면 안 되기 때문에 예외 처리했습니다. 그리고 내구도가 0 이하가 되면 장비를 파괴 처리하고, 무기라면 다시 기본 주먹이 장착되도록 했습니다.

방어구도 피격 시 내구도가 감소하고, 내구도가 0이 되면 장착에서 제거되도록 연결했습니다. 아직 밸런스 수치는 더 조정해야 하지만, 장비가 실제 전투 결과에 따라 소모되는 흐름이 생겼습니다.

 

📦 드랍 아이템 버리기 흐름 추가

지난번에 만든 드랍 아이템 구조에 이어, 오늘은 인벤토리에서 아이템을 바닥으로 버리는 흐름도 추가했습니다.

DropItemSpawner를 만들어 플레이어 주변 위치에 드랍 아이템을 생성하거나, 가까운 위치에 이미 드랍 아이템이 있으면 하나로 합치도록 했습니다.

InventoryDropItem에서는 스택형 아이템은 하나만 버리거나 전부 버릴 수 있게 나누고, 비스택형 아이템은 기존 ItemStack을 그대로 바닥에 넘기는 방식으로 잡았습니다.

아직 UI까지 완성된 것은 아니지만, 인벤토리에서 바닥으로 아이템을 보내는 기본 흐름은 시작했습니다.

 

📌 오늘의 회고

오늘은 대화창 작업과 장비 예외 처리를 같이 진행한 날이었습니다.

대화 데이터는 예전에 인스펙터에 직접 많이 넣어두는 방식으로 관리했는데, 이번에는 SO 데이터를 다시 DB 형태의 SO로 묶고 key 값으로 불러오는 구조를 만들었습니다. 확실히 key로 불러오는 방식이 더 관리하기 편할 것 같습니다.

기본 주먹 무기 처리도 필요했습니다. 무기를 장착하지 않았다고 해서 무기 슬롯이 완전히 비어 있으면 공격력 계산이나 UI 표시가 애매해질 수 있기 때문에, 기본 주먹을 특수 아이템처럼 넣어두는 방식으로 해결했습니다.

무기와 방어구 내구도도 드디어 실제 전투와 연결되기 시작했습니다. 몬스터를 때리면 무기 내구도가 줄고, 공격을 받으면 방어구 내구도가 줄어드는 흐름이 생겼습니다. 이제 장비가 단순히 장착만 되는 것이 아니라 실제 게임 플레이 중 소모되는 데이터가 되었습니다.

아직 대화창, 드랍 아이템 UI, 내구도 밸런스는 더 다듬어야 합니다. 그래도 오늘은 시스템들이 조금씩 실제 게임 흐름에 붙기 시작한 날이었습니다.