전체 코드 : 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();
}
}
결론
주요 특징 및 장점
- 모듈화된 설계
- 각 컴포넌트가 독립적으로 동작
- 높은 확장성과 재사용성
- 직관적인 UX
- 클릭 한 번으로 장비 착용 / 해제
- 실시간 스탯 변화 확인
- 시각적 피드백
- 안정적인 데이터 관리
- 예외 상황 처리 ( 인벤토리 공간 부족 등 )
- 확장 가능한 구조
- 새로운 장비 타입 쉽게 추가 가능
- 아이템 등급 시스템 확장 용이
- UI 테마 변경 가능
성능 최적화
- 델리게이트 기반 이벤트 시스템
- 필요한 경우에만 UI 업데이트
- 불필요한 UI 렌더링 방지
- 리소스 관리 최적화
- 아이템 아이콘 동적 로딩
- 텍스쳐 재사용을 통한 메모리 절약
이 시스템을 구현하면서 얻은 포인트
- 사용자 경험의 중요성 : 복잡한 내부 로직도 사용자에게는 직관적으로 보여야함
- 모듈화 : 각 기능을 독립적으로 설계하면 디버깅과 확장이 쉬워짐
'DirectX11 > Eternal Return 모작' 카테고리의 다른 글
| [DirectX 11 Eternal Return 모작] 12. NavMesh (0) | 2025.09.03 |
|---|---|
| [DirectX 11 Eternal Return 모작] 11. 스킬 시스템 (0) | 2025.09.03 |
| [DirectX 11 Eternal Return 모작] 9. 아이템 시스템 & 제작 시스템 (0) | 2025.09.03 |
| [DirectX 11 Eternal Return 모작] 8. State Machine & Delegate (0) | 2025.09.03 |
| [DirectX 11 Eternal Return 모작] 7. ResourceManager (0) | 2025.09.03 |