Problem with parent replication (SceneComponent and Actor)

Hello,

I have encountered issues with the replication of USceneComponent::AttachParent. Sometimes, the server performs an Attach or Detach on a replicated component, and on the clients, the position is incorrect. I analyzed the netcode behind this and identified five different errors.

Error 1: Client changes the parent without calling AttachToComponent / DetachFromComponent

If a replicated component A attaches to a replicated component B, it is possible that USceneComponent::OnRep_AttachChildren on component B is called before USceneComponent::PostRepNotifies on component A. If this happens, component B will see that child A in its AttachChildren array does not have B as its parent and will call SetAttachParent(this). As a result, OnRep_AttachParent on A will never be called because AttachParent already has the correct value.

This means child A is “attached” without going through AttachToComponent (and DetachFromComponent if it was already attached to another component). These functions contain important events and logic, such as OnAttachmentChanged, bWeldSimulatedBodies, and other functionality.

Why doesn’t OnRep_AttachChildren directly call AttachToComponent(KeepRelative) instead of using SetAttachParent?

Error 2: bWeldSimulatedBodies is not replicated, causing a physics simulation desync

If the server calls AttachToComponent with bWeldSimulatedBodies set to true on a parent inheriting from PrimitiveComponent, the client correctly goes through AttachToComponent, but its bWeldSimulatedBodies value is false because it is not replicated.

This means that if the child component on both the server and client has bSimulatePhysics set to true, it will not be attached and will keep the parent’s world position as its RelativeLocation, simulating physics while the server does not.

Why isn’t this parameter replicated like variables such as bShouldSnapWhenAttached?

The same issue exists for actor replication via AttachmentReplication. We could fix this bug by also replicating bWeldSimulatedBodies in FRepAttachment.

Error 3: KeepWorld instead of KeepRelative on the client

If the server detaches a component with KeepRelative, on the client, in USceneComponent::PostRepNotifies, the component is detached with KeepWorld, which creates a mismatch between the server and the client.

Another bug occurs if the server performs a KeepWorld detach but also modifies the transform at the same time. The client will do a KeepWorld detach, overwriting the new RelativeTransform.

Why perform a detach with KeepWorld instead of KeepRelative? The relative transform is already replicated, so it should be valid at that point.

The same issue exists with actor AttachmentReplication. If they are detached, the client always uses KeepWorld. If ReplicateMovement is true, it’s not a problem because there is an OnRep_ReplicatedMovement afterwards. But for scale, if the server did a KeepRelative, the client will do KeepWorld. Could we replicate KeepRelative or KeepWorld in FRepAttachment?

Error 4: Detach on BeginPlay

If a client joins the game after a component has been detached from its root component, or if two players are launched at the same time but the server detaches on BeginPlay, the client will still have a parent on this component. I do not see OnRep_AttachParent being called at any point, and I haven’t been able to understand why. Perhaps if we had an OnRep_ShouldBeAttached that sets bNetUpdateAttachment to true, the client would correctly go through the Detach in PostRepNotifies.

The same issue exists for actor AttachmentReplication: since the AttachmentReplication struct is not modified compared to its initial state, the client never receives any information and OnRep_AttachmentReplication is never called. Perhaps we should add a bShouldBeAttached set to true by default in FRepAttachment so that the client receives an OnRep of AttachmentReplication at the beginning?

[Attachment Removed]

Steps to Reproduce
Unzip the file AttachRep.zip and place it in a Unreal project using version 5.6 or newer.

Open the level “Lvl_SceneCompRep”. Inside, there is an actor called BP_TestAttachRep. This blueprint contains all the logic for the different errors, along with their comments.

To run a test in the level, modify the ErrorTest variable of BP_TestAttachRep, run the game with 2 PIE instances in Listen Server mode, and press Enter on the server to trigger the test.

There is also the level “Lvl_ActorRep” to test the bugs using AttachmentReplication for actors, with BP_TestAttachRepActor.

[Attachment Removed]

Error 5: bShouldSnapWhenAttached can be true even when RelativeLocation is not zero

In AttachToComponent, when calling functions such as SetShouldSnapLocationWhenAttached, this flag is set to true if the LocationRule is SnapToTarget, or if it is KeepWorld and the RelativeLocation after the detach (i.e., the world location) is equal to the parent’s RelativeLocation.

But why compare with the parent’s relative location and not the parent’s world location? If the parent has another parent, this boolean can be true even though the new RelativeLocation will not be zero.

This boolean is used by clients when receiving the new parent in PostRepNotifies, where if the RelativeLocation is not modified, it is set to zero (to fix UE-43355). This does not create issues in my game, but I was just wondering when reviewing the code.

[Attachment Removed]

Hi,

Thank you very much for these bug reports, including the detailed descriptions and repro project. I’ve created several issues for these, which should be reflected on the public tracker in a day or so. Much of this code is very old, but I’ve also reached out to the dev team to try and get some more context on the implementation. I’ll be sure to include it here if I am able to find more information.

To address each of these issues:

1) While I can’t speak as to why OnRep_AttachChildren doesn’t call AttachToComponent directly, this handling appears to have been added a few years ago to prevent issues due to the order that the client may spawn/attach components that were dynamically created on the server. Given that the attachment still appears to be set up correctly on the client (despite AttachToComponent not being called), it’s possible this bug went unnoticed. I’ve opened a new issue for this, UE-360636.

2) Given the age of this code, it’s difficult to say why bWeldSimulatedBodies is not replicated. I was unable to find any previous discussions or bug reports around replicating this, so I’ve opened a new issue for this, UE-360637.

3) Replicating the transform rules down from the server like you mentioned would likely require a good amount of refactoring, and it’s possible that this could introduce changes in behavior that lead to bugs in existing projects. That being said, the location should not get desynchronized like this after detaching, and so I’ve opened UE-360653 for this.

4) This is an odd bug, and after digging into it a bit, I noticed that even if you delay the detachment until after the client connects, the detachment is still not replicated to the client.

It looks as though the archetype for the SceneComponent/StaticMeshComponent has its AttachParent as nullptr. Because the archetype’s values are used for the initial comparisons when determining what properties need to replicate, when AttachParent gets set to nullptr before the component’s initial replication, the RepLayout doesn’t detect that the property’s value has changed from its default.

I added some extra logging in RepLayout to capture this:

LogRepCompares: VeryVerbose: CompareProperties: Owner: BlueprintGeneratedClass /Game/AttachRep/BP_TestAttachRep.BP_TestAttachRep_C CompareIndex: 1 HistoryIndex: 0
…
LogRepCompares: VeryVerbose: CompareProperties: Owner: Class /Script/Engine.StaticMeshComponent CompareIndex: 1 HistoryIndex: 0
…
LogRepCompares: VeryVerbose: CompareProperties_r: CmdIndex: 2 CmdType: PropertyObject Property: AttachParent
LogRepCompares: VeryVerbose:     Source: None, Shadow: None

I’ve opened an issue for this, UE-360666, and in the meanwhile, adding something like a bShouldBeDetached flag does seem like it could be a reasonable workaround.

5) The code that compares the relative location when determining whether to snap to the target was added to fix the following issue: https://issues.unrealengine.com/issue/UE-136337

If I recall correctly, the relative location is compared since the client will attach with the “Keep Relative” transform rule even if the server uses “Keep World.” This caused desync issues when the server didn’t detect any changes in the relative location. In the report that opened UE-136337, this occurred due to the relative transform remaining unchanged at (0,0,0).

Thanks,

Alex

[Attachment Removed]