DirectX11/Eternal Return 모작

[DirectX 11 Eternal Return 모작] 10. 인벤토리와 장비 시스템

Vfly 2025. 9. 3. 01:15

전체 코드 : https://github.com/HyangRim/DirectX11-Engine-Client

 

 

인벤토리 & 장비 시스템 : Delegate 기반 반응형 UI

1. 시스템 아키텍쳐

1.1 Delegate 기반 이벤트 시스템

class InventoryManager {
public:
    // 인벤토리 변화 알림 델리게이트
    Delegate::Delegate<> OnInventoryChanged;
    
    void NotifyInventoryChanged() {
        OnInventoryChanged(); // 모든 구독자에게 즉시 알림
    }
    
    void RegisterInventorySlots(const vector<shared_ptr<ItemSlot>>& inventorySlots) {
        // 각 슬롯에 클릭 이벤트 등록
        for (int i = 0; i < m_inventorySlots.size(); i++) {
            auto slot = m_inventorySlots[i];
            
            // 람다를 통한 이벤트 바인딩
            slot->OnSlotClicked.Push([this](int slotIndex, SLOTTYPE slotType) {
                OnInventorySlotClicked(slotIndex);
            });
            
            slot->OnSlotRightClicked.Push([this](int slotIndex, SLOTTYPE slotType) {
                OnSlotRightClicked(slotIndex);
            });
        }
        
        NotifyInventoryChanged(); // 초기 알림
    }
};

특징

  • Observer 패턴 : UI와 로직의 완전한 분리
  • 확장성 : 새로운 구독자를 런타임에 추가 가능

 

1.2 슬롯 관리 시스템

타입 기반 슬롯 검증

class ItemSlot : public Component {
public:
    // 델리게이트 선언 (슬롯 인덱스와 타입을 전달)
    Delegate::Delegate<int, SLOTTYPE> OnSlotClicked;
    Delegate::Delegate<int, SLOTTYPE> OnSlotRightClicked;
    
    void CreateSlot(Vec2 localPos, Vec2 size, int slotIndex) {
        m_slotIndex = slotIndex;
        m_slotPanel = m_parentPanel->GetUIPanel()->AddPanel(localPos, size, nullptr);
        
        // 슬롯 인덱스 표시 (디버그용)
        if (m_isNeedToShowSlotIndex) {
            m_slotPanel->AddD2DText(Vec2(27, 24), to_wstring(slotIndex), 10.f);
        }
        
        UpdateSlotUI(); // 초기 UI 구성
    }
    
    void UpdateSlotUI() {
        // 기존 UI 요소들 안전 제거
        if (m_slotPanel) {
            m_slotPanel->RemoveUIElementSafely(L"Button");
            m_slotPanel->RemoveUIElementSafely(L"ImageUI");
            m_slotButton.reset();
            m_iconImageUI.reset();
        }
        
        if (m_item) {
            UpdatePanel(); // 아이템이 있으면 UI 생성
        } else {
            if (m_slotType == SLOTTYPE::EQUIPMENT) {
                UpdateEquipmentSlot(); // 장비 슬롯 기본 아이콘 표시
            } else {
                HidePanel(); // 인벤토리 슬롯 숨김
            }
        }
    }
};

 

 

2. 인벤토리 시스템

2.1 슬롯 기반 아이템 관리

각 슬롯은 독립적인 컴포넌트로 설계되어 확장성과 재사용성을 보장

class ItemSlot : public Component
{
public:
    // 슬롯 타입별 분류
    void SetSlotType(SLOTTYPE slotType) { m_slotType = slotType; }
    
    // 아이템 설정/해제
    void SetItem(shared_ptr<Item> item);
    void ClearItem();
    
    // 선택 상태 관리
    void SetSelected(bool selected);
    
    // 델리게이트를 통한 이벤트 처리
    Delegate::Delegate<int, SLOTTYPE> OnSlotClicked;
    Delegate::Delegate<int, SLOTTYPE> OnSlotRightClicked;

private:
    shared_ptr<Item> m_item;
    SLOTTYPE m_slotType;
    int m_slotIndex;
    bool m_isSelected = false;
};

 

2.2 통합 인벤토리 매니저

인벤토리와 장비 슬롯을 하나의 매니저에서 통합 관리하여 일관성 있는 상호작용을 제공

