Skip to content

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::InteractCore/DemoQuestTypes.h).

Як працює зіставлення

Подія збігається з ціллю, коли істинні обидві умови:

  1. Objective->ObjectiveIdentifier == TargetIdentifier (точний збіг FName).
  2. 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, жодної специфіки плагіна:

  1. Отримайте посилання на потрібний менеджер: Get Quest Manager (Player) для власних Personal/Individual квестів гравця, або Get Party Quest Manager (World Context) для Shared-квестів (обидві функції — на Quest Blueprint Library).
  2. Відтягніть провід від отриманого піна посилання на компонент, почніть друкувати ім'я делегата (наприклад, On Objective Completed) і оберіть Bind Event to On Objective Completed.
  3. Редактор створить відповідну ноду Custom Event з параметрами делегата як вихідними пінами (Quest, Objective, …) — від них ведіть свою реакцію (програти звук, підсвітити пункт трекера, оновити віджет).

Widget Blueprint із живим трекером квестів, або Character/PlayerController Blueprint, що реагує на прогрес VFX/SFX, — два найчастіші місця для цього.

Порада щодо таймінгу: на мережевому клієнті PlayerState/GameState (а отже, і менеджер квестів, що на них живе) може зареплікуватися після того, як відпрацює BeginPlay, тож надто рання спроба прив'язки мовчки нічого не дасть — посилання на той момент ще null. Обгортайте прив'язку перевіркою на null і повторною спробою трохи пізніше (нода Delay, або повтор у OnRep_PlayerState), а не покладайтеся на те, що BeginPlay настане достатньо пізно.

Куди далі

  • Розміщення NPC, дощок оголошень і скринь, які викликають ці функції за вас: 05 — Компоненти сцени
  • Правила маршрутизації в мультиплеєрі — «який менеджер обробляє цю подію»: 06 — Мультиплеєр