PART 5 · 강의 4/7

퀘스트 시스템

목표 기반 퀘스트 시스템 아키텍처

01

퀘스트 시스템 구조

Data Asset 기반 퀘스트 정의

퀘스트 시스템은 UPrimaryDataAsset을 사용하여 퀘스트를 정의하고, GameplayTag로 목표 달성을 추적합니다. 이 설계는 퀘스트 콘텐츠를 데이터로 관리하여 디자이너가 C++ 코드 없이 새 퀘스트를 생성할 수 있게 합니다.

예시: 늑대 사냥

마을 주변의 늑대를 처치하여 주민들을 보호하세요.

  • 늑대 처치 3/5
  • 늑대 가죽 수집 1/3
  • 사냥꾼에게 보고
보상
EXP 500
💰 Gold 100
🗡️ 사냥꾼의 단검
02

퀘스트 정의

QuestDefinition 클래스 구현

QuestDefinition.h #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()); } };
03

퀘스트 인스턴스

런타임 퀘스트 진행 상태 관리

┌─────────────────────────────────────────────────────────────────┐
│                     퀘스트 상태 다이어그램                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│    ┌────────────┐     AcceptQuest()     ┌────────────┐          │
│    │ NotStarted │──────────────────────▶│ InProgress │          │
│    └────────────┘                       └─────┬──────┘          │
│                                               │                  │
│                          ┌────────────────────┼────────────────┐ │
│                          │                    │                │ │
│                          ▼                    ▼                │ │
│                   ┌────────────┐       ┌────────────┐          │ │
│                   │   Failed   │       │ Completed  │          │ │
│                   └────────────┘       └─────┬──────┘          │ │
│                                              │                  │ │
│                                              ▼                  │ │
│                                       ┌────────────┐           │ │
│                                       │  TurnedIn  │           │ │
│                                       └────────────┘           │ │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
QuestInstance.h // 퀘스트 상태 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; };
QuestInstance.cpp 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; }
04

퀘스트 매니저

GameInstance Subsystem 기반 관리

QuestManagerSubsystem.h 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; };
QuestManagerSubsystem.cpp 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)을 호출하면 관련 퀘스트가 자동으로 업데이트됩니다.

SUMMARY

핵심 요약

  • Data Asset 기반 — UPrimaryDataAsset으로 퀘스트를 정의하여 디자이너가 에디터에서 퀘스트를 생성할 수 있습니다.
  • GameplayTag 활용 — 목표 대상을 태그로 식별하여 유연한 목표 시스템을 구현합니다.
  • 상태 머신 — NotStarted → InProgress → Completed → TurnedIn 상태 전이를 관리합니다.
  • Subsystem 패턴 — GameInstanceSubsystem으로 전역 퀘스트 상태를 관리합니다.
  • 선행 퀘스트 — PrerequisiteQuests로 퀘스트 체인을 구현합니다.
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: 퀘스트 데이터 구조 설계

FQuestData 구조체에 QuestId, QuestName, Description, Objectives(TArray), Rewards를 정의하세요. FQuestObjective에는 Type(Kill/Collect/Talk), TargetId, RequiredCount, CurrentCount를 포함하세요.

실습 2: 퀘스트 상태 머신 구현

EQuestState(Unavailable, Available, Active, Completed, Failed)를 정의하고, UQuestSubsystem에서 퀘스트 상태 전환 로직을 구현하세요. AcceptQuest(), UpdateObjective(), CompleteQuest()를 BlueprintCallable로 노출하세요.

심화 과제: 퀘스트 트리거 및 조건 시스템

GameplayTag 기반으로 퀘스트 선행 조건(이전 퀘스트 완료, 최소 레벨)을 체크하는 시스템을 구현하세요. 월드의 트리거 볼륨, NPC 대화, 몬스터 처치 이벤트를 퀘스트 오브젝티브와 자동 연결하세요.