미리보기

구현내용
플레이어의 인벤토리의 아이템을 등록하여 사용할수있는
퀵슬롯 시스템을 구현하였습니다
검은사막의 예시입니다 아이템 및 스킬을 퀵슬롯으로 사용 가능합니다

사용한 클래스
| 사용한 클래스 | 사용 목적 |
| ItemProcessor(UObject) | 아이템ID -> 효과 데이터 변환 힐 / 버프 / 퍼센트 버프 등 실제 사용 처리 |
| QuickslotComponent(액터컴포넌트) | 실제 아이템 데이터 소스 아이콘 / 이름 / 설명의 근원지 |
| QuickSlotWidget(위젯) | 퀵슬롯UI, 아이콘 / 수량 표시 |
구현
- ItemProcessor
역할 : 아이템 사용 규칙 담당자
아이템을 쓴다 라는 개념을 하나의 책임으로 분리
ItemID -> FUserItemInfo 로 변환
[ DataTable 기반, 아이템마다 효과를 데이터로 분리 ]
구현 C++
헤더
더보기
.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "StatComponent.h"
#include "ItemProcessor.generated.h"
class AMainCharacter;
class UStatComponent;
struct FUseItemInfo;
UENUM(BlueprintType)
enum class EUseItemType : uint8
{
None UMETA(DisplayName = "None"),
HealFixed UMETA(DisplayName = "Heal Fixed"),
HealPercent UMETA(DisplayName = "Heal Percent"),
BuffStat UMETA(DisplayName = "Buff Stat"),
BuffRate UMETA(DisplayName = "Buff Rate"),
};
USTRUCT(BlueprintType)
struct FUseItemInfo
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
EUseItemType useItemType = EUseItemType::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
EBaseStatType buffStatType = EBaseStatType::Hp;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float value = 0.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float duration = 0.f;
UTexture2D* buffIcon = nullptr;
const FItemData* itemData = nullptr;
};
UCLASS(Blueprintable, BlueprintType)
class PORTFOLIOMS_API UItemProcessor : public UObject
{
GENERATED_BODY()
public:
void Init(class AMainCharacter* activeOwner);
void ApplyHeal(float amount);
void ApplyHealPercent(float percent);
UFUNCTION()
void ApplyBuffStat(EBaseStatType statType, float value, float duration, UTexture2D* icon);
UFUNCTION()
void ApplyBuffStatRate(EBaseStatType statType, float rate, float duration, UTexture2D* icon);
UFUNCTION()
void ProcessUseItem(const FUseItemInfo& info);
public:
UPROPERTY()
class AMainCharacter* ownerCharacter;
UPROPERTY()
UStatComponent* statComp;
UPROPERTY()
class UInventoryComponent* invenComp;
public:
UFUNCTION()
FUseItemInfo GetItemInfoByID(int32 itemID) const;
};
구현부
더보기
.cpp
#include "ItemProcessor.h"
#include "MainCharacter.h"
#include "ItemData.h"
#include "InventoryComponent.h"
#include "StatComponent.h"
void UItemProcessor::Init(AMainCharacter* activeOwner)
{
ownerCharacter = activeOwner;
if (ownerCharacter)
{
statComp = ownerCharacter->FindComponentByClass<UStatComponent>();
invenComp = ownerCharacter->FindComponentByClass<UInventoryComponent>();
}
}
void UItemProcessor::ApplyHeal(float amount)
{
statComp->ApplyHeal(amount);
}
void UItemProcessor::ApplyHealPercent(float percent)
{
float maxHp = statComp->GetStatTotal(EBaseStatType::Hp);
statComp->ApplyHeal(maxHp * percent);
}
void UItemProcessor::ApplyBuffStat(EBaseStatType statType, float value, float duration, UTexture2D* icon)
{
FTempBuff buff;
buff.statType = statType;
buff.bonusValue = value;
buff.duration = duration;
buff.buffIcon = icon;
statComp->ApplyTempBuff(buff);
}
void UItemProcessor::ApplyBuffStatRate(EBaseStatType statType, float rate, float duration, UTexture2D* icon)
{
FTempBuff buff;
buff.statType = statType;
buff.bonusRate = rate;
buff.duration = duration;
buff.buffIcon = icon;
statComp->ApplyTempBuff(buff);
}
void UItemProcessor::ProcessUseItem(const FUseItemInfo& info)
{
if (!statComp)
return;
switch (info.useItemType)
{
case EUseItemType::HealFixed:
ApplyHeal(info.value);
break;
case EUseItemType::HealPercent:
ApplyHealPercent(info.value);
break;
case EUseItemType::BuffStat:
ApplyBuffStat(info.buffStatType, info.value, info.duration, info.buffIcon);
break;
case EUseItemType::BuffRate:
ApplyBuffStatRate(info.buffStatType, info.value, info.duration, info.buffIcon);
break;
default:
break;
}
}
FUseItemInfo UItemProcessor::GetItemInfoByID(int32 itemID) const
{
FUseItemInfo useInfo;
const FItemData* itemData = invenComp->GetItemData(itemID);
if (!itemData)
return useInfo;
useInfo.itemData = itemData;
useInfo.buffIcon = itemData->itemIcon;
switch (itemID)
{
case 0: // 작은 회복 포션
useInfo.useItemType = EUseItemType::HealFixed;
useInfo.value = 125.f;
break;
case 1: // 든든한 회복 포션
useInfo.useItemType = EUseItemType::HealFixed;
useInfo.value = 300.f;
break;
case 2: // 위급한 회복 포션
useInfo.useItemType = EUseItemType::HealPercent;
useInfo.value = 0.5f; // 최대 HP 50% 회복
break;
case 3: // 궁극의 회복약
useInfo.useItemType = EUseItemType::HealPercent;
useInfo.value = 1.0f; // 최대 HP 100% 회복
break;
case 4: // 공격력 +30, 1분
useInfo.useItemType = EUseItemType::BuffStat;
useInfo.buffStatType = EBaseStatType::AttackPower;
useInfo.value = 30.f;
useInfo.duration = 60.f;
break;
case 5: // 공격력 +100, 1분
useInfo.useItemType = EUseItemType::BuffStat;
useInfo.buffStatType = EBaseStatType::AttackPower;
useInfo.value = 100.f;
useInfo.duration = 60.f;
break;
case 6: // 공격력 +100, 5분
useInfo.useItemType = EUseItemType::BuffStat;
useInfo.buffStatType = EBaseStatType::AttackPower;
useInfo.value = 100.f;
useInfo.duration = 300.f;
break;
case 7: // 공격력 +300, 5분
useInfo.useItemType = EUseItemType::BuffStat;
useInfo.buffStatType = EBaseStatType::AttackPower;
useInfo.value = 300.f;
useInfo.duration = 300.f;
break;
case 8: // 치명타 확률 +10%, 5분
useInfo.useItemType = EUseItemType::BuffRate;
useInfo.buffStatType = EBaseStatType::CriticalRate;
useInfo.value = 0.1f; // 10%
useInfo.duration = 300.f;
break;
case 9: // 치명타 확률 +20%, 5분
useInfo.useItemType = EUseItemType::BuffRate;
useInfo.buffStatType = EBaseStatType::CriticalRate;
useInfo.value = 0.2f; // 20%
useInfo.duration = 300.f;
break;
default:
useInfo.useItemType = EUseItemType::None;
break;
}
return useInfo;
}
- QuickSlotComponent
역할 : 퀵슬롯의 데이터 & 입력 처리
캐릭터에 붙는 액터 컴포넌트로 퀵슬롯에 들어간
아이템 정보와 사용 요청을 관리합니다
- 퀵슬롯 데이터 저장
- 슬롯 사용 관리
- 인벤토리 연동
- UI 갱신 알림
등을 관리합니다
구현C++
헤더
더보기
.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ItemSlot.h"
#include "QuickSlotComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnQuickSlotUpdated, int32, slotIndex, int32, itemID);
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class PORTFOLIOMS_API UQuickSlotComponent : public UActorComponent
{
GENERATED_BODY()
public:
UQuickSlotComponent();
protected:
virtual void BeginPlay() override;
public:
UPROPERTY(EditAnywhere)
int32 quickSlotCount = 2;
UPROPERTY()
TArray<FItemSlot> quickSlots;
public:
UFUNCTION()
void InitQuickSlots();
UFUNCTION()
void SetQuickSlot(int32 slotIdx, int32 itemID, int32 count);
UFUNCTION()
void SwapQuickSlot(int32 fromIdx, int toIdx);
UFUNCTION()
int32 GetQuickSlotItem(int32 slotIdx) const;
public:
UPROPERTY(BlueprintAssignable)
FOnQuickSlotUpdated onQuickSlotUpdated;
public:
UFUNCTION()
FItemSlot GetQuickSlot(int32 slotIdx) const;
public:
UFUNCTION()
void UseSlot(int32 slotIdx);
UPROPERTY()
class UInventoryComponent* inventoryComp;
UPROPERTY()
class UItemProcessor* itemProcessor;
};
구현부
더보기
.CPP
#include "QuickSlotComponent.h"
#include "QuickSlotWidget.h"
#include "ItemProcessor.h"
#include "InventoryComponent.h"
UQuickSlotComponent::UQuickSlotComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UQuickSlotComponent::BeginPlay()
{
Super::BeginPlay();
InitQuickSlots();
inventoryComp = GetOwner()->FindComponentByClass<UInventoryComponent>();
itemProcessor = NewObject<UItemProcessor>(this);
itemProcessor->Init(Cast<AMainCharacter>(GetOwner()));
}
void UQuickSlotComponent::InitQuickSlots()
{
quickSlots.Empty();
quickSlots.SetNum(quickSlotCount);
for (auto& slot : quickSlots)
slot.Clear();
}
void UQuickSlotComponent::SetQuickSlot(int32 slotIdx, int32 itemID, int32 count)
{
if (!quickSlots.IsValidIndex(slotIdx))
return;
FItemSlot& slot = quickSlots[slotIdx];
slot.itemID = itemID;
slot.inCount = count;
onQuickSlotUpdated.Broadcast(slotIdx, itemID);
}
void UQuickSlotComponent::SwapQuickSlot(int32 fromIdx, int toIdx)
{
if (!quickSlots.IsValidIndex(fromIdx) || !quickSlots.IsValidIndex(toIdx))
return;
quickSlots.Swap(fromIdx, toIdx);
onQuickSlotUpdated.Broadcast(fromIdx, quickSlots[fromIdx].itemID);
onQuickSlotUpdated.Broadcast(toIdx, quickSlots[toIdx].itemID);
}
int32 UQuickSlotComponent::GetQuickSlotItem(int32 slotIdx) const
{
return quickSlots.IsValidIndex(slotIdx) ? quickSlots[slotIdx].itemID : -1;
}
FItemSlot UQuickSlotComponent::GetQuickSlot(int32 slotIdx) const
{
return quickSlots.IsValidIndex(slotIdx) ? quickSlots[slotIdx] : FItemSlot();
}
void UQuickSlotComponent::UseSlot(int32 slotIdx)
{
if (!quickSlots.IsValidIndex(slotIdx))
return;
FItemSlot& slot = quickSlots[slotIdx];
//슬롯 비어있는경우
if (slot.itemID < 0 || slot.inCount <= 0)
return;
const FItemData* itemData = inventoryComp->GetItemData(slot.itemID);
if (!itemData)
return;
if (itemProcessor)
{
FUseItemInfo useInfo = itemProcessor->GetItemInfoByID(slot.itemID);
itemProcessor->ProcessUseItem(useInfo);
}
//퀵슬롯 차감
slot.inCount--;
//인벤토리 차감
inventoryComp->RemoveItem(slot.itemID, 1);
if (slot.inCount <= 0)
slot.Clear();
onQuickSlotUpdated.Broadcast(slotIdx, slot.itemID);
}
- QuickSlotWidget
역할 : 보여주기 + 입력 전달
- 아이콘 표시
- 수량 표시
- 단축키 입력 감지

