04 — Фрагменти¶
🇬🇧 English | 🇺🇦 Українська
Фрагмент — це одна здатність предмета. Тип предмета не має власної поведінки: він лише перелічує фрагменти, а вони роблять усю роботу.
Головне правило: фрагменти спільні¶
Об'єкт фрагмента живе всередині ассета ISItemDefinition. Він один на всю гру
і спільний для кожного екземпляра цього предмета.
Якщо сто гравців тримають смолоскипи — усі сто смолоскипів вказують на той самий
об'єкт UISFragment_Durability.
Тому всі хуки оголошені const, і запис у поле фрагмента під час гри — завжди помилка:
// НЕПРАВИЛЬНО - змінює ассет одразу для всіх смолоскипів у грі
CurrentDurability -= 1.0f;
// ПРАВИЛЬНО - змінює лише той смолоскип, який використали
Instance->ModifyStatValue(ISTags::Stat_Durability, -1.0f);
Змінний стан конкретної копії зберігається у UISItemInstance::StatValues — списку
числових значень із ключами-тегами.
Побічний наслідок, про який варто пам'ятати. Предмет, що має бодай одне значення в
StatValues, ніколи не об'єднується в стек — див. 01 — Основні поняття.
Stackable — кілька копій в одному слоті¶
| Поле | Опис |
|---|---|
MaxStackSize |
скільки копій вміщує один слот (мінімум 2) |
Без цього фрагмента предмет займає слот на кожну копію.
Типові значення: 99 для матеріалів і валюти, 20 для зілль, 5–10 для громіздкого.
Сам по собі фрагмент не гарантує, що два екземпляри об'єднаються в стек: за це
відповідає CanStackWith, і індивідуальний стан екземпляра завжди має останнє слово.
Якщо в предмета є власні дані — наприклад, Durability — два екземпляри з різними
значеннями ніколи не зіллються, навіть якщо обидва стекуються. Інакше пощерблений
меч міг би «розчинитися» в стопці нових. Тому Stackable + Durability разом мають
сенс лише тоді, коли предмет стартує без індивідуального штампа й отримує знос уже
потім.
Equippable — предмет можна вдягнути¶
| Поле | Опис |
|---|---|
EquipmentSlot |
слот за замовчуванням — куди предмет потрапляє, коли призначення не назване |
AlternativeSlots |
інші слоти, у яких предмет також може бути — те, що робить можливими дві руки й TrySwapSlots |
EquippedActorClass |
актор, що з'являється при вдяганні (необов'язково) |
AttachSocketName |
сокет на мешу персонажа |
AttachOffset |
доводка трансформа після прикріплення |
GrantedStats |
статистики, які предмет дає, поки вдягнений |
Слот, названий тут, має бути присутнім у AvailableSlots компонента спорядження —
інакше предмет нікуди вдягнути.
Візуальний актор створюється локально на кожній машині і не реплікується. Тому меч у руці персонажа не коштує трафіку понад сам запис про слот.
GrantedStats — декларативні: плагін лише підсумовує їх через GetTotalStatValue, а
що означає «броня 12», вирішує ваша гра.
Докладніше — 06 — Спорядження.
Один предмет — один слот. Кільце, яке пасує в будь-який із двох слотів, не виражається тут напряму: дайте обом слотам спільний тег (
Equipment.Slot.Accessory.Ring) і відкрийте кілька таких слотів на персонажі.
Consumable — предмет витрачається¶
| Поле | Опис |
|---|---|
ConsumeAmount |
скільки копій витрачає одне використання |
MaxCharges |
заряди на копію (0 = витрачаються копії) |
bDestroyWhenChargesSpent |
знищити після останнього заряду |
EffectTags |
що саме має статися — вашою мовою |
UseCooldown |
мінімум секунд між використаннями |
Два режими¶
Режим стеку (MaxCharges = 0) — випите зілля зникає.
Режим зарядів (MaxCharges > 0) — предмет лишається в слоті, витрачаючи заряди:
жезл на 10 закляттів, ліхтар із пальним, аптечка на 3 застосування.
Ефект реалізуєте ви¶
Фрагмент робить бухгалтерію — витрачає копію чи заряд — і повідомляє, що саме
було спожито, через EffectTags. Він ніколи не застосовує ефекти сам.
Це навмисне рішення: «відновити 50 здоров'я» означає різне в кожному проєкті, і плагін, який би це вгадував, помилявся б у більшості з них.
Підпишіться один раз — наприклад, у GameMode:
void AMyGameMode::BeginPlay()
{
Super::BeginPlay();
// Подія живе на підсистемі світу, а не на класі фрагмента: фрагмент
// спільний для всієї гри, а статик на ньому — спільний ще й для всіх
// світів у процесі. У PIE сервер і клієнт чули б використання один одного.
if (UISInventoryWorldSubsystem* Subsystem = UISInventoryWorldSubsystem::Get(this))
{
Subsystem->OnConsumableUsed.AddDynamic(this, &AMyGameMode::HandleConsumableUsed);
}
}
void AMyGameMode::HandleConsumableUsed(
UISItemInstance* Item, AActor* Instigator, const FGameplayTagContainer& EffectTags)
{
if (EffectTags.HasTag(MyTags::Effect_Heal))
{
ApplyHealing(Instigator, 50.0f);
}
else if (EffectTags.HasTag(MyTags::Effect_RestoreMana))
{
ApplyMana(Instigator, 30.0f);
}
}
Якщо ви користуєтеся Gameplay Ability System — це природне місце, щоб застосувати
GameplayEffect за тегом.
UseCooldown— навмисно простий кулдаун: один на фрагмент і спільний для всіх гравців світу. Для одиночної гри це те, що треба; для змагальної — очевидно ні, бо двоє гравців ділили б один таймер зілля. Кулдауни на гравця, категорії кулдаунів і зворотний відлік в UI належать вашим системам: залиштеUseCooldownнулем і рахуйте їх у себе, підписавшись наOnConsumableUsed.Сам кулдаун зберігає
UISInventoryWorldSubsystem— разом зі світом він і зникає. Саме тому він не статичне поле фрагмента: статик пережив би сесію PIE і змішав би час серверного та клієнтського світів, які в редакторі живуть в одному процесі.
Durability — предмет зношується¶
| Поле | Опис |
|---|---|
MaxDurability |
міцність нової копії |
DurabilityPerUse |
скільки коштує одне використання |
bDestroyWhenBroken |
знищити при досягненні нуля |
bCannotUseWhenBroken |
не давати користуватися зламаним |
Фрагмент повністю самодостатній: сам ставить нову копію на повну міцність, сам списує знос, сам відмовляє у використанні зламаного інструмента. Ядро інвентаря про міцність не знає нічого.
Корисні функції: GetDurabilityPercent (0–1, зручно для смужки), IsBroken, Repair.
Знос не від використання. Поставте DurabilityPerUse = 0, якщо ваша гра псує
спорядження з іншого місця (отримання удару, вплив середовища), і викликайте
ModifyStatValue напряму.
Weight — предмет має вагу¶
| Поле | Опис |
|---|---|
UnitWeight |
вага однієї копії |
Враховується лише інвентарями з ненульовим MaxWeight. В інших випадках значення суто
інформаційне — інтерфейс може його показувати.
Одиниця виміру ваша: кілограми, фунти, абстрактні «одиниці навантаження». Плагін лише порівнює суму з лімітом.
Власний фрагмент¶
У Blueprint¶
- Створіть Blueprint-клас, батько — ISItemFragment.
- Перевизначте потрібні події.
- Додайте його у масив
Fragmentsбудь-якого предмета.
Жодного C++ не потрібно.
У C++¶
UCLASS(DisplayName = "Soulbound")
class UISFragment_Soulbound : public UISItemFragment
{
GENERATED_BODY()
public:
// Запам'ятовуємо власника при створенні копії.
virtual void OnInstanceCreated_Implementation(UISItemInstance* Instance) const override
{
// Стан пишемо в екземпляр, а не в поля фрагмента.
Instance->SetStatValue(MyTags::Stat_BoundTo, 0.0f);
}
// Забороняємо предмету потрапити в чужий інвентар.
virtual bool CanBeAddedTo_Implementation(
UISItemInstance* Instance,
UISInventoryComponent* Inventory,
FText& OutReason) const override
{
if (!BelongsTo(Instance, Inventory))
{
OutReason = NSLOCTEXT("MyGame", "Bound", "Цей предмет належить іншому.");
return false;
}
return true;
}
};
Текст OutReason доходить до гравця через подію OnAddRejected — покажіть його
спливаючим повідомленням.
Як витратити предмет із фрагмента¶
Instance->ConsumeFromContainer(1); // так
Це прохання до того, хто тримає предмет, — байдуже, інвентар це чи слот спорядження. Не шукайте індекс слота самі:
// НІ - у вдягненого предмета індексу слота немає, і виклик тихо нічого не зробить
UISInventoryComponent* Inv = Instance->GetOwningInventory();
Inv->TryRemoveItemFromSlot(Inv->FindSlotOfInstance(Instance), 1);
Саме на цьому свого часу спіткнувся вбудований Consumable: випита вдягнена фляга
оголошувала ефект і не витрачалася.
Так само й із записом статистики: SetStatValue сам знаходить того, хто тримає
предмет, помічає слот до реплікації й піднімає подію — і для рюкзака, і для
спорядження.
Перелік хуків¶
Життєвий цикл¶
| Хук | Коли викликається |
|---|---|
OnInstanceCreated |
одразу після створення нової копії — тут ставлять стартові значення |
OnAddedToInventory |
предмет потрапив в інвентар |
OnRemovedFromInventory |
предмет виходить з інвентаря (ще всередині) |
OnInstanceCreated не викликається при поділі стеку, переміщенні чи завантаженні
збереження — інакше поламаний предмет «лікувався» б від кожного переносу.
Заборони — потрібна згода всіх фрагментів¶
| Хук | Питання |
|---|---|
CanBeAddedTo |
чи можна додати предмет у цей інвентар? |
CanStackWith |
чи можна об'єднати два стеки? |
CanBeUsed |
чи можна використати предмет зараз? |
Одне false зупиняє дію. Текст OutReason показують гравцеві.
Дії¶
| Хук | Коли |
|---|---|
IsUsable |
чи взагалі має сенс «використати» цей предмет |
OnUsed |
застосувати ефект використання |
OnEquipped / OnUnequipped |
предмет вдягли / знімають |
Модифікатори значень¶
| Хук | Що робить |
|---|---|
ModifyMaxStackSize |
впливає на розмір стеку |
ModifyUnitWeight |
впливає на вагу одиниці |
Ці два працюють ланцюжком: значення проходить крізь усі фрагменти по черзі. Саме
тому предмет без Stackable має стек 1 — початкове значення просто ніхто не змінив.
Детермінованість обов'язкова. Хуки-запити (
CanStackWith,ModifyMaxStackSize,ModifyUnitWeight) виконуються і на клієнтах. Якщо клієнт порахує інший розмір стеку, ніж сервер, гравець побачить розсинхронізований інвентар.Хуки, що змінюють стан (
OnUsed,OnAddedToInventory,OnEquipped…), виконуються лише на сервері.
Куди далі¶
- Побачити фрагменти в дії: 05 — Операції з інвентарем
- Розширити плагін власним кодом: 11 — Рецепти