Question about intended use of NetDeltaSerialize

Hey folks!

I am currently trying to implement a NetDeltaSerialize function for a struct, but it does not seem to behave the way I expect it to. Unfortunately, the FFastArraySerializer seems to be the only example in the entire engine to ever use net delta serialization and it has a huge amount of complexity where I am not entirely sure which parts of it are necessary for a more simple case and which aren’t.

From what I understand, the minimum steps for net delta serialization should be:

  • When writing:
    • Check which values differ compared to the OldState passed in via the FNetDeltaSerializeInfo (or send everything if we don’t have an old state yet)
    • Copy the current values to NewState in FNetDeltaSerializeInfo so we can use them as a reference the next time
    • Write all the changed values to the provided Writer archive
  • When reading:
    • Read all the changed values from the provided Reader archive and apply them

This seems to mostly work in my tests, BUT in bad network conditions some sent changes can get lost and will never arrive on the client. Alternatively, some changes seem to also be sent multiple times and arrive on the client multiple times. This will inevitably cause the state of server and client to drift apart.

Is there some mechanism I’m missing here that should allow us to handle this? Is it even feasible/reasonable to implement delta serialization without building upon FFastArraySerializer?

Thanks in advance,

Dave

Steps to Reproduce

  • Add the following code a C++ project:

`class FTestStructDeltaState : public INetDeltaBaseState
{
public:
bool IsStateEqual(INetDeltaBaseState* Other) override
{
return TestValue == static_cast<FTestStructDeltaState*>(Other)->TestValue;
}

public:
int32 TestValue = 0;
};

USTRUCT()
struct FTestStruct
{
GENERATED_BODY()

public:
UPROPERTY()
int32 TestValue = 0;

bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaInfo)
{
if (DeltaInfo.Writer)
{
// Save current state
auto NewState = static_cast<FTestStructDeltaState*>(DeltaInfo.NewState->Get());
if (NewState == nullptr)
{
NewState = new FTestStructDeltaState();
*DeltaInfo.NewState = TSharedPtr(NewState);
}

NewState->TestValue = TestValue;

// Nothing to do if value didn’t change compared to old state
const auto OldState = static_cast<FTestStructDeltaState*>(DeltaInfo.OldState);
if (OldState && OldState->TestValue == TestValue)
{
return false;
}

// Send difference between last sent state and current value
int32 DeltaToWrite = TestValue - (OldState ? OldState->TestValue : 0);
(*DeltaInfo.Writer) << DeltaToWrite;

GEngine->AddOnScreenDebugMessage(INDEX_NONE,
1.0f,
FColor::Blue,
*FString::Printf(TEXT(“Sent delta %i → %i”), DeltaToWrite, TestValue));
}
else if (DeltaInfo.Reader)
{
int32 ReceivedDelta;
(*DeltaInfo.Reader) << ReceivedDelta;

TestValue += ReceivedDelta;

GEngine->AddOnScreenDebugMessage(INDEX_NONE,
1.0f,
FColor::Green,
*FString::Printf(TEXT(“Received delta %i → %i”),
ReceivedDelta,
TestValue));
}

return true;
}
};

template <>
struct TStructOpsTypeTraits : public TStructOpsTypeTraitsBase2
{
public:
enum
{
WithNetDeltaSerializer = true,
};
};

UCLASS()
class AMyTestPawn : public APawn
{
GENERATED_BODY()

public:
AMyTestPawn()
{
bReplicates = true;
}

UPROPERTY(Replicated)
FTestStruct TestStruct;

UFUNCTION(Exec)
void ChangeTestValue()
{
if (HasAuthority())
{
++TestStruct.TestValue;
}
}

void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyTestPawn, TestStruct);
}
};`* Configure AMyTestPawn as your pawn class in your game mode

  • Go to Edit -> Editor Preferences -> Play and enable network emulation for everyone with the “Bad” profile
  • Start PIE as a listen server with two players
  • On the server instance, keep executing the ChangeTestValue function via the console
  • Observe that some deltas will not arrive on the client, leading to the values going out of sync over time

Hi,

This is a known limitation of NetDeltaSerialize, and it does understandably make it difficult to use this in most contexts. The custom delta serialization path is almost entirely used for fast array serialization now, and so we don’t generally recommend using custom delta serialization outside of fast arrays.

For more info, you can check out this related thread: [Content removed]

If you have any more questions, please don’t hesitate to reach out, but please note that support will be limited for the next two weeks due to the company break.

Thanks,

Alex

Thanks for the quick reply!

I guess in this case we’ll just have to rework our system in a way that makes the data compatible with FFastArraySerializer and then go with that.