Skip to content

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)

Підняти предмет з підлоги

Той самий принцип, ще один приклад: AISItemPickup (предмет, що лежить у світі — 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 EISItemUseResult Result = Inventory->TryUseItem(SlotIndex, GetPawn());

Що станеться — вирішують фрагменти: зілля витратиться, смолоскип втратить міцність, камінь відповість NotUsable.

Результат Значення
Success використано
NoItem слот порожній
NotUsable жоден фрагмент не робить предмет використовуваним
Blocked фрагмент заборонив (зламане, немає зарядів, кулдаун)

Останні три — нормальні відповіді. Показуйте їх гравцеві: Get Use Result Text (Result) дає готовий текст.

Instigator (другий аргумент) варто передавати — саме йому фрагменти застосовують ефекти.


Впорядкування

Inventory->CompactStacks();                        // об'єднати неповні стеки
Inventory->SortInventory(EISSortMode::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 FISInventoryEntry& Entry : Inventory->GetAllEntries())
{
    DrawSlot(Entry.SlotIndex, Entry.Instance);   // індекс беремо із запису
}

GetAllEntries повертає лише зайняті слоти. Список розріджений — не вважайте, що запис №N це слот №N (01 — Основні поняття).


Куди далі