03 — Авторинг квестів¶
🇬🇧 English | 🇺🇦 Українська
Квести й цілі — це UPrimaryDataAsset. Створюйте їх у Content Browser: Add → Miscellaneous → Data Asset, потім оберіть клас QuestObjectiveData або QuestData.
Спочатку створіть цілі, потім пошліться на них із квесту.
UQuestObjectiveData¶
| Властивість | Тип | Значення |
|---|---|---|
ObjectiveName |
FText |
Коротка відображувана назва ("Defeat the Bandit Leader"). |
ObjectiveDescription |
FText |
Довший опис для UI журналу квестів. |
ObjectiveType |
EQuestObjectiveType |
Яка дія завершує цю ціль — див. нижче. |
TargetCount |
int32 (≥ 1) |
Скільки разів має відбутися дія. |
bIsOptional |
bool |
Необов'язкові цілі не блокують завершення квесту. |
bIsSequential |
bool |
Якщо true, ця ціль стає активною лише після завершення попередніх послідовних цілей у цьому ж квесті. Див. 04. |
ObjectiveIdentifier |
FName |
Значення, з яким мають збігтися ігрові події (наприклад, "Bandit", "HealthPotion"). |
ExpectedEventID |
FName |
Використовується лише за ObjectiveType == Custom. Див. нижче. |
TimeLimit |
float секунд (за замовчуванням 0) |
0 = без ліміту. Незалежний дедлайн на цей крок, окремий від TimeLimit квесту. Див. «Таймерні квести» нижче. |
TimerWarningThreshold |
float секунд (за замовчуванням 0) |
0 = без попередження. Скільки секунд має залишитися, щоб спрацював OnObjectiveTimerWarning (один раз). Має сенс лише за TimeLimit > 0. |
CustomData |
TMap<FName, FString> |
Довільні пари ключ/значення для вашої власної логіки. |
ObjectiveIcon |
UTexture2D* |
Необов'язкова іконка для UI. |
Типи цілей і відповідна їм подія¶
ObjectiveType |
Збігається з EventID |
|---|---|
KillTarget |
"Kill" |
CollectItem |
"Collect" |
ReachLocation |
"Location" |
InteractWith |
"Interact" |
Custom |
Те, що вказано в ExpectedEventID (або будь-яка подія, якщо ExpectedEventID не заданий) |
Механіку зіставлення докладно розібрано в 04 — Подієвий прогрес — там же пояснюється важливий нюанс: дві різні цілі (в одному квесті або в різних квестах), що мають однакову пару (ObjectiveType, ObjectiveIdentifier), обидві отримають залік від однієї й тієї самої відповідної події. Це усвідомлена поведінка спільного заліку, а не баг — прочитайте той розділ, перш ніж покладатися на глобальну унікальність ідентифікаторів.
Custom-цілі¶
Custom-цілі існують для геймплею, який не вписується в Kill/Collect/Location/Interact — крафт, набір репутації, перемоги в PvP, будівництво тощо. ExpectedEventID звужує, яку категорію подій ціль приймає, тож Custom-ціль з ідентифікатором "HealthPotion" та ExpectedEventID = "Craft" реагує лише на подію крафту, а не на Collect-подію з тим самим ідентифікатором:
// Ціль: ObjectiveType = Custom, ObjectiveIdentifier = "HealthPotion", ExpectedEventID = "Craft"
UQuestBlueprintLibrary::NotifyCustomQuestEvent(PlayerState, "Craft", "HealthPotion", 1); // зарахується
UQuestBlueprintLibrary::NotifyCollectEvent(PlayerState, "HealthPotion", 1); // проігнорується
Якщо ExpectedEventID залишити порожнім, ціль прийме будь-який EventID з відповідним ідентифікатором — максимум гнучкості, мінімум захисту від випадкових збігів.
UQuestData¶
| Властивість | Тип | Значення |
|---|---|---|
QuestID |
FName |
Стабільний ідентифікатор, який використовують FindQuestByID і дані збереження. Обов'язково заповніть — див. 09. |
QuestName |
FText |
Відображувана назва. |
QuestDescription |
FText |
Повний наративний текст. |
QuestSummary |
FText |
Стислий опис для списку в журналі квестів. |
QuestCategory |
FName |
Довільний тег-фільтр ("MainStory", "SideQuest", …). |
Objectives |
TArray<UQuestObjectiveData*> |
Цілі, з яких складається квест. |
PrerequisiteQuests |
TArray<UQuestData*> |
Квести, які мають бути завершені раніше. Автоматично перевіряються на цикли — див. 09. |
CustomData |
TMap<FName, FString> |
Довільні метадані (ID NPC, що видає квест, тег сюжетної лінії, …). |
Rewards |
FQuestReward |
Див. нижче. |
QuestIcon |
UTexture2D* |
Іконка для UI. |
QuestGiverDisplayName |
FText |
Лише інформаційно. |
bCanAbandon |
bool (за замовчуванням true) |
Чи може гравець вручну скасувати квест. |
bAutoComplete |
bool (за замовчуванням false) |
true: завершується миттєво, щойно виконані всі обов'язкові цілі. false: гравець має активно здати квест (див. 05 — Компоненти сцени, UQuestReceiverComponent). |
TimeLimit |
float, секунди (за замовчуванням 0) |
0 = без обмеження. Див. «Таймерні квести» нижче. |
TimerWarningThreshold |
float, секунди (за замовчуванням 0) |
0 = без попередження. Скільки секунд має залишитися, щоб спрацював OnQuestTimerWarning (один раз). Має сенс лише за TimeLimit > 0. |
QuestSharingMode |
EQuestSharingMode |
Personal / Shared / Individual — див. 01. |
bAutoEnrollNewParticipants |
bool (за замовчуванням true) |
Лише для Shared/Individual — див. 06. |
MinPartySize / MaxPartySize |
int32 |
Обмеження розміру групи (Shared/Individual). 0 означає «без обмеження» для MaxPartySize; MinPartySize, що дорівнює 0 або 1, дозволяє соло-старт. |
FQuestReward¶
USTRUCT(BlueprintType)
struct FQuestReward
{
TArray<TSubclassOf<AActor>> ItemRewards;
TMap<FName, int32> RewardAmounts; // наприклад, "Gold" -> 50, "Carrots" -> 12, "ExperiencePoints" -> 100
};
Навмисно не має вбудованого поняття валюти, досвіду чи будь-якого іншого іменованого ресурсу — що взагалі таке «нагорода» (золото? морква? репутація? очки навички?) цілком визначається проєктом. Два дженерик-канали покривають будь-яку форму нагороди: RewardAmounts — для всього, що виражається іменованим числом, ItemRewards — для всього, що є дискретним заспавненим/виданим актором.
FQuestReward — це лише дані, нічого не витрачається автоматично, і плагін ніяк не читає та не інтерпретує ключі RewardAmounts сам. Реальну видачу реалізуйте в GiveQuestRewards(), маршрутизуючи кожен ключ туди, де він щось означає у вашій грі; див. 02 — Інтеграція без налаштування.
Auto-complete проти здачі квесту¶
bAutoComplete = true: квест завершується (і спрацьовують нагороди) одразу, щойно виконана остання обов'язкова ціль. Добре підходить для простих квестів на збір/убивство.bAutoComplete = false: квест стає готовим до здачі (IsQuestReadyToTurnIn()повернеtrue), але лишаєтьсяActive, доки щось не викличеCompleteQuest()— як правило,UQuestReceiverComponentна NPC чи скрині. Добре підходить для квестів із наративною розв'язкою в конкретному місці.
Таймерні квести¶
Установіть TimeLimit у додатне число секунд. Після старту UQuestManagerComponent кожен тік відлічує TimeRemaining для цього квесту. Автоматично відбуваються дві речі:
OnQuestTimerWarning(Quest, TimeRemaining)спрацьовує один раз, коли залишковий час падає доTimerWarningThreshold(0= без попередження, задається для кожного квесту — єдиного проєктного значення за замовчуванням більше немає) — використовуйте для миготливого попередження в UI.- Якщо таймер дійде до нуля раніше, ніж квест завершиться, квест автоматично провалюється (
FailQuestвикликається за вас).
Квест DQ_TimedDelivery на демо-карті (Content/Demo/DataAssets/Quests/) — робочий приклад: TimeLimit = 45, bAutoComplete = true, одна ціль ReachLocation.
Таймерні цілі¶
Цілі теж можуть нести власні TimeLimit/TimerWarningThreshold, повністю незалежні від квестових — квест загалом може бути без обмеження за часом і при цьому мати один 30-секундний крок «протримайся на місці», і навпаки. Відлік іде, лише поки конкретно ця ціль Active, і починається заново щоразу, коли ціль активується (зокрема й пізніша послідовна).
Що відбувається, коли таймер цілі доходить до нуля, залежить від bIsOptional:
- Обов'язкова ціль: весь квест провалюється (
FailQuest), точно так само, як при вичерпанні квестовогоTimeLimit. - Необов'язкова ціль: лише вона позначається як
Failed— квест триває і все ще може завершитися, щойно виконані обов'язкові цілі (те саме правилоbIsOptional, що вже виключає її з перевірки завершення).
OnObjectiveTimerWarning(Quest, Objective, TimeRemaining) — аналог OnQuestTimerWarning на рівні цілі, див. 04 — Подієвий прогрес.
Залишковий час і квесту, і будь-якої таймерної цілі видно наживо у вкладці Quests оверлея QuestSystemDebug — див. 09 — Валідація та чит-команди.
Пререквізити та валідація¶
Будь-який квест може перелічити інші квести в PrerequisiteQuests; CanStartQuest() відмовиться його запускати, доки не завершений кожен пререквізит. Граф залежностей автоматично перевіряється в редакторі (під час редагування та збереження) на предмет циклів — повний набір інструментів валідації й налагодження див. у 09 — Валідація та чити.
Пререквізити вільно перетинають режими шарингу (наприклад, Personal-квест може вимагати Shared-квест) — завершеність перевіряється на тому менеджері, який реально володіє конкретно цим пререквізитом, а не сліпо на менеджері перевірюваного квесту, тож завершення Shared-пререквізиту (відстежуване на party/GameState-менеджері) коректно розпізнається під час перевірки Personal-квесту. Один напрямок плагін не розв'язує сам: Shared-квест, що вимагає Personal/Individual-пререквізит — питання «чиє особисте завершення має зарахуватися для всієї групи» це рішення геймдизайну, яке плагін лишає вам, тому такий пререквізит перевіряється за власними завершеними квестами party-менеджера (до яких персональний квест ніколи не приєднується) і ніколи не буде виконаний автоматично.
Квести демо-сцени¶
Content/Demo/DataAssets/{Quests,Objectives}/ містить робочий, ігровий набір квестів, що задіює все описане вище — конкретний орієнтир на додачу до таблиць властивостей. QuestID/ObjectiveIdentifier — стабільні функціональні ключі (за ними працюють FindQuestByID і зіставлення ігрових подій відповідно); імена ассетів і відображувані Name — косметика, їх можна вільно перейменовувати, не чіпаючи ці поля.
Квести¶
| Ассет | QuestID |
Назва | Категорія | Режим доступу | Цілі |
|---|---|---|---|---|---|
DQ_AlchemistApprentice |
Quest_AlchemistApprentice |
Alchemist's Apprentice | Crafting | Personal | DO_CraftHealthPotions, DO_CraftFungi, DO_CraftManaPotion |
DQ_BanditThreat |
Quest_BanditThreat |
Bandit Threat | MainStory | Personal | DO_KillBandits |
DQ_CrystalWarmup |
Quest_CrystalWarmup |
Crystal Warmup | DemoV2 | Personal — пререквізит DQ_TimedDelivery |
DO_CollectCrystals |
DQ_HerbGathering |
Quest_HerbGathering |
Healing Herbs | — | Personal | DO_CollectHerbs |
DQ_IntroQuest |
Quest_Intro |
A Troubled Village | MainStory | Personal | DO_TalkToElder |
DQ_PartyHarvest |
Quest_PartyHarvest |
Party Harvest | DemoV2 | Shared | DO_HarvestBerries |
DQ_Pilgrimage |
Quest_Pilgrimage |
Pilgrimage | DemoV2 | Individual | DO_ReachShrine |
DQ_TargetPractice |
Quest_TargetPractice |
Target Practice | DemoV2 | Personal — пререквізит DQ_CrystalWarmup, ручна здача |
DO_KillDummies |
DQ_TimedDelivery |
Quest_TimedDelivery |
Timed Delivery | DemoV2 | Personal — TimeLimit = 45с |
DO_ReachDelivery |
DQ_TreasureHunt |
Quest_TreasureHunt |
The Treasure Hunt | — | Personal — послідовний, ручна здача | DO_FindMap → DO_DecodeMap → DO_LocateTreasure |
DQ_UrgentDelivery |
Quest_UrgentDelivery |
Urgent Delivery | — | Personal — TimeLimit = 25с |
DO_DeliverMedicine, DO_LocateChild |
Цілі¶
| Ассет | ObjectiveIdentifier |
Назва | Тип | Ціль |
|---|---|---|---|---|
DO_CollectCrystals |
EnergyCrystal |
Collect Energy Crystals | CollectItem | 3 |
DO_CollectHerbs |
MagicalHerb |
Collect Magical Herbs | CollectItem | 4 |
DO_CraftFungi |
Fungi |
Craft Fungi | Custom (ExpectedEventID="Craft") |
2 |
DO_CraftHealthPotions |
HealthPotion |
Craft Health Potions | Custom (ExpectedEventID="Craft") |
3 |
DO_CraftManaPotion |
ManaPotion |
Craft Mana Potion | Custom (ExpectedEventID="Craft") |
1 |
DO_DecodeMap |
Decoder |
Decode the Map | InteractWith | 1 |
DO_DeliverMedicine |
SickChild |
Deliver Medicine | CollectItem | 2 |
DO_FindMap |
AncientMap |
Find the Ancient Map | CollectItem | 1 |
DO_HarvestBerries |
PartyBerry |
Harvest Party Berries | CollectItem | 6 |
DO_KillBandits |
Bandit |
Defeat Bandits | KillTarget | 5 |
DO_KillDummies |
TrainingDummy |
Destroy Training Dummies | KillTarget | 3 |
DO_LocateChild |
ChildLocation |
Find the Sick Child | ReachLocation | 1 |
DO_LocateTreasure |
TreasureLocation |
Locate the Treasure | ReachLocation | 1 |
DO_ReachDelivery |
DeliveryPoint |
Reach the Delivery Point | ReachLocation | 1 |
DO_ReachShrine |
AncientShrine |
Reach the Ancient Shrine | ReachLocation | 1 |
DO_TalkToElder |
VillageElder |
Speak with the Elder | InteractWith | 1 |
Куди далі¶
- Підключення ігрових подій до цих цілей: 04 — Подієвий прогрес
- Розміщення акторів, що видають квести, на рівні: 05 — Компоненти сцени