Tutorial: Networked Physics - Pawn Tutorial

@F4ll_ouT Hello, we don’t have a client authoritative physics multiplayer solution in Unreal Engine at this point.

Disabling UPrimitiveComponent.bReplicatePhysicsToAutonomousProxy will only stop the client from running physics replication for the Autonomous Proxy actor, so you don’t get corrected to the servers state.

But there is no logic that makes the server correct itself to the autnomous proxy state. You will need to write a solution that sends the autonomous proxy state to the server and apply the state there.

Issues with client authority comes with interactions in physics, imagine you and another player both jumping onto a see-saw (a tipping board which is physics simulated) you both land on it at the same time in realtime, it takes time to send states over the network though so neither of you see eachother landing at the same time.

On your screen you see yourself land on it on the right side, pushing the left side up into the air. The other player sees themself land on left side pushing the right side up into the air.

You both send your autonomous proxy positions to the server which applies your positions, but what happens to the see-saw on the server?
Which one of the two players were correct? Both can’t be correct since you will see two completely different physics states but the server is also not allowed to say any of you were incorrect?

Who owns the see-saw? If you land on it at the same time and you try to take client ownership of the see saw then it becomes a race-condition for who gets their AActor::SetOwner call first to the server. But even if one of the clients got ownership of the see-saw the other autonomous proxy interacting with that see-saw is not allowed to be corrected so it might see the see saw getting snapped up into a different state but the autonomous proxy player might then clip into the physics collider or end up under the see saw instead of following it up.

If the server is the owner of the see saw then you could use Predictive Interpolation for the see-saw but the server is not allowed to correct players. So interactions will be subject to latency which results in sluggish interactions.

Your autonomous proxy will land on the see-saw and it’s not going to be allowed to push it down more than a little bit depending on how much Predictive Interpolation (or your own physics interpolation logic) allows it to move away from the server state it replicates with. So you will land on it and push it a little bit down and you will have to wait for the server to receive your autonomous proxy state where you are clipping into the see-saw on the server forcing the server to move the see-saw down and then you need to wait for that state to arrive from the server for the see saw to move down a bit which allows you to push it down a little bit further due to leniency in Predictive Interpolation. And so on..

My recommendations with networked physics is to take it slow, step by step ensure things work with each new thing you do since if you take many steps in a row it might be hard to debug what is causing issues. So if you follow the tutorial and then add smaller things continuously ensuring things work each time and then solving the issues that pop up you will be able to make complex pawns from this.

