Community Tutorial: Networked Physics - Pawn Tutorial

Here are my results so far with two complex pawns - vehicle and crane, it’s recorded with bad network profile and here is player on server controls the pawns and client observes the results.

I wonder is that the best result you could expect from that tech right now or in general it has to be better and I have to search for an error in some interpolation/decay stuff?

@MBobbo regarding the walls specifically, pink walls are AActors, not AStaticMeshActors. With debug draw both are rendered red

Very nice that you got both of those working, good job!

You can get better results than that yes, but I’d recommend to aim for the Average profile, not the Bad at the moment.
It’s hard to point towards specific fixes, networked physics is quite a complex area and it’s still experimental so we don’t have all the documentation, settings and features implemented yet.

But it looks to me like the issue for both vehicle and crane is the same thing that sim-proxies forward predict too much, the clients physics simulation is forward predicted, the autonomous proxy is acting in the forward predicted timeline, it’s moving forward sends that input to the server which then move forward and sends the input and state to sim-proxies which then move forward. That creates a time delay so sim-proxies act in the “interpolated timeline" behind the server while autonomous proxies act in the “forward predicted timeline”. And for physics interactions to work they need to act in the same timeline (preferably).

With bad networking you probably receive inputs and states from the server that should have been applied 10-15 frames ago, i.e. the forward predicted timeline is 15 frames ahead of the interpolated timeline. And to get sim-proxies into the forward predicted timeline we trigger a resimulation, sets physics back 15 frames, apply the received input there and then resimulate forward to the forward predicted timeline again. But sim-proxies only have inputs for the interpolated timeline, so for the first resim frame essentially. The next 14 frames during resimulation the sim-proxy will be using the latest received input which means the sim-proxy crane will turn right for 14 frames but we don’t know if it will actually turn right during those frames. Say when you receive the next input from the server the input is that the crane have stopped moving, but on the sim-proxy it has forward predicted moved right for 14 frames, so it has overshot its stopping point. It will rewind 15 frames and then apply input with no movement for 15 frames. Then rendering will cover up that correction by moving it smoothly back to the left from the overshot position.

This means that instant movements with networked physics is quite hard to make look well since everything needs to be forward predicted but if things can stop on a dime they will overshoot and need to get corrected backwards.

From a game design view it’s best to make the crane have momentum, so it doesn’t stop instantly, it slows down gradually to a full stop. That will help along with the following..

To make sim-proxies not fully forward predicted during resimulations the only thing we have at the moment is input decay, which means we decay the input during forward prediction for sim-proxies, so the crane can turn less and less right the further into forward prediction it comes.

I would say you need to balance input decay and you do that via NetworkPhysicsSettingsComponent and the DataAsset along with your implementation function for decaying inputs.

It is sometimes also better to make the input decay curve a flat line at say 0.35 instead of a curve or linear increase, it depends on what your pawn.
One downside with input decay is that it doesn’t scale with network conditions yet, so if you balance it to look good at Bad then it might not look good at Average.

UE 5.8 will see a couple of improvements to input decay and also a new way of keeping sim-proxies not fully forward predicted without decaying inputs.

You can also in the settings DataAsset make it trigger a resimulation when it receives inputs, that will make it resimulate very often but it will most likely also help you with the overshoot issue.

Also if you are making a coop / casual game then resimulation is a bit overkill at the moment, it’s designed for 100% server authority with dedicated servers.

We don’t have a solution for client authority between autonomous proxy and server when the pawn is physics-based at the moment though. But if you were to run client authority the replicated physics objects could use Predictive Interpolation which generally looks much better out of the box.
You can use that replication mode for physics pawns also but you will have input latency depending on your network latency in that case.

So the three things you should look into to start with is:

  • Add momentum to the cranes rotation
  • Balance Input Decay
  • Enable resimulation on received inputs

Thanks! Currently I’m playing with making smoother input (not discrete 0 and 1), decay curve and trigger resimulation - seems like it delivers pretty good results!

Yeah, maybe that’s an overkill for a coop game I’m doing right now, but unfortunately predictive interpolation doesn’t work for me at all, all my attempts were fallen into 2 categories:

  1. No immediate response on a client (so it was looking like it doesn’t work at all ), when I was trying to implement this example Networked Physics and UNetworkPhysicsComponent in Unreal Engine 5.4 - Devtricks (author said that probably something is broken in 5.7 in comparison with his own tests on earlier version)

  2. Physics looks broken and jerky (for example if I’m just set to predictive interpolation in your example)

Predictive interpolation is not meant for use on autonomous proxy, it’s only meant for sim-proxies.
For client-side authority you’d need to make the client not replicate physics and then either make a component yourself that sends states from the client to the server and applies it on the server or “hack” it by sending states inside the inputs via NetworkPhysicsComponent to the server so the server applies states.
But then you need to make sure that sim-proxies doesn’t apply those inputs and instead that they run predictive interpolation.

Hi again! I’m currently trying to push the approach further and check if I can create the following setups:

I have APhysicsPawn (APawn) with NetworkComponent

I have APhysicsCube (AActor) with NetworkComponent

