Games keep needing "entity B rides on entity A at a fixed offset": a passenger in a car, a cuffed player escorted by an officer, a carried object. The Framework has no concept of this, so each game builds its own. The obvious version (send the child's position like any other entity) can't stay in sync while they move.
What exists today
GetInterestDependency() covers the streaming half: a child pulls its parent into the viewer's interest set.
|
// --- Game extension points --- |
|
// Server interest: an entity this one makes no sense without (a seated ped needs its car). |
|
// Survivors of a per-type budget pull it in with them, uncounted. Followed one level. |
|
virtual NetworkEntity *GetInterestDependency() { |
|
return nullptr; |
|
} |
Everything else is up to each game: where the relationship is stored, whether the child's own transform is still sent, and what happens when the parent goes away.
Why sending the child's position doesn't work
In Hogwarts Legacy MP we hold an escorted player next to the officer. The server clamps the prisoner's streamed position near the officer, and the prisoner's own client attaches its pawn to its local copy of the officer. On the prisoner's screen it looks right. On everyone else's screen the prisoner trails the officer and swings wide in turns.
Tuning can't fix this. Observers draw the prisoner from the prisoner's streamed transform, which is:
- computed from the prisoner client's copy of the officer, which is already one relay old (officer → server → prisoner), then
- relayed a second time (prisoner → server → observer).
So observers get two separate transform streams sampled at different times, and the child arrives about one relay behind its parent. No clamping or smoothing makes two separately sampled streams line up in a turn.
How FiveM does it
FiveM replicates the relationship, not the child's position. CPhysicalAttachDataNode carries the parent id, an offset, an orientation and bone indices:
https://github.com/citizenfx/fivem/blob/ab3ee4506333e02e4beb847c3db5467ae263ee6e/code/components/citizen-server-impl/include/state/SyncTrees_Five.h#L587-L646
Every client attaches the child to its own copy of the parent and works out the child's pose each frame. The parent may lag on a given screen, but the child is placed from that same lagged copy, so the pair always moves as one body. That's why GTA RP escorts stay glued together through turns.
Suggested design
Attachment as server-written state on NetworkEntity, applied by the game on every client:
- State:
{ parent NetworkID, offset vec3, rotation quat, socket string } as ServerFields, sent on construction (for late joiners) and as a delta when it changes. The Framework never interprets socket; it's the game's bone or socket name.
- Streaming: the default
GetInterestDependency() returns the attachment parent, so games don't need to override it for this case.
- Transform: while attached, the server ignores the owner's upstream transform and stores parent pose × offset as the child's position, so server-side range, interest and proximity checks stay correct. Observers don't interpolate the child.
- Game hook: a client virtual such as
OnAttachmentChanged(), where the game performs the real attach (UE AttachToActor, a vehicle seat native, …). If the parent isn't constructed on that client yet, the Framework defers the call until it is.
- Lifecycle: detach automatically when the parent is destroyed; refuse cycles (with a depth cap) at attach time.
- Scripting:
entity.attachTo(parent, { offset, rotation, socket }), entity.detach(), entity.getAttachment(). Server-only in v1, so a client can't attach itself to another player or slip out of cuffs.
Open questions
- Should an owner be able to attach its own entity? FiveM allows it, and some cases are naturally owner-initiated, such as a player getting into a car. This could come later behind the delegation policy.
- Is a rigid attach enough for v1, or do we also need a "soft" mode where the child keeps its own transform relative to the parent (walking on a moving platform)?
Games keep needing "entity B rides on entity A at a fixed offset": a passenger in a car, a cuffed player escorted by an officer, a carried object. The Framework has no concept of this, so each game builds its own. The obvious version (send the child's position like any other entity) can't stay in sync while they move.
What exists today
GetInterestDependency()covers the streaming half: a child pulls its parent into the viewer's interest set.Framework/code/framework/src/networking/replication/network_entity.h
Lines 225 to 230 in 971dc63
Everything else is up to each game: where the relationship is stored, whether the child's own transform is still sent, and what happens when the parent goes away.
Why sending the child's position doesn't work
In Hogwarts Legacy MP we hold an escorted player next to the officer. The server clamps the prisoner's streamed position near the officer, and the prisoner's own client attaches its pawn to its local copy of the officer. On the prisoner's screen it looks right. On everyone else's screen the prisoner trails the officer and swings wide in turns.
Tuning can't fix this. Observers draw the prisoner from the prisoner's streamed transform, which is:
So observers get two separate transform streams sampled at different times, and the child arrives about one relay behind its parent. No clamping or smoothing makes two separately sampled streams line up in a turn.
How FiveM does it
FiveM replicates the relationship, not the child's position.
CPhysicalAttachDataNodecarries the parent id, an offset, an orientation and bone indices:https://github.com/citizenfx/fivem/blob/ab3ee4506333e02e4beb847c3db5467ae263ee6e/code/components/citizen-server-impl/include/state/SyncTrees_Five.h#L587-L646
Every client attaches the child to its own copy of the parent and works out the child's pose each frame. The parent may lag on a given screen, but the child is placed from that same lagged copy, so the pair always moves as one body. That's why GTA RP escorts stay glued together through turns.
Suggested design
Attachment as server-written state on
NetworkEntity, applied by the game on every client:{ parent NetworkID, offset vec3, rotation quat, socket string }asServerFields, sent on construction (for late joiners) and as a delta when it changes. The Framework never interpretssocket; it's the game's bone or socket name.GetInterestDependency()returns the attachment parent, so games don't need to override it for this case.OnAttachmentChanged(), where the game performs the real attach (UEAttachToActor, a vehicle seat native, …). If the parent isn't constructed on that client yet, the Framework defers the call until it is.entity.attachTo(parent, { offset, rotation, socket }),entity.detach(),entity.getAttachment(). Server-only in v1, so a client can't attach itself to another player or slip out of cuffs.Open questions