13 — Network model¶
🇬🇧 English | 🇺🇦 Українська
This chapter explains how InteractionSystem works in a networked game: what the client decides, what the server validates, what data is transferred and why an item can be rejected or released. The setup basics are in 04 — Multiplayer and VR; here is the internal mechanics needed for diagnostics and your own extensions. Unfamiliar terms are explained in the glossary.
In plain words. The client is a player who sees the world and asks permission. The server is the referee with the final version of the world. The player at once sees their own prompt and the motion of the item in their own hand, but who holds what is written down only by the referee.
Who decides what¶
| What | Where it is decided | How other machines learn |
|---|---|---|
| Focus, highlighting, prompts | Locally, on the player's machine | Not replicated |
| Camera and hand poses | The client reports, the server validates each hand separately | The unreliable RPC ServerUpdatePoses and a copy of the packet in every request; to other clients — the replicated transform of the interactor |
| Grab, use, release | Server | Reliable RPCs, replicated HeldGrip, ClientActionResult with a reason |
| Right to an item | Server, UInteractionClaimSubsystem |
The ledger itself is not replicated; the result is visible through HeldGrip and the item snapshot |
| Item motion, scale, attachment | Server; the owner solves motion in their own hand (Direct) or predicts it (Physics) |
The replicated snapshot of UInteractionManipulationComponent |
| Physics, embedding, impacts, damage | Server | Snapshot, multicast for sound and effects |
RPCs live on the UInteractorComponent of a pawn owned by the client. Items do not need to transfer network ownership.
The grab request path¶
- Client.
RequestGrabchecks that the component is active, the pawn is locally controlled, tracking is reliable and the hand is free. The component collects a packet of the poses of all active hands of the pawn, searches for the target with the same trace as focus, remembers it as pending, incrementsLocalRequestIdand sends a reliableServerRequestAction(Target, Grab, RequestId, Poses). - Server. Checks the conditions in the order of the rejection reasons table and returns the first reason that fired. If all checks pass,
UInteractionClaimSubsystem::TryClaimreserves the target for the hand, the server writesHeldGrip = {Target, RequestId}and calls the target'sBeginGrip. IfBeginGripreturnsfalse, the reservation andHeldGripare cancelled and the client receivesUnavailable. A target withIInteractableInterfacereceivesOnGrabwhen the first hand takes it. - Answer. To every request that reached a component active on the server, the server replies with a reliable
ClientActionResult(Target, Action, bAccepted, RequestId, Reason), andHeldGriparrives through replication. If the component is inactive on the server, the request is ignored without an answer. - Client. A rejection clears the pending target.
OnRep_HeldGripclears it only for the sameRequestId, so a late answer to an old request does not cancel a new one. TheOnActionResultevent receives the result and the reason. - Item. On the server
UInteractionManipulationComponentcallsOnGripStarted, rebuilds the grip baseline and immediately sends the snapshot.
Rejection reasons¶
Every server answer contains an EInteractionRejectReason. The server checks the conditions in this order and returns the first one that was not met:
| Reason | When |
|---|---|
Cooldown |
Less than MinActionInterval has passed since the previous accepted request of this source |
InvalidTarget |
The target does not exist or, in a networked game, is not replicated |
InvalidPose |
The pose packet is malformed or the pose of the source sending the action failed validation (distance, visibility from the pawn's eyes, timeout) |
HandBusy |
The hand already holds an item or the request is stale |
Claimed |
The target is held or reserved by another pawn |
NotReachable |
The server's repeated trace did not reach the target, although it could have been taken |
Unavailable |
The target refused: grabbing is off, there is no room, there is no ordinary action, or BeginGrip returned false |
None means the request was accepted. The reason is visible in the OnActionResult event and in the debug panel (15). How to turn the reasons into messages that make sense to a player — lesson 3.
An ordinary action¶
RequestInteraction has two variants:
- the hand holds an item — the target becomes this item without a trace, the server calls the grab target's
Use, and the manipulation calls theOnUsedevent; - the hand is free — the server repeats the trace for
Interactand callsInteractionif the target implementsIInteractableInterfaceand nobody reserves it.
Release and throw¶
RequestRelease(Linear, Angular) first discards infinite values. The client sends ServerRelease with the same RequestId it received the item with. The server performs the release only if the target and RequestId match HeldGrip. If the hand's pose failed validation or tracking is unreliable, an intentional release becomes TrackingLost, and there will be no throw.
On an intentional release (Released) the server clamps the velocities to MaxThrowSpeed and MaxAngularSpeed and passes them to EndGrip. The manipulation applies them only when the last hand releases the item, the profile has bAllowThrow=true and the body is simulated after the release. A target with the interface receives OnRelease with the reason and the clamped velocities. Your pawn computes the velocity in world coordinates; the demo VR pawn smooths the palm position difference between frames.
Pose packet¶
A packet is an array of up to eight entries, one for each active UInteractorComponent of the pawn:
| Field | Meaning |
|---|---|
Source |
A reference to this pawn's interactor |
Sequence |
The sample number of this source; the server applies only those newer than the last accepted |
Position |
World position, quantized to 0.1 cm |
Orientation |
World rotation |
bTracked |
Whether the device pose is reliable now |
Who sends. The periodic packet is sent by the first active interactor of the pawn on the client at its own PoseReplicationHz. The same packet is attached to every action and release request. Because each source numbers its own samples, the server does not care who sent the packet: if the first hand is deactivated on the client, the next one starts sending packets and the numbering does not break.
Packet validation. A packet is rejected as a whole only when it is malformed: the pawn has no controller, there are zero entries or more than eight, an entry refers to someone else's or a duplicated source, or the source sending the action is absent from the packet.
Per-sample validation. The remaining checks apply to each hand separately:
- the source is active on the server and its
Sequenceis newer than the last accepted; otherwise the sample is skipped without changes; - for
bTracked=true: coordinates are finite, the position is no farther thanMaxOriginDistancefrom the pawn; - the straight line from the pawn's eyes (
GetActorEyesViewPoint) to the position does not cross geometry that blocks the source'sTraceChannel. The pawn and the items in its hands are ignored. For near grip (bNearInteraction) touching a grab target closer thanNearRadius(at most 30 cm) is allowed.
An accepted sample moves the server copy of the hand. A rejected sample marks only this hand: it keeps the last accepted pose, its actions are rejected with InvalidPose, and a release does not throw the item. The other hand works on independently.
In plain words. If the left hand hid behind a door, the referee distrusts only the left hand. The rifle in the right hand stays in the right hand.
What other players see. The relative transform of the interactor is replicated to everyone except the owner (COND_SkipOwner): the owner receives no corrections of their own hand, and others see the server pose.
Timeouts, tracking and forced release¶
The server releases an item without a throw in these cases:
| Condition | Reason |
|---|---|
| the pawn lost its controller | Cancelled |
no sample at all arrives from a remote source for longer than PoseTimeout |
TrackingLost |
bTracked=false lasts longer than TrackingLossGrace |
TrackingLost |
CancelGrip(Reason) |
Reason; Released is replaced with Cancelled |
Deactivate or EndPlay of the interactor |
Cancelled |
the target became invalid: it fails ValidateSetup, the grip point disappeared, grabbing was turned off through SetGrabEnabled, the component is inactive |
Cancelled |
in Physics mode the body lagged behind the desired pose by more than BreakDistance over its closest approach |
Cancelled |
The timeout counts silence: rejected samples also confirm that the hand is still in contact. Recentered and TargetInvalid are meant for your code: call CancelGrip(Recentered) before a teleport or recenter so that old velocities do not become a throw.
Request identity¶
Every RequestGrab gets a new LocalRequestId. The server accepts only numbers newer than the last accepted for this hand and stores the target together with the number in HeldGrip atomically. A release must match both values. Therefore:
- a late release of the previous item does not release the new one;
- repeated delivery of an old request does not grab the item a second time;
- waiting for an answer (
HasRequestedGrip()) blocks new focus and a newRequestGrabof this hand.
Item state on clients¶
UInteractionManipulationComponent replicates one State structure: settings, hands and grip points, the number of hands, the grip baseline, the pose with scale, the lag of the physical body, the simulation flag, attachment, the external driver and the handle travel. The Revision field changes when the grip topology, attachment or physics must be applied again.
- Compact pose. The position is quantized to 0.01 cm, the scale to 0.01 (both
FVector_NetQuantize100), and the rotation is sent as a compressedFRotator. Changes smaller than 0.01 cm, 0.01° or 0.001 of scale are not sent. - Push model. The server marks
Stateas changed only when the snapshot really changed. If push model is on (net.IsPushModelEnabled=1and a build withWITH_PUSH_MODEL, see Performance), an unchanged snapshot is not even compared during replication. Without push model the behavior is the same, only the server compares the snapshot every time. - Clients do not simulate the item's physics: they smoothly move the pose toward the server's with
RemoteInterpolationSpeed. - The owner solves the pose from their own hands: for
Directalways, forPhysicswhile the server body does not lag by more than half ofOwnerPredictionTolerance; beyond that they smoothly move to the server pose (08). - An attached item on a client follows its parent component. If the reference to the parent is not replicated, the client still receives the world pose from the snapshot.
- A player who joined later receives the full snapshot and
HeldGrip, so they see the current owners and poses. - Important changes call
FlushNetDormancyandForceNetUpdate. Do not make moving items dormant.
Network roles¶
| Mode | Notes |
|---|---|
| Dedicated server | All checks and timeouts as described above |
| Listen server | The host's pawn is a local authority: actions run without RPCs, OnActionResult fires locally, there is no pose timeout for the host |
| Single-player | Like the host of a listen server; pose, distance and action interval checks are also performed |
| AI pawn on the server | IsLocallyControlled() returns true: focus is searched at FocusUpdateHz, actions run directly, OnActionResult fires on the server. If the AI does not need focus, reduce FocusUpdateHz; deactivating the component releases the held item |
Actor requirements¶
- The pawn has
Replicates=true;UInteractorComponentreplicates itself. - A target in a networked game has
Replicates=true, and its collision blocksTraceChannel. - A manipulation target is
Movable; it is either not replicated or is aUInteractionMeshComponent. - Do not enable
ReplicateMovementon actors withUInteractionManipulationComponent: the component turns it off itself so that motion has one "writer". - Components that items are attached to must be available for replication.
bGrabEnabledandbCanBeGrabbedare replicated: change them on the server.
What the server checks and what it does not¶
The server checks pose limits, visibility from the pawn's eyes, the interval between actions, the repeated trace, tags and CanInteract, the claim ledger, request and sample numbers, finiteness and speed limits. The server configuration has priority over the client's.
The server cannot verify exactly where the player aimed the hand within the permitted zone, and it does not check the throw direction. This is a limitation of any model with client-side tracking: geometric limits narrow a cheater's options but are not a complete anti-cheat system.
Automated network tests¶
The InteractionSystem.Network.* tests run a dedicated server and clients in one PIE process: competition for an item and the full grab cycle, a free hand hidden by an obstacle, a locally deactivated first hand, replication of the grab permission and prediction of a physical item under packet latency. The list is in 04. Check separate machines, packet loss and late join manually, for example with the console commands Net PktLag=100 and Net PktLoss=5 in PIE, with the debug panel.