@MBobbo , I added a callback to my code. Unfortunately, it doesn’t save you from crashing. I tried to debug and the crash occurs before calling OnPhysicsObjectUnregistered_Internal ((

…..

Everything seems to be working. It was my mistake. I forgot to add Chaos::ESimCallbackOptions::PhysicsObjectUnregister to Chaos::TSimCallbackObject

@mizarates Hello, discrete actions are hard with networked physics due to full forward prediction and complete server authority. There are a couple of things you can do to make them better but my recommendation is to have something that isn’t an instant action or not an impulse. For example a jet-pack applying a force is better than an impulse jump.

You can also have a delay so when you hit the button to jump the pawn crouches down a little before it jumps, which gives you time to send the jump input to the server and to simulated proxies which can then apply the actual jump on the correct frame but with a faster animation for the “load up”.

It’s good if you can build and design the game around the limitations of the solution if you are not able to dig in and improve the solutions for your needs.

Resimulation is not optimized yet and doesn’t scale well, it’s one of the larger goals for next year.
There is a Physics Replication LOD system in Unreal Engine already which I wrote a year ago but it’s not finished yet so it has flaws. It’s not used anywhere yet but you can test it out if you want to. It transitions replicated physics objects between the interpolated timeline (where it uses Predictive Interpolation) and the forward predicted timeline (where it uses Resimulation).

Predictive Interpolation is a cheaper physics replication mode, used in LEGO Fornite for example and handles scaling better than Resimulation does, but it’s not viable for physics-based pawns.

Enable Physics Replication LOD in Project Settings → Physics → Replication, there you have a setting to “Enabled Physics Replication LOD” along with some settings.

You then need to mark your pawn as the focal point of the LOD, you do this via the UNetworkPhysicsSettingsComponent which take a UNetworkPhysicsSettingsDataAsset. In the DataAsset there is a setting under “General Settings” called “Focal Point in Physics Replication LOD” which you enable. Put the settings component on your pawn and link the data asset to it.

There are some CVars for the LOD here p.ReplicationLOD.
If you use the debug draw CVar for the LOD you also need to enable Chaos DebugDraw: p.Chaos.DebugDraw.Enabled 1

There is no public documentation for this yet and it’s not a completed feature but it’s something I’ll work on next year. It might be hard to understand how it works but if you want to dig in the files are PhysicsReplicationLOD.h and .cpp

Hopefully by mid-end next yet I’ll have a tutorial up with this too.

1 Like

It works for me (the classic answer)
Is everything else working for you with resimulation and networking etc.?
Are you using async physics?
Do you have Enabled Physics Prediction ticked in project settings?

Are you sure that your game thread is not sending the PhysicsObject each frame or something via the AsyncInput to the physics thread?

@MBobbo Hi. I managed to fix the crash. However, I came across a strange behavior of a physical pawn. Perhaps you can tell me When the possession of the character occurs before the physical pawn, then the Actor Relevance stops working adequately. For example, the usual situation is when the distance was exceeded, the pawn was destroyed on the proxy client. However, after the approach, the pawn does not appear on the proxy client. She disappears forever. I have debugged and found out that it is not being updated.
SrcLocation
in

bool APhysicsPawn ::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation)

In SrcLocation
the coordinates of the place where the possession was committed are received. I’ve done a lot of experiments, and this problem only occurs with the physical pawn. I also want to take the liberty of noting that if you specify a physical pawn as the default pawn, then there will be no problem. However, if you make possession from a character to a physical pawn, then the Actor Relevance will be broken.

This function “IsNetRelevantFor” will not be called with iris. I disabled iris, because according to my observations, it does not play a role in this context.

First of all thanks for this tutorial, I really enjoyed it, also “somewhat” understood how it works, how we send input from game thread to physics engine and handle it. I didn’t have too many problems with tutorial but generally changed some stuff here and there and added a bounce vertical override to have a physics driven jump (atleast override).

Here is my code with minor additions

.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Physics/NetworkPhysicsComponent.h"
#include "MyPhysicsPawn.generated.h"


//----------------------------------------------------
//--------------------- Structs ----------------------
//----------------------------------------------------

// GameThread to PhysicsThread input data struct
struct FAsyncInputPhysicsPawn : public Chaos::FSimCallbackInput
{
	float MovementInput;
	float SteeringInput;
	float BounceInput;
	
	void Reset()
	{
		MovementInput = 0.0f;
		SteeringInput = 0.0f;
		BounceInput = 0.0f;
	}
};

// Networked input data struct
USTRUCT()
struct FNetInputPhysicsPawn : public FNetworkPhysicsPayload
{
	GENERATED_BODY()
 
	FNetInputPhysicsPawn()
		: MovementInput(0.0f), SteeringInput(0.0f), BounceInput(0)
	{
	}

	UPROPERTY()
	float MovementInput;
 
	UPROPERTY()
	float SteeringInput;
	
	UPROPERTY()
	float BounceInput;
 
	void InterpolateData(const FNetworkPhysicsPayload& MinData, const FNetworkPhysicsPayload& MaxData, float LerpAlpha) override;
	void MergeData(const FNetworkPhysicsPayload& FromData) override;
	void DecayData(float DecayAmount) override;
	bool CompareData(const FNetworkPhysicsPayload& PredictedData) const override;
 
	const FString DebugData() const override { return FString::Printf(TEXT("MovementInput: %f - SteeringInput :%f"), MovementInput, SteeringInput); }
};

// PhysicsThread to GameThread output data struct
struct FAsyncOutputPhysicsPawn : public Chaos::FSimCallbackOutput
{
	void Reset() {}
};

//----------------------------------------------------
//-------------- Async Networked Physics--------------
//----------------------------------------------------

USTRUCT()
struct FNetStatePhysicsPawn : public FNetworkPhysicsPayload
{
	GENERATED_BODY()
	FNetStatePhysicsPawn() {}
	void InterpolateData(const FNetworkPhysicsPayload& MinData, const FNetworkPhysicsPayload& MaxData, float LerpAlpha) override { }
	bool CompareData(const FNetworkPhysicsPayload& PredictedData) const override { return true; }
};

class FPhysicsPawnAsync : public Chaos::TSimCallbackObject<FAsyncInputPhysicsPawn, FAsyncOutputPhysicsPawn,
	(Chaos::ESimCallbackOptions::Presimulate | Chaos::ESimCallbackOptions::PhysicsObjectUnregister)>
	, TNetworkPhysicsInputState_Internal<FNetInputPhysicsPawn, FNetStatePhysicsPawn>
{
	friend class AMyPhysicsPawn;
	~FPhysicsPawnAsync() {}
	
	// TSimCallbackObject callbacks
	virtual void OnPostInitialize_Internal() override;
	virtual void ProcessInputs_Internal(int32 PhysicsStep) override;
	virtual void OnPreSimulate_Internal() override;
	virtual void OnPhysicsObjectUnregistered_Internal(Chaos::FConstPhysicsObjectHandle InPhysicsObject) override;

	// TNetworkPhysicsInputState_Internal callbacks
	virtual void BuildInput_Internal(FNetInputPhysicsPawn& Input) const override;
	virtual void ValidateInput_Internal(FNetInputPhysicsPawn& Input) const override;
	virtual void ApplyInput_Internal(const FNetInputPhysicsPawn& Input) override;
	virtual void BuildState_Internal(FNetStatePhysicsPawn& State) const override;
	virtual void ApplyState_Internal(const FNetStatePhysicsPawn& State) override;
	
public:
	Chaos::FConstPhysicsObjectHandle PhysicsObject = nullptr;
	
	
	// Move Networking
	void SetMovementInput_Internal(float InMovementInput) { MovementInput_Internal = InMovementInput; }
	const float GetMovementInput_Internal() const { return MovementInput_Internal; }
	
	// Steer Networking
	void SetSteeringInput_Internal(float InSteeringInput) { SteeringInput_Internal = InSteeringInput; }
	const float GetSteeringInput_Internal() const { return SteeringInput_Internal; }
	
	// Bounce Networking
	void SetBounceInput_Internal(float InBounceInput) { BounceInput_Internal = InBounceInput; }
	const float GetBounceInput_Internal() const { return BounceInput_Internal; }
	
private:
	float MovementInput_Internal;
	float SteeringInput_Internal;
	float BounceInput_Internal;
	
	// Helpers
	
	// For consuming single fire action consistently in async thread
	bool bHasProcessedJump = false;
};


//----------------------------------------------------
//--------------------- Pawn -------------------------
//----------------------------------------------------


UCLASS()
class SANDBOX_API AMyPhysicsPawn : public APawn
{
	GENERATED_BODY()

public:
	// Sets default values for this pawn's properties
	AMyPhysicsPawn();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

public:
	// Called every frame
	virtual void Tick(float DeltaTime) override;
	
	virtual void PostInitializeComponents() override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
	
	// Inputs
	UFUNCTION(BlueprintCallable, Category = "Game|PhysicsPawn")
	void SetForwardInput(const float InForwardInput)
	{
		ForwardInput_External = FMath::Clamp(InForwardInput, 0.0f, 1.0f);
	}
 
	UFUNCTION(BlueprintCallable, Category = "Game|PhysicsPawn")
	void SetBackwardInput(const float InBackwardInput)
	{
		BackwardInput_External = FMath::Clamp(InBackwardInput, 0.0f, 1.0f);
	}
 
	UFUNCTION(BlueprintCallable, Category = "Game|PhysicsPawn")
	void SetSteeringInput(const float InSteeringInput)
	{
		SteeringInput_External = FMath::Clamp(InSteeringInput, -1.0f, 1.0f);
	}
	
	UFUNCTION(BlueprintCallable, Category = "Game|PhysicsPawn")
	void SetBounceInput(const float InBounceInput)
	{
		BounceInput_External = FMath::Clamp(InBounceInput, 0.f, 1.0f);
	}


private:
	FPhysicsPawnAsync* PhysicsPawnAsync = nullptr;
	
	UPROPERTY()
	TObjectPtr<UNetworkPhysicsComponent> NetworkPhysicsComponent = nullptr;
	
	float ForwardInput_External = 0.0f;
	float BackwardInput_External = 0.0f;
 
	float SteeringInput_External = 0.0f;
	float BounceInput_External = 0.0f;
};

.cpp
#include "MyPhysicsPawn.h"
#include "Engine/Engine.h"
#include "Physics/Experimental/PhysScene_Chaos.h"
#include "PBDRigidsSolver.h"
#include "Chaos/PhysicsObjectInterface.h"
#include "Chaos/PhysicsObjectInternalInterface.h"

//----------------------------------------------------
//-------------- Async Networked Physics--------------
//----------------------------------------------------

void FPhysicsPawnAsync::OnPhysicsObjectUnregistered_Internal(Chaos::FConstPhysicsObjectHandle InPhysicsObject)
{
	if (PhysicsObject == InPhysicsObject)
	{
		PhysicsObject = nullptr;
	}
}

// Input to State application
/** Called on Autonomous-Proxy Client or Server if the server is controlling the pawn */
void FPhysicsPawnAsync::BuildInput_Internal(FNetInputPhysicsPawn& Input) const
{
	Input.MovementInput = MovementInput_Internal;
	Input.SteeringInput = SteeringInput_Internal;
	Input.BounceInput = BounceInput_Internal;
}
/** Validate incoming input data on the server, clamp to valid values */
void FPhysicsPawnAsync::ValidateInput_Internal(FNetInputPhysicsPawn& Input) const
{
	Input.MovementInput = FMath::Clamp(Input.MovementInput, -1.0f, 1.0f);
	Input.SteeringInput = FMath::Clamp(Input.SteeringInput, -1.0f, 1.0f);
	Input.BounceInput = FMath::Clamp(Input.BounceInput, 0.0f, 1.0f);
}
/** Called on Server and Sim-Proxy Clients each frame and on Clients during Resimulations */
void FPhysicsPawnAsync::ApplyInput_Internal(const FNetInputPhysicsPawn& Input)
{
	SetMovementInput_Internal(Input.MovementInput);
	SetSteeringInput_Internal(Input.SteeringInput);
	SetBounceInput_Internal(Input.BounceInput);
}

/** Called on Server */
void FPhysicsPawnAsync::BuildState_Internal(FNetStatePhysicsPawn& State) const
{
	
}

/** Called on Clients during resimulations */
void FPhysicsPawnAsync::ApplyState_Internal(const FNetStatePhysicsPawn& State)
{
	
}

void FPhysicsPawnAsync::OnPostInitialize_Internal()
{
	if (PhysicsObject)
	{
		Chaos::FWritePhysicsObjectInterface_Internal Interface = Chaos::FPhysicsObjectInternalInterface::GetWrite();
		if (Chaos::FPBDRigidParticleHandle* ParticleHandle = Interface.GetRigidParticle(PhysicsObject))
		{
			ParticleHandle->SetSleepType(Chaos::ESleepType::NeverSleep);
		}
	}
}

void FPhysicsPawnAsync::ProcessInputs_Internal(int32 PhysicsSteps)
{
	const FAsyncInputPhysicsPawn* AsyncInput = GetConsumerInput_Internal();
	if (AsyncInput == nullptr)
	{
		return;
	}
	Chaos::FPhysicsSolverBase* BaseSolver = GetSolver();
	if (!BaseSolver || BaseSolver->IsResimming())
	{
		return;
	}
	
	
	// Get inputs
	SetMovementInput_Internal(AsyncInput->MovementInput);
	SetSteeringInput_Internal(AsyncInput->SteeringInput);
	SetBounceInput_Internal(AsyncInput->BounceInput);
	
};

void FPhysicsPawnAsync::OnPreSimulate_Internal()
{
	if (!PhysicsObject)
	{
		return;
	}
	
	Chaos::FWritePhysicsObjectInterface_Internal Interface = Chaos::FPhysicsObjectInternalInterface::GetWrite();
	Chaos::FPBDRigidParticleHandle* ParticleHandle = Interface.GetRigidParticle(PhysicsObject);
	
	if (ParticleHandle == nullptr)
	{
		return;
	}
	
	if (const FAsyncInputPhysicsPawn* AsyncInput = GetConsumerInput_Internal())
	{
		SetMovementInput_Internal(AsyncInput->MovementInput);
		SetSteeringInput_Internal(AsyncInput->SteeringInput);
		SetBounceInput_Internal(AsyncInput->BounceInput);
	}
	
	// Calculate forces
	constexpr float ForceMultiplier = 1000.0f;
	constexpr float JumpMultiplier = 250.0f;

	const float InputLinearMovementForce = MovementInput_Internal * ForceMultiplier;
	const float InputLinearSteeringForce = SteeringInput_Internal * ForceMultiplier;
	
	const float InputAngularMovementForce = MovementInput_Internal * ForceMultiplier;
	const float InputAngularSteeringForce = SteeringInput_Internal * ForceMultiplier;
	
	const float InputBounceForce = BounceInput_Internal * JumpMultiplier;
	
	// Only process horizontal axis not vertical for now
	Chaos::FVec3 LinearMovement = Chaos::FVec3(InputLinearMovementForce, InputLinearSteeringForce, 0.0f);
	
	if (InputBounceForce > UE_SMALL_NUMBER)
	{
		// ONLY jump if we haven't already jumped for this specific button press
		if (!bHasProcessedJump)
		{
			Chaos::FVec3 CurrentV = ParticleHandle->GetV();

			// Get the last velocity and add to it
			if (CurrentV.Z < 0.0f)
			{
				CurrentV.Z = FMath::Abs(CurrentV.Z) * 0.8f; 
			}
			
			// Force Z to exactly the jump velocity but add to it so it's incremental
			CurrentV.Z += InputBounceForce; 

			// Apply the final calculated velocity back to the particle
			ParticleHandle->SetV(CurrentV);
			
			bHasProcessedJump = true; 
		}
	}
	else
	{
		bHasProcessedJump = false;
	}
	
	Chaos::FVec3 AngularMovement = Chaos::FVec3(0.0f, InputAngularMovementForce, InputAngularSteeringForce);
	
	// Apply Linear Forces 
	if (LinearMovement.SizeSquared() > UE_SMALL_NUMBER)
	{
		ParticleHandle->AddForce(LinearMovement, true);
	}
	
	// Apply Angular Forces
	if (AngularMovement.SizeSquared() > UE_SMALL_NUMBER)
	{
		ParticleHandle->AddTorque(AngularMovement, true);
	}
}


//----------------------------------------------------
//--------------------- Pawn -------------------------
//----------------------------------------------------

// Sets default values
AMyPhysicsPawn::AMyPhysicsPawn()
{
	// Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	bReplicates = true;
	
	// Only create a network physics component if physics prediction is enabled
	if (UPhysicsSettings::Get()->PhysicsPrediction.bEnablePhysicsPrediction)
	{
		static const FName NetworkPhysicsComponentName(TEXT("NetworkPhysicsComponent"));
		
		// Add network physics component as a subobject
		NetworkPhysicsComponent = CreateDefaultSubobject<UNetworkPhysicsComponent>(NetworkPhysicsComponentName);
		NetworkPhysicsComponent->SetNetAddressable(); // Make subobject component net addressable
		NetworkPhysicsComponent->SetIsReplicated(true);
	}
}

// Called when the game starts or when spawned
void AMyPhysicsPawn::BeginPlay()
{
	Super::BeginPlay();
	
}

void AMyPhysicsPawn::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);
}

