Skip to content

13 — Мережева модель

🇬🇧 English | 🇺🇦 Українська

Розділ пояснює, як InteractionSystem працює в мережевій грі: що вирішує клієнт, що перевіряє сервер, які дані передаються і чому предмет може бути відхилено чи відпущено. Основи налаштування описані в 04 — Мультиплеєр та VR; тут — внутрішня механіка, потрібна для діагностики й власних розширень. Незнайомі терміни пояснено у словнику.

Простими словами. Клієнт — гравець, який бачить світ і просить дозволу. Сервер — суддя, який має остаточну версію світу. Гравець одразу бачить свою підказку й рух предмета у власній руці, але хто що тримає, записує лише суддя.

Хто що вирішує

Що Де вирішується Як дізнаються інші машини
Фокус, підсвітка, підказки Локально, на машині гравця Не реплікуються
Пози камери й рук Клієнт повідомляє, сервер перевіряє кожну руку окремо Ненадійний RPC ServerUpdatePoses і копія пакета в кожному запиті; іншим клієнтам — реплікована трансформація interactor
Захоплення, використання, відпускання Сервер Надійні RPC, replicated HeldGrip, ClientActionResult з причиною
Право на предмет Сервер, UInteractionClaimSubsystem Сам реєстр не реплікується; результат видно через HeldGrip і snapshot предмета
Рух, масштаб, прикріплення предмета Сервер; власник розв'язує рух у своїй руці сам (Direct) або передбачає (Physics) Replicated snapshot UInteractionManipulationComponent
Фізика, втикання, удари, шкода Сервер Snapshot, multicast для звуку й ефектів

RPC розміщені на UInteractorComponent pawn, яким володіє клієнт. Предметам не потрібно передавати network ownership.

Шлях запиту захоплення

  1. Клієнт. RequestGrab перевіряє, що компонент активний, pawn керується локально, трекінг надійний і рука вільна. Компонент збирає пакет поз усіх активних рук pawn, шукає ціль тим самим трасуванням, що й фокус, запам'ятовує її як очікувану, збільшує LocalRequestId і надсилає надійний ServerRequestAction(Target, Grab, RequestId, Poses).
  2. Сервер. Перевіряє умови в порядку з таблиці причин відмови і повертає першу причину, що спрацювала. Якщо всі перевірки пройдено, UInteractionClaimSubsystem::TryClaim резервує ціль за рукою, сервер записує HeldGrip = {Target, RequestId} і викликає BeginGrip цілі. Якщо BeginGrip повертає false, резерв і HeldGrip скасовуються, а клієнт отримує Unavailable. Ціль з IInteractableInterface отримує OnGrab, коли її бере перша рука.
  3. Відповідь. На кожен запит, що дійшов до активного на сервері компонента, сервер відповідає надійним ClientActionResult(Target, Action, bAccepted, RequestId, Reason), а HeldGrip доходить реплікацією. Якщо компонент на сервері неактивний, запит ігнорується без відповіді.
  4. Клієнт. Відмова очищає очікувану ціль. OnRep_HeldGrip очищає її лише для того самого RequestId, тому запізніла відповідь на старий запит не скасовує новий. Подія OnActionResult отримує результат і причину.
  5. Предмет. UInteractionManipulationComponent на сервері викликає OnGripStarted, перебудовує опору хвату й одразу надсилає snapshot.

Причини відмови

Кожна відповідь сервера містить EInteractionRejectReason. Сервер перевіряє умови в такому порядку й повертає першу, що не виконалась:

Причина Коли
Cooldown Від попереднього прийнятого запиту цього джерела минуло менше MinActionInterval
InvalidTarget Ціль не існує або в мережевій грі не реплікується
InvalidPose Пакет поз некоректний або поза джерела, що надсилає дію, не пройшла перевірку (відстань, видимість від очей pawn, таймаут)
HandBusy Рука вже тримає предмет або запит застарів
Claimed Ціль тримає чи резервує інший pawn
NotReachable Повторне трасування сервера не дістало ціль, хоча її можна було б узяти
Unavailable Ціль відмовила: захоплення вимкнене, немає місця, немає звичайної дії або BeginGrip повернув false

None означає, що запит прийнято. Причину бачить подія OnActionResult і панель налагодження (15). Як перетворити причини на зрозумілі гравцеві повідомлення — урок 3.

Звичайна дія

