Push Model Networking – Unreal Engine

When working with state in a networked Unreal game you would usually create a replicated variable which sends updates to all connected clients when it changes. However this comes at a cost as the server has to compare every potential replicated variable every frame so that it can work out which have changed and if it should send an update. Fortunately we’ve been given a tool to help reduce this cost in the form of Push Model Networking and i’ll briefly run through how to use it and the gains you’d expect to see (and how to measure the difference!).

Setting Up The Example Project

We need to set up a project which is going to show off the power of push model networking. To do that we are going to need a lot of replicated variables that change fairly infrequently so that we have that high percentage of waste time. Here i’ve created a very simple component that can be applied to an actor which will occassionally update a replicated float.

Note: If you’re following along with the project make sure that you’re creating this in a source version of the engine as the prebuilt Epic launcher version does not have support for building server targets.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InefficientComponent.generated.h"


UCLASS()
class PUSHMODELNETWORKING_API UInefficientComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	UInefficientComponent();

protected:
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

public:
	virtual void BeginPlay() override;

protected:
	UPROPERTY(Replicated)
	float TestVar1 = 0.0f;

	UPROPERTY(EditDefaultsOnly)
	float VariableUpdateChance = 0.1f;

	UPROPERTY(EditDefaultsOnly)
	float TimeBetweenTicks = 0.1f;

private:
	void TryToUpdateVar();
};

#include "InefficientComponent.h"

#include "Net/UnrealNetwork.h"

UInefficientComponent::UInefficientComponent()
{
	SetIsReplicatedByDefault(true);

	PrimaryComponentTick.bStartWithTickEnabled = false;
	PrimaryComponentTick.bCanEverTick = false;
}

void UInefficientComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	
	DOREPLIFETIME(ThisClass, TestVar1);
}

void UInefficientComponent::BeginPlay()
{
	Super::BeginPlay();

	if (!IsNetMode(NM_Client))
	{
		FTimerHandle Handle;
		GetWorld()->GetTimerManager().SetTimer(Handle, this, &UInefficientComponent::TryToUpdateVar, TimeBetweenTicks, true);
	}
}

void UInefficientComponent::TryToUpdateVar()
{
	if (FMath::RandRange(0.0f, 1.0f) > VariableUpdateChance * TimeBetweenTicks)
	{
		return;
	}
	
	TestVar1 = FMath::RandRange(0.0f, 1000.0f);
}

Next up is to create an actor which contains many of these components. I’ve set up a blueprint with a sphere so that we can see where it’s placed in the world and 20 Inefficient Components on each. These have also been marked as Replicates and Always Relevant so we’re not fighting against relevancy issues during this test.

Then we need to set up a custom game mode BP which will spawn each of the actors on BeginPlay. This is fairly basic and is just looping through an X and Y loop to apply the spawn location offset. The offset is functionally redundant here but allows us to visualise how many actors we’re working with. For this test a TotalRowsCols value of 20 seems to give good results.

That should be everything required to test the performance of the example before we make any changes. I’ve set up a blank level for the clients initial scene and a level with the new game mode for the server start scene. Now all that’s needed is to add a server build target and then package some local builds.

What To Look For When Profiling

We’ve got a couple of tools that we can use to find the cost of these checks. The first should not be a surprise, Unreal Insights (found in /Engine/Binaries/Win64/UnrealInsights.exe). We can use insights with the cpu and frame channels enabled which will give us the value for NetBroadcastTickTime. For this test i’m running a single server instance, connecting with 4 clients (with the “Open 127.0.0.1” cmd) and taking a sample from 200ms of run time after the fourth client has connected.

You can see in this example that 167.4ms of the 198.6ms budget is under NetBroadcastTickTime. This is quite expensive and maybe not completely representative of what you’d see in a real project due to the exaggerated setup but gives us a good starting point to see what kind of improvements we can get.

The second tool is the Network Profiler (which can be found in /Engine/Binaries/DotNET/NetworkProfiler.exe). This has a few values included that the Unreal Insights Net view doesn’t and the one that we’re interested in this case is the Waste value. To enable logging for the Network Profiler we just need to add networkprofiler=true to our server start command and the resulting .nprof file will be in /Saved/Profiling.

By drilling down to capture 20 frames after all 4 clients had connected we can see which actors have cost us the most in terms of net traffic. The waste value on BP_InefficientActor of 92.43 represents the number of times out of 100 where the variable hasn’t changed. Let’s enable Push Model Networking and revisit these values.