메인 위젯 우측 하단에 추가해주었습니다.

구현 c++
구현부
더보기
.cpp
#include "QuickSlotWidget.h"
#include "SlotWidget.h"
#include "InventoryComponent.h"
#include "QuickSlotComponent.h"
void UQuickSlotWidget::NativeConstruct()
{
Super::NativeConstruct();
slotWidgets.Empty();
if (SlotWidget_1)
slotWidgets.Add(SlotWidget_1);
if (SlotWidget_2)
slotWidgets.Add(SlotWidget_2);
if (APlayerController* pc = GetOwningPlayer())
{
if (APawn* pawn = pc->GetPawn())
{
quickSlotComp = pawn->FindComponentByClass<UQuickSlotComponent>();
inventoryComp = pawn->FindComponentByClass<UInventoryComponent>();
if (quickSlotComp &&!quickSlotComp->onQuickSlotUpdated.IsAlreadyBound(this, &UQuickSlotWidget::UpdateQuickSlotUI))
quickSlotComp->onQuickSlotUpdated.AddDynamic(this, &UQuickSlotWidget::UpdateQuickSlotUI);
}
}
for (int32 i = 0; i < slotWidgets.Num(); i++)
{
USlotWidget* slot = slotWidgets[i];
if (!slot)
continue;
slot->slotType = ESlotWidgetType::QuickSlot;
slot->slotIdx = i;
if (quickSlotComp)
{
int32 itemID = quickSlotComp->GetQuickSlotItem(i);
UpdateQuickSlotUI(i, itemID);
}
}
}
void UQuickSlotWidget::UpdateQuickSlotUI(int32 slotIdx, int32 itemID)
{
if (!slotWidgets.IsValidIndex(slotIdx))
return;
USlotWidget* targetSlot = slotWidgets[slotIdx];
if (!targetSlot)
return;
FItemSlot slotData = quickSlotComp->GetQuickSlot(slotIdx);
if (itemID < 0)
{
targetSlot->UpdateSlot(FItemSlot(), FItemData());
return;
}
if (!inventoryComp)
return;
const FItemData* itemData = inventoryComp->itemDataMap.Find(itemID);
if (!itemData)
return;
targetSlot->UpdateSlot(slotData, *itemData);
}
사용방식
키보드 1번과 2번을 InputComponent와 연동시켜줍니다
void AMainCharacterController::InputKeyboard1Pressed()
{
mainCharacter->usingConsumSlotIdx = 0;
mainCharacter->ExcutePendingItems();
}
void AMainCharacterController::InputKeyboard2Pressed()
{
mainCharacter->usingConsumSlotIdx = 1;
mainCharacter->ExcutePendingItems();
}
소비 아이템 사용 요청이 들어왔을때
현재 지정된 소비 퀵슬롯에서 아이템을 조회하여 어떤아이템을 썻는지 기억합니다
void AMainCharacter::ExcutePendingItems()
{
if (bUsingItem)
return;
if (healRows.Num() == 0)
return;
FItemSlot slot = quickSlotComp->GetQuickSlot(usingConsumSlotIdx);
if (slot.itemID < 0)
return;
cachedUsingItemID = slot.itemID;
usingPotion->SetActorHiddenInGame(false);
FCharacterAnimDataTable* row = healRows[0];
UAnimInstance* animIst = GetMesh()->GetAnimInstance();
bUsingItem = true;
float playAnimTime = PlayAnimMontage(row->usingAnimation);
if (playAnimTime <= 0.f)
{
bUsingItem = false;
return;
}
FOnMontageEnded endDel;
endDel.BindUObject(this, &AMainCharacter::OnUseItemMontageEnded);
animIst->Montage_SetEndDelegate(endDel, row->usingAnimation);
}
ExcutePendingItems에서 실행한 맞는 애니메이션에서 노티파이를 호출하여
UseQuickSlot(인덱스) 를 호출합니다
void UMainCharacterAnimInstance::AnimNotify_UsingConsumItem()
{
if (!mainCharacter)
return;
if (mainCharacter->usingConsumSlotIdx == 0)
mainCharacter->UseQuickSlot(0);
else if (mainCharacter->usingConsumSlotIdx == 1)
mainCharacter->UseQuickSlot(1);
mainCharacter->SpawnConsumNa();
}
퀵슬롯 컴포넌트에서 UseSlot함수를 호출
void AMainCharacter::UseQuickSlot(int32 slotIdx)
{
FItemSlot slot = quickSlotComp->GetQuickSlot(slotIdx);
if (slot.itemID < 0 || slot.inCount <= 0)
return;
quickSlotComp->UseSlot(slotIdx);
}
퀵슬롯 컴포넌트의 함수를 호출합니다
void UQuickSlotComponent::UseSlot(int32 slotIdx)
{
if (!quickSlots.IsValidIndex(slotIdx))
return;
FItemSlot& slot = quickSlots[slotIdx];
//슬롯 비어있는경우
if (slot.itemID < 0 || slot.inCount <= 0)
return;
const FItemData* itemData = inventoryComp->GetItemData(slot.itemID);
if (!itemData)
return;
if (itemProcessor)
{
FUseItemInfo useInfo = itemProcessor->GetItemInfoByID(slot.itemID);
itemProcessor->ProcessUseItem(useInfo);
}
//퀵슬롯 차감
slot.inCount--;
//인벤토리 차감
inventoryComp->RemoveItem(slot.itemID, 1);
if (slot.inCount <= 0)
slot.Clear();
onQuickSlotUpdated.Broadcast(slotIdx, slot.itemID);
}
결과



영상
'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글
| Unreal - 스킬창 & 연동 (0) | 2026.01.01 |
|---|---|
| Unreal - 버프창 (0) | 2025.12.27 |
| Unreal - 퀘스트 시스템2 (0) | 2025.12.22 |
| Unreal - 가이드 마커 (0) | 2025.12.22 |
| Unreal - 대화 Npc (0) | 2025.12.22 |
