Пайплайн переноса
Эта страница объясняет, что происходит, когда игрок отпускает предмет над слотом, инвентарём или drop-зоной.
Главный смысл пайплайна: система проверяет, можно ли выполнить действие, выбирает подходящее поведение для цели и либо переносит предмет, либо оставляет инвентари в валидном состоянии.
Общая картина
flowchart TD
DROP["Игрок отпускает предмет"] --> TARGET{"Куда отпустили?"}
TARGET --> SLOT["На слот"]
TARGET --> AREA["На область инвентаря"]
SLOT --> CHECK{"Слот может принять предмет?"}
AREA --> AUTO["Найти подходящее слот"]
AUTO --> CHECK
CHECK -->|да| MOVE["Перенести или объединить стек"]
CHECK -->|нет| POLICY{"Что разрешает policy?"}
POLICY -->|Reject| REJECT["Отклонить перенос и вернуть предмет на место"]
POLICY -->|Swap| SWAP["Попробовать swap"]
POLICY -->|Find alternative| ALTERNATIVE["Попробовать другой слот"]
ALTERNATIVE --> CHECK
MOVE --> SYNC["Обновить UI и игровые данные через DataBinding"]
SWAP --> SYNC
Что происходит при переносе
Типичный перенос проходит так:
- Система определяет источник, цель и активные настройки drop policy.
- Если у инвентарей разные модели предметов, предмет приводится к модели целевого инвентаря.
- Система проверяет, может ли выбранная цель принять предмет.
- Если цель подходит, предмет переносится или объединяется со стеком в целевом слоте.
- Если цель не подходит,
DropPolicySettingsв соответствии с выбранными опциями решает, что делать дальше: отклонить перенос, найти другое место или попробовать swap. - При успехе отправляются события и обновляется DataBinding. При ошибке состояние возвращается к сохранённой копии, которая была сделана перед попыткой переноса.
Случаи
Цель заблокирована
Выбранный слот не может принять предмет (занят, несовместим, полон). Что дальше —
определяет BlockedTargetResolutionKind:
| Значение | Поведение |
|---|---|
Reject |
Запись падает. Ничего не двигается. |
FindAlternative |
Стратегия дает доступных кандидатов и настроенный PlacementCandidateOrderer упорядочивает их по приоритету. |
Swap |
Пытается выполнить одиночный swap с занятой целью. |
AllowSameInventoryAlternativePlacement определяет, может ли FindAlternative, при переносе предмета в рамках одного инвентаря на занятый слот, выбрать другой слот (новый) внутри того же инвентаря. Если нет то предмет остается где был до попытки переноса.
Drop в область / авто-перенос
Конкретного слота нет (отпустили на область инвентаря, либо двойной клик/авто-перенос
запустил AutoTransferService). Движок пропускает шаг явной цели и сразу берет кандидатов у стратегии и упорядочивает их.
Большой стек на несколько размещений
Стек из 10 — это всё равно одна запись, даже если цель раскидает его по нескольким размещениям. Движок:
- создаёт рабочий стек нужного размера;
- спрашивает у стратегии слоты кандидатов с свободным местом (Unique например по 1 предмету на слот);
- отделяет ровно это количество и применяет кандидата;
- обрабатывает кандидатов заново по уже изменённому состоянию и повторяет пока не закончится стек или слоты в инвентаре;
- возвращает не влезшее источнику.
Пример: 10 предметов в Unique-инвентарь с 6 свободными слотами → перенесено 6, возвращено 4.
Частично или только целиком
Параметр в policy инвентаря _allowPartial решает, что делать, если влезает лишь часть записи:
true=Allow— фиксируем то, что влезло, остаток возвращаем источнику.false=RequireFull— если нельзя разместить весь объём, запись откатывается.
Перемещение внутри одного инвентаря
При перемещении внутри одного инвентаря источник не освобождается вслепую: частичная попытка держит источник занятым и исключает его из списка кандидатов, чтобы целевые размещения не заняли те самые клетки, куда остаток должен вернуться. Полное перемещение может выполнить отдельную попытку с временно удалённым источником и обязано перенести запись целиком, иначе откат.
Занятая цель с обработчиком
Если предмет бросили на занятый слот, DataBinding целевого инвентаря может забрать эту ситуацию себе через IOccupiedSlotDropHandler.
Есть два варианта timing:
| Интерфейс | Когда проверяется | Когда использовать |
|---|---|---|
IPreRuleOccupiedSlotDropHandler |
до drop-правил целевого слота | drop на занятый слот на самом деле означает другую цель, например контейнер внутри слота |
IPostRuleOccupiedSlotDropHandler |
после drop-правил целевого слота | кастомное поведение должно сначала пройти обычные правила цели |
ExecuteOccupiedSlotDrop возвращает OccupiedSlotDropResult:
Handled— handler выполнил действие сам; обычное размещение, alternative placement и swap не запускаются.Rejected— handler запретил действие; перенос отменяется и состояние возвращается к сохранённой копии.Fallthrough— handler не забирает эту попытку; дальше работает обычный pipeline для занятого слота.
Batch (несколько записей сразу)
flowchart TD
A["Однократная проверка запроса<br/>CanStartTransfer / CanStartTransferAsync"] --> B["Запись (commit) 1"]
B --> C["Запись 2 видит зафиксированный результат записи 1"]
C --> D["Запись 3 ..."]
D --> E["Завершение переноса"]
- Записи переноса идут в порядке drag-context; каждая видит зафиксированный результат предыдущей.
- Упавшая запись откатывается сама; предыдущие переносы остаются.
- Выбранный слот цели применяется только к первой записи; остальные ведут себя как drop в область.
Batch-swap (несколько перетаскиваемых предметов на одну занятую цель с Swap)
отклоняется до любой мутации.
Частичный перенос применяется на запись. RequireFull никогда не отменяет предыдущие записи batch.
Валидации
Есть несколько видов проверок разрешающих и запрещающих перенос, и срабатывают они в разное время.
| Слой | Интерфейс | На какой вопрос отвечает | Когда срабатывает |
|---|---|---|---|
| Правила | IGlobalRule / IInventoryRule / ISlotRule |
Механически, можно ли сюда этот предмет? (фильтр типа, залоченный слот…) | старт перетаскивания и валидация цели |
| Вето на весь перенос | ITransferDomainHandler.CanStartTransfer |
Можно ли вообще начинать операцию? | один раз, до первой мутации |
| Асинхронное вето | IAsyncTransferDomainHandler.CanStartTransferAsync |
То же, но нужно что-то дождаться (сервер, диск) | один раз, только async-путь, до первой мутации |
| Проверка на commit | ITransferDomainHandler.CanCommitTransfer |
Можно ли зафиксировать это конкретное размещение? (хватает золота, владение) | прямо перед каждой мутацией кандидата |
| Побочные эффекты успеха | ITransferDomainHandler.OnTransferSucceeded |
Реакция после зафиксированного размещения | после успешного commit |
Правила вешаются напрямую на объекты слота или инвентаря. В DataBinding по умолчанию есть доступный для определения их аналог — CanStartDrag, CanDrop и CanSwap. Интерфейсами ITransferDomainHandler и IAsyncTransferDomainHandler в binding можно добавить дополнительные проверки и действия для всего переноса.
Когда срабатывают события
События и уведомления DataBinding для записи отправляются только после фиксации этой записи и до старта следующей. Это гарантирует, что следующая запись видит состояние, совпадающее с живым инвентарём.
Каждое событие add/remove несёт точный реально перенесённый sub-стек — никогда не
сырой DragEntry.Stack. Запись из 10, легшая как 6 + 4, даёт события суммарно на 10
адаптеров; запись из 10, где перенеслось 6, а 4 вернулись, — события на 6.
Точки расширения
Всё, что предназначено для подключения своей логики, в одном месте:
| Точка расширения | Тип | Для чего |
|---|---|---|
| Стратегия размещения | IStrategy / InventoryStrategyBase |
Как предметы занимают слоты: unique, stackable, separable или своя логика merge/create/capacity. Read-only; выдаёт кандидатов. |
| Топология | IInventoryTopology (SlotTopology, RectGridTopology, своя) |
Пространство клеток: сколько клеток, как проецируется форма, шаги ориентации и визуальные углы. Гекс-грид — просто топология с 6 шагами. |
| Форма размещения | IPlacementShape (RectPlacementShape, ComplexPlacementShape) |
Footprint предмета, включая непрямоугольные (L/T/крест) формы и их повороты. |
| Упорядочиватель кандидатов | PlacementCandidateOrderer |
Влияет на то, какой слот предпочтёт автоматическое размещение (merge-first, empty-first и т. д.). К явной цели не применяется. |
| Политика заблокированной цели | BlockedTargetResolutionKind + DropPolicySettings |
Выбор reject / find-alternative / swap, плюс частичный перенос и same-inventory опции. Только данные, задаётся в инспекторе. |
| Вето на весь перенос | ITransferDomainHandler.CanStartTransfer |
Разрешить/запретить всю операцию до любых изменений (например, «магазин закрыт»). |
| Асинхронное вето | IAsyncTransferDomainHandler.CanStartTransferAsync |
То же, когда ответ требует ожидания сервера, файла или БД. |
| Бизнес-проверка на commit | ITransferDomainHandler.CanCommitTransfer |
Разрешить/запретить одно конкретное размещение (хватает золота, владение). |
| Хук успеха | ITransferDomainHandler.OnTransferSucceeded |
Побочные эффекты после зафиксированного размещения (списать золото, аналитика). |
| Обработчик занятого слота | IPreRuleOccupiedSlotDropHandler / IPostRuleOccupiedSlotDropHandler |
Своё поведение при drop на занятый слот (надеть, вложить в контейнер). Реализуется на DataBinding. |
| Жизненный цикл динамических слотов | IDynamicSlotLifecycle |
Позволяет инвентарю расти/сжиматься; созданием/удалением управляет движок. |
| Конвертер предметов | IItemAdapterConverter (через CreateItemConverter()) |
Перевод предметов между инвентарями с разными моделями адаптеров. |
| Правила | IGlobalRule / IInventoryRule / ISlotRule |
Декларативные механические ограничения на трёх уровнях. |
| Drop-зоны | DropAreaBase |
Свои цели для отпускания (мусорка, продажа, спавн в мире) без вмешательства в пайплайн. |