11 — Поширені запитання¶
🇬🇧 English | 🇺🇦 Українська
Короткі відповіді на найчастіші запитання з посиланнями на розділ, де кожне розкрито докладно. Якщо щось тут суперечить розділу — правда на боці розділу; напишіть нам, і ми виправимо цю сторінку.
- Перші кроки
- Вибір режиму розподілу
- Створення квестів
- Чому не працює?
- UI та відображення
- Мультиплеєр
- Реліз і пакування
- Розширення плагіна
Перші кроки¶
Чи потрібно успадковувати PlayerState, GameState або GameMode?¶
Ні. Саме в цьому суть zero-config підходу: UQuestWorldSubsystem під час гри сам додає UQuestManagerComponent на кожен PlayerState і на GameState — з якими б класами ваш проєкт уже не працював. Підключіть плагін, увімкніть його — і API працює. Див. 02 — Інтеграція без налаштувань.
Якщо ви волієте розмістити компонент вручну (наприклад, на власному підкласі PlayerState) — будь ласка: підсистема помічає наявний компонент і не чіпає його. Автостворення можна й повністю вимкнути прапорцями bAutoCreatePlayerQuestManagers / bAutoCreatePartyQuestManager у Project Settings → Game → Quest System.
Чи потрібно писати C++?¶
Ні. Кожна функція UQuestBlueprintLibrary викликається з Blueprint, квести — це Data Assets, а світові компоненти чіпляються на будь-який Actor. Навіть видача нагород — це BlueprintNativeEvent: можна перевизначити GiveQuestRewards у Blueprint-підкласі UQuestManagerComponent і вказати на нього QuestManagerClass, не створивши жодного C++ файлу. 10 — Інтеграція з C++ існує на випадок, якщо ви хочете C++, а не тому, що він обов'язковий.
Де мають лежати ассети квестів?¶
Будь-де у вашому контенті. Пошук у рантаймі сканує Asset Registry за класом, тож структура тек — ваша справа, і переміщення ассетів нічого не ламає. Один виняток, який має значення: для зібраних білдів усе ще потрібен запис AssetManager типу «Quest» у DefaultGame.ini, що вказує на ваші справжні теки з квестами, інакше квести просто не потраплять до кука. Див. 09 — Валідація та чити.
Чи все це працює в одиночній грі?¶
Так — і не як побічний випадок. Усі три режими розподілу працюють у standalone; «груповий» квест в одиночній грі — просто група з одного гравця. Це зроблено навмисно, щоб одна й та сама карта й ті самі ассети квестів працювали в обох режимах без окремої гілки контенту. Див. 06 — Мультиплеєр.
Чи можна додати плагін до проєкту, який уже в розробці?¶
Так. Він не вимагає конкретного базового класу, конкретної системи вводу чи певної структури проєкту й не додає власного геймплею, доки ви не створите перший квест. Єдине, що варто продумати заздалегідь, — звідки саме ваш геймплейний код повідомлятиме про події; див. 04 — Подієвий прогрес.
Вибір режиму розподілу¶
Personal, Shared, Individual — що обрати?¶
- Personal — одиночний квест. Прогрес живе в одного гравця. Беріть його, якщо немає причин для іншого.
- Shared — один спільний пул прогресу на всю групу. Десять бандитів на всіх, а не кожному. Внесок кожного гравця відстежується окремо — для нагород чи таблиці лідерів.
- Individual — кожен отримує власну копію цілей, і квест вважається виконаним «для групи» лише тоді, коли свою частину закінчили всі учасники. Десять бандитів кожному.
Повна модель, зокрема те, де фізично лежать дані кожного режиму, — у 01 — Основні поняття.
Чи може той самий квест бути Personal для одного гравця й Shared для іншого?¶
Ні — QuestSharingMode є властивістю ассета квесту, тож він однаковий для всіх. Якщо потрібні обидві поведінки, зробіть два ассети.
Який режим брати, якщо гра суто одиночна?¶
Personal. Два інші існують заради координації кількох гравців: з одним гравцем Individual поводиться як Personal, але з зайвим обліком, а Shared без жодної користі переносить прогрес на менеджер GameState.
Створення квестів¶
Як зробити ланцюжок, де один квест відкриває наступний?¶
Додайте попередній квест до PrerequisiteQuests наступного. Після цього CanStartQuest вважатиме наступний недоступним, доки попередній не завершено. Плагін перевіряє весь граф на циклічні залежності — у редакторі під час збереження, на старті світу в development-білдах і на вимогу через QuestValidate. Див. 09 — Валідація та чити.
Як зробити, щоб цілі виконувались у чіткому порядку?¶
Увімкніть bIsSequential на тих цілях, що мають чекати своєї черги. Послідовна ціль лишається неактивною, доки не завершено попередню, а прогрес приймає лише активна ціль — саме це й дає сценарій «прийди сюди, а потім убий того». Див. 03 — Створення квестів.
Як зробити ціль, яку гравець може пропустити?¶
bIsOptional. Необов'язкові цілі ніколи не блокують завершення й ніколи не провалюють квест — зокрема й тоді, коли в необов'язкової таймерної цілі вичерпався час: провалиться лише сама ціль, а квест триватиме далі. Див. 03 — Створення квестів.
Моєму квесту потрібне те, чого вбудовані типи цілей не покривають.¶
Візьміть EQuestObjectiveType::Custom разом з ExpectedEventID і повідомляйте про подію через NotifyCustomQuestEvent. Це покриває «скрафти 3 мечі», «виграй 2 перегони», «протримайся 5 хвиль» — усе, що ваша гра здатна відстежити й назвати. Див. 04 — Подієвий прогрес.
Чи можна причепити власні дані до квесту, не змінюючи плагін?¶
Так — CustomData (TMap<FName, FString>) є і в ассеті квесту, і в ассеті цілі саме для цього. Набір властивостей навмисно тримають компактним і не прив'язаним до жанру; CustomData разом із довільним тегом QuestCategory — це передбачений запасний вихід, замість того щоб плагін обростав окремим полем під потреби кожної гри.
Квест має завершуватися сам чи коли гравець його здасть?¶
Це вирішує bAutoComplete на квесті. Лишіть вимкненим — і квест чекатиме в стані «готовий до здачі», доки щось не викличе CompleteQuest, зазвичай UQuestReceiverComponent на NPC-приймальнику. Вмикайте для квестів «зробив і забув», де кроку здачі немає.
Чому не працює?¶
Мій Shared-квест ніколи не показується доступним/активним/завершеним. І жодної помилки.¶
Це найчастіша помилка з цим плагіном, і мовчить вона за задумом, а не випадково. Прогрес Shared-квесту живе на менеджері GameState, а не гравця — тож GetQuestManager(Player) повертає менеджер, який про цей квест ніколи й не чув, і кожен запит ввічливо відповідає «ні».
Використовуйте GetQuestManagerForQuest(Player, Quest), який сам маршрутизує за режимом розподілу, або перевіряйте обидва менеджери, якщо збираєте список. Див. 08 — Довідник Blueprint-бібліотеки.
Я повідомляю про подію — і нічого не відбувається.¶
Пройдіться цим списком; перші чотири пункти покривають майже всі випадки:
- Чи квест справді активний? Події просувають лише активні квести.
- Чи збігається
TargetIdentifierзObjectiveIdentifier? Найчастіші винуватці — друкарські помилки та зайві пробіли. А от регістр — ні: цеFName, а порівнянняFNameне зважає на регістр, тож"bandit"і"Bandit"збігаються. Не марнуйте час на пошук різниці у великих/малих літерах. - Чи відповідає тип події типу цілі?
KillпросуваєKillTarget,Collect—CollectItemі так далі. Ціль типуCustomдодатково має збігтися за своїмExpectedEventID. - Чи ціль активна, а не чекає своєї черги? Ціль із
bIsSequentialігнорує прогрес, доки не завершено попередню. - Чи це не Shared-квест, який перевіряють не на тому менеджері? Див. попереднє запитання.
Найшвидший спосіб зрозуміти, що саме з цього, — відкрити вкладку Events у debug-оверлеї (консольна команда QuestOverlay). Вона показує кожну подію в момент надходження й підсвічує ті, що не збіглися ні з чим — питання «чому мій квест не просунувся» перетворюється на двосекундний погляд на екран. Див. 09 — Валідація та чити.
Одне вбивство просунуло одразу два різні квести. Це баг?¶
Ні, так і задумано. Зіставлення йде за типом цілі та ідентифікатором і не має поняття «якому квесту належало це вбивство» — тож якщо два активні квести гравця чекають на Kill/Bandit, один бандит зарахується обом. Це поведінка в дусі MMO, якої більшість проєктів і хоче; коли вона не потрібна — дайте цілям різні ідентифікатори. Див. 04 — Подієвий прогрес.
Гравець провалив квест і тепер не може почати його знову.¶
Це навмисно: провалений квест лишається у списку провалених, і CanStartQuest відмовляє йому, доки щось не викличе QuestReset для цього квесту. Інакше провалений квест мовчки активувався б повторно, залишаючись позначеним як провалений. Викликайте QuestReset (або однойменну консольну команду) там, де ваша гра дозволяє другу спробу.
У редакторі все працює, а в зібраному білді квестів немає.¶
Майже завжди це кук: ассети квестів, на які ніщо не посилається напряму, не потраплять у білд, якщо тип primary asset «Quest» в AssetManager (DefaultGame.ini) не вказує на теки, де вони насправді лежать. Пошуку в рантаймі цей запис не потрібен — а куку потрібен. Див. 09 — Валідація та чити.
Чи треба якось обережно викликати функції квестів із клієнта?¶
Ні. Кожен виклик, що змінює стан, сам перевіряє авторитет і перекидається на сервер — зокрема й для Shared-квестів. Варто пам'ятати лише одне: на клієнті ці функції повертають true оптимістично, щойно запит надіслано, тож будуйте UI на делегатах або подальшій перевірці стану, а не на цьому значенні. Див. 06 — Мультиплеєр.
UI та відображення¶
Чи є готовий журнал квестів або HUD, який можна взяти в реліз?¶
Ні, і це навмисно. Плагін постачає debug-оверлей — щільний інструмент розробника з усіма квестами, фільтрами, живою стрічкою подій і кнопками примусового завершення, — створений для діагностики контенту, а не для гравців. Ваш квестовий UI — це стиль і відчуття вашої гри, тож плагін дає дані й не лізе далі.
Будуйте свій на UQuestBlueprintLibrary (GetActiveQuests, GetQuestProgress, GetObjectiveProgressText, …) і підписуйтесь на делегати менеджера — OnQuestStateChanged, OnObjectiveUpdated, OnQuestCompleted, OnQuestTimerWarning тощо, — щоб оновлюватися за зміною, а не опитуванням.
Як показати в журналі цілі завершеного квесту, а не просто «виконано»?¶
Завершення квесту відкидає його живий прогрес, але плагін саме для цього зберігає знімок по кожній цілі. GetObjectiveProgress непомітно відкочується до нього, а GetFinishedQuestObjectives(Quest) віддає весь запис — тож екран історії може показати 3/3 і те, які необов'язкові цілі було справді виконано, замість 0/3. Див. 07 — Серіалізація.
Чи локалізуються тексти квестів?¶
Так — QuestName, QuestDescription, QuestSummary та тексти цілей мають тип FText, тож проходять звичайним конвеєром локалізації Unreal, як і будь-який інший текст. Ідентифікатори (ObjectiveIdentifier, QuestID, ID подій) — це FName і вони не призначені для показу; ніколи їх не локалізуйте, це ключі зіставлення.
Чи можна показати зворотний відлік для таймерного квесту?¶
GetQuestTimeRemaining / GetQuestTimeRemainingText — для дедлайну самого квесту, GetObjectiveTimeRemaining / ...Text — для незалежного таймера окремої цілі. Обидва повертають 0 (або порожній текст), коли жоден таймер не працює, тож прив'язувати їх без додаткових перевірок безпечно.
Мультиплеєр¶
Чи реплікуються квестові дані одного гравця іншим гравцям?¶
Так — масиви квестів реплікуються кожному спостерігачеві актора-хоста, а не лише клієнту-власнику. Це наслідок того, що один клас компонента виконує дві ролі: на GameState, у якого немає власника, він зберігає колективний Shared-прогрес, а це унеможливлює суцільну реплікацію «лише власнику». Вважайте записи інших гравців побічними й доступними лише для читання. Якщо потрібна сувора прив'язка до власника — це означає розділення компонента на два підкласи. Див. 06 — Мультиплеєр.
Що буде, якщо гравець приєднається вже після старту групового квесту?¶
Якщо у квесту ввімкнено bAutoEnrollNewParticipants (типово так), підсистема зарахує його автоматично, щойно з'явиться його PlayerState — drop-in кооп без жодного додаткового коду. Вимкніть прапорець для квестів, які мають лишатися закритими для тих, хто був на старті.
Чи можна вимагати справжню групу, щоб квест не можна було пройти соло?¶
Виставте MinPartySize на ассеті квесту. MaxPartySize обмежує з іншого боку. Обидва перевіряються під час старту групового квесту.
Що стається з груповим квестом, коли гравець виходить?¶
Видалення учасника прибирає його з ростера, а для Individual-квесту ще й провалює його власну копію. Якщо це був останній учасник, Shared-квест провалюється автоматично, а не висить без жодного виконавця.
Чи відновлюється прогрес квестів, коли гравець перепідключається?¶
Лише те, що ви збережете й відновите самі — див. запитання про серіалізацію нижче. Ростери груп навмисно не відбудовуються автоматично: хто саме вважається «тією самою групою» після розриву зв'язку — рішення геймдизайну, а не те, що плагін має вгадувати. Внески окремих гравців при цьому зберігаються. Див. 07 — Серіалізація.
Реліз і пакування¶
Як зберігати й завантажувати прогрес квестів?¶
ExportState() віддає звичайну структуру зі шляхами ассетів і прогресом, ImportState() повертає її назад. Це зроблено так, щоб її можна було вкласти у ваш USaveGame поруч з усім іншим, що ви зберігаєте, а не щоб плагін володів власним форматом збереження, який довелося б обходити. Див. 07 — Серіалізація.
Чи потраплять debug-оверлей або чит-команди в мій реліз-білд?¶
Ні. Чит-команди реєструються лише в не-Shipping білдах, а суто редакторські можливості оверлея (наприклад, «Open Asset») узагалі не компілюються в ігрові таргети. Якщо ви взагалі не хочете бачити інструменти розробника у своєму білді — усе це живе в окремому модулі QuestSystemDebug, від якого можна просто не залежати: базова логіка квестів на нього не спирається.
Чи є щось чутливе до продуктивності, про що варто знати?¶
Дві речі, обидві незначні за звичайних обсягів контенту:
- Таймерні квести реплікують свій відлік. Квест або ціль із таймером щотика оновлює залишок часу на сервері й реплікує його. Для кількох таймерних квестів це нормально; якщо плануєте багато одночасно — дешевше рахувати відлік на клієнті від часу старту.
- Пошук квестів сканує Asset Registry за класом. Це прохід по ассетах квестів, а не по всьому вашому контенту, і не робота щокадру — але результат варто кешувати, а не викликати
LoadAllQuestsу тіку.
Квести без таймерів не роблять жодної роботи щотика: менеджер сам вимикає власний тік, коли активних таймерних квестів немає.
Розширення плагіна¶
Як власне видати гравцеві нагороди?¶
Перевизначте GiveQuestRewards — він викликається рівно один раз на квест, із захистом від дублювання. Найчистіше місце — підклас UQuestManagerComponent (Blueprint чи C++), указаний як QuestManagerClass у Project Settings: тоді він діє всюди, не зачіпаючи жодного класу фреймворку. Не робіть видачу нагород у OnQuestCompleted: цей делегат спрацьовує після нагород і не має захисту від дублювання — він для UI, звуку й аналітики. Див. 10 — Інтеграція з C++.
Як зробити квестовою ціллю довільний об'єкт у світі?¶
Додайте на нього UQuestTargetComponent — на будь-який актор, жодного спеціального базового класу. Компонент поєднує подію, про яку треба повідомити, з тригером (перетин, шкода, взаємодія, знищення або ваш власний виклик) і реакцією (спрацювати раз, відроджуватись, повторюватись, сховати/знищити, заспавнити ефект). Додайте ще й компонент-маркер — і актор показуватиме маркер лише тоді, коли відповідна ціль справді активна. Див. 05 — Світові компоненти.
Чи можна використати власні візуали маркерів замість вбудованих?¶
Так. UQuestBlueprintMarkerComponent спавнить будь-який клас актора, який ви йому дасте — зробіть його Blueprint'ом із будь-яким мешем, віджетом, анімацією чи Niagara-ефектом. Реалізуйте на цьому акторі IQuestMarkerVisualInterface — і йому додатково повідомлятимуть про зміну стану маркера, тож він зможе реагувати, а не лише з'являтися й зникати. На одному акторі можуть співіснувати кілька компонентів-маркерів, якщо потрібно більше ніж один візуал. Див. 05 — Світові компоненти.
Звідки найкраще повідомляти про події?¶
Звідти, де ваша гра вже знає, що подія сталася, — з обробника шкоди, який дійшов висновку, що ворог загинув; з підібраного предмета; з тригер-об'єму, у який зайшли. Викликайте NotifyKillEvent / NotifyCollectEvent тощо з PlayerState гравця й дайте плагіну самому розібратися, яким квестам це цікаво. Квестовому коду не місце у ваших геймплейних системах, а геймплейний код не має напряму колупати стан квестів. Див. 04 — Подієвий прогрес.
Куди далі¶
- Контент усе ще поводиться дивно? 09 — Валідація та чити повністю описує debug-оверлей і консольні команди.
- Розширення на C++: 10 — Інтеграція з C++.