class InventoryManager
{
public:
    // 슬롯 등록 시스템
    void RegisterInventorySlots(const vector<shared_ptr<ItemSlot>>& inventorySlots);
    void RegisterEquipmentSlots(const vector<shared_ptr<ItemSlot>>& equipmentSlots);
    
    // 아이템 착용/해제 로직
    bool EquipItem(int inventorySlotIndex);
    bool UnequipItem(int equipmentSlotIndex);
    
    // 슬롯 간 아이템 이동
    bool MoveItem(int fromSlot, int toSlot, SLOTTYPE fromType, SLOTTYPE toType);

private:
    vector<shared_ptr<ItemSlot>> m_inventorySlots;
    vector<shared_ptr<ItemSlot>> m_equipmentSlots;
    
    // 선택된 슬롯 정보
    shared_ptr<ItemSlot> m_selectedSlot;
    int m_selectedSlotIndex = -1;
    SLOTTYPE m_selectedSlotType;
};

 

2.3 장비 타입별 슬롯 매핑

장비 타입에 따라 올바른 슬롯에만 착용할 수 있도록 제한

int InventoryManager::GetEquipmentSlotIndex(EquipmentType equipType)
{
    // 장비 타입에 따른 슬롯 인덱스 반환
    switch (equipType) {
    case EquipmentType::WEAPON: return 0;  // 무기 슬롯
    case EquipmentType::CHEST:  return 1;  // 상의 슬롯
    case EquipmentType::HEAD:   return 2;  // 머리 슬롯
    case EquipmentType::ARM:    return 3;  // 팔 슬롯
    case EquipmentType::LEG:    return 4;  // 다리 슬롯
    default: return -1;
    }
}
bool InventoryManager::EquipItem(int inventorySlotIndex) {
    auto inventorySlot = m_inventorySlots[inventorySlotIndex];
    auto item = inventorySlot->GetItem();
    
    // 타입 검증: 장비 아이템만 착용 가능
    if (item->GetItemType() != ITEMTYPE::EQUIPABLE) return false;
    
    auto equipItem = static_pointer_cast<EquipableItem>(item);
    if (!CanEquipItem(equipItem)) return false;
    
    // 장비 타입에 따른 슬롯 인덱스 자동 결정
    int equipSlotIndex = GetEquipmentSlotIndex(equipItem->GetEquipType());
    if (equipSlotIndex == -1) return false;
    
    auto equipmentSlot = m_equipmentSlots[equipSlotIndex];
    
    // 기존 장비가 있으면 자동 교체
    if (!equipmentSlot->IsEmpty()) {
        SwapItems(inventorySlot, equipmentSlot);
    } else {
        // 단순 이동
        equipmentSlot->SetItem(equipItem);
        m_player->WearEquipment(equipItem);
        inventorySlot->ClearItem();
    }
    
    NotifyInventoryChanged(); // UI 자동 동기화
    return true;
}

int InventoryManager::GetEquipmentSlotIndex(EquipmentType equipType) {
    switch (equipType) {
    case EquipmentType::WEAPON: return 0;  // 무기 슬롯
    case EquipmentType::CHEST:  return 1;  // 상의 슬롯  
    case EquipmentType::HEAD:   return 2;  // 머리 슬롯
    case EquipmentType::ARM:    return 3;  // 팔 슬롯
    case EquipmentType::LEG:    return 4;  // 다리 슬롯
    default: return -1;
    }
}

 

 

 

 

3. 아이템 스텟 적용 시스템

3.1 장비 착용 / 해제 로직

플레이어 클래스에서 장비 착용 시 자동으로 스텟이 적용되는 시스템

void Player::WearEquipment(shared_ptr<EquipableItem> _item)
{
    if (!_item) return;
    
    // 장비 타입에 따른 슬롯 결정
    EquipmentType itemType = _item->GetEquipType();
    int slotIndex = -1;
    
    switch(itemType) {
        case EquipmentType::HEAD:   slotIndex = 2; break;
        case EquipmentType::CHEST:  slotIndex = 1; break;
        case EquipmentType::ARM:    slotIndex = 3; break;
        case EquipmentType::LEG:    slotIndex = 4; break;
        case EquipmentType::WEAPON: slotIndex = 0; break;
    }
    
    // 기존 장비가 있다면 해제 후 교체
    if (m_curEquipment[slotIndex] != nullptr) {
        TakeOffEquipment(slotIndex);
    }
    
    // 새 장비 착용
    m_curEquipment[slotIndex] = _item;
    ApplyEquipStatus(_item->GetStatus()); // 스탯 적용
}

 

