You can replicate 'big data' in unreal with ReplicateSubobjects
Thought I'd share an interesting trick I learned a while back.
Devs often wonder how to send 'big data' in unreal. Unreal has pretty big internal buffers for replication, and there are ways you can make them larger, but the problem with just buffering more(assuming you can fit your data into that buffer) is that you starve out other replicated data for the actor while that bundle is fragmented and sent through, and if your data is larger than what can reasonably fit into the buffer, that approach isn't viable at all.
Some notes about replicating big data
* RPCs are useless for big data. you want to use replication * You don't want to starve out replicating other data for the actor * You don't want to starve out replicating other actor data * You don't want to defeat the normal replication priority mechanisms in the engine. * You want some control over the fragmentation * You have to plan for the worst case scenario of the entire data set needing to send, such as for late joiners.
Anyone that has done Unreal networking is probably familiar with ReplicateSubobjects. It's a function that allows you to replicate your own UObject instances as part of an actor or component. I use it for inventory items, and in fact, components are replicated with actors as subobjects. Internally, the engine keeps track of what objects have been replicated to the client.
One interesting use case for ReplicateSubobjects, is to fragment 'big data' into smaller chunks, small enough to avoid starving out other replication, and selectively replicating the subobjects based on the size of the bundle.
Here is an implementation for the use case I needed. This was for replicating the 'cells' for a fire simulation. In my use case, not only is the data too large to replicate by other means, but if a fire is actively burning, the data will change state at runtime as cells catch fire and burn out.
The central trick with this approach is to use the fact that Channel->ReplicateSubObject only returns true if there's something to replicate for that subobject. The channel knows if the client already has that subobject and if the subobject hasn't changed, it will return false, allowing the loop to try and send the next one, and even giving you the opportunity to just break out of the loop after only sending as much data as you want to send.
Couple things of note. If your data is static and doesn't change at runtime, or changes very infrequently, you may not need the extra logic I have in this example of the round robin looping. If it may change frequently or rapidly at runtime, like my use case, you will need to round robin to ensure subobjects aren't starved out.
Also, I highly recommend using the fast array serializer within your subobjects to minimize their replication cost with the delta compression.
One caveat with this approach is that since the fragmentation is custom, you have to handle the situation where you have to wait on all the fragments to be updated client side, if the data represents something that can only be used in its entirety. I tried this, but didn't need that for my situation, It causes problems for use cases with periods of rapid change. I can explain how to do this if anyone is interested.
Ultimately in my use case, the worst case scenario of a raging fire sim burning when a late joiner joins, they would receive individual subobjects over multiple frames that were large-ish, but the fast array serializer afterwards ensured iterative changes were tiny, so subsequent replication updates would send all 'dirty' subobjects each time, effectively becoming basically atomic after the initial period of 'too big to replicate directly'
bool UDynamicsISMComponent::ReplicateSubobjects(class UActorChannel* Channel, class FOutBunch* Bunch, FReplicationFlags* RepFlags) { bool wrote = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);
const int64 BaseBunchSize = Bunch->GetNumBytes();
TSet<int32> ReplicatedBatchIndices; for(int32 i = 0; i < BatchInstances.Num(); ++i) { // don't use loop counter 'i' // use a round robin indexing to avoid starving out certain subobjects during periods of rapid change in one of them const int32 BatchIndex = ReplicateSubObjectCycleIndex++ % BatchInstances.Num();
TObjectPtr<UDynamicsISMBatch> BatchInstance = BatchInstances[BatchIndex]; if(IsValid(BatchInstance)) { if(Channel->ReplicateSubobject(BatchInstance, *Bunch, *RepFlags)) { ReplicatedBatchIndices.Add(BatchIndex);
wrote |= true;
// we aLLow multiple subobjects to write if the changes are small enough const int64 SubObjectBunchSize = Bunch->GetNumBytes() - BaseBunchSize; if(SubObjectBunchSize > 5000) { break; } } } }
const int64 SubObjectBytes = Bunch->GetNumBytes() - BaseBunchSize; if(SubObjectBytes > 0 && GameCvars::DynamicsDebugReplication.GetValueOnGameThread()) { UE_LOG(LogTemp, Warning, TEXT("ReplicateSubobjectIndices[Bytes: %s, Indices: %s]"), *FString::FormatAsNumber(Bunch->GetNumBytes() - BaseBunchSize), *FString::JoinBy(ReplicatedBatchIndices, TEXT(","), [](int32 Index) { return FString::FormatAsNumber(Index); })); } return wrote; }