Гайд Lyra Inventory Fix 04: Решаем проблему с валидацией, инкапсуляцией и имплементацией.

Начинающий
Статус
Оффлайн
Регистрация
9 Июл 2023
Сообщения
9
Реакции[?]
11
Поинты[?]
11K
Фикс 1: Валидация, инкапсуляция
Первый небольшой фикс мы начнем с комментария в LyraInventoryManagerComponent.cpp на строке 136: TODO: Would prefer to not deal with this here and hide it further?. Он указывает на то, что валидация (nullptr и другие проверки состояния Entry.Instance) выполняется прямо в компоненте. Это нарушает инкапсуляцию и засоряет логику компонента инвентаря.

Где будем искать решение? Спрячем логику валидации в подсистему, где ей и место, а компоненту оставим только вызов готовых методов. Такой подход делает код чище, надёжнее и легче для поддержки.
В своих потугах использую UE5.5.0.

Что мы делаем?
  1. Переносим логику проверки валидности и очистки в ULyraInventorySubsystem.
  2. Добавляем методы для:
    • Валидации экземпляров предметов (например, на nullptr).
    • Фильтрации списка предметов — возвращает только валидные.
    • Очистки невалидных предметов — автоматически убирает мусор.
  3. Обновляем FLyraInventoryList в компоненте, чтобы он делегировал эту работу подсистеме.
LyraInventorySubsystem.h:
public:
    /** Validate if an inventory item instance is still valid */
    UFUNCTION(BlueprintCallable, Category="Lyra|Inventory")
    bool IsValidItemInstance(const ULyraInventoryItemInstance* Instance) const;

    /** Get all valid items from an inventory entry array */
    UFUNCTION(BlueprintCallable, Category="Lyra|Inventory")
    TArray<ULyraInventoryItemInstance*> GetValidItemsFromList(const TArray<struct FLyraInventoryEntry>& Entries) const;

private:
    /** Remove invalid items from management */
    void CleanupInvalidItems();
LyraInventorySubsystem.cpp:
void ULyraInventorySubsystem::CleanupInvalidItems()
{
    ManagedItems.RemoveAll([](const ULyraInventoryItemInstance* Item) {
        return !IsValid(Item);
    });
}

bool ULyraInventorySubsystem::IsValidItemInstance(const ULyraInventoryItemInstance* Instance) const
{
    return IsValid(Instance) && Instance->GetItemDef() != nullptr && ManagedItems.Contains(Instance);
}

TArray<ULyraInventoryItemInstance*> ULyraInventorySubsystem::GetValidItemsFromList(const TArray<FLyraInventoryEntry>& Entries) const
{
    TArray<ULyraInventoryItemInstance*> Results;
    Results.Reserve(Entries.Num());
 
    for (const FLyraInventoryEntry& Entry : Entries)
    {
        if (IsValidItemInstance(Entry.Instance))
        {
            Results.Add(Entry.Instance);
        }
    }
 
    return Results;
}
LyraInventoryManagerComponent.h:
// Добавляем после #include "LyraInventoryManagerComponent.generated.h"
class ULyraInventorySubsystem;

// В FLyraInventoryEntry после строки friend ULyraInventoryManagerComponent;
friend ULyraInventorySubsystem;
Обновляем FLyraInventoryList
Теперь компонент инвентаря делегирует проверку валидности и очистку подсистеме.
LyraInventoryManagerComponent.cpp:
// Полностью заменяем реализацию функции FLyraInventoryList::GetAllItems()
TArray<ULyraInventoryItemInstance*> FLyraInventoryList::GetAllItems() const
{
    if (UGameInstance* GameInstance = OwnerComponent->GetWorld()->GetGameInstance())
    {
        if (ULyraInventorySubsystem* InventorySubsystem = GameInstance->GetSubsystem<ULyraInventorySubsystem>())
        {
            return InventorySubsystem->GetValidItemsFromList(Entries);
        }
    }
 
    return TArray<ULyraInventoryItemInstance*>();

}
Что мы сделали?
is4.PNG

  1. Добавили методы в подсистему:
    • IsValidItemInstance — проверяет валидность экземпляра.
    • GetValidItemsFromList — фильтрует и возвращает валидные предметы.
    • CleanupInvalidItems — удаляет невалидные предметы.
  2. Перенесли логику валидации из компонента в подсистему:
    • FLyraInventoryList теперь вызывает методы подсистемы, а не сам проверяет nullptr.
    • Код компонента стал чище и теперь только делегирует задачи.
  3. Улучшили инкапсуляцию и масштабируемость:
    • Вся логика валидации сосредоточена в одном месте — в ULyraInventorySubsystem.
    • Добавление новой логики (например, асинхронной проверки или логирования ошибок) теперь проще.
