구현내용
플레이어가 NPC 에게 퀘스트를 의뢰하고, 해당 퀘스트를 완료하면
보상을 획득하는 NPC 의뢰 기반으로 퀘스트 수락 - 진행 - 완료 - 보상 까지
이어지는 퀘스트 시스템을 구현하였습니다.
타르코프 퀘스트창처럼 원하는 퀘스트를 의뢰하고
목표를 이루면 보상을 받도록 설계하였습니다.

구현된 퀘스트는 총 3가지이며
내용은 이와 같습니다
- 적 처치하기
- 퀘스트 아이템 수집하기
- 물건 전달하기
사용된 클래스
| 사용된 클래스 | 사용 목적 |
| QuestManager(UObject) | 게임 전체 퀘스트 데이터를 총괄 관리하는 중앙 관리자 (퀘스트 수락 가능 여부체크, 완료 조건, 보상 데이터 반환) |
| QuestComponent(액터컴포넌트) | 플레이어가 보유한 퀘스트를 관리하는 컴포넌트 (현재 진행 중인 퀘스트 목록 관리, QuestManager에 조건 충족 여부 질의) |
| QuestWidget(위젯) | 퀘스트 진행 상황을 보여주는 메인 퀘스트 UI |
| ShopQuestWidget(위젯) | NPC(의뢰인)과 상호작용할 때 표시되는 퀘스트 UI |
| ShopWidgetSlot(위젯) | 퀘스트 하나를 표현하는 UI 단위 슬롯 |
흐름은 이와 같습니다
- Npc 상호작용
- ShopQuestWidget (퀘스트 목록 표시)
- ShopWidgetSlot (퀘스트 선택)
- QuestComponent (수락 / 진행 / 완료 처리)
- QuestManager (데이터 검증 & 보상 정보 제공 )
- QuestWidget (진행 상황 UI 반영)
구현
[ QuestManager ]
게임에 존재하는 모든 퀘스트의 원본 데이터를 관리하고, 퀘스트의 수락 - 진행 - 완료 조건 및
보상 정보를 중앙에서 판단하는 시스템 관리 역할을 합니다
C++ 구현 코드
.H
#pragma once
#include "CoreMinimal.h"
#include "QuestManager.generated.h"
UENUM(BlueprintType)
enum class EQuestType : uint8
{
Kill UMETA(DisplayName = "Kill"),
Collect UMETA(DisplayName = "Collect"),
Deliver UMETA(DisplayName = "Deliver")
};
USTRUCT(BlueprintType)
struct FQuestData
{
GENERATED_BODY()
UPROPERTY()
int32 QuestID = 0;
UPROPERTY()
EQuestType questType;
UPROPERTY()
FText questTitle;
UPROPERTY()
FText questDescription;
UPROPERTY()
int32 targetNpcID = 0;
UPROPERTY()
int32 targetID = 0;
UPROPERTY()
int32 targetCount = 0;
UPROPERTY()
int32 rewardItemID = 0;
UPROPERTY()
int32 rewardGold = 0;
UPROPERTY()
int32 rewardSilver = 0;
UPROPERTY()
int32 rewardItemCount = 0;
};
USTRUCT(BlueprintType)
struct FQuestProgress
{
GENERATED_BODY()
UPROPERTY()
int32 progressQuestID = 0;
UPROPERTY()
int32 progressCurrentCount = 0;
UPROPERTY()
bool bCompleted = false;
};
UCLASS()
class PORTFOLIOMS_API UQuestManager : public UObject
{
GENERATED_BODY()
public:
UFUNCTION()
void InitializeQuestManager();
UFUNCTION()
void AddQuest(const FQuestData& questData);
const FQuestData* GetQuestData(int32 questID) const
{
return questDataMap.Find(questID);
}
public:
UPROPERTY()
TMap<int32, FQuestData> questDataMap;
};
.CPP
#include "QuestManager.h"
#define LOCTEXT_NAMESPACE "QuestManager"
void UQuestManager::InitializeQuestManager()
{
//==퀘스트 1 : 사무라이 처치 ========
{
FQuestData quest;
quest.QuestID = 1;
quest.questType = EQuestType::Kill;
quest.questTitle = LOCTEXT("Quest_KillSamurai_Title", "사무라이 처치하기");
quest.questDescription = LOCTEXT("Quest_KillSamurai_Description", "사무라이 3명을 처치하세요!");
quest.targetID = 1; //사무라이
quest.targetCount = 3;
quest.rewardGold = 500;
quest.rewardSilver = 800;
AddQuest(quest);
}
//==퀘스트 2 : 아이템 모아오기 ========
{
FQuestData quest;
quest.QuestID = 2;
quest.questType = EQuestType::Collect;
quest.questTitle = LOCTEXT("Quest_CollectCore_Title", "코어 수집");
quest.questDescription = LOCTEXT("Quest_CollectCore_Description", "적을 처치하여 코어를 2개 수집하세요!");
quest.targetID = 19; //코어
quest.targetCount = 2;
quest.rewardItemID = 3; //파워엘릭서
quest.rewardItemCount = 30;
AddQuest(quest);
}
//==퀘스트 3 : 아이템 배달하기 ========
{
FQuestData quest;
quest.QuestID = 3;
quest.questType = EQuestType::Deliver;
quest.questTitle = LOCTEXT("Quest_Delivery_Title", "소중한 물건");
quest.questDescription = LOCTEXT("Quest_Delivery_Description", "시엘에게 상자를 전달하세요!");
quest.targetID = 18; //상자
quest.targetCount = 1;
quest.rewardGold = 300;
quest.rewardSilver = 1500;
AddQuest(quest);
}
}
void UQuestManager::AddQuest(const FQuestData& questData)
{
//중복방지
if (questDataMap.Contains(questData.QuestID))
return;
questDataMap.Add(questData.QuestID, questData);
}
[ QuestComponent ]
플레이어가 현재 수락한 퀘스트를 관리하고, 퀘스트의 진행 ~ 완료 처리를 담당하는
플레이어 전용 로직 컴포넌트입니다.
C++ 구현 코드
.H
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "QuestManager.h"
#include "QuestComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnQuestProgressUpdated, int32, questID, const FQuestProgress&, progress);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnQuestCompleted, int32, questID);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnQuestAccepted,int32, questID);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnQuestDecline, int32, questID);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnQuestCompletedPush, int32, questID);
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class PORTFOLIOMS_API UQuestComponent : public UActorComponent
{
GENERATED_BODY()
public:
UQuestComponent();
virtual void BeginPlay() override;
public:
void Initialize(UQuestManager* activeQuestManager);
UFUNCTION()
void AcceptQuest(int32 questID);
UFUNCTION()
void OnEnemyKilled(int32 enemyID);
UFUNCTION()
void OnItemCollected(int32 itemID, int32 itemCount);
UFUNCTION()
void OnDeliverItem(int32 itemID, int32 npcID);
UFUNCTION()
void DeclineQuest(int32 questID);
UFUNCTION()
void CompleteQuest(int32 questID, int32 rewardItemID, int32 rewardItemCount);
UFUNCTION()
void GetReward(int32 rewardItemID, int32 rewardItemCount);
UFUNCTION()
void CompleteQuestGetReward(int32 questID);
public:
UPROPERTY()
TMap<int32, FQuestProgress> activeQuest;
UPROPERTY()
UQuestManager* questManager;
public:
UPROPERTY()
FOnQuestProgressUpdated onQuestProgressUpdated;
UPROPERTY()
FOnQuestCompleted onQuestCompleted;
UPROPERTY()
FOnQuestAccepted onQuestAccepted;
UPROPERTY()
FOnQuestDecline onQuestDecline;
UPROPERTY()
FOnQuestCompletedPush onQuestCompletedPush;
public:
const FQuestData* GetQuestData(int32 questID) const;
//helper func
public:
void EachActiveQuest(TFunction<bool(FQuestProgress&, const FQuestData*)> chcekFunc);
//cast
public:
class AMainCharacter* mainChar;
class UInventoryComponent* invenComp;
};
.CPP
#include "QuestComponent.h"
#include "InventoryComponent.h"
#include "MainCharacter.h"
#include "QuestManager.h"
UQuestComponent::UQuestComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UQuestComponent::BeginPlay()
{
Super::BeginPlay();
mainChar = Cast<AMainCharacter>(GetOwner());
if (!mainChar)
return;
invenComp = mainChar->invenComp;
}
void UQuestComponent::Initialize(UQuestManager* activeQuestManager)
{
questManager = activeQuestManager;
}
void UQuestComponent::AcceptQuest(int32 questID)
{
if (!questManager)
return;
if (activeQuest.Contains(questID))
return;
const FQuestData* questData = questManager->GetQuestData(questID);
if (!questData)
return;
FQuestProgress setProgress;
setProgress.progressQuestID = questID;
setProgress.progressCurrentCount = 0;
setProgress.bCompleted = false;
activeQuest.Add(questID, setProgress);
onQuestAccepted.Broadcast(questID);
}
void UQuestComponent::OnEnemyKilled(int32 enemyID)
{
EachActiveQuest([enemyID](FQuestProgress& progress, const FQuestData* questData)
{
if (questData->questType != EQuestType::Kill)
return false;
if (questData->targetID != enemyID)
return false;
progress.progressCurrentCount++;
if (progress.progressCurrentCount >= questData->targetCount)
progress.bCompleted = true;
return true;
});
}
void UQuestComponent::OnItemCollected(int32 itemID, int32 itemCount)
{
EachActiveQuest([itemID, itemCount](FQuestProgress& progress, const FQuestData* questData)
{
if (questData->questType != EQuestType::Collect)
return false;
if (questData->targetID != itemID)
return false;
progress.progressCurrentCount += itemCount;
if (progress.progressCurrentCount >= questData->targetCount)
progress.bCompleted = true;
return true;
});
}
void UQuestComponent::OnDeliverItem(int32 itemID, int32 npcID)
{
EachActiveQuest([itemID, npcID](FQuestProgress& progress, const FQuestData* questData)
{
if (questData->questType != EQuestType::Deliver)
return false;
if (questData->targetID != itemID)
return false;
if (questData->targetNpcID != npcID)
return false;
progress.progressCurrentCount++;
if (progress.progressCurrentCount >= questData->targetCount)
progress.bCompleted = true;
return true;
});
}
void UQuestComponent::DeclineQuest(int32 questID)
{
if (!activeQuest.Contains(questID))
return;
activeQuest.Remove(questID);
onQuestDecline.Broadcast(questID);
}
void UQuestComponent::CompleteQuest(int32 questID, int32 rewardItemID, int32 rewardItemCount)
{
if (!questManager)
return;
if (!activeQuest.Contains(questID))
return;
FQuestProgress progress = activeQuest[questID];
if (!progress.bCompleted)
return;
const FQuestData* questData = questManager->GetQuestData(questID);
if (!questData)
return;
onQuestCompleted.Broadcast(questID);
}
void UQuestComponent::GetReward(int32 rewardItemID, int32 rewardItemCount)
{
if (!invenComp)
return;
invenComp->AddItem(rewardItemID, rewardItemCount);
}
void UQuestComponent::CompleteQuestGetReward(int32 questID)
{
if (!questManager || !invenComp)
return;
const FQuestData* questData = questManager->GetQuestData(questID);
if (!questData)
return;
// 보상 지급
if (questData->rewardItemID > 0 && questData->rewardItemCount > 0)
invenComp->AddItem(questData->rewardItemID, questData->rewardItemCount);
if (questData->rewardGold > 0)
invenComp->AddGold(questData->rewardGold);
if (questData->rewardSilver > 0)
invenComp->AddSilver(questData->rewardSilver);
onQuestCompletedPush.Broadcast(questID);
}
const FQuestData* UQuestComponent::GetQuestData(int32 questID) const
{
return questManager->GetQuestData(questID);
}
void UQuestComponent::EachActiveQuest(TFunction<bool(FQuestProgress&, const FQuestData*)> chcekFunc)
{
if (!questManager)
return;
for (auto& questElem : activeQuest)
{
FQuestProgress& progress = questElem.Value;
const FQuestData* questData =
questManager->GetQuestData(progress.progressQuestID);
if (!questData || progress.bCompleted)
continue;
const bool bChanged = chcekFunc(progress, questData);
if (bChanged)
{
onQuestProgressUpdated.Broadcast(progress.progressQuestID, progress);
if (progress.bCompleted)
onQuestCompleted.Broadcast(progress.progressQuestID);
}
}
}
[ ShopQuestWidget ]
NPC와 상호작용 시 상점 기능과 퀘스트 수락 - 진행 - 완료 를 통합적으로 처리하는 UI 컨트롤러 위젯
사용하는 버튼이 많아서 각 버튼마다 개별 함수를 바인딩하지않고
TMap 자료구조를 사용하여 버튼과 데이터 (QuestID, ItemID 등)을 매핑해 관리하였습니다.
C++ 구현 코드
.CPP
void UShopQuestWidget::NativeConstruct()
{
Super::NativeConstruct();
mainCon = Cast<AMainCharacterController>(GetOwningPlayer());
ownerPawn = Cast<APawn>(GetOwningPlayerPawn());
mainChar = Cast<AMainCharacter>(GetOwningPlayerPawn());
questComp = mainChar->FindComponentByClass<UQuestComponent>();
if(ownerPawn)
inventoryComp = ownerPawn->FindComponentByClass<UInventoryComponent>();
WidgetSwitcher_Main->SetActiveWidget(Overlay_Main);
Button_MainExit->OnClicked.AddDynamic(this, &UShopQuestWidget::ClickedButtonMainExit);
Button_MainUseShop->OnClicked.AddDynamic(this, &UShopQuestWidget::ClickedButtonMainUseShop);
Button_MainQuest->OnClicked.AddDynamic(this, &UShopQuestWidget::ClickedButtonMainQuest);
//shop
Button_ShopPotion->OnClicked.AddDynamic(this, &UShopQuestWidget::ClickButtonShopPotion);
Button_ShopMaterial->OnClicked.AddDynamic(this, &UShopQuestWidget::ClickButtonShopMaterial);
Button_ShopUpgrade->OnClicked.AddDynamic(this, &UShopQuestWidget::ClickButtonShopUpgrade);
Button_ShopTrade->OnClicked.AddDynamic(this, &UShopQuestWidget::ClickButtonShopTrade);
//quest
Overlay_QuestDisplay->SetVisibility(ESlateVisibility::Collapsed);
Button_Quest1->OnClicked.AddDynamic(this, &UShopQuestWidget::OnClickQuest1);
Button_Quest2->OnClicked.AddDynamic(this, &UShopQuestWidget::OnClickQuest2);
Button_Quest3->OnClicked.AddDynamic(this, &UShopQuestWidget::OnClickQuest3);
InsertHoverEvent();
BindHoverEvents();
InsertShopBuyButtonID();
InitBuyCountTextBoxe();
InitShopCountButton();
InsertQuestProgressButtons();
inventoryComp->onCurrencyUpdated.AddDynamic(this, &UShopQuestWidget::UpdateCurrencyText);
questComp->onQuestProgressUpdated.AddDynamic(this, &UShopQuestWidget::OnQuestProgressUpdated);
WidgetSwitcher_ShopSelect->SetActiveWidget(Overlay_ShopPotion);
SetQuestProgressMapInsert();
//StartTyping(firstConversation);
}
void UShopQuestWidget::ClickedButtonMainExit()
{
if (!mainCon)
return;
InitBuyCountTextBoxe();
WidgetSwitcher_Main->SetActiveWidget(Overlay_Main);
WidgetSwitcher_ShopSelect->SetActiveWidget(Overlay_ShopPotion);
mainCon->invenWidget->SetVisibility(ESlateVisibility::Collapsed);
Overlay_QuestDisplay->SetVisibility(ESlateVisibility::Collapsed);
mainCon->overlappingNpc->EndInteraction(mainCon);
}
void UShopQuestWidget::ClickedButtonMainUseShop()
{
onTypingShopFinishedCallBack = [this]()
{
InitBuyCountTextBoxe();
WidgetSwitcher_Main->SetActiveWidget(Overlay_Shop);
mainCon->invenWidget->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
};
StartTyping(shopInsertConversation);
}
void UShopQuestWidget::ClickedButtonMainQuest()
{
StartTyping(questStarConversation);
mainCon->invenWidget->SetVisibility(ESlateVisibility::Collapsed);
Overlay_QuestDisplay->SetVisibility(ESlateVisibility::Collapsed);
WidgetSwitcher_Main->SetActiveWidget(Overlay_Quest);
}
void UShopQuestWidget::InsertHoverEvent()
{
buttonHoverAnimSettings.Empty();
buttonHoverAnimSettings.Add(FButtonHoverAnimSettings(Button_MainUseShop,MainHoverUseShopAnim,MainUnHoverUseShopAnim));
buttonHoverAnimSettings.Add(FButtonHoverAnimSettings(Button_MainQuest,MainHoverQuestAnim,MainUnHoverQuestAnim));
buttonHoverAnimSettings.Add(FButtonHoverAnimSettings(Button_MainExit,MainHoverExitAnim,MainUnHoverExitAnim));
}
void UShopQuestWidget::BindHoverEvents()
{
for (FButtonHoverAnimSettings& hoverAnimSettings : buttonHoverAnimSettings)
{
if (!hoverAnimSettings.button)
continue;
hoverAnimSettings.button->OnHovered.AddDynamic(this, &UShopQuestWidget::OnMainButtonHovered);
hoverAnimSettings.button->OnUnhovered.AddDynamic(this, &UShopQuestWidget::OnMainButtonUnHovered);
}
}
void UShopQuestWidget::OnMainButtonHovered()
{
for (const FButtonHoverAnimSettings& hoveredSetting : buttonHoverAnimSettings)
{
if (hoveredSetting.button && hoveredSetting.button->IsHovered())
{
if (currentHoveredMainButton == hoveredSetting.button)
return;
if (currentHoveredMainButton)
{
for (const FButtonHoverAnimSettings& prevSetting : buttonHoverAnimSettings)
{
if (prevSetting.button == currentHoveredMainButton && prevSetting.unHoverAnim)
{
StopAnimation(prevSetting.hoverAnim);
StopAnimation(prevSetting.unHoverAnim);
PlayAnimation(prevSetting.unHoverAnim);
break;
}
}
}
currentHoveredMainButton = hoveredSetting.button;
if (hoveredSetting.hoverAnim)
{
StopAnimation(hoveredSetting.unHoverAnim);
StopAnimation(hoveredSetting.hoverAnim);
PlayAnimation(hoveredSetting.hoverAnim);
}
return;
}
}
}
void UShopQuestWidget::OnMainButtonUnHovered()
{
if (!currentHoveredMainButton)
return;
for (const FButtonHoverAnimSettings& unHoveredSetting : buttonHoverAnimSettings)
{
if (unHoveredSetting.button == currentHoveredMainButton && unHoveredSetting.unHoverAnim)
{
StopAnimation(unHoveredSetting.hoverAnim);
StopAnimation(unHoveredSetting.unHoverAnim);
PlayAnimation(unHoveredSetting.unHoverAnim);
break;
}
}
currentHoveredMainButton = nullptr;
}
void UShopQuestWidget::StartTyping(const FText& sayText)
{
GetWorld()->GetTimerManager().ClearTimer(th_typingMainHandle);
fullConversationText = sayText;
fullConversationString = sayText.ToString();
CurrentTypingString.Empty();
typingIdx = 0;
TextBlock_MainConversation->SetText(FText::GetEmpty());
GetWorld()->GetTimerManager().SetTimer(th_typingMainHandle,this,&UShopQuestWidget::TypingTick,typingInterval,true);
}
void UShopQuestWidget::TypingTick()
{
if (typingIdx >= fullConversationString.Len())
{
// 타이핑 종료
GetWorld()->GetTimerManager().ClearTimer(th_typingMainHandle);
if (onTypingShopFinishedCallBack)
{
onTypingShopFinishedCallBack();
onTypingShopFinishedCallBack = nullptr;
}
return;
}
// 한 글자씩 추가하기
CurrentTypingString.AppendChar(fullConversationString[typingIdx]);
typingIdx++;
TextBlock_MainConversation->SetText(FText::FromString(CurrentTypingString));
}
void UShopQuestWidget::RandomNoMoneyConversation()
{
TArray<FText> randomConversations = { noMoneyConversation1, noMoneyConversation2 };
int32 randomIdx = FMath::RandRange(0, randomConversations.Num() - 1);
StartTyping(randomConversations[randomIdx]);
}
void UShopQuestWidget::RandomBuyConversation()
{
TArray<FText> randomConversations = { buyItemConversation1, buyItemConversation2 };
int32 randomIdx = FMath::RandRange(0, randomConversations.Num() - 1);
StartTyping(randomConversations[randomIdx]);
}
void UShopQuestWidget::ClickButtonShopPotion()
{
WidgetSwitcher_ShopSelect->SetActiveWidget(Overlay_ShopPotion);
}
void UShopQuestWidget::ClickButtonShopMaterial()
{
WidgetSwitcher_ShopSelect->SetActiveWidget(Overlay_ShopMaterial);
}
void UShopQuestWidget::ClickButtonShopUpgrade()
{
WidgetSwitcher_ShopSelect->SetActiveWidget(Overlay_ShopUpgrade);
}
void UShopQuestWidget::ClickButtonShopTrade()
{
WidgetSwitcher_ShopSelect->SetActiveWidget(Overlay_ShopTrade);
}
void UShopQuestWidget::UpdateCurrencyText()
{
TextBlock_ShopCurrentGold->SetText(FText::AsNumber(inventoryComp->currentGold));
TextBlock_ShopCurrentSilver->SetText(FText::AsNumber(inventoryComp->currentSilver));
}
void UShopQuestWidget::InsertShopBuyButtonID()
{
shopBuyButtonMap.Empty();
shopBuyButtonMap.Add(Button_ShopBuyID0, { 0, 50, EShopCurrencyType::Silver, EditableTextBox_ShopBuyID0 });
shopBuyButtonMap.Add(Button_ShopBuyID1, { 1, 80, EShopCurrencyType::Silver, EditableTextBox_ShopBuyID1 });
shopBuyButtonMap.Add(Button_ShopBuyID2, { 2, 5, EShopCurrencyType::Gold, EditableTextBox_ShopBuyID2 });
shopBuyButtonMap.Add(Button_ShopBuyID3, { 3, 8, EShopCurrencyType::Gold, EditableTextBox_ShopBuyID3 });
shopBuyButtonMap.Add(Button_ShopBuyID4, { 4, 12, EShopCurrencyType::Gold, EditableTextBox_ShopBuyID4 });
shopBuyButtonMap.Add(Button_ShopBuyID5, { 5, 16, EShopCurrencyType::Gold, EditableTextBox_ShopBuyID5 });
shopBuyButtonMap.Add(Button_ShopBuyID6, { 6, 25, EShopCurrencyType::Gold, EditableTextBox_ShopBuyID6 });
shopBuyButtonMap.Add(Button_ShopBuyID7, { 7, 50, EShopCurrencyType::Gold, EditableTextBox_ShopBuyID7 });
shopBuyButtonMap.Add(Button_ShopBuyID8, { 8, 25, EShopCurrencyType::Gold, EditableTextBox_ShopBuyID8 });
shopBuyButtonMap.Add(Button_ShopBuyID9, { 9, 50, EShopCurrencyType::Gold, EditableTextBox_ShopBuyID9 });
shopBuyButtonMap.Add(Button_ShopBuyID10, { 10,800, EShopCurrencyType::Silver, EditableTextBox_ShopBuyID10 });
shopBuyButtonMap.Add(Button_ShopBuyID11, { 11,1500,EShopCurrencyType::Silver, EditableTextBox_ShopBuyID11 });
shopBuyButtonMap.Add(Button_ShopBuyID12, { 12,1500,EShopCurrencyType::Silver, EditableTextBox_ShopBuyID12 });
shopBuyButtonMap.Add(Button_ShopBuyID13, { 13,12, EShopCurrencyType::Gold, EditableTextBox_ShopBuyID13 });
shopBuyButtonMap.Add(Button_ShopBuyID14, { 14,45, EShopCurrencyType::Gold, EditableTextBox_ShopBuyID14 });
shopBuyButtonMap.Add(Button_ShopBuyID15, { 15,300, EShopCurrencyType::Silver, EditableTextBox_ShopBuyID15 });
for (const TPair<UButton*, FShopBuyInfo>& buyPair : shopBuyButtonMap)
{
if (buyPair.Key)
buyPair.Key->OnClicked.AddDynamic(this, &UShopQuestWidget::OnClickShopBuyButton);
}
}
void UShopQuestWidget::OnClickShopBuyButton()
{
if (!inventoryComp)
return;
for (const TPair<UButton*, FShopBuyInfo>& shopBuyPair : shopBuyButtonMap)
{
UButton* buyButton = shopBuyPair.Key;
const FShopBuyInfo& buyInfo = shopBuyPair.Value;
if (buyButton->IsHovered())
{
int32 buyCount = GetBuyCount(buyInfo.countTextBox);
int32 totalPrice = buyInfo.itemPrice * buyCount;
if (buyInfo.currencyType == EShopCurrencyType::Gold)
{
if (inventoryComp->currentGold < totalPrice)
{
RandomNoMoneyConversation();
shopNpc->PlayShopReaction(EShopNpcReaction::NoMoney);
return;
}
inventoryComp->SubstractGold(totalPrice);
}
else if (buyInfo.currencyType == EShopCurrencyType::Silver)
{
if (inventoryComp->currentSilver < totalPrice)
{
RandomNoMoneyConversation();
shopNpc->PlayShopReaction(EShopNpcReaction::NoMoney);
return;
}
inventoryComp->SubstactSilver(totalPrice);
}
inventoryComp->AddItem(buyInfo.itemID, buyCount);
if (buyInfo.countTextBox)
buyInfo.countTextBox->SetText(FText::AsNumber(1));
shopNpc->PlayShopReaction(EShopNpcReaction::BuySuccess);
RandomBuyConversation();
return;
}
}
}
void UShopQuestWidget::InitBuyCountTextBoxe()
{
for (TPair<UButton*, FShopBuyInfo>& countPair : shopBuyButtonMap)
{
if (countPair.Value.countTextBox)
{
countPair.Value.countTextBox->SetText(FText::AsNumber(1));
}
}
}
int32 UShopQuestWidget::GetBuyCount(UEditableTextBox* textBox)
{
if (!textBox)
return 1;
FString inputNum = textBox->GetText().ToString();
if (inputNum.IsEmpty())
return 1;
for (TCHAR& Char : inputNum)
{
if (!FChar::IsDigit(Char))
return 1;
}
int32 count = FCString::Atoi(*inputNum);
return FMath::Max(count, 1);
}
void UShopQuestWidget::InitShopCountButton()
{
shopCountButtonMap.Empty();
//0
shopCountButtonMap.Add(Button_ID0Left, EditableTextBox_ShopBuyID0);
shopCountButtonMap.Add(Button_ID0Right, EditableTextBox_ShopBuyID0);
//1
shopCountButtonMap.Add(Button_ID1Left, EditableTextBox_ShopBuyID1);
shopCountButtonMap.Add(Button_ID1Right, EditableTextBox_ShopBuyID1);
//2
shopCountButtonMap.Add(Button_ID2Left, EditableTextBox_ShopBuyID2);
shopCountButtonMap.Add(Button_ID2Right, EditableTextBox_ShopBuyID2);
//3
shopCountButtonMap.Add(Button_ID3Left, EditableTextBox_ShopBuyID3);
shopCountButtonMap.Add(Button_ID3Right, EditableTextBox_ShopBuyID3);
//4
shopCountButtonMap.Add(Button_ID4Left, EditableTextBox_ShopBuyID4);
shopCountButtonMap.Add(Button_ID4Right, EditableTextBox_ShopBuyID4);
//5
shopCountButtonMap.Add(Button_ID5Left, EditableTextBox_ShopBuyID5);
shopCountButtonMap.Add(Button_ID5Right, EditableTextBox_ShopBuyID5);
//6
shopCountButtonMap.Add(Button_ID6Left, EditableTextBox_ShopBuyID6);
shopCountButtonMap.Add(Button_ID6Right, EditableTextBox_ShopBuyID6);
//7
shopCountButtonMap.Add(Button_ID7Left, EditableTextBox_ShopBuyID7);
shopCountButtonMap.Add(Button_ID7Right, EditableTextBox_ShopBuyID7);
//8
shopCountButtonMap.Add(Button_ID8Left, EditableTextBox_ShopBuyID8);
shopCountButtonMap.Add(Button_ID8Right, EditableTextBox_ShopBuyID8);
//9
shopCountButtonMap.Add(Button_ID9Left, EditableTextBox_ShopBuyID9);
shopCountButtonMap.Add(Button_ID9Right, EditableTextBox_ShopBuyID9);
//10
shopCountButtonMap.Add(Button_ID10Left, EditableTextBox_ShopBuyID10);
shopCountButtonMap.Add(Button_ID10Right, EditableTextBox_ShopBuyID10);
//11
shopCountButtonMap.Add(Button_ID11Left, EditableTextBox_ShopBuyID11);
shopCountButtonMap.Add(Button_ID11Right, EditableTextBox_ShopBuyID11);
//12
shopCountButtonMap.Add(Button_ID12Left, EditableTextBox_ShopBuyID12);
shopCountButtonMap.Add(Button_ID12Right, EditableTextBox_ShopBuyID12);
//13
shopCountButtonMap.Add(Button_ID13Left, EditableTextBox_ShopBuyID13);
shopCountButtonMap.Add(Button_ID13Right, EditableTextBox_ShopBuyID13);
//14
shopCountButtonMap.Add(Button_ID14Left, EditableTextBox_ShopBuyID14);
shopCountButtonMap.Add(Button_ID14Right, EditableTextBox_ShopBuyID14);
//15
shopCountButtonMap.Add(Button_ID15Left, EditableTextBox_ShopBuyID15);
shopCountButtonMap.Add(Button_ID15Right, EditableTextBox_ShopBuyID15);
for (auto& countButtonPairs : shopCountButtonMap)
{
if (countButtonPairs.Key)
countButtonPairs.Key->OnClicked.AddDynamic(this, &UShopQuestWidget::OnClickedShopCountButton);
}
}
void UShopQuestWidget::OnClickedShopCountButton()
{
for (auto& shopCountPair : shopCountButtonMap)
{
UButton* button = shopCountPair.Key;
UEditableTextBox* textBox = shopCountPair.Value;
if (!button->IsHovered())
continue;
int32 currentValue = GetBuyCount(textBox);
if (button->GetName().Contains(TEXT("Left")))
currentValue--;
else if (button->GetName().Contains(TEXT("Right")))
currentValue++;
currentValue = FMath::Clamp(currentValue, 1, 999);
textBox->SetText(FText::AsNumber(currentValue));
return;
}
}
void UShopQuestWidget::SetShopNpc(AShopQuestNPC* npc)
{
shopNpc = npc;
}
void UShopQuestWidget::OnClickQuest1()
{
Overlay_QuestDisplay->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
WidgetSwitcher_QuestSelect->SetActiveWidget(Overlay_Quest1);
}
void UShopQuestWidget::OnClickQuest2()
{
Overlay_QuestDisplay->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
WidgetSwitcher_QuestSelect->SetActiveWidget(Overlay_Quest2);
}
void UShopQuestWidget::OnClickQuest3()
{
Overlay_QuestDisplay->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
WidgetSwitcher_QuestSelect->SetActiveWidget(Overlay_Quest3);
}
void UShopQuestWidget::InsertQuestProgressButtons()
{
// === Accept ===
questAcceptButtonMap.Add(Button_Quest1Accept, 1);
questAcceptButtonMap.Add(Button_Quest2Accept, 2);
questAcceptButtonMap.Add(Button_Quest3Accept, 3);
// === Decline ===
questDeclineButtonMap.Add(Button_Quest1Decline, 1);
questDeclineButtonMap.Add(Button_Quest2Decline, 2);
questDeclineButtonMap.Add(Button_Quest3Decline, 3);
// === Complete ===
questCompleteButtonMap.Add(Button_Quest1Complete, 1);
questCompleteButtonMap.Add(Button_Quest2Complete, 2);
questCompleteButtonMap.Add(Button_Quest3Complete, 3);
BindQuestButtons();
}
void UShopQuestWidget::BindQuestButtons()
{
for (auto& Elem : questAcceptButtonMap)
{
if (Elem.Key)
Elem.Key->OnClicked.AddDynamic(this, &UShopQuestWidget::OnQuestAcceptClicked);
}
for (auto& Elem : questDeclineButtonMap)
{
if (Elem.Key)
Elem.Key->OnClicked.AddDynamic(this, &UShopQuestWidget::OnQuestDeclineClicked);
}
for (auto& Elem : questCompleteButtonMap)
{
if (Elem.Key)
Elem.Key->OnClicked.AddDynamic(this, &UShopQuestWidget::OnQuestCompleteClicked);
}
}
void UShopQuestWidget::OnQuestAcceptClicked()
{
if (!questComp)
return;
for (auto& mapElem : questAcceptButtonMap)
{
if (mapElem.Key && mapElem.Key->IsHovered())
{
int32 questID = mapElem.Value;
questComp->AcceptQuest(questID);
StartTyping(questAcceptConversation);
SetQuestProgressUI(questID, "InProgress");
return;
}
}
}
void UShopQuestWidget::OnQuestDeclineClicked()
{
if (!questComp)
return;
for (auto& mapElem : questDeclineButtonMap)
{
if (mapElem.Key && mapElem.Key->IsHovered())
{
int32 questID = mapElem.Value;
questComp->DeclineQuest(questID);
StartTyping(questDeclineConversation);
SetQuestProgressUI(questID, "Available");
return;
}
}
}
void UShopQuestWidget::OnQuestCompleteClicked()
{
if (!questComp)
return;
for (auto& mapElem : questCompleteButtonMap)
{
if (mapElem.Key && mapElem.Key->IsHovered())
{
int32 questID = mapElem.Value;
questComp->CompleteQuestGetReward(questID);
StartTyping(questCompleteConversation);
SetQuestProgressUI(questID, "Complete");
mapElem.Key->SetVisibility(ESlateVisibility::Collapsed);
return;
}
}
}
void UShopQuestWidget::SetQuestProgressMapInsert()
{
// Quest 1
questProgressUIMap.Add(1, FQuestProgressUI(
WidgetSwitcher_Quest1Progress,
Overlay_Quest1ProgressAvailable, Overlay_Quest1ProgressInPorgress, Overlay_Quest1ProgressComplete
));
// Quest 2
questProgressUIMap.Add(2, FQuestProgressUI(
WidgetSwitcher_Quest2Progress, Overlay_Quest2ProgressAvailable, Overlay_Quest2ProgressInPorgress, Overlay_Quest2ProgressComplete
));
// Quest 3
questProgressUIMap.Add(3, FQuestProgressUI(
WidgetSwitcher_Quest3Progress, Overlay_Quest3ProgressAvailable, Overlay_Quest3ProgressInPorgress, Overlay_Quest3ProgressComplete
));
}
void UShopQuestWidget::SetQuestProgressUI(int32 questID, FString state)
{
if (!questProgressUIMap.Contains(questID))
return;
FQuestProgressUI& progressUI = questProgressUIMap[questID];
if (!progressUI.switcher)
return;
if (state == "Available")
progressUI.switcher->SetActiveWidget(progressUI.available);
else if (state == "InProgress")
progressUI.switcher->SetActiveWidget(progressUI.inProgress);
else if (state == "Complete")
progressUI.switcher->SetActiveWidget(progressUI.complete);
}
void UShopQuestWidget::OnQuestProgressUpdated(int32 questID, const FQuestProgress& progress)
{
if (!questProgressUIMap.Contains(questID))
return;
if (progress.bCompleted)
SetQuestProgressUI(questID, "Complete");
else
SetQuestProgressUI(questID, "InProgress");
}
[ QuestWidgetSlot ]
QuestWidgetSlot 은 퀘스트 하나를 UI로 표현하는 최소 단위 역할을 하는 위젯입니다
위젯은 미션이름, 미션내용, 현재진행도 / 전체진행도 로 구성되어있습니다.