RequestInteraction має два варіанти:

  • рука тримає предмет — ціллю стає цей предмет без трасування, сервер викликає Use цілі захоплення, а маніпуляція — подію OnUsed;
  • рука вільна — сервер повторює трасування для Interact і викликає Interaction, якщо ціль реалізує IInteractableInterface і її ніхто не резервує.

Відпускання і кидок

RequestRelease(Linear, Angular) спершу відкидає нескінченні значення. Клієнт надсилає ServerRelease з тим RequestId, яким отримав предмет. Сервер виконує відпускання лише тоді, коли ціль і RequestId збігаються з HeldGrip. Якщо поза руки не пройшла перевірку або трекінг ненадійний, навмисне відпускання стає TrackingLost, і кидка не буде.

Під час навмисного відпускання (Released) сервер обрізає швидкості до MaxThrowSpeed і MaxAngularSpeed та передає їх EndGrip. Маніпуляція застосовує їх лише тоді, коли предмет відпускає остання рука, профіль має bAllowThrow=true, а тіло після відпускання симулюється. Ціль з інтерфейсом отримує OnRelease з причиною й обмеженими швидкостями. Швидкість обчислює ваш pawn у світових координатах; VR-pawn демо згладжує різницю позицій долоні між кадрами.

Пакет поз

Пакет — це масив до восьми записів, по одному на кожен активний UInteractorComponent pawn:

Поле Значення
Source Посилання на interactor цього pawn
Sequence Номер зразка цього джерела; сервер застосовує лише новіші за останній прийнятий
Position Світова позиція, квантована до 0.1 см
Orientation Світовий поворот
bTracked Чи надійна поза пристрою зараз

Хто надсилає. Періодичний пакет надсилає перший активний interactor pawn на клієнті з частотою свого PoseReplicationHz. Той самий пакет додається до кожного запиту дії та відпускання. Оскільки кожне джерело нумерує власні зразки, серверу байдуже, хто саме надіслав пакет: якщо на клієнті вимкнути першу руку, пакети почне надсилати наступна, і нумерація не зіб'ється.

Перевірка пакета. Пакет відкидається цілком лише тоді, коли він зіпсований: у pawn немає контролера, записів нуль або понад вісім, запис посилається на чуже або дубльоване джерело, або в пакеті немає джерела, що надсилає дію.

Перевірка кожного зразка. Решта перевірок стосується кожної руки окремо:

  1. джерело активне на сервері, а Sequence новіший за останній прийнятий; інакше зразок пропускається без змін;
  2. для bTracked=true: координати скінченні, позиція не далі за MaxOriginDistance від pawn;
  3. пряма від очей pawn (GetActorEyesViewPoint) до позиції не перетинає геометрію, що блокує TraceChannel джерела. Pawn і предмети в його руках ігноруються. Для ближнього хвату (bNearInteraction) допускається дотик до цілі захоплення ближче за NearRadius (не більше 30 см).

Прийнятий зразок переміщує серверну копію руки. Відхилений зразок позначає лише цю руку: вона зберігає останню прийняту позу, її дії відхиляються з InvalidPose, а відпускання не кидає предмет. Друга рука працює далі незалежно.

Простими словами. Якщо ліва рука сховалася за дверима, суддя не вірить лише лівій руці. Гвинтівка в правій руці залишається в правій руці.

Що бачать інші гравці. Відносна трансформація interactor реплікується всім, крім власника (COND_SkipOwner): власник не отримує корекцій своєї руки, а інші бачать серверну позу.

Таймаути, трекінг і примусове відпускання

Сервер відпускає предмет без кидка в таких випадках:

Умова Причина
pawn утратив контролер Cancelled
від віддаленого джерела довше за PoseTimeout не надходить жодного зразка TrackingLost
bTracked=false триває довше за TrackingLossGrace TrackingLost
CancelGrip(Reason) Reason; Released замінюється на Cancelled
Deactivate або EndPlay interactor Cancelled
ціль стала невалідною: не проходить ValidateSetup, точка хвату зникла, захоплення вимкнули через SetGrabEnabled, компонент неактивний Cancelled
у режимі Physics тіло відстало від бажаної пози на BreakDistance більше, ніж на своєму найближчому підході Cancelled

Таймаут рахує саме тишу: відхилені зразки теж підтверджують, що рука на зв'язку. Recentered і TargetInvalid призначені для вашого коду: викликайте CancelGrip(Recentered) перед телепортом або recenter, щоб старі швидкості не стали кидком.

Ідентичність запитів

