InventorySystem
Назначение
Встраиваемый инвентарь с ограничениями через набор проверяющих правил и свойствами вместимости (масса, объём, пачка, контейнер), объявленными для предметов пакета ItemsSystem.
- Инвентарь — не запись БД, а поле в настройке хозяина (сундук, персонаж, торговец, труп). Живёт внутри модели хозяина и разделяет с ней жизненный цикл.
- Ограничения задаются набором верификаторов, а не фиксированными полями. Правила, которого нет в наборе, ничего не ограничивает.
- Вместимость измеряется свойствами предметов. Масса и объём — свойства, инвентарь их суммирует.
- Однородные предметы складываются в пачки по совпадению типа и наличию свойства пачки.
- Контейнер — тоже предмет: сумка несёт инвентарь свойством, её масса и объём считаются вместе с содержимым.
- В сохранение уезжает не предмет, а пара «идентификатор пресета и состояние»; восстановление идёт через шину предметов.
Вне ответственности: слоты и позиционирование, интерфейс перетаскивания, торговля, крафт, экипировка.
Зависимости
Собирается при включённом тумблере itemsSdk в SdkSettings — под тем же define-ключом USING_VORTEX_ITEMS, что и пакет предметов; отдельного тумблера нет. Сборка: ru.vortex.sdk.inventorysystem, ссылается на ru.vortex.sdk.itemssystem. Обратной зависимости нет — пакет предметов об инвентарях не знает. Требуется Odin Inspector.
Архитектура
| Единица | Роль |
|---|---|
Inventory |
Встраиваемая модель: набор верификаторов, политика пачек, стартовое наполнение, состав, упакованное сохранение |
InventoryVerifier |
Правило «пускать ли предмет». Полиморфно, [SerializeReference] |
InventoryController |
Операции над составом: добавить, изъять, разбить, уничтожить, переместить |
StackController |
Единственный, кто меняет количество в пачке; держит owner-ключ замка |
InventoryBus |
Событие уничтожения инвентаря |
InventoryRegistry |
Отладочный поиск дубликатов предметов между инвентарями |
| Свойства | Stack, Mass, Volume, Container, ContainerMass, Rigid/SoftVolume |
Верификаторы
Ограничения — набор правил в настройке инвентаря. Базовое правило объявляет три метода, все принимают инвентарь параметром (ссылку не хранят):
| Метод | Отдаёт |
|---|---|
CanPlace |
пустить ли предмет |
GetMax |
предел измеряемой величины (фильтры — 0) |
GetCurrent |
текущее значение на составе (фильтры — 0) |
Обобщённое QuantityVerifier реализует «сумма измеряемой величины против предела» — считает по требованию, без кеша, и потому всегда актуально (включая распад веса). Массовое и объёмное правила добавляют только способ измерить один предмет. Фильтры (CategoryFilter, Require/ForbidProperty<T>) наследуются от базы напрямую и на пару чтения отдают ноль — измеряемой величины у них нет.
Предел и сумма — расширенное целое (long): десять тысяч предметов с большими единичными величинами выходят за 32 бита, а переполнение означало бы отрицательную занятость с бесконечной вместимостью.
Поставочные правила:
| Правило | Настройка |
|---|---|
MassVerifier |
предел суммарной массы |
VolumeVerifier |
предел суммарного объёма |
CategoryFilterVerifier |
набор категорий и признак «белый/чёрный список» |
RequirePropertyVerifier<T> |
обобщённое: требуется свойство назначения T |
ForbidPropertyVerifier<T> |
обобщённое: запрещено свойство назначения T |
Два последних настройки не имеют: интерфейс назначения — тип, в инспекторе не выбирается, зато полиморфный выбор работает по классу. Проект объявляет наследника в одну строку:
[Serializable] public class RequireQuestTag : RequirePropertyVerifier<IQuestTag> { }
Свойства вместимости
Живут в этом пакете, для ItemsSystem это «свойства снаружи». Обращение по интерфейсу назначения, и внутри предмета интерфейс занимает ровно одно свойство.
| Свойство | Назначение | Величина |
|---|---|---|
StackProperty |
IStackProperty |
максимум пачки (настройка), текущее количество (реактивное, сохраняется) |
MassProperty |
IMassProperty |
масса единицы × количество пачки |
VolumeProperty |
IVolumeProperty |
объём единицы × количество пачки |
ContainerProperty |
IContainerProperty |
вложенный инвентарь |
ContainerMassProperty |
IMassProperty |
свой вес + масса содержимого |
RigidVolumeProperty |
IVolumeProperty |
свой объём, не зависит от наполнения |
SoftVolumeProperty |
IVolumeProperty |
свой объём + объём содержимого |
Масса контейнера — отдельный класс на том же назначении IMassProperty, что и обычная масса: двух свойств на одном назначении в предмете не бывает, поэтому сумка несёт либо простую массу, либо контейнерную. Жёсткий и мягкий объём — выбор дизайнера классом.
Реактивность
Три события на инвентаре дают реактивность без полинга: OnItemAdded/OnItemRemoved (состав) и OnItemChanged (содержимое предмета). Последнее приходит пробросом: инвентарь подписан на ItemModel.OnChanged своих предметов и переизлучает его. Поэтому изменение значения свойства (количество, распад веса, зачарование) доходит до UI, даже когда его сделал внешний контроллер, не знающий про инвентарь. Верификаторы считают занятое по требованию, без кеша.
Контракт
Вход. Инвентарь настраивается в пресете хозяина. Состав меняется только через InventoryController.
Гарантии.
| № | Инвариант |
|---|---|
| Inv-1 | Предметы в инвентаре построены только шиной предметов |
| Inv-2 | Занятое согласовано с составом — пересчёт по требованию, без кеша |
| Inv-3 | Количество в пачке меняет только StackController, он же поднимает сигнал изменения |
| Inv-4 | Состав меняется только через контроллер |
| Inv-5 | Перемещение не теряет предмет — «проверка приёмника, затем изъятие» |
| Inv-6 | Контейнер не может оказаться внутри себя или своего потомка |
| Inv-7 | Добавление не приводит к превышению; уже возникшее превышение законно и пакетом не устраняется |
| Inv-8 | Количество пачки не выходит за Max: стартовый набор клампится, MergeIn и разбиение предел уважают |
Передача владения. Add и Move забирают владение предметом. При полном слиянии в существующие пачки входящий экземпляр поглощается и обнуляется — переиспользовать его ссылку нельзя.
Ограничения.
- Однородность предметов при слиянии определяется совпадением типа и наличием свойства пачки; состав и состояние прочих свойств не сверяются — за корректность стекуемых типов отвечает настройка.
- Верификаторы гейтят только размещение. Запретов на изъятие в этой версии нет.
- Идентичность класса свойства в сохранении — полное имя со сборкой; переименование сборки ломает сохранения предметов.
Использование
1. Объявить инвентарь в пресете хозяина
public class ChestPreset : RecordPreset<ChestModel>
{
[SerializeField] private Inventory inventory;
public Inventory Inventory => inventory;
}
public class ChestModel : Record
{
public Inventory Inventory { get; private set; }
// ...
}
Инвентарь переезжает в модель через CopyFrom при построении записи — как обычное свойство. Верификаторы, политика и стартовое наполнение приезжают из настройки.
2. Настроить в инспекторе
Набор верификаторов (полиморфный выбор класса), политика пачек, стартовое наполнение (пары «пресет предмета + количество»).
3. Операции
if (chest.Inventory.Add(item)) { /* размещён */ }
var taken = chest.Inventory.Take(item); // изъять целиком
var part = chest.Inventory.TakeAmount(item, 5); // отделить 5 из пачки
chest.Inventory.Drop(item); // изъять и уничтожить
item.Move(from: chest.Inventory, to: bag.Inventory);
4. Читать состав и занятое
foreach (var it in chest.Inventory.Items) { /* ... */ }
foreach (var v in chest.Inventory.Verifiers)
ShowBar(v.GetCurrent(chest.Inventory), v.GetMax(chest.Inventory)); // фильтры отдадут 0/0
5. Уничтожение
chest.Inventory.Dispose(); // содержимое удаляется рекурсивно, затем событие OnInventoryDestroyed
Событие приходит после удаления содержимого — оно для уборки, не для подбора. Кому содержимое нужно (высыпать на землю), забирает его до Dispose.
API
InventoryController (extension-методы)
| Член | Описание |
|---|---|
CanPlace(item) |
Структурная проверка цикла + опрос набора правил |
Add(item) |
Добавить с учётом политики пачек |
Take(item) |
Изъять целиком, вернуть предмет |
TakeAmount(item, n) |
Отделить n из пачки новым экземпляром |
Drop(item) |
Изъять и уничтожить |
Move(from, to) |
Переместить между инвентарями |
CountOf(presetGuid) |
Сколько единиц предмета данного типа суммарно, сквозь пачки |
CanAdd(presetGuid, n) |
Влезет ли n единиц — тихой пробой (без создания предмета и события) по верификаторам |
RemoveCount(presetGuid, n) |
Списать n единиц сквозь пачки; всё или ничего |
AddCount(presetGuid, n) |
Добавить n единиц, нарезая на пачки не крупнее Max; всё-или-ничего (откат при нехватке) |
Inventory
| Член | Описание |
|---|---|
Items |
Состав для чтения; первое обращение материализует инвентарь |
Verifiers |
Набор правил |
Policy |
Политика пачек |
OnItemAdded |
В состав вошёл новый экземпляр предмета |
OnItemRemoved |
Экземпляр покинул состав (изъятие или уничтожение) |
OnItemChanged |
У предмета в составе изменилось содержимое — количество, распад, добавленное/убранное свойство |
Dispose() |
Уничтожить с содержимым |
Три события дают реактивность без полинга: подпишись один раз и обновляй UI по дельтам. OnItemAdded/OnItemRemoved поднимает контроллер при изменении состава; OnItemChanged приходит пробросом от ItemModel.OnChanged подписанных предметов. Материализация (первичная загрузка/стартовое наполнение) событий не поднимает — это начальный снимок, его читают через Items, а на события подписываются для дельт.
InventoryBus
| Член | Описание |
|---|---|
OnInventoryDestroyed |
Инвентарь уничтожен вместе с содержимым |
Отладка
Флаг inventoryDebugMode в DebugSettings, подчинён глобальному отладочному режиму. Обе проверки заметно нагружают производительность:
- логирование слияния предметов с расходящимся составом или состоянием (сравнение через сериализацию в строку);
- поиск дубликатов после новой игры и загрузки — все живые инвентари отдают содержимое в общее множество, каждый повторный экземпляр логируется;
- протокол уничтожения — сколько и каких предметов удалено вместе с инвентарём.
Поиск дубликатов видит только материализованные инвентари, но только они держат живые ссылки на предметы, поэтому только между ними и возможен дубль по ссылке.
Защиту от попадания предмета в два инвентаря нельзя поставить в предмет — это развернуло бы зависимость сборок. Поэтому проверка диагностическая, в отладочном контуре.
Принятая дисциплина
- Однородность при слиянии не проверяется. Меч с полной прочностью и меч со сломанной сольются в пачку, состояние второго исчезнет. Осознанный размен: сверка на каждом добавлении дорога, а правильный набор стекуемых типов — вопрос настройки. Отладочный флаг помогает найти такие случаи.
- Настроечные данные не сохраняются. Умолчание сериализации — «сохраняется». Забытая пометка исключения на настроечном поле уведёт значение в сейв, перекроет настройку при загрузке, и правка баланса молча перестанет доходить до старых инвентарей. Настроечные поля инвентаря (верификаторы, политика, наполнение) хранятся полями, а не свойствами, поэтому сериализатор их не видит; новые свойства предметов делят поля на настроечные и игровые по правилам
ItemsSystem.
Граничные случаи
| Ситуация | Поведение |
|---|---|
| Пустой набор верификаторов | Влезает всё |
| Добавление в перегруженный инвентарь | Отказ: любая добавка не проходит проверку. Разгрузка приводит в норму |
| Загрузка сверх текущих ограничений | Проходит целиком, инвентарь остаётся в превышении; проверка на пути восстановления не применяется |
Стартовое наполнение сверх Max |
Количество клампится до Max с ошибкой в лог; многопачечный набор — несколько записей |
| Стартовое количество у нестекуемого предмета | Один экземпляр, ошибка в лог |
| Полное слияние входящего | Входящий поглощён и обнулён; владение забрано |
| Разбиение пачки | Новый экземпляр из пресета; состояние сверх количества не восстанавливается |
| Контейнер в себя или потомка | Отказ с ошибкой в лог (Inv-6) |
| Битое упакованное сохранение | Инвентарь пуст, ошибка в лог; исходная строка не затирается — шанс на восстановление сохраняется |
| Неразрешимый пресет предмета в сохранении | Болванка по правилам ItemsSystem; судьбу решает игровая логика |
| Чтение массы контейнера | Материализует вложенный инвентарь как побочный эффект: OnItemCreated для содержимого может сработать раньше, в том числе при отклонённом CanPlace. Суммы при этом верны — следствие ленивой модели |
| Уничтожение инвентаря | Содержимое удаляется рекурсивно, затем событие; повторный Dispose — no-op |