To save you some time with working out the server start command, you can use this! It’s often recommended that you only run one profiler at a time but this test is simple enough that i’ve captured both at the same time. Just save this as a .bat file outside of your build folder and make sure the path is correct.

START ./WindowsServer/PushModelNetworking/Binaries/Win64/PushModelNetworkingServer.exe -server -log -trace=cpu,frame networkprofiler=true

Enabling The Push Model Networking

The first thing we need to do to enable Push Model Networking is to set the net.IsPushModelEnabled cvar to 1 in our DefaultEngine.ini. You can also optionally add net.PushModelSkipUndirtiedReplication which will skip actor updates if the actor does not contain any dirty properties but as this is fairly new as of 5.3 there are still a couple of issues with using it in your projects. I’ll include this option for this test as this should be more stable in the near future.

[SystemSettings]
net.IsPushModelEnabled=1
net.PushModelSkipUndirtiedReplication=1

bWithPushModel must also be set to true in the Server.Target.cs.

public PushModelNetworkingServerTarget(TargetInfo Target) : base(Target)
{
	Type = TargetType.Server;
	
	bWithPushModel = true;
		
	//...
}

We also need to add the NetCore module reference to our build.cs:

PrivateDependencyModuleNames.AddRange(new string[] { "NetCore" });

In our UInefficientComponent::GetLifetimeReplicatedProps function we now need to set up the variable to be push based. We can do that with the following code in place of the previous DOREPLIFETIME:

FDoRepLifetimeParams Params;
Params.bIsPushBased = true;
	
DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, TestVar1, Params);

Each time the variable changes we now also need to set the variable to dirty so that the server can send the updated version in the next net update. We can do this just under where we set it with MARK_PROPERTY_DIRTY_FROM_NAME. The FROM_NAME variant of this macro is preferred over the MARK_PROPERTY_DIRTY version as it includes compile time checks for valid properties.

void UInefficientComponent::TryToUpdateVar()
{
	if (FMath::RandRange(0.0f, 1.0f) > VariableUpdateChance * TimeBetweenTicks)
	{
		return;
	}
	
	TestVar1 = FMath::RandRange(0.0f, 1000.0f);
	MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, TestVar1, this);
}

With all of that added we can compile the server target again and see if anything has changed. Due to the bWithPushModel being added this will trigger a full recompile.

Comparing Results

Taking another capture in Unreal Insights with 4 clients shows that our NetBroadcastTickTime is down to only 76.5ms out of the 199.8ms selection. Compared to the previous 167.4ms it shows that it’s over twice as fast for just enabling this one optimisation. We are testing with a single replicated variable per actor though, your results are likely to be very different when testing with less actors but more variables per actor.

If we look at the nprof capture we can see that the waste percentage is still around the same value at 92.58. Since this value is not zero as we would expect, my assumption is that it does not take push model networking into consideration. However we can see that a capture of 20 frames is giving us a total ms of 224.71ms compared with the previous 425.39ms which reiterates that this is a considerable improvement.

I was also interested to know how the test performs when push mode is enabled but net.PushModelSkipUndirtiedReplication is set to 0. The following results show a NetBroadcastTickTime of 138.5ms which is substantially slower than with it being set to 1 but is still an improvement compared to push mode being disabled. It makes sense that this test case is particularly good with the cvar enabled though as it’s so simple, you’re unlikely to see such huge improvements with more complex actors with many variables all being set each frame. There is also the net.PushModelSkipUndirtiedFastArrays cvar which may be worth testing in your own project if you use any fast arrays in conjunction with push mode variables.

It’s also worth knowing that many core Unreal classes have set up variables to use push based replication which should give improvements by just enabling it in your own project. I would also assume that Fortnite uses this and that Epic would recommend that you use it in your own project. Mileage will vary though based on how your project is set up so there’s a chance you don’t see any positive effects (but I would hope that you don’t see any negative effects either!).

I may revisit this topic in the future to test with more variations, experiment with how more variables on the same component changes the results and maybe even alter the variable change timings. For now though I think this shows that it’s a feature worth knowing about and that it could be beneficial to use in your own projects.

Project Files

Feel free to download the project yourself and continue testing! I’ve not extensively used this feature personally so would love to know if you can find more improvements or if i’ve missed anything. This is built in UE 5.3.2 so just make sure you’re on that version or newer. Just right click the .uproject to Generate Visual Studio project files and run from there.

Leave a Reply

Your email address will not be published. Required fields are marked *