The goal is to be able to apply some forces to this cube from client’s APhysicsPawn.

What I tried:

  1. APhysicsPawn passes Chaos::FPhysicsObjectHandle CubeBody into it’s own AsyncPhysicsInput and applies forces there. It works on server, but on client side I see constant corrections back to the server, as I understand because of the fact that APhysicsCube is an AActor and it’s not owned by client

  2. Then I tried to do the following (doesn’t work as well):

a) define ForceInput for FNetInputPhysicsCube;

b) When APhysicsPawn wants to interact with that, client sends request to the server to set ownership for this client and get local inputs:

 TargetCube->SetOwner(GetController());

TargetCube->NetComp->SetIsRelayingLocalInputs(true);

c) Once this is done, on tick of APhysicsPawn I send desired Force to APhysicsCube

d) On tick of APhysicsCube this force goes into its async component

Does the second approach look more-less correct in theory or it’s not the way it will work? It that possible at all in general? :slight_smile:

Also I wonder what’s the proper approach for implementing discrete stuff like jumps? I see that in general it has to rely on some counter like int32 JumpCount that is incrementing in GameThread when player wants to jump and then performing comparison in physics thread with the previous value.

However for comparison I have to store previous jump count somewhere, is that the case where I have to use states structs? Because if I store that in SimCallback the jumps doesn’t work great with any network delays

UPD: no, seems like my jumps are fine. It was the significant drops in FPS when resim is triggered (because of network physics settings set this way on a vehicles), even when vehicle is stationary without inputs. So when I have 60 components in the scene with network component it all goes crazy. Seems like I need some custom sleep logic for most of these components (they are building blocks I have to be able to apply forces to).

Oh, so many nuances :slight_smile:

Hello, the NetworkPhysicsComponent should only be used for complex assets, like your character, vehicles and your crane for example.
It’s a very heavy component at the moment since there is not much optimizations done yet, it will continuously send inputs and states over the network for every physics frame even when nothing changes. So you can’t scale a game with this yet, having 60 of them will most likely not work well.

When it comes to applying forces to other objects you don’t need the NetworkPhysicsComponent.
SetIsRelayingLocalInputs is meant to be used when you have a complex asset that the character is able to control, like your crane for example it could be an AActor instead of an APawn. And you sit your character inside the crane and then take ownership of it and set SetIsRelayingLocalInputs which makes the NetworkPhysicsComponent able to send inputs from your client to the server.

As mentioned before, networked physics is currently designed around dedicated servers and full server authority. So your approach of taking ownership of a physics object that you want to apply a force to is not the flow it’s designed for at the moment.
Instead the idea is that you apply the force to the other FPhysicsObject on the physics thread both on client and on the server. When it’s done correct, since client and server are in sync, the force gets applied on the correct frame and things stay in sync.

There are multiple ways of deciding to apply the force and to do it on the physics thead. You can do it from the game thread also in a certain way but it’s not really recommended.

  1. Do a trace on the physics thread to find physics particles and apply the force to those physics particles. This is still experimental though and might change but you find physics thread trace API here: FGenericPhysicsInterface_Internal. If you sync the ChaosMover plugin you should find examples or it being used there if you search.
    • You can do this when you hit your input to shoot for example
    • You can also do this from other logic than a complex character, you can have a trampoline on the ground which does traces on the physics thread and applies a force or impulse to physics objects.

2. You can register for ISimCallbackObject.OnContactModification_Internal and find particles you collide with to apply a force to on the physics thread.

3. You can do the trace on the game thread and then queue up a lambda function to run on a specified physics frame. You need to do this on the client and network the physics frame and physics object to the server (via your pawn) so the server can queue up the same lambda for the same frame.

Number 1 and 2 are best since they are decided on the physics thread and will resimulate correctly.
Number 3 is a client authoritative flow and it won’t resim correctly since the decision is made on the game thread, but the lambda will play out again during resimulation at least on the frame it was scheduled for.

Here is a code example of alternative 3:

APlayerController* Controller;

FVector ForceVector;

FPhysicsSolver* Solver;

const int32 ServerFrame = Controller->GetPhysicsTimestamp().ServerFrame;

{
	const int32 CorrespondingFrame = ServerFrame - FrameOffset;

	FConstPhysicsObjectHandle PhysicsObject = RootComponent->GetPhysicsObjectByName(NAME_None);

	Solver->EnqueueCommandScheduled_External(CorrespondingFrame, [PhysicsObject]

	{

		FWritePhysicsObjectInterface_Internal Interface = FPhysicsObjectInternalInterface::GetWrite();

		if (FPBDRigidParticleHandle* Handle = Interface.GetRigidParticle(PhysicsObject))

		{
			Handle->AddForce(ForceVector);
		}
	});
}

You’d need to then send the ServerFrame and the AActor (that you get the rootcomponent and physics object from) from client to server and then schedule that same lambda for the CorrespondingFrame on the server too.

I’m going to write more tutorials eventually when I get time about some fundamental knowledge and then some gameplay implementations.

Thank you so much for your response! I will try to implement approach number 3! What’s the FrameOffset in this context btw? Let’s say I have average network profile and I have ServerFrame 4256 and LocalFrame 4060, should I basically send 4060 for client (execute lambda now) and 4256 for server (this force was applied on client when for server it was 4256)

