Гайд Lyra Inventory Fix 03: Исправляем UE-127172. Делаем как надо, а не как "пока работает"

Начинающий
Статус
Оффлайн
Регистрация
9 Июл 2023
Сообщения
9
Реакции[?]
11
Поинты[?]
11K
И снова тодошки в прототипе системы инвентаря от Lyra… На 93-й строке LyraInventoryManagerComponent.cpp мы наткнулись на вырубленное в камне послание от эпиков:
TODO: Using the actor instead of component as the outer due to UE-127172. Все ясно! Тодошка из отряда костылеобразных.
Как говорится, "нет ничего более постоянного, чем временное решение". Но не сегодня! Мы избавим код от этого костыля и реализуем правильное решение, воспользовавшись нашим подсистемным подходом. Пора управлять экземплярами предметов грамотно и централизованно.

В чем проблема?
Баг UE-127172 вынуждал использовать актор вместо компонента в качестве "внешнего объекта" для экземпляров предметов. Однако такое решение:
  • Ломает иерархию владения: экземпляры должны принадлежать компоненту, а не актору.
  • Портит модульность и усложняет отладку.
  • Выглядит как типичный "костыль", с которым стыдно показываться в приличном коде.
Наше решение: подсистема как менеджер экземпляров предметов
Мы добавим централизованное управление жизненным циклом предметов через реализованную в прошлый раз подсистему ULyraInventorySubsystem.
Lyra Inventory Fix 02: Подсистема для фрагментов инвентаря. Решаем TODO с умом.
Ох уж эти TODO-комментарии от Epic Games... Они, как забытые под диваном носки: вроде лежат себе тихо, но каждый раз, проходя мимо, ты слышишь их шёпот: "Вернись! Не оставляй меня здесь!". И, заглянув под диван, на 49-й строке LyraInventoryItemDefinition.h мы как раз и нашли один такой: Todo: Make into a subsystem instead? прямо перед ULyraInventoryFunctionLibrary. Что ж, пора отправить его на заслуженную пенсию и навести порядок
Подсистема теперь будет:
  • Создавать экземпляры предметов.
  • Инициализировать их правильно.
  • Управлять их жизненным циклом, очищая их при деинициализации.
В своих потугах использую UE5.5.0.

Добавляем методы в ULyraInventorySubsystem
LyraInventorySubsystem.h:
public:
    /** Create a new inventory item instance */
    UFUNCTION(BlueprintCallable, Category="Lyra|Inventory")
    ULyraInventoryItemInstance* CreateInventoryItemInstance(UObject* Outer, TSubclassOf<ULyraInventoryItemDefinition> ItemDef);

    /** Initialize a newly created inventory item instance */
    void InitializeItemInstance(ULyraInventoryItemInstance* Instance, TSubclassOf<ULyraInventoryItemDefinition> ItemDef);

private:

    /** Cache of created inventory items for proper lifecycle management */
    UPROPERTY()
    TArray<TObjectPtr<ULyraInventoryItemInstance>> ManagedItems;
Новые методы:
  1. CreateInventoryItemInstance — создаёт экземпляр предмета и настраивает его внешний объект (outer).
  2. InitializeItemInstance — выполняет дополнительную инициализацию экземпляра.
  3. ManagedItems — массив для отслеживания всех созданных экземпляров.
LyraInventorySubsystem.cpp:
// Надеюсь знаем куда вставлять?
#include "LyraInventoryItemInstance.h"

// Добавляем в функцию очистку управляемых предметов
void ULyraInventorySubsystem::Deinitialize()
{
    // Cleanup managed items
    ManagedItems.Empty(); // Вот эта строка
    RegisteredInventoryComponents.Empty();
    Super::Deinitialize();
}

// И добавляем реализации объявленных функций:
ULyraInventoryItemInstance* ULyraInventorySubsystem::CreateInventoryItemInstance(UObject* Outer, TSubclassOf<ULyraInventoryItemDefinition> ItemDef)
{
    if (!ItemDef || !Outer)
    {
        return nullptr;
    }

    // Create the instance with the provided outer
    ULyraInventoryItemInstance* NewInstance = NewObject<ULyraInventoryItemInstance>(Outer);
    if (NewInstance)
    {
        InitializeItemInstance(NewInstance, ItemDef);
        ManagedItems.Add(NewInstance);
    }

    return NewInstance;
}

