Community Tutorial: Networked Physics - Pawn Tutorial

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)
{
}