Перейти к содержанию

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-authoritative SyncDictionary(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)

Свой модуль — рецепт

  1. Компонент рядом с Interactable, реализующий IInteractionModule (сетевое состояние — NetworkBehaviour + SyncVar). Инвентарь и PlayerID доступны через переданный InteractionContext.
  2. CheckLocal — только чтение, верни InteractionCheck.Allow/Deny с UX-переопределениями.
  3. CheckServer — та же проверка по серверным данным; ApplyServer — мутация состояния. Оба вызываются только сервером, апдейт долетит через SyncVar.
  4. По желанию: IInteractionCostDescriptor для строки стоимости, UnityEvents для визуала, interactable.RaiseChanged() при репликации состояния (обновит промпт у смотрящих).

Отладка

  • InteractionLogger (в сцене) — подписан на все события системы, группы логов включаются галочками в инспекторе или через публичные свойства в рантайме (позже — команды Quantum Console). Контекст-меню «Log local inventory» — дамп инвентаря.
  • Editor/ — рантайм-инспекторы: NetworkInventory (живое содержимое), NetworkToggle / NetworkLock / NetworkSeat (текущее сетевое состояние) — всё в Play mode.
  • Сцена InteractionsSandbox автостартует хост с двумя локальными игроками — гонки и occupancy проверяются без второго билда.