Преимущества нового подхода
  • Централизованная валидация — всё сосредоточено в подсистеме.
  • Чистый и модульный код — компонент больше не занимается чужими заботами.
  • Автоматическая очистка — подсистема убирает невалидные предметы, предотвращая ошибки и утечки.
  • Гибкость — в будущем можно добавить расширенные проверки или асинхронное обновление списка.
Вывод
Мы спрятали логику валидации туда, где ей место — в ULyraInventorySubsystem. Компонент инвентаря стал проще, а код — чище и надёжнее. Как говорится: "Меньше проверок руками — больше времени на крутые фичи!"

Фикс 2: Имплементация
Молодость… Вечно что-то unimplemented оставляют, а потом чешут репу: "Так и должно быть или кто-то заснул на клавише?" Ну давайте разберём этот вопрос как опытный старик, который видел все — и баги, и багфиксы, и бессонные ночи программистов.
Два метода AddEntry — зачем они нужны?
  • ULyraInventoryItemInstance* AddEntry(TSubclassOf<ULyraInventoryItemDefinition> ItemClass, int32 StackCount)
    Это наш молодой и амбициозный метод. Он создаёт новый предметс нуля:
    • Берёт определение предмета (ItemClass).
    • Создаёт экземпляр через подсистему.
    • Устанавливает размер стека (потому что не всегда нужен одиночный батончик хлеба — иногда их надо 20).
    • Используется при создании новых предметов: игрок нашёл лопату, квест наградил булкой, или админ выдал баночку зелья.
  • void AddEntry(ULyraInventoryItemInstance* Instance)
    А вот это старый волк в системе — работает с уже существующими экземплярамипредметов:
    • Экземпляр уже есть — неважно, откуда он: репликация, загрузка сохранёнки или переложили из другого инвентаря.
    • Нужна только проверка валидности (а то мало ли, пока предмет полз к нам, превратился в тыкву).
    • Подсистема может его дополнительно инициализировать, если что-то не так.
Зачем вообще два метода?
Потому что жизнь сложнее, чем кажется.
  1. Первый метод (AddEntry с ItemClass) — для создания нового экземпляра. Это как роддом для предметов: класс определили, вес (стек) назначили, и вот вам новый герой в системе инвентаря.
  2. Второй метод (AddEntry с Instance) — для существующих предметов. Например:
    • Репликация: предмет приехал по сети — надо его добавить.
    • Загрузка сохранения: предметы уже есть в сейве, их просто надо вернуть в инвентарь.
    • Перемещение между инвентарями: переложили лопату из рюкзака в сундук.
    • Синхронизация: при сетевой игре надо следить, чтобы предметы были везде одинаковы.
Если бы второго метода не было, пришлось бы каждый раз "рождать" новый экземпляр, а это уже совсем не по делу — у нас тут не ферма клонов.
Почему unimplemented() — это плохо?
Потому что забыть реализовать метод — это как забыть завинтить гайку в колесе: всё работает, пока машина не разгонится. Этот метод очень нужен, и сейчас он пустует как заброшенная деревенская хата.
Как его реализовать?
Давайте правильно заполним пробел. Вот что получится:
LyraInventoryManagerComponent.cpp:
void FLyraInventoryList::AddEntry(ULyraInventoryItemInstance* Instance)
{
    // Добавляем реализацию вместо unimplemented();
    if (!Instance || !OwnerComponent)
    {
        return;
    }

    AActor* OwningActor = OwnerComponent->GetOwner();
    if (!OwningActor || !OwningActor->HasAuthority())
    {
        return;
    }

    // Check if the instance is already managed by the inventory subsystem
    if (UGameInstance* GameInstance = OwnerComponent->GetWorld()->GetGameInstance())
    {
        if (ULyraInventorySubsystem* InventorySubsystem = GameInstance->GetSubsystem<ULyraInventorySubsystem>())
        {
            if (!InventorySubsystem->IsValidItemInstance(Instance))
            {
                // If not managed, initialize it through the subsystem
                InventorySubsystem->InitializeItemInstance(Instance, Instance->GetItemDef());
            }
        }
    }

    // Add to inventory
    FLyraInventoryEntry& NewEntry = Entries.AddDefaulted_GetRef();
    NewEntry.Instance = Instance;
    NewEntry.StackCount = 1;
    NewEntry.LastObservedCount = 1;

    BroadcastChangeMessage(NewEntry, /[I]OldCount=[/I]/ 0, /[I]NewCount=[/I]/ 1);
}
Теперь всё на своих местах:
  1. AddEntry(ItemClass, StackCount) — создаёт новый предмет.
  2. AddEntry(Instance) — добавляет существующий экземпляр.
Вывод:
Два метода — два подхода: создать или добавить. Оба важны, оба выполняют свои роли. А если какой-то из них пустует и ждёт реализации — не медлите, как старик на утренней рыбалке. Лучше сразу доделать, чем потом искать баг, когда всё уже развалилось!
 
Последнее редактирование:
Сверху Снизу