05 — Операції з інвентарем¶
🇬🇧 English | 🇺🇦 Українська
Усе, що можна зробити з інвентарем: видати, забрати, перекласти, використати, впорядкувати. Плюс фільтри, вага й події для UI.
Одне правило на всі операції¶
Кожен метод, що змінює стан, називається Try* і викликається звідусіль — з
клієнта чи з сервера, з Blueprint чи з C++:
Inventory->TryAddItem(Definition, 3, Remainder);
Ви ніколи не пишете HasAuthority() і не обираєте між «серверною» та «клієнтською»
версією виклику. Чому це працює — 09 — Мультиплеєр.
Що повертає
Try*на клієнті.trueозначає «запит надіслано», а не «успішно виконано» — остаточне рішення завжди за сервером. Тому інтерфейс будують на подіях, а не на поверненому значенні. Див. події нижче.
Видати предмет¶
int32 Remainder = 0;
const bool bAdded = Inventory->TryAddItem(PotionDef, 5, Remainder);
if (Remainder > 0)
{
// Влізло не все - показати «Інвентар повний» або кинути решту на землю.
}
Спершу поповнюються наявні неповні стеки, потім відкриваються нові слоти. Часткова
вдача — нормальний результат, а не помилка: Remainder каже, скільки не влізло.
Дізнатися заздалегідь, чи влізе¶
const int32 WillFit = Inventory->CanAcceptItemCount(PotionDef, 5);
Чесна відповідь із урахуванням фільтрів, місця в неповних стеках, вільних слотів і ліміту ваги. Саме її варто показувати як попередній перегляд підбирання чи розмір покупки в крамниці.
Забрати предмет¶
Із конкретного слота — коли гравець діяв на видимий йому слот:
Inventory->TryRemoveItemFromSlot(SlotIndex, 1);
За типом, не дбаючи про слоти — коли витрачаються матеріали:
int32 Remainder = 0;
Inventory->TryRemoveItem(IronOreDef, 10, Remainder);
Друга форма охоплює скільки завгодно стеків і спорожнює найменші першими — так залишки консолідуються, замість того щоб розсипатися по інвентарю.
Спершу перевірте, потім витрачайте. Часткове списання, яке потім «не вдалося», залишить гравця без матеріалів і без результату:
if (Inventory->HasItem(IronOreDef, 10)) // все або нічого { Inventory->TryRemoveItem(IronOreDef, 10, Remainder); CraftSword(); }Або скористайтеся
TakeItemFromіз бібліотеки — вона робить цю перевірку сама.
Перекласти між інвентарями¶
Це найважливіше місце в усьому розділі, бо напрямок виклику має значення.
// Забрати зі скрині (лутання)
PlayerInventory->TryTransferFrom(ChestInventory, SlotIndex);
// Покласти у скриню
PlayerInventory->TryTransferTo(ChestInventory, SlotIndex);
Обидва виклики робляться на інвентарі гравця, а не скрині. Причина суто мережева: скриня у світі не має мережевого з'єднання, тож запит клієнта через неї не дійде до сервера (09 — Мультиплеєр).
У Blueprint є готовий вузол, що обирає напрямок сам:
Move Item Between Actors (From Actor, To Actor, From Slot)
Підняти предмет з підлоги¶
Той самий принцип, ще один приклад: AISDemoItemPickup (предмет, що лежить у світі —
07 — Лут і предмети у світі) теж не має власного
з'єднання, тож підбирання викликається так само — на інвентарі гравця:
PlayerInventory->TryCollect(Pickup);
Не Pickup->TryCollect(...). Різниця не косметична: варіант на пікапі — суто
серверний виклик, який з клієнта просто нічого не зробить. Варіант на інвентарі —
звичайний Try*, як і решта цієї сторінки: викликається звідки завгодно, і сам
вирішує, локально виконати запит чи переслати на сервер.
Швидке перекладання¶
Поведінка shift-click, коли гравець не обирає слот призначення:
PlayerInventory->TryQuickMoveTo(ChestInventory, SlotIndex);
Спершу зливається у відповідні неповні стеки, потім займає перший вільний слот. Те, що не влізло, лишається на місці — повна ціль просто перекладе менше, а не знищить предмети.
Перетягування всередині інвентаря¶
Inventory->TrySwapSlots(SlotA, SlotB); // основа drag-and-drop
Inventory->TrySplitStack(SlotIndex, 4); // відділити 4 копії в новий слот
TrySwapSlots об'єднує стеки, якщо предмети сумісні, і працює, коли один зі
слотів порожній — тобто заодно є операцією «перемістити сюди».
Поділ зберігає стан обох половин: половина пощербленого стеку лишається пощербленою.
Використати предмет¶
const EISDemoItemUseResult Result = Inventory->TryUseItem(SlotIndex, GetPawn());
Що станеться — вирішують фрагменти: зілля витратиться, смолоскип втратить міцність,
камінь відповість NotUsable.
| Результат | Значення |
|---|---|
Success |
використано |
NoItem |
слот порожній |
NotUsable |
жоден фрагмент не робить предмет використовуваним |
Blocked |
фрагмент заборонив (зламане, немає зарядів, кулдаун) |
Останні три — нормальні відповіді. Показуйте їх гравцеві:
Get Use Result Text (Result) дає готовий текст.
Instigator (другий аргумент) варто передавати — саме йому фрагменти застосовують
ефекти.
Впорядкування¶
Inventory->CompactStacks(); // об'єднати неповні стеки
Inventory->SortInventory(EISDemoSortMode::ByName); // впорядкувати й ущільнити
CompactStacks м'якший: він зливає розсипані стеки одного предмета, не чіпаючи
решту. Рюкзак із 3+7+12 стрілами матиме 22 стріли в найранішому з цих слотів, а все
інше лишиться там, де було.
SortInventory переставляє все й пакує від слота 0. Режими: ByName,
ByStackCount, ByType, ByWeight.
Сортування помітно рухає предмети під відкритим UI. Викликайте його з явної дії гравця (кнопка «Сортувати»), а не автоматично.
Фільтри: що інвентар взагалі приймає¶
| Поле | Дія |
|---|---|
AllowedItemTags |
якщо не порожнє — предмет має мати хоча б один із цих тегів |
BlockedItemTags |
предмет із будь-яким із цих тегів відхиляється |
BlockedItemTags перевіряється першим і завжди виграє. Це дозволяє дозволити
широку категорію і вирізати з неї винятки:
Крамниця:
AllowedItemTags: Item.Type.Weapon ← торгуємо зброєю
BlockedItemTags: Item.Property.QuestItem ← але не квестовою
Перевірити наперед: CanAcceptItem(Definition).
Обмеження слотів¶
Один інвентар може мати спеціалізовані ділянки — без другого компонента:
SlotRestrictions:
└── [0]: FirstSlot 0, LastSlot 3, RequiredTags: Item.Type.Ammo
Слоти 0–3 стають патронним поясом на початку звичайного рюкзака. Слоти, не покриті жодним правилом, приймають те, що дозволяють загальні фільтри.
Перевірити конкретний слот: CanSlotAcceptItem(SlotIndex, Definition) — саме цей
виклик має робити drag-and-drop UI при наведенні, щоб показати правильний курсор.
Вага¶
| Поле | Дія |
|---|---|
MaxWeight |
ліміт; 0 = вага не враховується |
Вага одиниці береться з фрагмента Weight. Предмети без нього нічого не важать і
ніколи не відхиляються за вагою.
Слоти й вага — незалежні ліміти: будь-який із них може стати вузьким місцем.
const float Current = Inventory->GetCurrentWeight(); // для смужки навантаження
Вага рахується й показується навіть тоді, коли MaxWeight = 0, — просто не
обмежує. Саме тому інспектор пише 5.50 weight carried (no limit set): число
чесне, ліміту немає.
Як правильно задавати місткість¶
Звичайний випадок — у Details-панелі компонента. Відкрийте Blueprint персонажа
чи скрині, виберіть компонент Inventory і задайте MaxSlots та MaxWeight там. Це
значення потрапляє в дефолти класу, тож і сервер, і клієнти отримують його однаково,
без жодної реплікації.
Зміна під час гри — просто присвоєння, але на сервері.
// Апгрейд рюкзака. Виконувати там, де є авторитет.
if (Inventory->HasContainerAuthority())
{
Inventory->MaxSlots = 40;
Inventory->MaxWeight = 120.f;
}
Обидва поля реплікуються, тож клієнти побачать нову місткість, а їхній UI перемалюється. Присвоєння на клієнті нічого доброго не дасть: наступне оновлення з сервера перезапише його — рівно як із будь-яким реплікованим полем.
Чого не існує: глобального дефолта ваги в Project Settings. Там є тільки
DefaultInventorySlots для автостворюваних інвентарів; вага в них завжди 0, доки ви
не задасте її самі.
На що звернути увагу. Ліміт ваги не поширюється на вдягнене: екіпірування виймає предмет з інвентаря, і його вага зникає з підрахунку. Якщо у вашій грі обладунки мають важити на гравці — рахуйте цю частину самі, наприклад через
GetTotalStatValueна компоненті спорядження.
Стартові предмети¶
StartingItems (мапа «тип предмета → кількість») наповнює інвентар на початку гри,
на сервері. Зручно для фіксованого вмісту скрині, асортименту торговця чи стартового
набору тестового персонажа.
Для випадкового вмісту беріть таблицю луту — 07 — Лут і предмети у світі.
Події для UI¶
Саме на них будують інтерфейс — не на поверненому значенні Try*.
| Подія | Коли спрацьовує |
|---|---|
OnInventoryChanged |
будь-яка зміна; несе тип зміни |
OnItemAdded |
слот, що був порожнім, отримав предмет |
OnItemRemoved |
слот спорожнів (предмет ще читабельний під час виклику) |
OnItemChanged |
предмет той самий, але дані змінилися (стек, міцність) |
OnAddRejected |
додавання відхилено; несе текст причини |
Усі спрацьовують і на сервері, і на клієнтах — той самий обробник обслуговує обидва режими.
Inventory->OnInventoryChanged.AddDynamic(this, &UMyWidget::HandleInventoryChanged);
Inventory->OnAddRejected.AddDynamic(this, &UMyWidget::ShowRejectionToast);
OnAddRejected варто прив'язати одразу: без неї гравець не зрозуміє, чому підбирання
нічого не дало.
Малювання сітки¶
for (const FISDemoInventoryEntry& Entry : Inventory->GetAllEntries())
{
DrawSlot(Entry.SlotIndex, Entry.Instance); // індекс беремо із запису
}
GetAllEntries повертає лише зайняті слоти. Список розріджений — не вважайте,
що запис №N це слот №N (01 — Основні поняття).
Куди далі¶
- Вдягнути предмет: 06 — Спорядження
- Зробити скриню з лутом: 07 — Лут і предмети у світі
- Повний перелік методів: 10 — Довідник