Regarding approach #1, I was trying to do something like that in my NetworkPhysicsPawn in presimulate callback (assuming I’m calculating GetWorldForcePosn() input in game thread and then send it to network component, the physics particle is constant for simplicity, so I don’t need to do traces)

This works fine for a server, but on a client it’s being played back to the server’s position. I though that this could work because for the root body of the pawn it works just great, but seems like for others bodies server doesn’t apply that forces. Is that possible to utilize these callbacks for such logic or I have to define some additional logic like “send that input on server too, so the server will apply that in physics thread for this body on its side”?

void APhysicsPawn::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	const bool bIsLocallyControlled = IsLocallyControlled();

	if (bIsLocallyControlled)
	{
		if (FAsyncInputPhysicsPawn* AsyncInput = PhysicsPawnAsync->GetProducerInputData_External())
		{
            // other input states here
			AsyncInput->Vector2Input = GetForceWorldPosn();	

		}	
	
	}
	
}




void FPhysicsPawnAdvAsync::OnPreSimulate_Internal()
{

// skipped the pawn movement logic for the pawn root body, works fine

// Object that we want to push, TargetObjectHandle_Internal is defined on PostInitializeComponents

Chaos::FPBDRigidParticleHandle* ParticleHandlePush = Interface.GetRigidParticle(TargetObjectHandle_Internal);

if (ParticleHandlePush && Vector2Input_Internal.Size()>0.5)
{

// Skipped the logic that calculates the force from Vector2Input_Internal

	ParticleHandlePush->AddForce(Force, true);
	ParticleHandlePush->AddTorque(WorldTorque, true);
	
}

}

That’s the result I’ve achieved with the following code (approach 3, I hope I understood it right) server of the left, client on the right, so for average network profile it works nearly perfect I would say, for bad one obviously quite bad, but it’s quite extreme I guess.


void APhysicsPawn::ApplyForceToActor(AActor* TargetActor, FVector WorldForcePosn,FVector WorldForceNormal)
{
	if (!TargetActor || !GetController()) return;

	APlayerController* PC = Cast<APlayerController>(GetController());
	if (!PC) return;


	const int32 ServerFrame = PC->GetPhysicsTimestamp().ServerFrame;
	const int32 ClientFrame = PC->GetPhysicsTimestamp().LocalFrame;


	UE_LOG(LogTemp, Warning, TEXT("Server %d, Client %d"), ServerFrame, ClientFrame);

	
	if (!HasAuthority())
	{
		EnqueuePhysicsForceCommand(TargetActor, WorldForcePosn, WorldForceNormal, ClientFrame);
	}

	Server_ApplyForceToActor(TargetActor, WorldForcePosn, WorldForceNormal, ServerFrame);
}


bool APhysicsPawn::Server_ApplyForceToActor_Validate(AActor* TargetActor, FVector ForceVector, FVector ForceNormal, int32 InteractionFrame)
{
	return true; 
}

void APhysicsPawn::Server_ApplyForceToActor_Implementation(AActor* TargetActor, FVector ForceVector, FVector ForceNormal, int32 InteractionFrame)
{
	EnqueuePhysicsForceCommand(TargetActor, ForceVector, ForceNormal, InteractionFrame);
}

void APhysicsPawn::EnqueuePhysicsForceCommand(AActor* TargetActor, FVector ForceVector, FVector ForceNormal, int32 TargetFrame)
{
	if (!TargetActor) return;

	UPrimitiveComponent* RootPrim = Cast<UPrimitiveComponent>(TargetActor->GetRootComponent());
	if (!RootPrim) return;

	FPhysScene* PhysScene = RootPrim->GetWorld()->GetPhysicsScene();
	if (!PhysScene) return;

	Chaos::FPhysicsSolver* Solver = PhysScene->GetSolver();
	if (!Solver) return;


	Chaos::FConstPhysicsObjectHandle PhysicsObject = RootPrim->GetPhysicsObjectByName(NAME_None);
	if (!PhysicsObject) return;

	float PushStrength = CharacterUtilsComponent->MovementParams.ForcePush;


	Solver->EnqueueCommandScheduled_External(TargetFrame,
		[PhysicsObject, ForceVector, ForceNormal,PushStrength]() 
		{
			Chaos::FWritePhysicsObjectInterface_Internal Interface = Chaos::FPhysicsObjectInternalInterface::GetWrite();

			if (Chaos::FPBDRigidParticleHandle* Handle = Interface.GetRigidParticle(PhysicsObject))
			{
				Chaos::FVec3 WorldCenterOfMass = Chaos::FParticleUtilitiesGT::GetCoMWorldPosition(Handle);
				Chaos::FVec3 Force = -ForceNormal.GetSafeNormal() * PushStrength;

				const Chaos::FVec3 WorldTorque = Chaos::FVec3::CrossProduct(ForceVector - WorldCenterOfMass, Force);

				Handle->AddForce(Force, true);
				Handle->AddTorque(WorldTorque, true);
			}
		});
}