Unreal - 퀵슬롯

2025. 12. 27. 15:38·Unreal 프로젝트 다이어리/두번째 프로젝트

미리보기

구현내용

플레이어의 인벤토리의 아이템을 등록하여 사용할수있는

퀵슬롯 시스템을 구현하였습니다

 

검은사막의 예시입니다 아이템 및 스킬을 퀵슬롯으로 사용 가능합니다

 

사용한 클래스

사용한 클래스 사용 목적
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

역할 : 퀵슬롯의 데이터 & 입력 처리

캐릭터에 붙는 액터 컴포넌트로 퀵슬롯에 들어간

아이템 정보와 사용 요청을 관리합니다

 

  1. 퀵슬롯 데이터 저장
  2. 슬롯 사용 관리
  3. 인벤토리 연동
  4. 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

역할 : 보여주기 + 입력 전달

  1. 아이콘 표시
  2. 수량 표시
  3. 단축키 입력 감지

 

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

 

구현 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
'Unreal 프로젝트 다이어리/두번째 프로젝트' 카테고리의 다른 글
  • Unreal - 스킬창 & 연동
  • Unreal - 버프창
  • Unreal - 퀘스트 시스템2
  • Unreal - 가이드 마커
lucodev
lucodev
언리얼 포폴개발 일기
  • lucodev
    루코 개발테이블
    lucodev
  • 전체
    오늘
    어제
    • 분류 전체보기 (236)
      • Unreal 프로젝트 다이어리 (132)
        • 첫번째 프로젝트 (73)
        • 두번째 프로젝트 (59)
      • Unreal 팁 (8)
      • Unreal 디버깅 (8)
      • C++ 프로그래머스 (52)
        • Stack,Queue (7)
        • Hash (4)
        • Heap (2)
        • Sort (5)
        • Exhaustive search (5)
        • Greedy (2)
        • BFS , DFS (7)
        • Graph (2)
        • Dynamic Programming (1)
        • C++ Math (2)
        • 기타 문제 (14)
      • C++ 백준 (5)
      • C++ 팁 (1)
      • 개인 코테 & 스타디 <비공개> (29)
        • 코드 개인보관함 (9)
        • 코딩테스트+@ (11)
        • 알고리즘 스타디 (6)
        • 알고리즘 스타디 과제 (3)
        • 비공개 (0)
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 링크

  • 공지사항

  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 태그

    언리얼 상호작용
    언리얼 시퀀스
    언리얼 ui
    unreal npc
    언리얼 비헤이비어트리
    언리얼 behavior tree
    unreal 인벤토리
    unreal
    언리얼 인벤토리
    언리얼 parkour
    unreal 세키로
    언리얼
    unreal inventory
    unreal 상호작용
    언리얼 인터렉션
    언리얼 behaviortree
    언리얼 세키로
    unreal 파쿠르
    언리얼 컷씬
    언리얼 파쿠르
  • hELLO· Designed By정상우.v4.10.3
lucodev
Unreal - 퀵슬롯
상단으로

티스토리툴바