3.2 실시간 스텟 적용 / 해제

장비 착용 시 즉시 플레이어 스탯에 반영되는 시스템

void Player::ApplyEquipStatus(const ItemStatus& _equipStatus)
{
    // 현재 HP/스태미나 비율 보존
    float hpRatio = static_cast<float>(m_status.hp) / static_cast<float>(m_status.max_HP);
    float staminaRatio = static_cast<float>(m_status.stamina) / static_cast<float>(m_status.max_Stamina);
    
    // 스탯 적용
    m_status.max_HP += _equipStatus.maxHP;
    m_status.max_Stamina += _equipStatus.maxSP;
    m_status.hitAttack += _equipStatus.attackPower;
    m_status.hitSpeed += _equipStatus.attackSpeed;
    m_status.defense += _equipStatus.defense;
    m_status.cooldownReduction += _equipStatus.cooldownReduction;
    m_status.healing += _equipStatus.hpRegen;
    m_status.healing_Stamina += _equipStatus.spRegen;
    
    // 비율에 따른 현재 HP/스태미나 재계산
    m_status.hp = static_cast<int>(m_status.max_HP * hpRatio);
    m_status.stamina = m_status.max_Stamina * staminaRatio;
    
    // UI 실시간 업데이트
    if (auto manager = m_uiManager.lock()) {
        manager->GetGameHUD()->UpdateStatBar();
        manager->GetStatusUI()->UpdatePlayerStatus();
    }
}

 

3.3 장비 해제 시 스탯 복원

void Player::TakeOffEquipment(int _index)
{
    if (_index < 0 || _index >= 5) return;
    if (m_curEquipment[_index] == nullptr) return;
    
    // 인벤토리 빈 공간 확인
    int emptyInventoryIDX = -1;
    for (int idx = 0; idx < 10; ++idx) {
        if (m_inventory[idx] == nullptr) {
            emptyInventoryIDX = idx;
            break;
        }
    }
    
    if (emptyInventoryIDX == -1) return; // 인벤토리 공간 없음
    
    // 스탯 해제 및 아이템 이동
    ReleaseEquipStatus(m_curEquipment[_index]->GetStatus());
    m_inventory[emptyInventoryIDX] = move(m_curEquipment[_index]);
    m_curEquipment[_index] = nullptr;
    
    SOUND->PlaySound(L"SFX/PickUpItem.wav", 5, 0.5f);
}

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

장비 장착시 신발 부위에는 신발 아이템만, 팔 부위에는 팔 아이템만 장착 가능함.

또한 장비 장착시 / 해제시 스탯변화

 

 

4. 아이템 시각화

4.1 아이템 등급별 시각적 차별화

void ItemSlot::UpdatePanel() {
    if (!m_item || !m_slotPanel) return;
    
    m_slotPanel->SetVisible(true);
    
    // 아이템 등급에 따른 슬롯 배경 Material 선택
    ITEMGRADE itemGrade = m_item->GetItemGrade();
    wstring btnMaterialTag = L"Img_Item_Slot_";
    
    switch (itemGrade) {
    case ITEMGRADE::COMMON:    btnMaterialTag += L"Common"; break;
    case ITEMGRADE::UNCOMMON:  btnMaterialTag += L"Uncommon"; break;
    case ITEMGRADE::RARE:      btnMaterialTag += L"Rare"; break;
    case ITEMGRADE::EPIC:      btnMaterialTag += L"Epic"; break;
    case ITEMGRADE::LEGENDARY: btnMaterialTag += L"Legendary"; break;
    }
    
    // 동적 버튼 생성
    Vec2 panelSize = m_slotPanel->GetSize();
    m_slotButton = m_slotPanel->AddButton(
        panelSize / 2.f,
        m_slotSize * (1 / RESOLUTION_CONSTANT),
        RESOURCES->Get<Material>(btnMaterialTag)->Clone(),
        L"Button"
    );
    
    // 이벤트 핸들러 등록
    m_slotButton->OnClick += [this]() {
        OnSlotClicked(m_slotIndex, m_slotType);
    };
    
    m_slotButton->OnRightClick += [this]() {
        OnSlotRightClicked(m_slotIndex, m_slotType);
    };
    
    // 아이템 아이콘 생성
    int32 itemID = m_item->GetItemID();
    wstring imgMaterialTag = L"ItemIcon_" + to_wstring(itemID);
    
    m_iconImageUI = m_slotPanel->AddImageUI(Vec2(0.f), L"ImageUI");
    m_iconImageUI->AddImageLayer(0, panelSize / 2.f, m_slotSize, 
        RESOURCES->Get<Material>(imgMaterialTag)->Clone(), 1);
}

 

 

