Physic-driven gameplay over network

Hello folks!

I am on this topic for a while now: Correctly networking our gameplay, which uses physics based movement. I started out creating my basic multiplayer setup, including an online testing environment using Steam, then tried to fit our game to work in a networked setup.

The game: Basically airhockey, where the player controls a paddle and needs to hit the puck to hit something with it.

My problem: I ended up with synchronization issues between the clients, meaning that the puck or even the other player is not at the same position in both clients.

How I am currently doing the movement:

I have my own MovementComponent derived from UPawnMovementComponent, which just adds a force to my paddle


void UPaddlePawnMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	if (!PawnOwner || !UpdatedComponent || ShouldSkipUpdate(DeltaTime))
	{
		return;
	}

	FVector DesiredMovementThisFrame = ConsumeInputVector().GetClampedToMaxSize(1.0f) * DeltaTime * MovementSpeed;
	if (!DesiredMovementThisFrame.IsNearlyZero())
	{
		GetOwner()->FindComponentByClass<UPrimitiveComponent>()->AddForce(DesiredMovementThisFrame);
	}
};

One step higher: I am adding to the InputVector in my own Pawn class, which is declared like this:


class MMUNREAL_GIT_API APaddlePawn : public APawn
{
	GENERATED_BODY()

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

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;

	// Called to get this pawn's movement component
	virtual UPawnMovementComponent* GetMovementComponent() const override;
	
	UFUNCTION()
	void MoveForward(float AxisValue);
	UFUNCTION()
	void MoveRight(float AxisValue);

	UFUNCTION(Server, Reliable, WithValidation)
	void ServerMoveForward(float AxisValue);
	UFUNCTION(Server, Reliable, WithValidation)
	void ServerMoveRight(float AxisValue);
};

… and implemented like this:


// Called to bind functionality to input
void APaddlePawn::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
	Super::SetupPlayerInputComponent(InputComponent);

	InputComponent->BindAxis("MoveForward", this, &APaddlePawn::MoveForward);
	InputComponent->BindAxis("MoveRight", this, &APaddlePawn::MoveRight);
}

UPawnMovementComponent* APaddlePawn::GetMovementComponent() const
{
	return FindComponentByClass<UPawnMovementComponent>();
}

void APaddlePawn::MoveForward(float AxisValue)
{
	if (!Controller || AxisValue == 0.0f) return;
	if (IsLocallyControlled()) {
		ServerMoveForward(AxisValue);
	}
}

void APaddlePawn::MoveRight(float AxisValue)
{
	if (!Controller || AxisValue == 0.0f) return;
	if (IsLocallyControlled()) {
		ServerMoveRight(AxisValue);
	}
}

void APaddlePawn::ServerMoveForward_Implementation(float AxisValue) {
	if (GetMovementComponent())
	{
		GetMovementComponent()->AddInputVector(GetActorForwardVector() * AxisValue);
	}
}
bool APaddlePawn::ServerMoveForward_Validate(float AxisValue) {
	return true;
}

void APaddlePawn::ServerMoveRight_Implementation(float AxisValue) {
	if (GetMovementComponent())
	{
		GetMovementComponent()->AddInputVector(GetActorRightVector() * AxisValue);
	}
}
bool APaddlePawn::ServerMoveRight_Validate(float AxisValue) {
	return true;
}

I have set the replication settings in my Paddle-Blueprint & Puck-Blueprint like this: (Always relevant = true, Replicate Movement = true, Net Load on Client = true and of course Replicates = true)
MM_paddle-replication.JPG

With this setup, at first everything appears to work fine, but after playing a while the clients get out of sync (= player paddles as well as puck are at different locations)

If anybody might see something wrong with the code or architecture I’d really appreciate feedback! Currently I’m just really stuck. Have been trying different approaches, researching on the web, but nothing really seems to work.

Thank you!

1 Like

May be you should check the tickcomponent function, inside it you should check whether it’s on the server side, if it is , then apply the force, let the server side code move the paddle, and client just replicate the movement. I’m also stuck by the physic sync problem. There is a good web site you should visit http://gafferongames.com/, there’s a bunch of great articles talk about how to sync physic. Hope that useful. Good luck!

Hey thorcxcx! First: Thanks for the response :slight_smile: Funny! I studied exactly the same resource (gafferongames) over the last days. It is incredibly well written and explained!

I’ve had some progress since I started this thread. I studied the engine code for replicating movement and now I am at an architecture that syncs perfectly. There still are some issues I have to sort out regarding laggy players, but for my first iteration I’m quite happy now.

What I’ve changed:

  • added a default lag to quickly test real online multiplayer (added “PktLag=80” in DefaultEngine.ini). I figured that 80ms is a common average.
  • let the player locally add physics movement on input immediately AND send the input to the server
  • the server will then send back the updated movement & position state. I implement the handling of this updated state in a local client on my own now:
  • in my custom Pawn class header file:

virtual void PostNetReceivePhysicState() override;

  • actual implementation (you could either use the commented ConditionalApplyRigidBodyState with a custom ErrorCorrection struct, or simply implement it yourself, as I did here in a very minimal way):

void APaddlePawn::PostNetReceivePhysicState()
{
	UPrimitiveComponent* RootPrimComp = Cast<UPrimitiveComponent>(RootComponent);
	if (RootPrimComp)
	{
		FRigidBodyState NewState;
		ReplicatedMovement.CopyTo(NewState);

		FRigidBodyErrorCorrection ErrorCorrection;
		ErrorCorrection.LinearDeltaThresholdSq = 2.f;
		ErrorCorrection.BodySpeedThresholdSq = 0.5f;

		//FVector DeltaPos(FVector::ZeroVector);
		//RootPrimComp->ConditionalApplyRigidBodyState(NewState, ErrorCorrection, DeltaPos);

		FVector UpdatedPos = FMath::VInterpTo(GetActorLocation(), NewState.Position, GetWorld()->DeltaTimeSeconds, 0.1f);
		RootPrimComp->SetWorldTransform(FTransform(NewState.Quaternion, UpdatedPos), false, nullptr, ETeleportType::TeleportPhysics);

		FVector UpdatedLinVel = FMath::VInterpTo(RootPrimComp->GetPhysicsLinearVelocity(), NewState.LinVel, GetWorld()->DeltaTimeSeconds, 0.1f);
		RootPrimComp->SetPhysicsLinearVelocity(NewState.LinVel);
	}
}

Keep in mind though, that I am missing rotation and angular velocity here! That’s because we don’t use it in our game. If you need it, just look how it’s implemented in the engine code @UPrimitiveComponent::ApplyRigidBodyState.

In a next iteration I will try to further improve this, especially with a look at this section of Gaffer on Games: http://gafferongames.com/networked-physics/snapshots-and-interpolation/

Hope this may help you in your project as well. Would like to hear your thought about this approach :slight_smile:

// edit: I also tried it exactly like you said, but then any lag >50 feels really bad. There is also a good discussion about it here: [Video] Player-Controlled Replicating Physics Movement, Simulating Physics! - C++ Gameplay Programming - Unreal Engine Forums

When it comes to networked physics that is crucial for gameplay, I think the general strategy to adopt is to simulate physics only on the server, and only replicate the actual position and rotation of the object on the clients.

I have yet to familiarize myself with UE4 networking, but basically you should find a way to turn off physics simulation of the puck on all clients, and make them replicate the position of the server’s puck directly

The downside is that you’ll get a tiny lag between the moment you hit the puck and the actual impulse on the puck, because your client will have to send some kind of “HitPuck” message to the server, but that is the only way I know to make reliable networked physics gameplay