Interaction System
Сетевая система взаимодействий на композиции компонентов: server-authoritative логика (PurrNet), локальные UX-проверки без доверия клиенту и anti-flicker обратная связь при задержках сети. Игровые объекты собираются в инспекторе из 1–3 модулей — без отдельных классов под каждый тип объекта.
Карта папок
InteractionSystem/
├── Interactor/ сторона игрока: прицеливание, ввод, pending-запросы, промпт
├── Interactables/ объекты мира: Interactable + подключаемые модули
├── Inventory/ приватный сетевой инвентарь и определения предметов
├── NetworkOwnershipVisibility/ owner-only приватность identity + доки по PurrNet-спавну
├── Hud/ HUD локального игрока (промпт, кошелёк)
├── Editor/ рантайм-инспекторы состояния
├── InteractionLogger.cs логгер событий системы (+ живая документация по хукам)
├── Data/ профили подсветки, предметы (.asset)
└── InteractablePrefabs/ готовые примеры композиции
Ядро: объект мира
Interactable— sealed composition root, ничего от него не наследуется. Кэширует соседниеIInteractionModule-компоненты, агрегирует их проверки и стоимость (BuildCostHint), управляет подсветкой (Highlight Plus). На сервере выполняет двухфазную обработкуProcessInteractionOnServer: сначалаCheckServerу всех модулей, и только если все согласны —ApplyServerу всех. Мульти-модульный объект никогда не применяется наполовину (ключ не тратится на уже потраченную дверь).IInteractionModule— контракт модуля:CheckLocal(UX-подсказка, без авторитета),CheckServer/ApplyServer(авторитетная логика).IInteractionCostDescriptor— опциональная строка стоимости для тултипа («[-1 Key]»).InteractionCheck— результат локальной проверки: allow/deny + переопределение тултипа/подсветки/скрытия.InteractionResult— причина ответа сервера (Success / Busy / MissingRequiredItem / ...).
Модули
| Модуль | Состояние | Назначение |
|---|---|---|
NetworkToggle |
SyncVar<bool> |
двоичное состояние мира: дверь, рычаг, грядка; опция one-shot |
NetworkSeat |
SyncVar<bool> + SyncVar<PlayerID> |
занимаемое одним игроком место: стол крафта, терминал |
NetworkLock |
SyncVar<bool> |
одноразовый замок: требует (и опц. тратит) ключ при первом использовании |
InventoryTransaction |
— (читает инвентари) | требования / трата / награда предметами |
Визуал (анимации, звук) модули не трогают — он подключается через их UnityEvents
(On Turned On/Off, On Occupied/Released, On Unlocked) прямо на префабе.
Сторона игрока
PlayerInteractor— прицеливание (RaySensor), ввод, отправка запросов, события для UI. Оркестрирует три plain-хелпера (все[SerializeField]-настройки остаются на нём):PendingRequestTracker— запросы в полёте: дубликаты к одной цели запрещены, расходующий инвентарь запрос может быть только один одновременно;BusyLockTracker— автомат busy-лока и feedback-тиров (см. ниже);InteractionPromptPresenter— тултип + подсветка из цели и feedback-тира.NetworkInventory— дочерний объект player prefab с server-authoritativeSyncDictionary(ownerAuth: false). Identity принадлежит игроку, но словарь изменяет только сервер.NoVisibilityоставляет реплику только серверу и owner-клиенту. СобытиеChangedнесёт дельту (reward/spend) для локального UI.PlayerInteractorтоже owner-only (NoVisibility). Остальные клиенты получают публичную реплику аватара, но не input/RPC-компонент и не приватный инвентарь.- Оба приватных identity используют
OwnerPrivate: owner-RPC проверяются, TargetRpc работает на host, а disconnect не удаляет компоненты persistent player prefab. PlayerHud/InteractionPromptView— HUD-сервис (тоже через ServiceTag) и вью промпта с fade через LitMotion.
Флоу взаимодействия
прицел на объект ──► CheckLocal всех модулей (только UX: тултип/подсветка/скрытие)
│
нажатие E (принимается только при feedback == None)
│
pending.Add + busyLock.BeginLock ── объект прячется ДО RPC (на хосте сервер отвечает
│ синхронно — новое состояние не мигает)
ServerRpc ──► сервер: CheckServer(все) ─неудача─► TargetRpc(reason) ─► onReject, лок снят
│ │успех
│ ApplyServer(все) [+ Despawn для pickup]
│ │
◄── репликация SyncVar / SyncDictionary + TargetRpc(Success)
│
busyLock держит объект скрытым, пока не пройдёт LocalActionCooldown И новое состояние
реально не реплицируется (state epoch сдвинулся) ──► feedback = None, объект открыт
Ответственности: клиент — только предпросмотр и презентация, никаких изменений
состояния; сервер — единственный источник истины (инвентарь, SyncVar, despawn).
PlayerInteractor передаёт InteractionContext(PlayerID, NetworkInventory) через весь
локальный и серверный flow. Модули читают один и тот же инвентарь без DI и глобального
поиска; сервер перед применением проверяет, что RPC sender является owner интерактора.
Приватная часть player prefab
Player prefab — виден всем
└── owner-only identities
├── PlayerInteractor
└── NetworkInventory
Оба компонента входят в тот же prefab и получают ownership вместе с корнем через штатный
PlayerSpawner (поэтому не влияют на его проверку first connect / reconnect). Спавнятся они
вместе со всем префабом — AlwaysVisible, атомарно, кросс-ссылки целы, — а сразу после
назначения owner компонент NetworkOwnershipVisibility вешает на них NoVisibility и отписывает
всех, кроме owner+сервера. Что важно для взаимодействий/инвентаря:
- Доступность: инвентарь и интерактор есть у владельца и на хосте (сервер проверяет
транзакции и шлёт RPC); у прочих клиентов их нет вовсе —
SyncDictionaryим не синкается. - Очередность: приватная часть приезжает владельцу атомарно с корнем, а cull чужих идёт
уже после owner. Статический
NoVisibilityне годится: PurrNet не переоценивает видимость на спавне до назначения owner, и владелец не получил бы свою же приватку — нужен рантайм-рефреш (это и делает компонент). - Инвариант: на момент спавна
SyncDictionaryпуст (наполняется только после cull) — иначе начальное содержимое успело бы уйти чужим до отписки.
Детали — сам компонент, DefaultOwner, активность vs видимость — в
NetworkOwnershipVisibility/README.md; порядок спавна PurrNet,
реплики и флоу (старт хоста / клиент / реконнект) — в
NetworkOwnershipVisibility/SpawnFlow.md.
В проекте под NetworkOwnershipVisibility заведены оба — NetworkInventory и
PlayerInteractor. Для инвентаря видимость обязательна (иначе SyncDictionary течёт чужим);
для интерактора — по выбору (его логика всё равно под isOwner, и toggle бы справился), но раз
на репликах он мёртвый груз, его проще куллить целиком. Камеру/ввод у владельца включает toggle.
Feedback-тиры (NetworkFeedback)
Одна шкала времени на нажатие, отсчёт после LocalActionCooldown объекта:
None ──нажатие──► Busy (скрыт: кулдаун/ожидание) ──+waitingAfter──► Waiting («ждём сеть...»)
──+stalledAfter──► Stalled («ошибка сети»)
Быстрый round-trip никогда не показывает Waiting — короткие ожидания просто скрыты.
Рецепты композиции (реальные префабы)
| Префаб | Компоненты | Поведение |
|---|---|---|
| CoinPickup / KeyPickup | Interactable(Pickup) + InventoryTransaction(reward) |
подобрать, объект исчезает |
| PickupShovel | то же | небесконечный (нестакаемый) предмет |
| IteractableDoor | Interactable + NetworkLock(key) + NetworkToggle |
первый раз за ключ, дальше свободный toggle |
| Chest / KeyVendor | Interactable + InventoryTransaction(cost+reward) |
обмен предметов (вендор) |
| GardenPlot | Interactable + InventoryTransaction + NetworkToggle(oneShot) |
одноразовый сбор с требованием инструмента |
| CraftingTable | Interactable + NetworkSeat |
занял — для других «In use», повторное нажатие — выход |
| PotionPickup | Interactable(Pickup) |
подбор без наград (чистый despawn) |
Свой модуль — рецепт
- Компонент рядом с
Interactable, реализующийIInteractionModule(сетевое состояние —NetworkBehaviour+SyncVar). Инвентарь иPlayerIDдоступны через переданныйInteractionContext. CheckLocal— только чтение, верниInteractionCheck.Allow/Denyс UX-переопределениями.CheckServer— та же проверка по серверным данным;ApplyServer— мутация состояния. Оба вызываются только сервером, апдейт долетит через SyncVar.- По желанию:
IInteractionCostDescriptorдля строки стоимости, UnityEvents для визуала,interactable.RaiseChanged()при репликации состояния (обновит промпт у смотрящих).
Отладка
InteractionLogger(в сцене) — подписан на все события системы, группы логов включаются галочками в инспекторе или через публичные свойства в рантайме (позже — команды Quantum Console). Контекст-меню «Log local inventory» — дамп инвентаря.- Editor/ — рантайм-инспекторы:
NetworkInventory(живое содержимое),NetworkToggle/NetworkLock/NetworkSeat(текущее сетевое состояние) — всё в Play mode. - Сцена
InteractionsSandboxавтостартует хост с двумя локальными игроками — гонки и occupancy проверяются без второго билда.