void ULyraInventorySubsystem::InitializeItemInstance(ULyraInventoryItemInstance* Instance, TSubclassOf<ULyraInventoryItemDefinition> ItemDef)
{
    if (!Instance || !ItemDef)
    {
        return;
    }

    Instance->SetItemDef(ItemDef);

    // Initialize with fragments
    if (const ULyraInventoryItemDefinition* Definition = ItemDef.GetDefaultObject())
    {
        for (const ULyraInventoryItemFragment* Fragment : Definition->Fragments)
        {
            if (Fragment)
            {
                Fragment->OnInstanceCreated(Instance);
            }
        }
    }
}
Что происходит:
  • Созданный экземпляр привязывается к компоненту, а не к актору.
  • Экземпляры добавляются в массив ManagedItems, что позволяет правильно управлять их жизненным циклом.
  • При деинициализации подсистемы массив очищается, предотвращая утечки памяти.
Обновляем FLyraInventoryList::AddEntry
Теперь вместо кустарного создания экземпляров мы будем пользоваться нашей новой подсистемой.
LyraInventoryManagerComponent.cpp:
/* Заменяем вот этот код:
    FLyraInventoryEntry& NewEntry = Entries.AddDefaulted_GetRef();
    NewEntry.Instance = NewObject<ULyraInventoryItemInstance>(OwnerComponent->GetOwner());  //[USER=891005]@Todo[/USER]: Using the actor instead of component as the outer due to UE-127172
    NewEntry.Instance->SetItemDef(ItemDef);
    for (ULyraInventoryItemFragment* Fragment : GetDefault<ULyraInventoryItemDefinition>(ItemDef)->Fragments)
    {
        if (Fragment != nullptr)
        {
            Fragment->OnInstanceCreated(NewEntry.Instance);
        }
    }
    NewEntry.StackCount = StackCount;
    Result = NewEntry.Instance;

    //const ULyraInventoryItemDefinition* ItemCDO = GetDefault<ULyraInventoryItemDefinition>(ItemDef);
    MarkItemDirty(NewEntry);

    return Result;
...на вот этот:*/
    // Create the item instance through the subsystem
    if (UGameInstance* GameInstance = OwnerComponent->GetWorld()->GetGameInstance())
    {
        if (ULyraInventorySubsystem* InventorySubsystem = GameInstance->GetSubsystem<ULyraInventorySubsystem>())
        {
            FLyraInventoryEntry& NewEntry = Entries.AddDefaulted_GetRef();
            NewEntry.Instance = InventorySubsystem->CreateInventoryItemInstance(OwnerComponent, ItemDef);
            if (NewEntry.Instance)
            {
                NewEntry.StackCount = StackCount;
                NewEntry.LastObservedCount = StackCount;
                Result = NewEntry.Instance;
                //MarkItemDirty(NewEntry);
                BroadcastChangeMessage(NewEntry, /[I]OldCount=[/I]/ 0, /[I]NewCount=[/I]/ StackCount);
            }
        }
    }

    return Result;
Изменения:
  1. AddEntry вызывает метод CreateInventoryItemInstance из подсистемы.
  2. Внешним объектом (outer) теперь корректно выступает компонент, а не актор.
  3. Инициализация экземпляра проводится через InitializeItemInstance.
LyraInventoryItemInstance.h:
// После строки "friend struct FLyraInventoryList;" добавим:
    friend class ULyraInventorySubsystem;
Что мы сделали?
  1. Создали централизованное управление экземплярами предметов:
    • Создание, инициализация и очистка управляются через подсистему.
    • Правильная иерархия владения: компонент теперь внешний объект.
  2. Исправили проблему UE-127172:
    • Избавились от временного решения с актором.
    • Система стала чище, логичнее и модульнее.
  3. Добавили новые возможности:
    • Масштабируемость: можно легко добавить кастомную логику создания предметов.
    • Управление жизненным циклом: больше никаких утечек или зомби-объектов.
Преимущества нового подхода
  • Чистота кода: логика создания и управления экземплярами сосредоточена в одном месте — в подсистеме.
  • Правильная иерархия: предметы принадлежат компоненту, как и положено.
  • Расширяемость: легко добавить новый функционал — например, пуллинг объектов или асинхронную инициализацию.
  • Оптимизация: управление массивом экземпляров позволяет избежать утечек.
Новые методы CreateInventoryItemInstance и InitializeItemInstance доступны из Blueprint. Настройка внешнего объекта теперь происходит автоматически.

Вывод
Мы убрали баг UE-127172 с корнем и заменили костыль на надёжное, централизованное решение. Подсистема теперь рулит всем процессом создания и управления экземплярами, следит за порядком и даже подметает следы при завершении работы.

Как говорится, "Хороший код — это когда даже временные решения выглядят намеренно"... но мы-то знаем, что лучше решить проблему сразу, чем прятать её за TODO.
 
Последнее редактирование:
Сверху Снизу