Кожен RequestGrab отримує новий LocalRequestId. Сервер приймає лише номери, новіші за останній прийнятий для цієї руки, і зберігає ціль разом із номером у HeldGrip атомарно. Відпускання мусить збігтися з обома значеннями. Тому:

  • запізніле відпускання попереднього предмета не відпускає новий;
  • повторна доставка старого запиту не захоплює предмет удруге;
  • очікування відповіді (HasRequestedGrip()) блокує новий фокус і новий RequestGrab цієї руки.

Стан предмета на клієнтах

UInteractionManipulationComponent реплікує одну структуру State: налаштування, руки й точки хвату, кількість рук, опору хвату, позу з масштабом, відставання фізичного тіла, ознаку симуляції, прикріплення, зовнішній драйвер і хід ручки. Поле Revision змінюється, коли треба повторно застосувати топологію хвату, прикріплення чи фізику.

  • Компактна поза. Позиція квантується до 0.01 см, масштаб — до 0.01 (обидва FVector_NetQuantize100), поворот передається стиснутим FRotator. Зміни, менші за 0.01 см, 0.01° чи 0.001 масштабу, не надсилаються.
  • Push model. Сервер позначає State зміненим лише тоді, коли знімок справді змінився. Якщо push model увімкнено (net.IsPushModelEnabled=1 і збірка з WITH_PUSH_MODEL, див. Продуктивність), незмінний знімок навіть не порівнюється під час реплікації. Без push model поведінка та сама, лише сервер порівнює знімок щоразу.
  • Клієнти не симулюють фізику предмета: вони плавно наближають позу до серверної з RemoteInterpolationSpeed.
  • Власник розв'язує позу від власних рук: для Direct — завжди, для Physics — поки серверне тіло не відстає більше ніж на половину OwnerPredictionTolerance; далі він плавно переходить до серверної пози (08).
  • Прикріплений предмет на клієнті слідує за батьківським компонентом. Якщо посилання на батька не реплікується, клієнт і далі отримує світову позу зі snapshot.
  • Гравець, що приєднався пізніше, отримує повний snapshot і HeldGrip, тож бачить поточних власників і пози.
  • Важливі зміни викликають FlushNetDormancy і ForceNetUpdate. Не робіть рухомі предмети dormant.

Ролі мережі

Режим Особливості
Dedicated server Усі перевірки й таймаути, як описано вище
Listen server Pawn хоста — локальний authority: дії виконуються без RPC, OnActionResult спрацьовує локально, таймауту пози для хоста немає
Одиночна гра Як у хоста listen-сервера; перевірки пози, відстані й інтервалу дій також виконуються
AI-pawn на сервері IsLocallyControlled() повертає true: фокус шукається з частотою FocusUpdateHz, дії виконуються напряму, OnActionResult спрацьовує на сервері. Якщо AI не потрібен фокус, зменште FocusUpdateHz; деактивація компонента відпускає утримуваний предмет

Вимоги до акторів

  • Pawn має Replicates=true; UInteractorComponent реплікується сам.
  • Ціль у мережевій грі має Replicates=true, а її колізія блокує TraceChannel.
  • Ціль маніпуляції — Movable; вона або не реплікується, або є UInteractionMeshComponent.
  • Не вмикайте ReplicateMovement на акторах із UInteractionManipulationComponent: компонент вимикає його сам, щоб рух мав одного «письменника».
  • Компоненти, до яких прикріплюють предмети, мають бути доступні для реплікації.
  • bGrabEnabled і bCanBeGrabbed реплікуються: змінюйте їх на сервері.

Що перевіряє сервер, а що — ні

Сервер перевіряє межі пози, видимість від очей pawn, інтервал між діями, повторне трасування, теги й CanInteract, реєстр захоплень, номери запитів і зразків, скінченність і межі швидкостей. Конфігурація сервера має пріоритет над клієнтською.

Сервер не може перевірити, куди саме гравець навів руку в межах допустимої зони, і не перевіряє напрямок кидка. Це обмеження будь-якої моделі з клієнтським трекінгом: геометричні межі звужують можливості читера, але не є повною античит-системою.

Автоматичні тести мережі

Тести InteractionSystem.Network.* запускають dedicated server і клієнтів в одному процесі PIE: конкуренцію за предмет і повний цикл захоплення, закриту перешкодою вільну руку, локально вимкнену першу руку, реплікацію дозволу на захоплення й передбачення фізичного предмета під затримкою пакетів. Перелік — у 04. Окремі машини, втрати пакетів і late join перевіряйте вручну, наприклад консольними командами Net PktLag=100 і Net PktLoss=5 у PIE, з панеллю налагодження.