Unreal - 퀘스트 시스템

2025. 12. 19. 22:45·Unreal 프로젝트 다이어리/두번째 프로젝트

구현내용

플레이어가 NPC 에게 퀘스트를 의뢰하고, 해당 퀘스트를 완료하면

보상을 획득하는 NPC 의뢰 기반으로 퀘스트 수락 - 진행 - 완료 - 보상 까지

이어지는  퀘스트 시스템을 구현하였습니다.

 

타르코프 퀘스트창처럼 원하는 퀘스트를 의뢰하고

목표를 이루면 보상을 받도록 설계하였습니다.

 

구현된 퀘스트는 총 3가지이며

내용은 이와 같습니다

  • 적 처치하기
  • 퀘스트 아이템 수집하기
  • 물건 전달하기

사용된 클래스

사용된 클래스 사용 목적
QuestManager(UObject) 게임 전체 퀘스트 데이터를 총괄 관리하는 중앙 관리자
(퀘스트 수락 가능 여부체크, 완료 조건, 보상 데이터 반환)
QuestComponent(액터컴포넌트) 플레이어가 보유한 퀘스트를 관리하는 컴포넌트
(현재 진행 중인 퀘스트 목록 관리, QuestManager에
조건 충족 여부 질의)
QuestWidget(위젯) 퀘스트 진행 상황을 보여주는 메인 퀘스트 UI
ShopQuestWidget(위젯) NPC(의뢰인)과 상호작용할 때 표시되는 퀘스트 UI
ShopWidgetSlot(위젯) 퀘스트 하나를 표현하는 UI 단위 슬롯

 

흐름은 이와 같습니다

  1. Npc 상호작용
  2. ShopQuestWidget (퀘스트 목록 표시)
  3. ShopWidgetSlot (퀘스트 선택)
  4. QuestComponent (수락 / 진행 / 완료 처리)
  5. QuestManager (데이터 검증 & 보상 정보 제공 )
  6. 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
'Unreal 프로젝트 다이어리/두번째 프로젝트' 카테고리의 다른 글
  • Unreal - 가이드 마커
  • Unreal - 대화 Npc
  • Unreal - 상호작용 프롬프트
  • Unreal - 상점 Npc
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)
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 링크

  • 공지사항

  • 블로그 메뉴

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

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

티스토리툴바