Tutorial for creating a custom physics-driven pawn that works in multiplayer through physics resimulation.
https://dev.epicgames.com/community/learning/tutorials/MoBq/unreal-engine-networked-physics-pawn-tutorial
I just copy pasted full files and did other setting up stuff just to check out. And client inputs doesnât seem to work for some reason. Listen server can move and everything works perfectly, however when trying to move as client, sphere moves then gets corrected back to where it started without moving on server. Any idea what I could have missed?
Hello, did you find what it was?
Sounds like itâs networking (iris, pushbased) but could also be that the inputs being sent from client to server arenât populated correctly on the client or that the server doesnât apply the inputs received I doubt those two though.
Iâd recommend printing out logs along the flow or breakpointing it.
Does it hit BuildInput_Internal() on the client and ApplyInput_Internal() on the server?
Is the input value lost between those callbacks?
Check if the server receive the input RPC: UNetworkPhysicsComponent::ServerReceiveInputCollection_Implementation()
You can print this debug log to see the contents of the incoming inputs (there will be multiple inputs in each RPC)
ClientInputCollection.DebugCollection(FString::Printf(TEXT(âServerReceiveInputCollection_Implementationâ));
Hi, I tried to follow the guide step by step but I encounter the same problem like @Nicat .
I tried it on 5.7 (also bUseIris = true; mentioned in Introduction to Iris is no longer available I guess).
Do you have any idea what I could have missed?
Tried this as well, didnât manage to make it work, pawn is always corrected to initial state
Hi. I also came across the fact that the client cannot move. There is a feeling that the input is not being transmitted to the server. Could you please show your iris configuration settings?
Hi everyone, will some collective efforts seems like there are some promising results with that! Iâm attaching the full code as it works now for me, Iâve commented updated parts. Itâs the part of my project so input handling is done in my parent class
Please note that I DONâT understand this module in details yet, but it works! Iâve attached the video with bad network emulation
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "BobrCharacterCore.h"
#include "Physics/NetworkPhysicsComponent.h"
#include "NetworkAnotherApproach.generated.h"
UCLASS()
class BOBR_API APhysicsPawn : public ABobrCharacterCore
{
GENERATED_BODY()
public:
APhysicsPawn();
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
public:
virtual void PreInitializeComponents() override;
virtual void PostInitializeComponents() override;
virtual void Tick(float DeltaTime) override;
/** Set forward input, clamped from 0.0 to 1.0 */
UFUNCTION(BlueprintCallable, Category = "Game|PhysicsPawn")
void SetForwardInput(const float InForwardInput);
/** Set backward input, clamped from 0.0 to 1.0 */
UFUNCTION(BlueprintCallable, Category = "Game|PhysicsPawn")
void SetBackwardInput(const float InBackwardInput);
/** Set steering input, clamped from -1.0 to 1.0 */
UFUNCTION(BlueprintCallable, Category = "Game|PhysicsPawn")
void SetSteeringInput(const float InSteeringInput);
private:
class FPhysicsPawnAsync* PhysicsPawnAsync = nullptr;
UPROPERTY()
TObjectPtr<UNetworkPhysicsComponent> NetworkPhysicsComponent = nullptr;
/** Movement input on Game Thread */
float ForwardInput_External = 0.0f;
float BackwardInput_External = 0.0f;
/** Steering input on Game Thread */
float SteeringInput_External = 0.0f;
};
// Networked input data struct
USTRUCT()
struct FNetInputPhysicsPawn : public FNetworkPhysicsPayload
{
GENERATED_BODY()
FNetInputPhysicsPawn()
: MovementInput(0.0f), SteeringInput(0.0f)
{
}
UPROPERTY()
float MovementInput;
UPROPERTY()
float SteeringInput;
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); }
};
// Networked state data struct
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; }
};
// GameThread to PhysicsThread input data struct
struct FAsyncInputPhysicsPawn : public Chaos::FSimCallbackInput
{
float MovementInput;
float SteeringInput;
bool bUseMovementInputForSimulation = false; // NEW
void Reset()
{
MovementInput = 0.0f;
SteeringInput = 0.0f;
bUseMovementInputForSimulation = false; // NEW
}
};
// PhysicsThread to GameThread output data struct
struct FAsyncOutputPhysicsPawn : public Chaos::FSimCallbackOutput
{
void Reset() {}
};
class FPhysicsPawnAsync : public Chaos::TSimCallbackObject<FAsyncInputPhysicsPawn, FAsyncOutputPhysicsPawn,
(Chaos::ESimCallbackOptions::Presimulate | Chaos::ESimCallbackOptions::Rewind)>
, TNetworkPhysicsInputState_Internal<FNetInputPhysicsPawn, FNetStatePhysicsPawn>
{
friend APhysicsPawn;
~FPhysicsPawnAsync() {}
// TSimCallbackObject callbacks
virtual void OnPostInitialize_Internal() override;
virtual void ProcessInputs_Internal(int32 PhysicsStep) override;
virtual void OnPreSimulate_Internal() 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;
private:
Chaos::FConstPhysicsObjectHandle PhysicsObject = nullptr;
float MovementInput_Internal;
float SteeringInput_Internal;
TWeakObjectPtr<APawn> OwningPawn; // NEW
public:
void SetMovementInput_Internal(float InMovementInput) { MovementInput_Internal = InMovementInput; }
const float GetMovementInput_Internal() const { return MovementInput_Internal; }
void SetSteeringInput_Internal(float InSteeringInput) { SteeringInput_Internal = InSteeringInput; }
const float GetSteeringInput_Internal() const { return SteeringInput_Internal; }
};
#include "NetworkAnotherApproach.h"
#include "Engine/Engine.h"
#include "Physics/Experimental/PhysScene_Chaos.h"
#include "PBDRigidsSolver.h"
#include "Chaos/PhysicsObjectInternalInterface.h"
APhysicsPawn::APhysicsPawn()
{
PrimaryActorTick.bCanEverTick = true;
// Only create a network physics component if physics prediction is enabled
if (UPhysicsSettings::Get()->PhysicsPrediction.bEnablePhysicsPrediction)
{
static const FName NetworkPhysicsComponentName("NetworkPhysicsComponent");
// Add network physics component as a subobject
NetworkPhysicsComponent = CreateDefaultSubobject<UNetworkPhysicsComponent>(NetworkPhysicsComponentName);
NetworkPhysicsComponent->SetNetAddressable(); // Make subobject component net addressable
NetworkPhysicsComponent->SetIsReplicated(true);
}
}
void APhysicsPawn::PreInitializeComponents()
{
Super::PreInitializeComponents();
UPrimitiveComponent* RootSimulatedComponent = Cast<UPrimitiveComponent>(GetRootComponent());
check(RootSimulatedComponent);
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);
PhysicsPawnAsync->OwningPawn = this; // New
}
}
}
}
if (NetworkPhysicsComponent && PhysicsPawnAsync)
{
// Register the input and state structs along with PT interface in the networked physics component
NetworkPhysicsComponent->CreateDataHistory<FNetInputPhysicsPawn, FNetStatePhysicsPawn>(PhysicsPawnAsync);
NetworkPhysicsComponent->SetPhysicsObject(PhysicsPawnAsync->PhysicsObject); // New
}
}
void APhysicsPawn::PostInitializeComponents()
{
Super::PostInitializeComponents();
}
void APhysicsPawn::BeginPlay()
{
Super::BeginPlay();
}
void APhysicsPawn::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
}
// Updated
void APhysicsPawn::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (FAsyncInputPhysicsPawn* AsyncInput = PhysicsPawnAsync->GetProducerInputData_External())
{
const bool bIsLocallyControlled = IsLocallyControlled();
const ENetMode NetMode = GetNetMode();
AsyncInput->bUseMovementInputForSimulation = bIsLocallyControlled && (NetMode != NM_DedicatedServer);
if (bIsLocallyControlled)
{
AsyncInput->MovementInput = float(MovementMap.MoveForward) - float(MovementMap.MoveBackward);
AsyncInput->SteeringInput = float(MovementMap.MoveLeft) - float(MovementMap.MoveRight); // MovementMap is my own structure, you can use from the one from example
}
else
{
AsyncInput->MovementInput = 0.0f;
AsyncInput->SteeringInput = 0.0f;
}
}
}
void APhysicsPawn::SetForwardInput(const float InForwardInput)
{
ForwardInput_External = FMath::Clamp(InForwardInput, 0.0f, 1.0f);
}
void APhysicsPawn::SetBackwardInput(const float InBackwardInput)
{
BackwardInput_External = FMath::Clamp(InBackwardInput, 0.0f, 1.0f);
}
void APhysicsPawn::SetSteeringInput(const float InSteeringInput)
{
SteeringInput_External = FMath::Clamp(InSteeringInput, -1.0f, 1.0f);
}
/** Calculate and 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);
}
/** 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);
}
/** Decay input during resimulation if input is predicted */
void FNetInputPhysicsPawn::DecayData(float DecayAmount)
{
MovementInput = MovementInput * (1.0f - DecayAmount);
SteeringInput = SteeringInput * (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;
return (bHasDiff == false);
}
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);
}
}
}
/** Process marshaled data through async inputs, this is called before OnPreSimulate_Internal */
void FPhysicsPawnAsync::ProcessInputs_Internal(int32 PhysicsSteps)
{
const FAsyncInputPhysicsPawn* AsyncInput = GetConsumerInput_Internal();
if (!AsyncInput)
{
return;
}
Chaos::FPhysicsSolverBase* BaseSolver = GetSolver();
if (!BaseSolver || BaseSolver->IsResimming())
{
return;
}
if (AsyncInput->bUseMovementInputForSimulation)
{
SetMovementInput_Internal(AsyncInput->MovementInput);
SetSteeringInput_Internal(AsyncInput->SteeringInput);
}
};
void FPhysicsPawnAsync::OnPreSimulate_Internal()
{
if (!PhysicsObject)
{
return;
}
Chaos::FWritePhysicsObjectInterface_Internal Interface = Chaos::FPhysicsObjectInternalInterface::GetWrite();
Chaos::FPBDRigidParticleHandle* ParticleHandle = Interface.GetRigidParticle(PhysicsObject);
if (ParticleHandle == nullptr)
{
return;
}
// Calculate forces
constexpr float ForceMultiplier = 300000.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;
Chaos::FVec3 LinearMovement = Chaos::FVec3(InputLinearMovementForce, InputLinearSteeringForce, 0.0f);
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);
}
}
/** 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;
}
/** 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);
}
/** 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);
}
/** Called on Server */
void FPhysicsPawnAsync::BuildState_Internal(FNetStatePhysicsPawn& State) const
{
}
/** Called on Clients during resimulations */
void FPhysicsPawnAsync::ApplyState_Internal(const FNetStatePhysicsPawn& State)
{
}
And regarding Iris:
DefaultEngine.ini
[SystemSettings]
net.SubObjects.DefaultUseSubObjectReplicationList=1
net.IsPushModelEnabled=1
net.Iris.UseIrisReplication=1
net.Iris.PushModelMode=1
SetupIrisSupport(Target); in build.cs
This fix was suggested to me by a developer in discord. The likely problem was that the clientâs input was being overwritten on the server with the local value of the variables. No one claims that this is the right way, but this fix works.
.h
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Physics/NetworkPhysicsComponent.h"
#include "PhysicsPawn.generated.h"
UCLASS()
class NETWORKPHYSICS_API APhysicsPawn : public APawn
{
GENERATED_BODY()
public:
APhysicsPawn();
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
public:
virtual void PreInitializeComponents() override;
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
/** Set forward input, clamped from 0.0 to 1.0 */
UFUNCTION(BlueprintCallable, Category = "Game|PhysicsPawn")
void SetForwardInput(const float InForwardInput);
/** Set backward input, clamped from 0.0 to 1.0 */
UFUNCTION(BlueprintCallable, Category = "Game|PhysicsPawn")
void SetBackwardInput(const float InBackwardInput);
/** Set steering input, clamped from -1.0 to 1.0 */
UFUNCTION(BlueprintCallable, Category = "Game|PhysicsPawn")
void SetSteeringInput(const float InSteeringInput);
private:
class FPhysicsPawnAsync* PhysicsPawnAsync = nullptr;
UPROPERTY()
TObjectPtr<UNetworkPhysicsComponent> NetworkPhysicsComponent = nullptr;
/** Movement input on Game Thread */
float ForwardInput_External = 0.0f;
float BackwardInput_External = 0.0f;
/** Steering input on Game Thread */
float SteeringInput_External = 0.0f;
};
// Networked input data struct
USTRUCT()
struct FNetInputPhysicsPawn : public FNetworkPhysicsPayload
{
GENERATED_BODY()
FNetInputPhysicsPawn()
: MovementInput(0.0f), SteeringInput(0.0f)
{}
UPROPERTY()
float MovementInput;
UPROPERTY()
float SteeringInput;
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); }
};
// Networked state data struct
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; }
};
// GameThread to PhysicsThread input data struct
struct FAsyncInputPhysicsPawn : public Chaos::FSimCallbackInput
{
float MovementInput;
float SteeringInput;
bool bUseMovementInputForSimulation = false;
void Reset()
{
MovementInput = 0.0f;
SteeringInput = 0.0f;
bUseMovementInputForSimulation = false;
}
};
// PhysicsThread to GameThread output data struct
struct FAsyncOutputPhysicsPawn : public Chaos::FSimCallbackOutput
{
void Reset() {}
};
class FPhysicsPawnAsync : public Chaos::TSimCallbackObject<FAsyncInputPhysicsPawn, FAsyncOutputPhysicsPawn,
(Chaos::ESimCallbackOptions::Presimulate | Chaos::ESimCallbackOptions::Rewind)>
, TNetworkPhysicsInputState_Internal<FNetInputPhysicsPawn, FNetStatePhysicsPawn>
{
friend APhysicsPawn;
~FPhysicsPawnAsync() {}
// TSimCallbackObject callbacks
virtual void OnPostInitialize_Internal() override;
virtual void ProcessInputs_Internal(int32 PhysicsStep) override;
virtual void OnPreSimulate_Internal() 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;
private:
Chaos::FConstPhysicsObjectHandle PhysicsObject = nullptr;
float MovementInput_Internal;
float SteeringInput_Internal;
TWeakObjectPtr<APhysicsPawn> OwningPawn;
public:
void SetMovementInput_Internal(float InMovementInput) { MovementInput_Internal = InMovementInput; }
const float GetMovementInput_Internal() const { return MovementInput_Internal; }
void SetSteeringInput_Internal(float InSteeringInput) { SteeringInput_Internal = InSteeringInput; }
const float GetSteeringInput_Internal() const { return SteeringInput_Internal; }
};
.cpp
// Copyright Epic Games, Inc. All Rights Reserved.
âPhysicsPawn.hâ
âEngine/Engine.hâ
âPhysics/Experimental/PhysScene_Chaos.hâ
âPBDRigidsSolver.hâ
âChaos/PhysicsObjectInternalInterface.hâ
APhysicsPawn::APhysicsPawn()
{
PrimaryActorTick.bCanEverTick = true;
// Only create a network physics component if physics prediction is enabled
if (UPhysicsSettings::Get()->PhysicsPrediction.bEnablePhysicsPrediction)
{
static const FName NetworkPhysicsComponentName("NetworkPhysicsComponent");
// Add network physics component as a subobject
NetworkPhysicsComponent = CreateDefaultSubobject<UNetworkPhysicsComponent, UNetworkPhysicsComponent>(NetworkPhysicsComponentName);
NetworkPhysicsComponent->SetNetAddressable(); // Make subobject component net addressable
NetworkPhysicsComponent->SetIsReplicated(true);
}
}
void APhysicsPawn::PreInitializeComponents()
{
Super::PreInitializeComponents();
UPrimitiveComponent\* RootSimulatedComponent = Cast<UPrimitiveComponent>(GetRootComponent());
check(RootSimulatedComponent);
if (UWorld\* World = GetWorld())
{
if (FPhysScene\* PhysScene = World->GetPhysicsScene())
{
if (Chaos::FPhysicsSolver\* Solver = PhysScene->GetSolver())
{
PhysicsPawnAsync = Solver->CreateAndRegisterSimCallbackObject_External<FPhysicsPawnAsync>();
if (ensure(PhysicsPawnAsync))
{
PhysicsPawnAsync->PhysicsObject = RootSimulatedComponent->GetPhysicsObjectByName(NAME_None);
PhysicsPawnAsync->OwningPawn = this;
}
}
}
}
if (NetworkPhysicsComponent && PhysicsPawnAsync)
{
NetworkPhysicsComponent->CreateDataHistory<FNetInputPhysicsPawn, FNetStatePhysicsPawn>(PhysicsPawnAsync);
NetworkPhysicsComponent->SetPhysicsObject(PhysicsPawnAsync->PhysicsObject);
}
}
void APhysicsPawn::BeginPlay()
{
Super::BeginPlay();
}
void APhysicsPawn::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
}
void APhysicsPawn::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (!PhysicsPawnAsync)
{
return;
}
if (FAsyncInputPhysicsPawn\* AsyncInput = PhysicsPawnAsync->GetProducerInputData_External())
{
const bool bIsLocallyControlled = IsLocallyControlled();
const ENetMode NetMode = GetNetMode();
AsyncInput->bUseMovementInputForSimulation = bIsLocallyControlled && (NetMode != NM_DedicatedServer);
if (bIsLocallyControlled)
{
AsyncInput->MovementInput = ForwardInput_External - BackwardInput_External;
AsyncInput->SteeringInput = SteeringInput_External;
}
else
{
AsyncInput->MovementInput = 0.0f;
AsyncInput->SteeringInput = 0.0f;
}
}
}
void APhysicsPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
}
void APhysicsPawn::SetForwardInput(const float InForwardInput)
{
ForwardInput_External = FMath::Clamp(InForwardInput, -1.0f, 1.0f);
}
void APhysicsPawn::SetBackwardInput(const float InBackwardInput)
{
BackwardInput_External = FMath::Clamp(InBackwardInput, 0.0f, 1.0f);
}
void APhysicsPawn::SetSteeringInput(const float InSteeringInput)
{
SteeringInput_External = FMath::Clamp(InSteeringInput, -1.0f, 1.0f);
}
/** Calculate and 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);
}
/** 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);
}
/** Decay input during resimulation if input is predicted */
void FNetInputPhysicsPawn::DecayData(float DecayAmount)
{
MovementInput = MovementInput * (1.0f - DecayAmount);
SteeringInput = SteeringInput * (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;
return (bHasDiff == false);
}
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);
}
}
}
/** Process marshaled data through async inputs, this is called before OnPreSimulate_Internal */
void FPhysicsPawnAsync::ProcessInputs_Internal(int32 PhysicsSteps)
{
const FAsyncInputPhysicsPawn* AsyncInput = GetConsumerInput_Internal();
if (!AsyncInput)
{
return;
}
Chaos::FPhysicsSolverBase\* BaseSolver = GetSolver();
if (!BaseSolver || BaseSolver->IsResimming())
{
return;
}
if (AsyncInput->bUseMovementInputForSimulation)
{
SetMovementInput_Internal(AsyncInput->MovementInput);
SetSteeringInput_Internal(AsyncInput->SteeringInput);
}
}
void FPhysicsPawnAsync::OnPreSimulate_Internal()
{
if (!PhysicsObject)
{
return;
}
Chaos::FWritePhysicsObjectInterface_Internal Interface = Chaos::FPhysicsObjectInternalInterface::GetWrite();
Chaos::FPBDRigidParticleHandle\* ParticleHandle = Interface.GetRigidParticle(PhysicsObject);
if (ParticleHandle == nullptr)
{
return;
}
// Calculate forces
constexpr float ForceMultiplier = 300000.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;
Chaos::FVec3 LinearMovement = Chaos::FVec3(InputLinearMovementForce, InputLinearSteeringForce, 0.0f);
Chaos::FVec3 AngularMovement = Chaos::FVec3(0.0f, InputAngularMovementForce, InputAngularSteeringForce);
// UE_LOG(LogTemp, Warning, TEXT("йДĐșŃŃДД Đ·ĐœĐ°ŃĐ”ĐœĐžĐ” MyValue: %f"), MovementInput_Internal);
// 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);
}
}
/** 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;
}
/** 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);
}
/** 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);
}
/** Called on Server */
void FPhysicsPawnAsync::BuildState_Internal(FNetStatePhysicsPawn& State) const
{
}
/** Called on Clients during resimulations */
void FPhysicsPawnAsync::ApplyState_Internal(const FNetStatePhysicsPawn& State)
{
}
@MBobbo so at the end seems like the approach works great for one-body actors! Even with bad network connection. I want to say thank you for your contribution into this topic!
Out of curiosity, do you think itâs possible to expand the approach towards the multibody system? letâs say itâs 2 bodies with a UPhysicsConstrantComponent?
For now I was trying to connect two pawn with UNetworkPhysicsComponent setup with AConstraintActor or generate 2 NetworkPhysicsComponent within single pawn but without good result. Since you are an expert here, maybe you have an idea?
Good to see some progress here, Iâm putting it on my todo list to test this out in the public 5.7 version and take a look at the posted fix above and will reply in here in a few days when I get the time! It sounds like either an Iris setup issue or possibly as mentioned that the server stomps the incoming inputs but Iâve never seen that issue on my side.
@mizarates Iâve not properly approached using constraints between resimulation replicated objects yet, but resimulation does take constraints into consideration so it might work out of the box with the correct setup.
The UNetworkPhysicsComponent is only needed on things that you want to have either custom inputs for and/or custom states (outside of position, rotation, velocity, angular velocity).
If you for example have a vehicle and want to drag a boulder behind you, then the boulder doesnât need a UNetworkPhysicsComponent since itâs just a dynamic simulated object, unles you want it to have a health value for example which would be a custom state. The boulder needs to have Replicate, ReplicateMovement and PhysicsReplicationMode set to Resimulation though. And then you constrain the two different actors (vehicle and boulder).
It doesnât work if you make one blueprint asset with both vehicle and boulder since the boulder will be a child and networked physics doesnât replicate simulated child components out or the box, it only replicates simulated root components, since itâs relying on AActor::ReplicatedMovement which replicates the root components movement.
Thank you very much for your reply! Looking forward to hear updates about public 5.7 from you ![]()
So, regarding constraints and my general experience. First of all, I want to say that unfortunately having UNetworkPhysicsComponent only on controllable actor doesnât work for me when I collide with anything in the scene (with replication, replicate motion and resimulation). Object Iâm colliding with starts correcting itself quite dramatically, nothing smooth.
So I did a base AActor that has UNetworkPhysicsComponent setup without any inputs/outputs and without anything in tick. This way it works perfect and I can collide with quite complex systems of these objects without any issue
For constraints, I guess it more-less works for me now with the following setup:
-
I have APawn with control inputs and AActor with âemptyâ network component.
-
I have AConstraintActor on them;
-
In APawn setup of network component I have additional FVector input for target constraint position
-
Iâm getting Chaos::FPBDJointConstraintHandle* pointer to the constraint I want to control in OnPreSimulate_Internal and apply FVector input there
So this way it works quite well, response is fast and I see no significant corrections with average network profile. But itâs really interesting why just having child body without network component doesnât work!
The things you collide with in the scene that are correcting badly, are they based on AActor blueprint assets or are they blueprints of AStaticMeshActors or maybe shapes added directly to the scene without a blueprint?
If the two last then you have another duplicate setting for the physics replication mode that also needs to be set to Resimulation, else the replication of them pops over to âDefaultâ which doesnât work alongside resimulation.
(This is probably also a bug with StaticMeshActor movement replication and Iris currently in 5.7)
You should also be able to see that by looking at the color of the debug draw you print out, if itâs anything else than red then itâs not running resimulation.
And if there is no debug draw at all then it might not replicate which will also cause bad corrections.
Double checked now with creating an AActor from scratch with static mesh with physics as root component. Checked debug and saw red color and collided with that, seems like it actually works fine. Maybe in my previous tests these actors were AStaticMeshActors
Thank you for clarification!
(maybe they works slightly less responsive than with network component but since with bad network profile everything works not great I guess itâs my imagination, haha)
@Nicat @mizarates
@RobinWND1 bUseIris = true is no longer available indeed but itâs not needed anymore, Iâll try to get the Iris documentation fixed there.
I have updated the tutorial and the full files nowm, the main issue was essentially what Mizarates found that the server was consuming default AsyncInput values for the player inputs which stomped the networked player inputs.
The tick function now looks like this:
void
void APhysicsPawn::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;
}
}
}
}
The change is that I flipped the if-statements for IsLocallyControlled() and PhysicsPawnAsync->GetProducerInputData_External() so that we only produce an AsyncInput if we are the locally controlled player (Autonomous_Proxy).
Doing this makes the server and sim-proxies never get an AsyncInput so the networked inputs applied via FPhysicsPawnAsync::ApplyInput_Internal() are no longer stomped by FPhysicsPawnAsync::ProcessInputs_Internal().
If you need to send other non-networked data via AsyncInput on the server or sim-proxy then this approach doesnât work, youâll need to add some more logic to handle that:
- You can make
FPhysicsPawnAsync::ProcessInputs_Internalknow that itâs a sim-proxy or server and opt out from applying the networked data in those cases. - You can mark MovementInput and SteeringInput in AsyncInput as mutable and overwrite them inside
FPhysicsPawnAsync::ApplyInput_Internalto then letFPhysicsPawnAsync::ProcessInputs_Internalapply the AsyncInput which now has the networked data in it. - You can make MovementInput and SteeringInput TOptional and only set values for them in the Tick function when
IsLocallyControlled().
I also changed the initialization flow a bit, instead of using PreInitializeComponents() I use PostInitializeComponents()and I call CreateDataHistory after we have created and registered FPhysicsPawnAsync. The function now looks like this:
void APhysicsPawn::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);
}
}
}
}
}
}
}
In the tutorial I also wrote the Iris setup so you donât need to go to the Iris documentation to see what you need to do.
And I added setup to add âChaosâ dependency which I noticed was needed when running on a clean UE 5.7 project.
For me things work now when I follow the setup and copy the files.
Thank you a lot for an update!
Iâve updated my code to follow your updates, seems like it works fine for my pawn (single body) and crane (two body with constraint). However I canât move my custom vehicle (main body (root) + 4 constraints + 4 bodies Iâm applying physics to in OnPreSimulate_Internal()). But regarding vehicle I have to double check few things.
2 things you may find interesting:
- Here is comparison of the Actor BP with static mesh in the root with replicate + replicate movement + resim vs Actor BP with âemptyâ network component. Seems like my feeling was right and without network component it doesnât work smooth
- I found that after possessing back my pawn if you donât move it the body goes to sleep in 4 ticks (physics material settings). Iâve double check that I set body to never sleep but seems like something is changing during possession procedure. For now I fixed that by setting very huge number in physics material settings to never go to sleep.
Currently Iâm trying to simulate a more complex actor (vehicle) and have faced 2 issues that looks quite random to me and Iâm not sure where to look at.
The issues happens only when play as a client:
- Sometimes my input randomly holds at some fixed value and vehicle keeps moving forever with that input (no desync, server and client looks the same)
- Sometimes my vehicle doesnât react at input at all when I possess it (I mean physical input, camera rotates just fine)
Maybe itâs the same issue and #2 is just #1 that happened before I possess the vehicle.
The setup of the vehicle is following: mainmesh and 4 additional physics bodies (wheels) with constraints. In OnPreSimulate_Internal() I apply forces to these 4 wheels, I have pointers to them in my async component.
Some additional info, this doesnât work for me at all, vehicle keeps turning back to initial position:
// Tick
const bool bIsLocallyControlled = IsLocallyControlled();
if (bIsLocallyControlled)
{
if (FAsyncInputPhysicsPawn* AsyncInput = PhysicsVehicleAsync->GetProducerInputData_External())
{
AsyncInput->MovementInput = float(MovementMap.MoveForward) - float(MovementMap.MoveBackward);
AsyncInput->SteeringInput = SteeringInput_External;
}
}
//
void FPhysicsVehicleAsync::ProcessInputs_Internal(int32 PhysicsSteps)
{
const FAsyncInputPhysicsPawn* AsyncInput = GetConsumerInput_Internal();
if (!AsyncInput)
{
return;
}
Chaos::FPhysicsSolverBase* BaseSolver = GetSolver();
if (!BaseSolver || BaseSolver->IsResimming())
{
return;
}
SetMovementInput_Internal(AsyncInput->MovementInput);
SetSteeringInput_Internal(AsyncInput->SteeringInput);
};
This (the previous scheme from my messages), works:
// tick
if (FAsyncInputPhysicsPawn* AsyncInput = PhysicsVehicleAsync->GetProducerInputData_External())
{
const bool bIsLocallyControlled = IsLocallyControlled();
const ENetMode NetMode = GetNetMode();
AsyncInput->bUseMovementInputForSimulation = bIsLocallyControlled && (NetMode != NM_DedicatedServer);
if (bIsLocallyControlled)
{
AsyncInput->MovementInput = float(MovementMap.MoveForward) - float(MovementMap.MoveBackward);
AsyncInput->SteeringInput = SteeringInput_External;
}
else
{
AsyncInput->MovementInput = 0.0f;
AsyncInput->SteeringInput = SteeringInput_External;
}
}
void FPhysicsVehicleAsync::ProcessInputs_Internal(int32 PhysicsSteps)
{
const FAsyncInputPhysicsPawn* AsyncInput = GetConsumerInput_Internal();
if (!AsyncInput)
{
return;
}
Chaos::FPhysicsSolverBase* BaseSolver = GetSolver();
if (!BaseSolver || BaseSolver->IsResimming())
{
return;
}
if (AsyncInput->bUseMovementInputForSimulation)
{
SetMovementInput_Internal(AsyncInput->MovementInput);
SetSteeringInput_Internal(AsyncInput->SteeringInput);
}
};
UPDATE:
Seems like I found the place where itâs happening, at some point AsyncInput becomes nullptr and the vehicle uses the last input
void FPhysicsVehicleAsync::ProcessInputs_Internal(int32 PhysicsSteps)
{
const FAsyncInputPhysicsPawn* AsyncInput = GetConsumerInput_Internal();
if (!AsyncInput)
{
UE_LOG(LogTemp, Error, TEXT(âNO ASYNC INPUTâ));
return;
}
An AActor blueprint asset set to Replicate, ReplicateMovement and PhysicsReplicationMode: Resimulation should see no difference with or without an empty NetworkPhysicsComponent on it. The component does nothing else than provide an API for, and networking of, your custom inputs and states. It doesnât handle transform replication for example (itâs done via AActor::ReplicatedMovement).
Have you created the blueprint of the wall as a AStaticMeshActor instead of AActor?
There are issues with sleeping physics objects and resimulation still, your left walls might be sleeping when you interact with them which might contribute to the stuttering.
If you enable the debug draw, does it render as Red for both walls?
One of the main debugging tools for resimulation is Chaos Visual Debugger (CVD), you find it under Tools â Debug â Chaos Visual Debugger. There you can record, in PIE for example, and then play back what happens in physics, and you will see resimulations and can see if there are differences during a resimulation between the left and right wall. Be sure to set Timeline Sync to Network Tick, and under Data Channels you can enable more things/stages to record.
You can also see the sleeping state of particles in CVD if you select it and then look under âParticle Dynamics Miscâ.
I canât answer how to do specific implementations, like how you set up your vehicle, but it doesnât sound like a an issue with the asset setup. Itâs more likely an issue with possession, have you tried setting the vehicle as the default pawn so you spawn in with it?
You could try calling UNetworkPhysicsComponent::SetIsRelayingLocalInputs on the client after you have taken possession of it. Itâs really meant for if you implement the component on AActor instead of APawn and you want to take controll over it, so an AActor vehicle that your character sits in and you still possess your character but also control and own the AActor vehicle.
There has not been much development around either Pawn repossession or AActor ownership with relay enabled yet though.
Only time AsyncInput should be null in this flow where itâs created in Tick is when physics ticks more than once for each game tick. It will only produce an AsyncInput for the first physics step then. But it should not stay like that for more than 2 physics ticks, since by default physics can step at max 3 times between each game thread tick.
Have you checked if your possessed vehicle is actually returning true when calling IsLocallyControlled()?
Hi! First of all, thanks a lot for the tutorial â it helped me get much deeper into UNetworkPhysicsComponent and resimulation.
Iâm trying to adapt your approach to a ragdoll pawn in UE 5.7 (listen server + one client) and I have a few specific questions about the expected behavior and settings:
- Should âReplicate Physics To AutonomousProxyâ be enabled?
In your tutorial setup, this checkbox is never mentioned or enabled.
In my case:
-
If âReplicate Physics To AutonomousProxyâ is OFF:
-
the AutonomousProxy simulates locally, but never seems to receive correct physics corrections from the server,
-
visual debug shows that movement/transform replication does arrive on the GT,
-
but the updated physics state doesnât propagate to the PT side for the AutonomousProxy.
-
-
If I turn âReplicate Physics To AutonomousProxyâ ON, the behavior changes:
-
the autonomous pawn becomes much more âstuckâ to the server,
-
but the poses on the server copy and client proxies are still noticeably incorrect.
-
Question:
In your recommended setup for resimulation, should âReplicate Physics To AutonomousProxyâ stay disabled (as in your tutorial), or is enabling it actually required in 5.7 for some cases?
- GT â PT update on the AutonomousProxy
A strange thing Iâm seeing:
-
On SimProxy and on the server, the physics frame/state clearly propagates from GT to PT (physics proxy updates every frame).
-
On the AutonomousProxy, with âReplicate Physics To AutonomousProxyâ disabled, the physics state on PT is not updated from the replicated movement â effectively, GT receives ReplicatedMovement, but the physics proxy on PT side remains âstaleâ.
I did not override OnRep_ReplicatedMovement() in my pawn, so this should all be the default engine behavior.
Question:
Is it expected that, with resimulation + default pawn setup, the AutonomousProxyâs physics proxy on PT is not updated unless âReplicate Physics To AutonomousProxyâ is enabled?
If not, what is the correct way (in 5.7) to ensure that the AutonomousProxyâs PT physics state is updated from GT/ReplicatedMovement without relying on that flag?
- Target frame going out of rewind history and forcing PredictiveInterpolation
Iâm also hitting the following issue on resim:
-
Sometimes the resimulation target frame index ends up outside the rewind history (e.g. TargetFrame=77, rewind bounds=(0,5) or even negative indices like -2, -3, -4 very early in the session).
-
When that happens, NP2 logs warnings and falls back to PredictiveInterpolation for that pawn.
-
This happens even with:
-
substepping disabled,
-
async substepping disabled,
-
async physics on the skeletal mesh disabled,
-
and relatively small latency (local network / single client).
-
Question:
In your experience, what are the most likely causes for the resim target frame to fall outside the rewind history in 5.7, and how would you recommend fixing or mitigating this?
For example:
-
Should I explicitly increase rewind buffer size via CVar / component settings?
-
Could this be related to tick order (e.g. Network Tick vs Physics Tick) in a listen-server setup?
-
Or is it more likely to be a misuse of NetworkPhysicsComponent API on my side?
Any hints on these three points (especially regarding AutonomousProxy PT updates and the âReplicate Physics To AutonomousProxyâ flag in 5.7) would be extremely helpful.
Thanks again for the tutorial and for any time you can spare to look at this!
Hello, âReplicate Physics To AutonomousProxyâ is on by default and it should be on when using resimulation yes.
If you set it to false then RepMovement data will not make it to the physics thread inside PhysicsReplication.cpp and therefore it will not be used to either trigger resimulations and also not to correct the actor/pawn when resimulation is triggered from other actors/pawns, like a sim-proxy.
Itâs meant to be disabled if you want to have client authority while using physics replication, but the NetworkPhysicsComponent is written for server authority. You might be able to make it client authoritative by sending your transform data inside inputs to the server, but thatâs not something Iâm planning on supporting for the time being.
When it comes to the warning log about receiving target states outside the bounds of rewind history, if it only happens at the very start, when connecting or when spawning the pawn itâs not something to worry about, itâs normal for now.
The rewind history is by default 1 second long, so if you tick physics at 30hz it will have 30 frames of history (actually 32 since for CPU optimization it initializes the circular buffer size upwards to nearest power of 2). You can set the time you want to max support under Project Settings â Physics â Replication
If it happens during gameplay itâs a problem and it can come from different sources:
- High Round Trip Time (ping), so you receive data from the server too late.
- CPU performance might be bad on either client or server
- If server is struggling it can end up ticking slower than clients, so you will receive data from the server too late.
- If the clients is struggling it might end up ticking slower than the server which means it might end up behind the server (client should be forward predicted) and then it ends up receiving data from the server too early.
- If you alt-tab out of the game or focus on another window it might lose CPU priority and tick slower until you focus the game again.
When the client is out of sync and receive data out of rewind bounds it canât perform resimulations and instead it uses a different physics replication mode as a worst-case fallback.
When it comes to the ragdoll pose not being synchronized, thatâs not part of physics replication, it will only sync the root particles state.
If you have arms, legs, head, etc. that you want to simulate in physics and to synchronize and resimulate you will need to write a custom solution that send each of the ragdoll parts physics particle state and then pass that state into FPhysicsReplication::SetReplicatedTarget.
You can look at how AActor::ReplicatedMovement networks the physics state and passes it over to physics replication for reference. Look at AActor::GatherCurrentMovement which when physics is simulating gets the state from PhysicsReplicationCache instead of from the root components transform.
