04 — Подієвий прогрес¶
🇬🇧 English | 🇺🇦 Українська
Ігровий код ніколи не чіпає лічильники цілей напряму. Він повідомляє, що щось сталося, а квест-система сама розбирається, яким активним цілям це важливо.
Надсилання події¶
Універсальна точка входу — NotifyQuestEvent, доступна на компоненті-менеджері та, зручніше, як статичні функції UDemoQuestBlueprintLibrary, що приймають APlayerState* і самі маршрутизують до потрібного менеджера:
// Гравець убив бандита
UDemoQuestBlueprintLibrary::NotifyKillEvent(PlayerState, "Bandit");
// Гравець зібрав 3 яблука
UDemoQuestBlueprintLibrary::NotifyCollectEvent(PlayerState, "Apple", 3);
// Гравець досяг контрольної точки
UDemoQuestBlueprintLibrary::NotifyLocationReachedEvent(PlayerState, "ForestShrine");
// Гравець взаємодіяв з NPC
UDemoQuestBlueprintLibrary::NotifyInteractionEvent(PlayerState, "VillageElder");
// Будь-що інше (крафт, репутація, PvP...)
UDemoQuestBlueprintLibrary::NotifyCustomQuestEvent(PlayerState, "Craft", "HealthPotion", 1);
// Або, для повного контролю, загальна форма, на якій ґрунтуються всі перелічені вище:
UDemoQuestBlueprintLibrary::NotifyQuestEvent(PlayerState, EventID, TargetIdentifier, Count);
Їх безпечно викликати і з клієнтського, і з серверного коду — усередині вони за потреби самі перекидаються на сервер через RPC. Count за замовчуванням 1 і може бути більшим (наприклад, підібрали стос із 5 яблук одразу).
Канонічні EventID також доступні як константи C++, щоб не набирати рядкові літерали: DemoQuestEvents::Kill, DemoQuestEvents::Collect, DemoQuestEvents::Location, DemoQuestEvents::Interact (у Core/DemoQuestTypes.h).
Як працює зіставлення¶
Подія збігається з ціллю, коли істинні обидві умови:
Objective->ObjectiveIdentifier == TargetIdentifier(точний збігFName).- EventID сумісний з
Objective->ObjectiveType— див. таблицю в 03 — Авторинг квестів.Custom-цілі додатково вимагають, щоб EventID збігався зExpectedEventID(якщо той заданий).
Перевіряється кожна ціль у кожному поточному активному квесті, що обробляється цим менеджером. Тому одна подія може просунути одразу кілька цілей і одразу кілька квестів.
⚠ Спільний залік між квестами з однаковим ідентифікатором¶
Про це варто сказати явно, бо під час першої зустрічі це дивує: зіставлення йде лише за (ObjectiveType, ObjectiveIdentifier) — система не знає й не переймається тим, якому квесту «належить» ціль. Якщо два різні активні квести (або дві цілі всередині одного квесту) обидва визначають KillTarget / "Bandit", убивство одного бандита надсилає одну подію, і ця одна подія зараховується обом цілям.
Квест A: "Bandit Threat" — Defeat Bandits 0/5
Квест B: "Bandit Threat 2222" — Defeat Bandits 0/2
NotifyKillEvent(PlayerState, "Bandit") // одне вбивство
Квест A: Defeat Bandits 1/5 (+1)
Квест B: Defeat Bandits 1/2 (+1) <- обом зараховано від одного й того самого вбивства
Це усвідомлена поведінка, а не баг. Це той самий «спільний залік убивств», який використовує більшість ігор з одночасно активними квестами на вбивство (наприклад, два активні квести, обидва вимагають убивати вовків): у світі немає окремої копії кожного ворога під кожен квест, тому одне вбивство природним чином зараховується в усі відповідні активні квести. Якщо за вашим дизайном потрібен ексклюзивний залік (убивство має просувати лише один із кількох відповідних квестів), це усвідомлене рішення, яке ви реалізуєте самі — доведеться вирішити, який квест «перемагає», коли підходить кілька, а плагін це за вас не вирішує.
Область дії цього спільного заліку — у межах одного менеджера: власні Personal + Individual квести гравця ділять залік один з одним (вони живуть на одному й тому самому менеджері PlayerState); Shared-квести ділять залік один з одним окремо, на менеджері GameState. Personal-квест одного гравця ніколи не впливає на квести іншого гравця.
Послідовні цілі¶
Ціль з bIsSequential = true стартує як Inactive і стає Active лише після того, як завершені всі раніші обов'язкові послідовні цілі в цьому ж квесті — це дозволяє будувати багатокрокові квести («Крок 1 виконано. Тепер крок 2.») без додаткового коду. OnSequentialObjectiveActivated спрацьовує, коли розблоковується черговий крок; використовуйте це для оновлення трекера квестів.
Непослідовні цілі всі Active з моменту старту квесту й можуть виконуватися в будь-якому порядку.
Пряме встановлення прогресу¶
Більшість цілей просуваються подіями, але є дві низькорівневі функції для випадків, коли потрібен точний контроль:
// Додати до поточного значення (обмежується TargetCount)
QuestManager->UpdateObjectiveProgress(QuestData, Objective, /*Amount*/ 1);
// Установити точне значення (обмежується діапазоном [0, TargetCount])
QuestManager->SetObjectiveProgress(QuestData, Objective, /*NewProgress*/ 5);
Обидві є на UDemoQuestManagerComponent і, як і NotifyQuestEvent, автоматично маршрутизуються через сервер.
Підписка на прогрес¶
Підписуйтеся на делегати менеджера замість опитування:
| Делегат | Спрацьовує, коли |
|---|---|
OnQuestStarted(Quest) |
Квест розпочато. |
OnQuestStateChanged(Quest, NewState) |
Будь-який перехід стану. |
OnObjectiveUpdated(Quest, Objective, Progress) |
Змінився лічильник цілі. |
OnObjectiveCompleted(Quest, Objective) |
Ціль завершена (спрацьовує один раз). |
OnSequentialObjectiveActivated(Quest, Objective) |
Розблоковано послідовний крок. |
OnQuestCompleted(Quest, Rewards) |
Квест завершено — після того, як нагороди вже видані; використовуйте лише для UI/FX. |
OnQuestFailed(Quest) |
Квест провалено (вичерпався таймер або явний виклик FailQuest). |
OnQuestAvailable(Quest) |
Пререквізити квесту щойно виконалися (через CheckAndNotifyAvailableQuests). |
OnQuestTimerWarning(Quest, TimeRemaining) |
Таймерний квест перетнув власний поріг попередження (UDemoQuestData::TimerWarningThreshold). |
OnObjectiveTimerWarning(Quest, Objective, TimeRemaining) |
Таймерна ціль перетнула власний поріг попередження (UDemoQuestObjectiveData::TimerWarningThreshold) — незалежно від порога квесту. |
OnQuestEventNotified(EventID, TargetIdentifier, Count, Player, bMatchedAnyObjective) |
Кожен виклик NotifyQuestEvent/NotifyPartyQuestEvent, оброблений цим менеджером — включно з тими, що ні на що не вплинули (крім party-менеджера, коли у гравця взагалі немає активного Shared-квесту для перевірки — див. нижче). |
OnQuestEventNotified відрізняється за своєю природою від делегатів вище: це не хук геймплейної реакції, а діагностичний — він спрацьовує на кожну подію, яку бачить цей менеджер, незалежно від того, чи стало bMatchedAnyObjective істинним. Це найшвидший спосіб відповісти на питання «чому квест не просунувся?» (одрук у TargetIdentifier, не той EventType, ціль ще не Active тощо). Один виняток, лише на party-менеджері: NotifyQuestEvent безумовно форвардить туди кожну подію на випадок, якщо вона важлива для Shared-квесту, тож у гравця без активної участі в Shared-квесті інакше виходив би постійний, беззмістовний «unmatched»-запис на кожну єдину подію — натомість NotifyPartyQuestEvent у такому разі просто не транслює делегат. Оверлей модуля QuestSystemDemoDebug (клавіша J у демо) підписується на нього — разом з OnQuestStateChanged та OnObjectiveUpdated — щоб вести живу вкладку Events — див. 09 — Валідація та чит-команди.
Прив'язка до делегата в Blueprint¶
Кожен делегат вище — це звичайний член BlueprintAssignable, тож прив'язка до нього — стандартний патерн Unreal, жодної специфіки плагіна:
- Отримайте посилання на потрібний менеджер:
Get Quest Manager (Player)для власних Personal/Individual квестів гравця, абоGet Party Quest Manager (World Context)для Shared-квестів (обидві функції — наQuest Blueprint Library). - Відтягніть провід від отриманого піна посилання на компонент, почніть друкувати ім'я делегата (наприклад, On Objective Completed) і оберіть Bind Event to On Objective Completed.
- Редактор створить відповідну ноду
Custom Eventз параметрами делегата як вихідними пінами (Quest,Objective, …) — від них ведіть свою реакцію (програти звук, підсвітити пункт трекера, оновити віджет).
Widget Blueprint із живим трекером квестів, або Character/PlayerController Blueprint, що реагує на прогрес VFX/SFX, — два найчастіші місця для цього.
Порада щодо таймінгу: на мережевому клієнті PlayerState/GameState (а отже, і менеджер квестів, що на них живе) може зареплікуватися після того, як відпрацює BeginPlay, тож надто рання спроба прив'язки мовчки нічого не дасть — посилання на той момент ще null. Обгортайте прив'язку перевіркою на null і повторною спробою трохи пізніше (нода Delay, або повтор у OnRep_PlayerState), а не покладайтеся на те, що BeginPlay настане достатньо пізно.
Куди далі¶
- Розміщення NPC, дощок оголошень і скринь, які викликають ці функції за вас: 05 — Компоненти сцени
- Правила маршрутизації в мультиплеєрі — «який менеджер обробляє цю подію»: 06 — Мультиплеєр