// Called every frame
void AMyPhysicsPawn::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
 
	if (PhysicsPawnAsync)
	{
		if (IsLocallyControlled())
		{
			if (FAsyncInputPhysicsPawn* AsyncInput = PhysicsPawnAsync->GetProducerInputData_External())
			{
				AsyncInput->MovementInput = ForwardInput_External - BackwardInput_External;
				AsyncInput->SteeringInput = SteeringInput_External;
				AsyncInput->BounceInput = BounceInput_External;
				
				// Bounce input is one off go like an impulse so we immediately revert input.
				BounceInput_External = 0.0f;
			}
		}
	}
}

void AMyPhysicsPawn::PostInitializeComponents()
{
	Super::PostInitializeComponents();
 
	if (UPrimitiveComponent* RootSimulatedComponent = Cast<UPrimitiveComponent>(GetRootComponent()))
	{
		if (UWorld* World = GetWorld())
		{
			if (FPhysScene* PhysScene = World->GetPhysicsScene())
			{
				if (Chaos::FPhysicsSolver* Solver = PhysScene->GetSolver())
				{
					// Create async callback object to run on Physics Thread
					PhysicsPawnAsync = Solver->CreateAndRegisterSimCallbackObject_External<FPhysicsPawnAsync>();
					if (ensure(PhysicsPawnAsync))
					{
						PhysicsPawnAsync->PhysicsObject = RootSimulatedComponent->GetPhysicsObjectByName(NAME_None);
 
						if (NetworkPhysicsComponent)
						{
							// Register the input and state structs along with PT interface in the networked physics component
							NetworkPhysicsComponent->CreateDataHistory<FNetInputPhysicsPawn, FNetStatePhysicsPawn>(PhysicsPawnAsync);
						}
					}
				}
			}
		}
	}
}

