퀘스트 시스템
목표 기반 퀘스트 시스템 아키텍처
퀘스트 시스템 구조
Data Asset 기반 퀘스트 정의
퀘스트 시스템은 UPrimaryDataAsset을 사용하여 퀘스트를 정의하고, GameplayTag로 목표 달성을 추적합니다. 이 설계는 퀘스트 콘텐츠를 데이터로 관리하여 디자이너가 C++ 코드 없이 새 퀘스트를 생성할 수 있게 합니다.
예시: 늑대 사냥
마을 주변의 늑대를 처치하여 주민들을 보호하세요.
-
✓늑대 처치 3/5
- 늑대 가죽 수집 1/3
- 사냥꾼에게 보고
보상
퀘스트 정의
QuestDefinition 클래스 구현
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"
#include "QuestDefinition.generated.h"
// 퀘스트 목표 유형
UENUM(BlueprintType)
enum class EQuestObjectiveType : uint8
{
Kill, // 처치
Collect, // 수집
Talk, // 대화
Explore, // 탐험
Interact // 상호작용
};
// 퀘스트 목표 구조체
USTRUCT(BlueprintType)
struct FQuestObjective
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FText Description;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
EQuestObjectiveType Type = EQuestObjectiveType::Kill;
// 목표 대상 식별용 태그 (예: Enemy.Wolf)
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTag TargetTag;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
int32 RequiredCount = 1;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
bool bOptional = false;
};
// 퀘스트 보상 구조체
USTRUCT(BlueprintType)
struct FQuestReward
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
int32 ExperiencePoints = 0;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
int32 Gold = 0;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TArray<TSoftObjectPtr<UItemDefinition>> Items;
};
// 퀘스트 정의 (Primary Data Asset)
UCLASS(BlueprintType)
class MYGAME_API UQuestDefinition : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest")
FText QuestName;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest",
meta = (MultiLine = true))
FText Description;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest")
int32 RequiredLevel = 1;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest")
TArray<FQuestObjective> Objectives;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest")
FQuestReward Rewards;
// 선행 퀘스트
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest")
TArray<TSoftObjectPtr<UQuestDefinition>> PrerequisiteQuests;
// 다음 퀘스트 (체인)
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Quest")
TSoftObjectPtr<UQuestDefinition> NextQuest;
virtual FPrimaryAssetId GetPrimaryAssetId() const override
{
return FPrimaryAssetId(TEXT("Quest"), GetFName());
}
};
퀘스트 인스턴스
런타임 퀘스트 진행 상태 관리
┌─────────────────────────────────────────────────────────────────┐ │ 퀘스트 상태 다이어그램 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────┐ AcceptQuest() ┌────────────┐ │ │ │ NotStarted │──────────────────────▶│ InProgress │ │ │ └────────────┘ └─────┬──────┘ │ │ │ │ │ ┌────────────────────┼────────────────┐ │ │ │ │ │ │ │ ▼ ▼ │ │ │ ┌────────────┐ ┌────────────┐ │ │ │ │ Failed │ │ Completed │ │ │ │ └────────────┘ └─────┬──────┘ │ │ │ │ │ │ │ ▼ │ │ │ ┌────────────┐ │ │ │ │ TurnedIn │ │ │ │ └────────────┘ │ │ │ │ └─────────────────────────────────────────────────────────────────┘
// 퀘스트 상태
UENUM(BlueprintType)
enum class EQuestState : uint8
{
NotStarted,
InProgress,
Completed,
Failed,
TurnedIn
};
// 목표 진행 상황
USTRUCT(BlueprintType)
struct FQuestObjectiveProgress
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly)
int32 CurrentCount = 0;
UPROPERTY(BlueprintReadOnly)
bool bCompleted = false;
};
// 퀘스트 인스턴스
UCLASS(BlueprintType)
class MYGAME_API UQuestInstance : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly)
TSoftObjectPtr<UQuestDefinition> QuestDefinition;
UPROPERTY(BlueprintReadOnly)
EQuestState State = EQuestState::NotStarted;
UPROPERTY(BlueprintReadOnly)
TArray<FQuestObjectiveProgress> ObjectiveProgress;
void Initialize(UQuestDefinition* Definition);
bool UpdateObjective(int32 ObjectiveIndex, int32 Count);
bool IsComplete() const;
};
void UQuestInstance::Initialize(UQuestDefinition* Definition)
{
QuestDefinition = Definition;
State = EQuestState::InProgress;
// 목표별 진행 상황 초기화
ObjectiveProgress.SetNum(Definition->Objectives.Num());
for (FQuestObjectiveProgress& Progress : ObjectiveProgress)
{
Progress.CurrentCount = 0;
Progress.bCompleted = false;
}
}
bool UQuestInstance::UpdateObjective(int32 ObjectiveIndex, int32 Count)
{
if (!ObjectiveProgress.IsValidIndex(ObjectiveIndex))
return false;
UQuestDefinition* QuestDef = QuestDefinition.LoadSynchronous();
if (!QuestDef)
return false;
FQuestObjectiveProgress& Progress = ObjectiveProgress[ObjectiveIndex];
const FQuestObjective& Objective = QuestDef->Objectives[ObjectiveIndex];
Progress.CurrentCount = FMath::Min(
Progress.CurrentCount + Count,
Objective.RequiredCount);
if (Progress.CurrentCount >= Objective.RequiredCount)
{
Progress.bCompleted = true;
}
// 전체 완료 체크
if (IsComplete())
{
State = EQuestState::Completed;
}
return true;
}
bool UQuestInstance::IsComplete() const
{
UQuestDefinition* QuestDef = QuestDefinition.Get();
if (!QuestDef)
return false;
for (int32 i = 0; i < ObjectiveProgress.Num(); ++i)
{
const FQuestObjective& Objective = QuestDef->Objectives[i];
const FQuestObjectiveProgress& Progress = ObjectiveProgress[i];
// 필수 목표가 완료되지 않았으면 false
if (!Objective.bOptional && !Progress.bCompleted)
{
return false;
}
}
return true;
}
퀘스트 매니저
GameInstance Subsystem 기반 관리
UCLASS()
class MYGAME_API UQuestManagerSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
// 델리게이트
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
FOnQuestStateChanged,
UQuestInstance*, Quest,
EQuestState, NewState);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
FOnQuestObjectiveUpdated,
UQuestInstance*, Quest,
int32, ObjectiveIndex,
int32, NewCount);
UPROPERTY(BlueprintAssignable)
FOnQuestStateChanged OnQuestStateChanged;
UPROPERTY(BlueprintAssignable)
FOnQuestObjectiveUpdated OnQuestObjectiveUpdated;
// 퀘스트 수락
UFUNCTION(BlueprintCallable, Category = "Quest")
bool AcceptQuest(UQuestDefinition* QuestDef);
// 퀘스트 완료 (보상 지급)
UFUNCTION(BlueprintCallable, Category = "Quest")
bool CompleteQuest(UQuestInstance* Quest);
// 진행 상황 업데이트 (처치, 수집 시 호출)
UFUNCTION(BlueprintCallable, Category = "Quest")
void UpdateQuestProgress(
FGameplayTag TargetTag,
EQuestObjectiveType Type,
int32 Count = 1);
// 조회
UFUNCTION(BlueprintPure, Category = "Quest")
TArray<UQuestInstance*> GetActiveQuests() const;
UFUNCTION(BlueprintPure, Category = "Quest")
bool IsQuestCompleted(UQuestDefinition* QuestDef) const;
protected:
UPROPERTY()
TArray<TObjectPtr<UQuestInstance>> ActiveQuests;
UPROPERTY()
TArray<TObjectPtr<UQuestInstance>> CompletedQuests;
};
bool UQuestManagerSubsystem::AcceptQuest(UQuestDefinition* QuestDef)
{
if (!QuestDef)
return false;
// 이미 진행 중인지 확인
for (UQuestInstance* Quest : ActiveQuests)
{
if (Quest->QuestDefinition.Get() == QuestDef)
return false;
}
// 선행 퀘스트 확인
for (const TSoftObjectPtr<UQuestDefinition>& PreReq :
QuestDef->PrerequisiteQuests)
{
if (!IsQuestCompleted(PreReq.LoadSynchronous()))
return false;
}
// 퀘스트 인스턴스 생성
UQuestInstance* NewQuest = NewObject<UQuestInstance>(this);
NewQuest->Initialize(QuestDef);
ActiveQuests.Add(NewQuest);
OnQuestStateChanged.Broadcast(NewQuest, EQuestState::InProgress);
return true;
}
void UQuestManagerSubsystem::UpdateQuestProgress(
FGameplayTag TargetTag,
EQuestObjectiveType Type,
int32 Count)
{
for (UQuestInstance* Quest : ActiveQuests)
{
if (Quest->State != EQuestState::InProgress)
continue;
UQuestDefinition* QuestDef = Quest->QuestDefinition.Get();
if (!QuestDef)
continue;
// 각 목표 확인
for (int32 i = 0; i < QuestDef->Objectives.Num(); ++i)
{
const FQuestObjective& Obj = QuestDef->Objectives[i];
// 타입과 태그가 일치하는지 확인
if (Obj.Type == Type && Obj.TargetTag.MatchesTag(TargetTag))
{
if (Quest->UpdateObjective(i, Count))
{
OnQuestObjectiveUpdated.Broadcast(
Quest, i, Quest->ObjectiveProgress[i].CurrentCount);
if (Quest->State == EQuestState::Completed)
{
OnQuestStateChanged.Broadcast(
Quest, EQuestState::Completed);
}
}
}
}
}
}
몬스터가 사망할 때 UpdateQuestProgress(FGameplayTag::RequestGameplayTag("Enemy.Wolf"), EQuestObjectiveType::Kill, 1)을 호출하면 관련 퀘스트가 자동으로 업데이트됩니다.
핵심 요약
- Data Asset 기반 — UPrimaryDataAsset으로 퀘스트를 정의하여 디자이너가 에디터에서 퀘스트를 생성할 수 있습니다.
- GameplayTag 활용 — 목표 대상을 태그로 식별하여 유연한 목표 시스템을 구현합니다.
- 상태 머신 — NotStarted → InProgress → Completed → TurnedIn 상태 전이를 관리합니다.
- Subsystem 패턴 — GameInstanceSubsystem으로 전역 퀘스트 상태를 관리합니다.
- 선행 퀘스트 — PrerequisiteQuests로 퀘스트 체인을 구현합니다.
도전 과제
배운 내용을 직접 실습해보세요
FQuestData 구조체에 QuestId, QuestName, Description, Objectives(TArray
EQuestState(Unavailable, Available, Active, Completed, Failed)를 정의하고, UQuestSubsystem에서 퀘스트 상태 전환 로직을 구현하세요. AcceptQuest(), UpdateObjective(), CompleteQuest()를 BlueprintCallable로 노출하세요.
GameplayTag 기반으로 퀘스트 선행 조건(이전 퀘스트 완료, 최소 레벨)을 체크하는 시스템을 구현하세요. 월드의 트리거 볼륨, NPC 대화, 몬스터 처치 이벤트를 퀘스트 오브젝티브와 자동 연결하세요.