Tech Note: Replicated TArray Repnotify Not Being Called

Tech Note: Replicated TArray Repnotify Not Being Called

Article written by Alex K.

Description

This is related to this open issue: https://issues.unrealengine.com/issue/UE-119459

The RepNotify function for a replicated TArray should be called on the client if at least one of the following changes occurs when receiving an update from the server:

  • A new item is added
  • An existing item is removed
  • An existing item is altered in some way

However, there have been reports of this OnRep not being called if just an existing item in the array is changed while the dimensions of the array remain the same. This can occur if the array’s size is set after construction, such as in PostLoad or OnConstruction, or if the client predictively adds items to the array.

When determining if a Repnotify should be called, the engine compares the received property against a shadow state. That shadow state is initialized using the object’s archetype, essentially the state of the object that is expected to be the same between the client and server (similar to its class default object). This archetype is initialized before calls such as PostLoad or OnConstruction, which means that if a replicated array’s initial values and size are set outside of the default constructor, the array’s archetype (and therefore its shadow state) will not have these values. This can also occur if the client predictively adds items to the array, as these changes will not be reflected in the shadow array. This seems to be the root cause of the issue: the dimensions of the array are the same on the client/server, but these items are not in the array’s shadow state.

When changes to the array property are received on the client, the class’s RepLayout first compares the size of the local array on the client to the size sent from the server (see PrepRecievedArray in RepLayout.cpp). Since the arrays are the same size, the RepNotify function will not be added to the list to be called. This is why if the dimensions of the array are changed (or the property’s RepNotify condition is set to REPNOTIFY_Always), the RepNotify gets called.

After checking the dimensions, the RepLayout then iterates through the individual items in the array, checking if each item has changed by comparing it against the corresponding item in the shadow array. For each item, it first checks that the index in the ShadowArray is valid, and then if the items are not identical, then the array’s RepNotify will be added to the list to be called. From ReceiveProperties_r in RepLayout.cpp:

// If ShadowArrayBuffer is valid, then we know that our ShadowArray pointer is also valid and pointing to a valid array.

// So we just need to make sure we’re not going outside the bounds of the array.

ArrayStackParams.ShadowData = (ShadowArrayBuffer && i < ShadowArray->Num()) ? (ShadowArrayBuffer + ElementOffset) : nullptr;

ArrayStackParams.RepNotifies = ArrayStackParams.ShadowData ? StackParams.RepNotifies : nullptr;

Because these items don’t exist in the ShadowArray, as the engine iterates through the array, these indexes are considered invalid and the RepNotify is skipped.

Potential Impact

Moderate: A replicated property’s RepNotify function not being called can impact gameplay in a variety of ways depending on the implementation of that OnRep, with certain actors not behaving as expected when a replicated value is received on the client.

Solution

While the cause of this issue may not be clear, this behavior is not entirely unexpected. Changing the value of replicated properties on the client is not really a supported operation, and it can lead to problems like this within the replication process. When possible, it’s recommended to avoid modifying replicated properties on the client.

If the array’s initial values are being set in a function like PostLoad or OnConstruction, it may be better to handle this initialization in the class’s constructor. This way, the class’s archetype will have the correct initial length and values for the array, and the replicator and shadow state can be initialized with the property’s intended initial values. Depending on when the array is being initialized, another option could be to detect if the current instance is the authority and avoid making changes to the array if it is not.

If the issue is due to the client predictively adding items to the array before receiving the replicated values from the server, it may help to instead add this predicted item to a separate, non-replicated array. The replicated array’s RepNotify function could then handle cleaning up predicted items in this array as necessary.

The simplest workaround is to set the property’s RepNotify condition to REPNOTIFY_Always, which will ensure that the property’s RepNotify function is always called whenever an update is received from the server. This may not be ideal in all situations though, as the RepNotify function will be called even if an update doesn’t actually change the property.

Finally, another option is to use Fast TArray Replication (see NetSerialization.h for more info). Fast arrays provide separate callbacks for changes, additions, and removals, and using fast array replication can offer performance improvements for large sets, with the caveat that the order of the array’s elements is not guaranteed to be consistent between the server and clients.

Get more answers on the Knowledge Base!

3 Likes

Also the same issue with blueprints: