Lyra Inventory Fix 05: Ограничения инвентаря. Логика стопок и "сколько вешать в граммах?"
Смотрю, ты задал себе амбициозную цель. Это как пытаться поставить границу вселенной — можно подумать, что все ограничения и так очевидны, но на деле каждый такой кусочек кода — это как тщательно подобранный рецепт, где ничего не должно быть лишним.
Вот мы и добрались до конца начала середины первой половины разработки улучшений нашего универсального инвентаря на базе прототипы Lyra. Как видите, система еще не особо вылизана, но это всё поправимо, не переживай — такого даже у Толкина не было, так что идём в верном направлении.
Итак, прежде чем мы бросимся в омут продвинутого функционала с головой, разберемся с одной вещью, без которой все твои бездонные карманы рискуют лопнуть: система ограничений инвентаря. Вес? Количество ячеек? Всё сразу? Почему бы и нет.
Пора разгребать TODO в LyraInventoryItemDefinition.cpp: TODO: Add support for stack limit / uniqueness checks / etc...
Что мы тут имеем? Нас просят добавить поддержку ограничений для инвентаря. И как тут не вспомнить диету — выложил лишнего, встал на весы, и вуаля — лишний вес не позволяет ни двигаться, ни прыгать. Так и тут: без ограничений инвентарь станет свалкой, в которой будут валяться не только полезные предметы, но и тысячи бесполезных.
Спойлер: Для выполнения дальнейших телодвижений необходимо завершить предыдущие гайды.
- Lyra Inventory Fix 01: Фильтрация предметов. Когда порядок — не роскошь, а необходимость
- Lyra Inventory Fix 02: Подсистема для фрагментов инвентаря. Решаем TODO с умом.
- Lyra Inventory Fix 03: Исправляем UE-127172. Делаем как надо, а не как "пока работает"
- Lyra Inventory Fix 04: Решаем проблему с валидацией, инкапсуляцией и имплементацией.
В своих потугах будем использовать UE5.5.0.
1. Ограничим «неограниченное»
Как говорил тот мудрый (и слегка пьяный) человек: "Не впихивай невпихуемое!" — и это касается и инвентаря. Так что пришло время внедрить два критически важных ограничения, которые позволят нам контролировать размеры предметов в инвентаре и их вес.
1.1 Добавляем MaxStackSize и Weight
Помните, что говорил один довольно умный и в меру нетрезвый человек? «Не впихивай невпихуемое!». Так вот, пора вписать её в ваш код. Ну что ж, начнем с двух фундаментальных ограничителей: MaxStackSize и Weight. Оба — базовые свойства, значит им самое место в ULyraInventoryItemDefinition, чтобы ни один предмет не посмел стать безразмерным.
MaxStackSize — наш главный арбитр, решающий судьбу предметов:
- MaxStackSize = 0 — стопка бесконечна. Поздравляю, у вас есть волшебный инвентарь, куда поместится и гора лопат, и сундук с кирпичами.
- MaxStackSize = 1 — никаких хитростей, предмет не стакается — каждый экземпляр идет в отдельный слот. Ручная граната не терпит соседей.
- MaxStackSize > 1 — предметы собираются в стеки до указанного предела. Например, пачка стрел или бутылок с зельями.
Теперь, как насчет веса? Когда у вас есть камни в кармане — ничего страшного, можно двигаться. Но как только эти камни превращаются в мешок, вы начинаете чувствовать, что жизнь не такая уж легкая.
Weight — вес предмета, который мы добавляем в инвентарь. Например, это может быть тот же мешок с камнями или пара тяжёлых артефактов. Это не только влияет на эстетику инвентаря, но и на сам геймплей — чем больше вес, тем медленнее персонаж, меньше маневренности.
Реализация
Теперь давайте встроим эти ограничения прямо в ULyraInventoryItemDefinition, чтобы все предметы имели свои чёткие границы.
Теперь у каждого предмета будет чёткое ограничение по максимальному количеству в стеке и весу.
Эти два ограничения — как дорожные знаки на трассе разработки. "Вот тут едешь быстро, а тут надо сбавить". Без них мы рискуем наделать в инвентаре бардак. И кто будет отвечать за это? Конечно, вы, как хороший разработчик. Теперь предметы сдохнут в коробке только тогда, когда им скажем «стоп».
1.2 Добавляем ограничения для инвентаря
Двигаемся от лопат к карманам — пора установить ограничения не только для предметов, но и для самого инвентаря. Ведь если в вашем кармане начнут появляться камни, то ещё пару мешков с железом и всё, нужно будет ехать к хирургу. Давайте закрутим гайки, чтобы инвентарь не стал местом, где всё уходит в полный хаос.
- MaxInventorySlots — максимальное количество слотов. Этот параметр нужно добавить в компонент ULyraInventoryManagerComponent, чтобы ограничить доступное пространство.
- MaxInventoryWeight — максимальный вес, который игрок может унести. Как только он превысит этот предел, нужно будет либо избавиться от лишнего, либо жить с ограничениями. В реальной жизни не каждый может унести десятикилограммовую сумку с продуктами, так почему бы не ограничить вес и в игре?
Простой метод подсчета текущего веса позволит вам наконец-то перестать терпеть эти "карманы без дна".
LyraInventoryManagerComponent.h:
LyraInventoryManagerComponent.cpp:
Задав ограничения для инвентаря, мы сделали систему более реальной и управляемой. Как говорится, "у каждого есть свой предел". Пределы слотов и веса в инвентаре помогут сбалансировать систему, не давая игроку становиться пылесосом для хлама, и повысят уровень вызова для геймплея.
1.3 Частичный подбор предметов
Вот мы и подошли к тому самому моменту, когда инвентарь превращается в нечто большее, чем просто сумка с дырками. Как мой дедушка говорил: «Не суй в карман больше, чем можешь унести». Так вот, если твоим персонажем движет страсть к ломать и таскать, то давайте ограничим его жажду до здравого смысла, а не до бесконечности.
Теперь нужно добавить логику, которая будет отслеживать, сколько ещё можно запихнуть в инвентарь, учитывая все ограничения. И это не просто ограничение на количество — нужно учесть и вес, потому что лопата с кирпичом — это одно, а лопата с воздушным шариком — совсем другое.
1.3.1. Метод для расчёта максимального количества предметов
Как говорил мой дед: «Не суй в карман больше, чем можешь унести». Ведь если персонаж потеет от лопаты, ему уж точно не стоит таскать целый сарай.
Добавим метод в ULyraInventoryManagerComponent, который будет вычислять, сколько ещё можно уместить в инвентарь, исходя из текущего веса и доступных слотов.
Как это будет работать:
- Метод будет проверять текущий вес и количество предметов в инвентаре.
- Определит, сколько ещё можно добавить с учётом ограничений на вес и количество слотов.
- Вернёт максимально возможное количество предметов, которые можно добавить, не превышая предельные параметры.
LyraInventoryManagerComponent.cpp:
Здесь начинается магия — вместо того, чтобы тупо отказывать игроку, если он хочет втиснуть в инвентарь больше, чем можно, мы даем ему столько, сколько можем. Да, мы не злые маги, мы — практичные: это прощение, не пустое, а ограниченное. Это:
- Заполняем существующие стеки до предела — давай, удачи, но без фанатизма.
- Используем свободные слоты для новых стеков — каждый новый слот будет вашим другом.
- Обрезаем количество в зависимости от веса и слотов — не хочу видеть лишние предметы, если они не помещаются.
Так что, даже если инвентарь уже переполнен, мы не ставим крест на вашем приключении. Часть успеха гарантирована. Стрелы вместо их отсутствия? Логично. Модульно. Приятно.
В функции CanAddItemDefinition(TSubclassOf ItemDef, int32 StackCount) находим нашу тодошку, удаляем ее и вместо болванки возвращаем максимально допустимое количество предметов:
LyraInventoryManagerComponent.cpp:
Далее мы беспощадно фаршируем функцию AddItemDefinition(TSubclassOf ItemDef, int32 StackCount). Для этого добавим 100 грамм логики в AddItemDefinition:
- Двухпроходный алгоритм добавления предметов
- Первый проход: заполняет существующие стеки до их лимита
- Второй проход: создает новые стеки для оставшихся предметов
LyraInventoryManagerComponent.cpp:
Как-то так. Никаких сложных вычислений, только два цикла и проверка на месте. Если слот не вмещает — создаём новый.
2. Плитки инвентаря: иконки и количество
Помимо железных ограничений, давайте взглянем на визуальную составляющую. У нас два недочета:
Теперь о визуале. Все эти предметы, которые мы упаковали, должны не только существовать, но и быть видимыми.
- Иконки предметов — потому что этот камень не может выглядеть как пустое пространство. Он должен быть с иконкой. Чисто для эстетики.
- Количество предметов в стеке — отображать, что, если у тебя 20 бутылок с зельем, ты не одинок в этом инвентаре. У тебя их целая куча.
2.1 Добавляем иконки
Пора нашему хламу обзавестись иконками. Lyra использует систему фрагментов (ULyraInventoryItemFragment), что даёт отличную модульность. Это означает, что вместо того, чтобы настраивать все свойства для каждого определенного предмета инвентаря, вы выбираете, какие свойства у него есть, добавляя один или несколько ULyraInventoryItemFragment' к каждому ULyraInventoryItemDefinition.
Чтобы добавить иконку к нашему камню, нам нужно добавить один из этих фрагментов. Для примера, откройте TestID_Rock файл чертежа и добавьте новый InventoryFragment_QuickBarIcon. После этого установите для Brush любую текстуру, которая вам нравится. Всё просто.
Мы ещё не закончили — а вы что, думали, так просто отделаетесь? Элемент в инвентаре представлен виджетом W_InventoryTile, который, как вы, вероятно, уже заметили, выглядит абсолютно пустым. Если заглянуть внутрь, там лишь сиротливое событие OnListItemObjectSet, как забытый список покупок без единого товара. Мы добавляем фрагмент в инвентарь, но плитки инвентаря ничего не знают о наших предметах. Вот это мы и будем исправлять.
OnListItemObjectSet вызывается каждый раз, когда наша плитка обновляется в своём родительском контейнере и получает объект, который ей предстоит представить. Ключ к разгадке спрятан в W_InventoryGrid. Если открыть его и заглянуть в Construct функцию, станет ясно, что он загружает каждый элемент инвентаря в CommonTileView как ULyraInventoryItemInstance. Что это значит для нас? Возвращаемся в W_InventoryTile, приводим ListItemObject к ULyraInventoryItemInstance — и вот она, зацепка. Из экземпляра предмета можно извлечь прикреплённый InventoryFragment_QuickBarIcon, который мы уже реализовали.
Если вы раньше ковырялись в Lyra, то наверняка сталкивались с FindFragmentByClass — методом старым, как программирование на C++. Эта функция, подобно опытному охотнику, находит первый фрагмент нужного класса. И нам это как раз на руку: используем её, чтобы достать QuickBarIcon, а затем устанавливаем нашему виджету кисть (или текстуру, если вы хотите звучать менее поэтично).
Когда закончите, результат должен быть таким:
Теперь, когда вы нажмете «Играть» и подберете камень, он наконец перестанет притворяться зелёным артефактом из закромов кода. Небольшой довод Blueprint до ума — и инвентарь заиграет новыми красками.
2.2 Отображаем количество
Никакая стопка не имеет смысла, если на ней нет циферки. В структуре FLyraInventoryEntry есть StackCount для отслеживания количества предметов в каждом стеке, но он упорно прячется от Blueprints. Добавляем поддержку количества через GameplayTag, вытаскиваем через GetStatTagStackCount и передаем в UI. Опять же, аккуратно, без лишнего вмешательства в базовую структуру. Это подобно тому, как Lyra обрабатывает количество боеприпасов.
Откройте свой LyraInventoryManagerComponent.cpp файл. В начале, чуть ниже существующего UE_DEFINE_GAMEPLAY_TAG_STATIC мы определим новый тег геймплея, который будет использоваться для представления количества стеков каждого предмета:
Внутри FLyraInventoryList::AddEntry(TSubclassOf<ULyraInventoryItemDefinition> ItemDef, int32 StackCount) функции добавим следующую строку после NewEntry. StackCount = StackCount;
Внутри FLyraInventoryList::AddEntry(ULyraInventoryItemInstance* Instance) функции добавим следующую строку после NewEntry. StackCount: = 1;
Но погодите, это ещё не всё! У нас пока нет текстового компонента для отображения количества предметов в стеке. Виджет W_InventoryTile как бы намекает: «Добавь текстовый блок и оставь меня в покое». Так что именно это мы и сделаем. Добавляем компонент для отображения количества и идём дальше.
Теперь возвращаемся к OnListItemObjectSet и используем наш тег Lyra.Inventory.Item.Count, который мы определили в C++. С его помощью вытаскиваем StackCount из ULyraInventoryItemInstance и обновляем текст нашего нового компонента. Магия закончена — инвентарь наконец-то будет показывать количество предметов.
Когда всё готово, результат должен выглядеть вот так:
Теперь, чтобы проверить это, запустите игру и подберите несколько предметов. Обратите внимание, что предметы в настоящее время не будут объединяться в одну стопку. Нам нужно установить желаемый MaxStackSize в шаблоне предмета инвентаря (ItemDefinition), прикрепленном к кубам вашего уровня.
И теперь каждое добавление предмета обновляет StackCount, а плитка в инвентаре показывает цифру. Визуальный порядок восстановлен: десять стрел, три зелья, две гранаты — всё как полагается.
4. Итоги: система готова, но не "на авось"
Инвентарь не просто пережил свою переработку — он теперь живёт и понимает, что с ним можно делать. Мы разгребли старый TODO, добавили ограничения, и, что важнее, сделали его модульным и гибким. Теперь этот инвентарь не просто лужа данных — это система, которая:
- Знает свои пределы.
- Уважает их.
- Управляет стеками, считает вес и, что самое главное, делает это с умом.
- Частичный подбор? Умеет! Не впихиваем невпихуемое — или по крайней мере даём шанс впихнуть, но строго по правилам.
А ведь ещё и плитки инвентаря стали нормальными: с иконками и количеством. Всё красиво, всё логично.
Мораль: никогда не делай систему инвентаря на «авось». Это как собирать гаражные полки: если не задать ограничения и не учесть каждый угол, месяц спустя ты с разочарованием смотришь на заваленный хлам, который пришлось бы перебрать, а потом снова начать с нуля. Код, который не ломается, должен жить вечно.
Теперь, когда основа готова, можно спокойно расширять систему. Хотите новые ограничения или механики? Давайте. Мы уже создали фундамент, а значит, можно строить небоскрёбы.
Вперёд, коллеги!
Дальше — больше. В следующий раз займёмся обновлением стандартных функций для подбора пикапов. Разработаем новые, чтобы наш инвентарь заработал по полной программе. И, конечно, в коде ещё ждут штыри — старые TODO, которые как сорняки в саду, но нам нужно их аккуратно выдергивать, как по-настоящему опытным садоводам. Погнали!