C++ 구현 코드
.CPP
void UQuestWidgetSlot::NativeConstruct()
{
Super::NativeConstruct();
}
void UQuestWidgetSlot::SetQuestData(const FText& questName, const FText& questDescription, int32 current, int32 goal, bool bCompleted)
{
TextBlock_QuestName->SetText(questName);
TextBlock_QuestDescription->SetText(questDescription);
TextBlock_Current->SetText(FText::AsNumber(current));
TextBlock_Goal->SetText(FText::AsNumber(goal));
UpdateQuestImage(bCompleted);
}
void UQuestWidgetSlot::UpdateQuestImage(bool bCompleted)
{
if (bCompleted)
Image_QuestCheck->SetBrush(completeBrush);
else
Image_QuestCheck->SetBrush(progressBrush);
}
void UQuestWidgetSlot::UpdateProgress(int32 current, bool bCompleted)
{
TextBlock_Current->SetText(FText::AsNumber(current));
UpdateQuestImage(bCompleted);
if (bCompleted)
PlayAnimation(Complete, 0.f, 1, EUMGSequencePlayMode::Forward, 3.f);
}
결과
원하는 퀘스트를 골라서 수락할수 있습니다

퀘스트를 수락하면 좌측상단에 퀘스트가 추가됩니다


퀘스트가 진행중일때 포기하기 를 누르면 퀘스트를 포기할수 있습니다

퀘스트를 진행하면 진행도가 변경되고 진행도가 채워지게되면 퀘스트 완료창으로 변경됩니다


퀘스트진행도가 꽉 차게되면 퀘스트창은 완료창으로 넘어가며 완료버튼을 누르면
퀘스트매니저에 등록된 보상을 받을수 있습니다


영상
'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글
| Unreal - 가이드 마커 (0) | 2025.12.22 |
|---|---|
| Unreal - 대화 Npc (0) | 2025.12.22 |
| Unreal - 상호작용 프롬프트 (4) | 2025.12.19 |
| Unreal - 상점 Npc (1) | 2025.12.17 |
| Unreal - Loot Notification 위젯 (0) | 2025.12.14 |