// Called to bind functionality to input
void AMyPhysicsPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
}

/** Interpolate input between two inputs */
void FNetInputPhysicsPawn::InterpolateData(const FNetworkPhysicsPayload& MinData, const FNetworkPhysicsPayload& MaxData, float LerpAlpha)
{
	const FNetInputPhysicsPawn& MinInput = static_cast<const FNetInputPhysicsPawn&>(MinData);
	const FNetInputPhysicsPawn& MaxInput = static_cast<const FNetInputPhysicsPawn&>(MaxData);
	MovementInput = FMath::Lerp(MinInput.MovementInput, MaxInput.MovementInput, LerpAlpha);
	SteeringInput = FMath::Lerp(MinInput.SteeringInput, MaxInput.SteeringInput, LerpAlpha);
	BounceInput = FMath::Lerp(MinInput.BounceInput, MaxInput.BounceInput, LerpAlpha);
}

/** Merge input into this input to take both inputs into account */
void FNetInputPhysicsPawn::MergeData(const FNetworkPhysicsPayload& FromData)
{
	const FNetInputPhysicsPawn& FromInput = static_cast<const FNetInputPhysicsPawn&>(FromData);
	MovementInput = FMath::Max(MovementInput, FromInput.MovementInput);
	SteeringInput = FMath::Max(SteeringInput, FromInput.SteeringInput);
	BounceInput = FMath::Max(BounceInput, FromInput.BounceInput);
}

