Skip to content

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

  1. Створіть Blueprint-клас, батько — ISItemFragment.
  2. Перевизначте потрібні події.
  3. Додайте його у масив 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…), виконуються лише на сервері.


Куди далі