아이템 등급에 따라 아이템 배경 이미지가 다름

 

4.2 동적 아이템 아이콘 로딩

void ItemManager::LoadResources()
{
    shared_ptr<Shader> shader = make_shared<Shader>(L"ImageShader.fx");
    
    auto SetupUIMaterial = [&](shared_ptr<Material> material) {
        material->SetShader(shader);
        material->SetRenderQueue(RenderQueue::Transparent);
        material->SetTransparent(true);
        material->SetRenderingMode(RenderingMode::Forward);
    };
    
    wstring prefixPath = L"..\\Resources\\Textures\\UI\\ItemIcon\\";
    
    // 아이템 ID 기반 아이콘 로딩
    for (int i = 0; i < itemIconID.size(); i++) {
        shared_ptr<Material> itemIcon = make_shared<Material>();
        SetupUIMaterial(itemIcon);
        
        wstring tag = L"ItemIcon_" + to_wstring(itemIconID[i]);
        wstring path = prefixPath + tag + L".png";
        auto itemIconTexture = RESOURCES->Load<Texture>(tag, path);
        
        itemIcon->SetDiffuseMap(itemIconTexture);
        RESOURCES->Add(tag, itemIcon);
    }
}

 

 

5.실시간 UI 업데이트 시스템 ( 인벤토리 )

5.1 Delegate 기반 이벤트 시스템

인벤토리 변화를 실시간으로 UI에 반영

class InventoryManager
{
public:
    // 인벤토리 변화 알림 델리게이트
    Delegate::Delegate<> OnInventoryChanged;
    
    void NotifyInventoryChanged() {
        OnInventoryChanged(); // 모든 구독자에게 알림
    }

private:
    void RegisterInventorySlots(const vector<shared_ptr<ItemSlot>>& inventorySlots) {
        m_inventorySlots = inventorySlots;
        
        // 각 슬롯에 클릭 이벤트 등록
        for (int i = 0; i < m_inventorySlots.size(); i++) {
            auto slot = m_inventorySlots[i];
            if (slot) {
                slot->OnSlotClicked.Push([this](int slotIndex, SLOTTYPE slotType) {
                    OnInventorySlotClicked(slotIndex);
                });
            }
        }
        
        NotifyInventoryChanged(); // 초기 알림
    }
};

 

5.2 스탯 UI 실시간 반영

void Player::SetHP(int32 _value)
{
    if (_value > m_status.max_HP)
        m_status.hp = m_status.max_HP;
    else
        m_status.hp = _value;
    
    // UI 매니저를 통한 즉시 업데이트
    if (auto manager = m_uiManager.lock()) {
        manager->GetGameHUD()->UpdateStatBar();
    }
}

 

 

결론

주요 특징 및 장점

  1. 모듈화된 설계
    1. 각 컴포넌트가 독립적으로 동작
    2. 높은 확장성과 재사용성
  2. 직관적인 UX
    1. 클릭 한 번으로 장비 착용 / 해제
    2. 실시간 스탯 변화 확인
    3. 시각적 피드백
  3. 안정적인 데이터 관리
    1. 예외 상황 처리 ( 인벤토리 공간 부족 등 )
  4. 확장 가능한 구조
    1. 새로운 장비 타입 쉽게 추가 가능
    2. 아이템 등급 시스템 확장 용이
    3. UI 테마 변경 가능

성능 최적화

  1. 델리게이트 기반 이벤트 시스템
    1. 필요한 경우에만 UI 업데이트
    2. 불필요한 UI 렌더링 방지
  2. 리소스 관리 최적화
    1. 아이템 아이콘 동적 로딩
    2. 텍스쳐 재사용을 통한 메모리 절약

이 시스템을 구현하면서 얻은 포인트

  1. 사용자 경험의 중요성 : 복잡한 내부 로직도 사용자에게는 직관적으로 보여야함
  2. 모듈화 : 각 기능을 독립적으로 설계하면 디버깅과 확장이 쉬워짐