/** Decay input during resimulation if input is predicted */
void FNetInputPhysicsPawn::DecayData(float DecayAmount)
{
	MovementInput = MovementInput * (1.0f - DecayAmount);
	SteeringInput = SteeringInput * (1.0f - DecayAmount);
	BounceInput = BounceInput * (1.0f - DecayAmount);
}

/** Compare predicted input with correct input from server and trigger resim if they differ */
bool FNetInputPhysicsPawn::CompareData(const FNetworkPhysicsPayload& PredictedData) const
{
	const FNetInputPhysicsPawn& PredictedInput = static_cast<const FNetInputPhysicsPawn&>(PredictedData);
	bool bHasDiff = false;
	bHasDiff |= MovementInput != PredictedInput.MovementInput;
	bHasDiff |= SteeringInput != PredictedInput.SteeringInput;
	bHasDiff |= BounceInput != PredictedInput.BounceInput;
	return (bHasDiff == false);
}```

I am willing to experiment with it more and your tutorials in future.

Hello Markus, thank you for your response!

I’ve briefly tried LODs options, seems like it kinda works, but didn’t have a chance to test it properly. So it make the situation with tons of physics actors more-less acceptable but for sure for now it’s better to avoid that.

So I’m creating a work-around now with the logic of replacing some physics bodies with static meshes if some conditions are satisfied. However I see potential issue happening during deletion of the physics object with resim option - client’s pawn “stutters” and not moving much for some time, then it moves rapidly to expected position. (even if the pawn is not close to the deleted body and connection is quite stable)

Some tech details:

  • I’m performing deletion on server;
  • Physics body is set to movable, replicated, replicate movement, resim mode (no issues interacting with it normally);
  • I was trying to disable collisions/disable physics before deletion but it didn’t help;
  • Doing that playing as server has no issues;
  • I don’t see any spikes in profiles during the issue.

Do you have any ideas about that or any recommendation about the proper way to delete physics objects from the scene?

Hi everyone,

First of all, thank you so much for this amazing tutorial! We are currently using this networked physics system for our upcoming online game, “Soap Slide,” and it has been working remarkably well. Even under poor network conditions, the resimulation and prediction handle everything smoothly with almost no noticeable jitter.

However, I’ve been seeing some persistent warnings in the logs that I’m curious about. While they don’t seem to cause any “catastrophic” gameplay issues, I’d like to understand their root cause to ensure the project stays clean as we move toward release.

Specifically, I’m getting these Iris and Physics replication logs:

Plaintext

LogIris: Warning: Trying to change non-existing custom conditional for RepIndex 2 in protocol NetworkPhysicsComponent
LogIris: Warning: Trying to change non-existing custom conditional for RepIndex 3 in protocol NetworkPhysicsComponent
LogIris: Warning: Trying to change non-existing custom conditional for RepIndex 6 in protocol NetworkPhysicsComponent

LogPhysics: Warning: FPhysicsReplication DESYNCED - received target frame (0) out of rewind data bounds (0, 0) - Client is behind the server, client might be dropping frames. - Target will use EPhysicsReplicationMode::Pred

LogConsoleManager: Warning: Console object named 'np2.DebugNetworkPhysicsPrediction' already exists but is being registered again!

Has anyone else encountered these when using the NetworkPhysicsComponent with Iris? I’m particularly curious if the Iris warnings suggest a mismatch in the replication protocol or if the DESYNCED log is something I should be concerned about long-term, despite the movement feeling solid.

Any insights would be greatly appreciated. Thanks again for the great contribution to the community!

Hello, thank you for using networked physics!

LogIris Issue

This gets fixed in UE 5.8 it’s because there are two different networking flows inside the NetworkPhysicsComponent, one meant for Iris and one meant for RepGraph. Some replicated properties gets disabled from replication depending on which one is being used, and the disabled properties had COND_None set instead of COND_Custom. To disable a property from replication it needs to be marked with custom conditions. This is nothing you need to do anything about it will fix itself in 5.8.

FPhysicsReplication DESYNCED

You will see this at the very start of your game, at that point it’s not an issue so you can disregard it. Normally it has 0 or a negative number printed here: “received target frame (0)“.

When you get this while playing though it indicates issues:

  • “Rewind data bounds (0, 0)” - I expect this is not the actual log you got, it should look something like this “(10, 42)” as an example
    • 42 = Current (latest) physics frame number
    • 10 = The earliest physics frame number we have cached data for in the RewindHistory
  • (5) out of rewind data bounds (10, 42)
    • You received a state from the server for frame 5, you are currently at frame 42 and have history back to frame 10 so frame 5 is too far behind for you to rewind to and resimulate from.
    • In this case the client is too far ahead of the server.
      • Maybe your server is running with bad performance which will drop physics frames in the worst case which makes your client simulate more physics frames than the server making it end up further ahead than the server.
      • Maybe your network round trip time (latency/ping) betwen client and server is too high. 42 - 5 = 37 frames round trip, at 30hz physics (33.33ms) that’s 37 * (1 /30) = 1,23seconds. By default the RewindData initializes to support 1s (1000ms) of latency, you can adjust this in Project “Settings → Physics → Replication → Max Supported Latency Prediction” but I’d recommend to keep it somewhere between 600-1000 since higher values will cost a lot of memory and resimulating 1000ms worth of physics will take a lot of CPU and games generally don’t behave well with anywhere near that high RTT anyway.
  • (45) out of rewind data bounds (10, 42)
    • You received a state from the server for frame 45, you are currently at frame 42, the client should be ahead of the server since the client is the one producing inputs and sending those to the server to get applied there on the correct frame.
    • In this case the client is behind the server, which comes from bad performance, the client is most likely “dropping” physics frames. For example at 30hz physics if your game ticks slower than 10hz then it will drop physics frames because it can only queued up 3 physics frames to be executed in a row. It it needed to dispatch 4 physics frames the last frame will just not be performed in an attempt to not spiral the CPU consumption down to a halt.
      • Note that when a client is not in focus in windows it might get deprioritized and tick slower resulting in dropped physics frames.
      • Hitches like large garbage collections can also cause this since if you have a hitch of 500ms then only 100ms of that will be used to step physics with (at 30hz physics)

When the client is too far ahead or when it’s behind the server the server will start telling the client to speed up or slow down physics to try to get it into the RewindHistory data bounds again. If that fails to correct it via time dilation within 5 seconds it will do a hard correction by recalculating the “tick offset” between client and server.
CVar: np2.TickOffsetCorrectionTimeLimit 5000

You also have some CVar settings for how the TimeDilation works, the console will autocomplete to show you the varios settings if you type this: np2.TimeDilation

np2.DebugNetworkPhysicsPrediction duplicate registration

This is also being fixed in UE5.8, no need to do anything before that.

I will do a “Fundamentals” tutorial when I get time to go over most of how the whole system works, debugging and balancing etc.

2 Likes

Thank you so much for the detailed explanation!

I suspected that some of these logs were related to the internal transition between Iris and RepGraph, but having your confirmation is incredibly helpful for our peace of mind as we approach the release of our game.

It’s great to know that the Iris and console registration warnings are already on the roadmap for UE 5.8. Regarding the desync logs, our gameplay currently feels very smooth even under poor network conditions, which speaks volumes about how well the resimulation and time dilation are working behind the scenes.

I’m closely following your documentation and tutorials, and I’m really looking forward to that ‘Fundamentals’ guide whenever you have the time. Your contributions are making a huge difference for those of us diving into the new networked physics workflow.

Thanks again for the help and keep up the amazing work, reis!

1 Like

Hi @MBobbo, seems like in general I’ve managed to overcome some major difficulties and almost everything is running smooth now :slight_smile: However, I still have 2 questions left:

  1. Is there a way to understand what exact physics body triggered the resimulation? Let’s say I have a scene which is quite stationary in my opinion, but I see that performance is heavily goes down because of constant resim trigger (I see many background physics tasks), but I’m not sure how to understand what component asks for that.

  2. About deletion of the physics actors with resim enabled. Let’s say I have AActor with StaticMesh in the root and resim mode. All interaction of that actor with pawn works smooth. However, when I delete this actor, I see significant stutter on a client in terms of it’s motion is interrupted and not fully evaluated (imagine you are moving 100% left when deletion happened and during the stutter you move only 5-15% for some time, no fps drops).

I’m doing deletion on server straightforwardly like that:

void UWeaponComponent::Server_ReplacePhysicsBodyWithStaticMesh_Implementation(FForceApplyPrms ForcePrms)
{
	if (!ForcePrms.TargetActor) return;


// this causes motion stutter for client's pawn
	ForcePrms.TargetActor->Destroy();


}

Hello, unfortunately there is no good way yet of showing the object that caused the resimulation.
If you have unreal engines source code then you could breakpoint in FRewindData::RequestResimulation() and try to track it down from there. It passes in the physics object (particle) that requests the resimulation there but particle is a physics thread entity so it doesn’t have the AActor reference on it but it might have a debug name if you inspect it or you could print out the pointer address for each PhysicsObject in a log and then find it there.

It’s on the todo-list to add a feature in Chaos Visual Debugger to show which particle(s) caused resimulations.
You could also look in CVD to see which particle actually gets corrected visually when the resim starts but it might be a bit hard, specially if it’s a small correction of a rotation for example.

About deletion, I’ve not seen issues with this before, we create and delete objects at runtime while running resimulation and it supports it. It would also be interesting to see how it plays out in CVD. When, where and why is it moving slower.
If might be that you don’t have frame drops on the client but the server might which results in some physics frames getting dropped (which would essentially slow down the simulation).

I’m really enjoying experimenting with Networked Physics also with ModularVehicles so far. Here is what I am working on.

I managed to get a 3 pawn setup working correctly (all using ModularVehiclePawn, with one locally controlled and the others simulated), and everything behaves as expected with resimulation.

However, I ran into issues when trying a mixed setup:

  • 1 × ModularVehiclePawn (locally controlled)
  • 2 × ModularVehicleActor

In this case, input forwarding became problematic. Even when explicitly forwarding input via RPCs from the pawn/controller to the actors, the simulation modules on the actors appear unresponsive.

Using the same logic but switching all three to ModularVehiclePawn (even with only one being locally controlled) works correctly.

My question is:
Is Networked Physics currently intended to be Pawn-only for input-driven simulation
Or is there a supported way to integrate actor based physics objects into the resimulation input pipeline?

A second question about geometry collections:

I noticed that sockets defined on ModularVehicleUnion work correctly, but sockets on child Geometry Collections do not appear to update while simulating even it’s attached to parent root bone.

Physics and animation behave correctly, but socket transforms never seem to reflect the simulated motion.

I found that creating sub Geometry Collections and disabling their simulation allows sockets to work, but this feels more like a workaround.

Is there a supported way to have sockets follow child Geometry Collections while they are simulating, or is this currently a limitation of Chaos GC socket evaluation? This become important while I was trying to create custom pawns like an industrial vehicle (Escavator) where multiple bodies are working together. I was able to make an arm that is basically acts like a hydraulic rotator and it works nicely in multiplayer, however when it comes to a little bit cosmetics the sockets starts being pain on geometry collections.

Thanks!

This is how it looks like in CVD. The character is moving around an actor with physics with resim and at some point I trigger the command for deletion of this physics body on server and spawn of static mesh component with collision on this place. (I’ve checked without spawning static mesh, the result is the same)

You can see that on server the input is updated correctly and the motion is smooth, and on a client I see some harsh movement here and there, while I see no frames drops, and there no any collision between two bodies at all.

Hi @MBobbo,

First of all, thank you for sharing this system; it’s been incredibly helpful for my project. I have a few technical questions regarding its implementation and behavior in real-world scenarios. I would be very grateful if you could share your insights.

1. Architectural Structure: Separation of Physic and Game Threads I have implemented both the Game Thread and the Physics Thread logic (Async) within the same Character class. Is there a specific architectural reason for keeping these together in one file? If I were to move the physics thread logic into a separate file or class to improve code organization, would this cause any synchronization issues or performance overhead within the NPP framework?

2. Input Payload and Performance: Scalability and Redundancy Currently, I am sending a significant amount of input data every frame to the physics thread:

C++

AsyncInput->bShouldBounce = bShouldBounce_External;
AsyncInput->bFire = bFire_External;
AsyncInput->bIsGrounded = bIsGrounded_External;
AsyncInput->bIsAiming = bIsAiming_External;
AsyncInput->bJump = bJump_External;
AsyncInput->TargetRotation = TargetRotation_External;
AsyncInput->CurrentPowerValue = CurrentPowerValue_External;
AsyncInput->bDriftLeft = bDriftLeft_External;
AsyncInput->bDriftRight = bDriftRight_External;
AsyncInput->bBrake = bBrake_External;
AsyncInput->bAccelerate = bAccelerate_External;

Is sending this many inputs every frame considered ‘heavy’ or suboptimal for the NPP system? What is the recommended limit for input data size, and are there specific negative consequences I should be aware of? Additionally, how does the ‘Redundant Inputs’ setting within the NPP Data Asset interact with this constant stream of frame-by-frame inputs?

3. The ‘Steam Gap’: Local Simulation vs. Real-World Desync I have encountered a puzzling issue regarding desync and jitter. When testing in the Editor—even with simulated high latency and bad network conditions—the game runs flawlessly with zero jitter, rubberbanding, or desync logs. However, when I publish the game as a Steam Demo and play over the actual internet, the client experiences severe rubberbanding, jitter, and constant desync logs, even when the ping is below 100ms.

What could be causing this drastic difference between local network simulation and the Steam/Live environment? Is this a known issue related to the experimental nature of NPP (specifically 5.5/5.7 versions), or could there be an underlying synchronization problem that local simulation fails to capture?

Thanks in advance for your time and help!