Physics networking replication questions

I have been working with the roller ball tutorial and I have been learning about networking by attempting to add movement replication to the balls so that it can be multiplayer.
I have also changed it so that the movement is not done through torque, but instead by simulating the rotation of the entire level (still needs a bit of work though).
I’ve got it kinda working, however there are a few problems:

When I test it with more than one player, it seems laggy, and I am unable to tell if it’s because of the network traffic is too much, or if my computer is losing FPS. So if someone knowledgeable in this area could tell me if I’m doing something unnecessary in my networking code I would be very grateful.

Another big problem is that if its run as a listen server (clarification because I don’t know exact terminology: Whenever the dedicated server is off, 1 player or otherwise.) then the host cannot move, but the rest can.

Video of singleplayer, dedicated server:

Video of 2 player, dedicated server:

Video of singleplayer, no dedicated server:

I apologize for the music in the videos, I always forget shadowplay captures it.

Relevant parts of SuperRollerRacerBall.h




public: 
	FRotator currentRotation = FRotator(0, 0, 0);
	FVector movementForce = FVector(0, 0, 0);

	float SpringArmYaw = 0.f;
	float rotationScale = 30.0f;

	FVector targetPosition;

protected:

	/** Called for side to side input */
	void MoveRight(float Val);

	/** Called to move ball forwards and backwards */
	void MoveForward(float Val);

	/** Called to apply forces to ball*/
	void ApplyMovementForce(FVector force, float rotationYaw);
	
	UFUNCTION(Server, WithValidation, Reliable)
	void Server_ApplyMovementForce(FVector force, float rotationYaw);
	bool Server_ApplyMovementForce_Validate(FVector force, float rotationYaw);
	void Server_ApplyMovementForce_Implementation(FVector force, float rotationYaw);

	UFUNCTION(NetMulticast, Reliable)
	void Multicast_InterpolatePhysicsPosition(FVector position, FVector velocity, FRotator rotation);
	
	void Multicast_InterpolatePhysicsPosition_Implementation(FVector position, FVector velocity, FRotator rotation);


Relevant parts of SuperRollerRacerBall.cpp


void ASuperRollerRacerBall::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	if (!Ball->IsSimulatingPhysics()) {
		Ball->SetSimulatePhysics(true);
	}


	//Lerp Spring Arm Camera
	{
		float lerpSpeed = 2.0f;

		currentRotation.Yaw = Ball->GetPhysicsLinearVelocity().Rotation().Yaw;

		FQuat targetRotation = FQuat(currentRotation + defaultSpringArmRotation);

		SpringArm->RelativeRotation = FQuat::Slerp(SpringArm->RelativeRotation.Quaternion(), targetRotation, lerpSpeed * DeltaSeconds).Rotator();
		SpringArmYaw = SpringArm->RelativeRotation.Yaw;
	}

	//Lerp position to server's
	if (Role < ROLE_Authority) {
		if (FVector::Dist(targetPosition, GetActorLocation()) > 50) {
			SetActorLocation(FMath::VInterpTo(GetActorLocation(), targetPosition, DeltaSeconds, 4.0f));
			GEngine->AddOnScreenDebugMessage(INDEX_NONE, 2.0f, FColor::Green, FString::SanitizeFloat(FVector::Dist(targetPosition, GetActorLocation())));
		}
	}

	ApplyMovementForce(movementForce, SpringArmYaw);

}


void ASuperRollerRacerBall::MoveRight(float Val)
{
	movementForce.Y = FMath::Sin(FMath::DegreesToRadians(Val * -rotationScale)) * Ball->BodyInstance.MassScale * GetWorld()->GetGravityZ();
	currentRotation.Roll = Val * -rotationScale;
}

void ASuperRollerRacerBall::MoveForward(float Val)
{
	movementForce.X = FMath::Sin(FMath::DegreesToRadians(Val * -rotationScale)) * Ball->BodyInstance.MassScale * GetWorld()->GetGravityZ();
	currentRotation.Pitch = Val * rotationScale;
}

void ASuperRollerRacerBall::ApplyMovementForce(FVector force, float rotationYaw)
{
	if (Role < ROLE_Authority) {
		FVector rotatedMovementForce = force.RotateAngleAxis(rotationYaw, FVector(0, 0, 1));

		FVector xComp = FVector(rotatedMovementForce.X, 0, 0);
		FVector yComp = FVector(0, rotatedMovementForce.Y, 0);

		FVector normal = FVector::CrossProduct(xComp, yComp);
		normal.Normalize();

		float radians = FMath::Acos(FVector::DotProduct(normal, FVector::UpVector));

		rotatedMovementForce.Z = FMath::Abs(FMath::Cos(FMath::DegreesToRadians(radians))) * Ball->BodyInstance.MassScale * GetWorld()->GetGravityZ() - GetWorld()->GetGravityZ();

		Ball->AddForce(rotatedMovementForce, NAME_None, true);

		Server_ApplyMovementForce(force, rotationYaw);
	}
}


bool ASuperRollerRacerBall::Server_ApplyMovementForce_Validate(FVector force, float rotationYaw) {

	return true;
}

void ASuperRollerRacerBall::Server_ApplyMovementForce_Implementation(FVector force, float rotationYaw) {

	ApplyMovementForce(force, rotationYaw);

	FVector rotatedMovementForce = force.RotateAngleAxis(rotationYaw, FVector(0, 0, 1));

	FVector xComp = FVector(rotatedMovementForce.X, 0, 0);
	FVector yComp = FVector(0, rotatedMovementForce.Y, 0);

	FVector normal = FVector::CrossProduct(xComp, yComp);
	normal.Normalize();

	float radians = FMath::Acos(FVector::DotProduct(normal, FVector::UpVector));

	rotatedMovementForce.Z = FMath::Abs(FMath::Cos(FMath::DegreesToRadians(radians))) * Ball->BodyInstance.MassScale * GetWorld()->GetGravityZ() - GetWorld()->GetGravityZ();

	Ball->AddForce(rotatedMovementForce, NAME_None, true);

	Multicast_InterpolatePhysicsPosition(GetActorLocation(), Ball->GetPhysicsLinearVelocity(), GetActorRotation());

}

void ASuperRollerRacerBall::Multicast_InterpolatePhysicsPosition_Implementation(FVector position, FVector velocity, FRotator rotation)
{

	targetPosition = position;

	//Ball->SetAllPhysicsLinearVelocity(velocity); //I don't know if this is necessary or not.

	Ball->SetWorldRotation(rotation);

}

I think you’re flooding the network since you’re making Server RPC:s which in turn make Multicast RPC:s in each tick. The problem becomes exponentially worse which each player that joins.
I wouldn’t make this game this way, it seems virtually impossible.

Let the server handle the force computations and only replicate what it deems necessary by setting bReplicatesMovement = true;
I think it only replicates the root location of the ball (which is a lot more efficient) and lets the client interpolate and make it look smooth.

It’s all about tricking the player while keeping costs at a minimum.

I just tested it with the network profiler, and its showing outgoing bandwidth as 1.6 KB/s which doesn’t sound bad at all, but I guess I don’t know the standard. Is it good/bad?

Edit: messed it up at first. Outgoing bandwidth is actually 23.6 KB/s on a listen server with 3 players.

You need to build in prediction too, otherwise as soon as lag is introduced - your clients will feel delay from their input.

Networked Physics is no easy task unfortunately. Most people usually settle for a Client-Authoritative approach.

Okay I’ve simulated 2 players each with 300 simulated lag. They play fairly smoothly still, the only problem is when the player tries to change direction it takes like half a second to show on the other client (as expected because of the lag). Theres pretty much no way to fix that though unless you predicted player input ha. I am gonna check the network profiler, I have made some optimizations that hopefully take it down quite a bit. I’ll update this post with the results.

Okay I’m not sure how it reports it, like if it shows all the calls for all clients, but when profile the host for a 2 player listen server it shows 6.7 KB/s outgoing bandwidth

You will never make a playable game replicating physics.
You also wouldn’t need to if physics engine were deterministic; simulations would be the same on both client and server if that were the case.
But multiplatform game engines have no way to implement a deterministic physics engine that could work on all the CPU they support.

The physics being the same on both engines would still need replication. Player input would change the locations of objects on the server before a client could react to it and something may bounce out of sync very quickly.
“You will never make a playable game replicating physics.”
Just look at Rocket League.
I’m not saying its not going to be a challenge. The physics I’m using in my game are very simple, its just a racing game. This can definitely be done.

My advice would be to ignore using the underlying physics engine, and instead create a similar version of the CharacterMovementComponent and do it that way. Does mean you’ll have to calculate your own physics.

Syncing timestamps and moves etc is extremely difficult. It’s not a challenge for the faint-hearted!

3 years ago Microsoft paid $76 millions for a company working on deterministic physics/destruction software which were supposed to replicate minimal data and work without lag spikes… It looks like didn’t work for what they wanted so they went further and bought Havok too.

I’m currently still using the same physics engine, but the nature of my game allows me to assume certain things about movement so that I can predict it a bit better, I saw a post from you (TheJamsh) from a while back where you had a video of yours, I think I’m not running into a lot of the problems you were because I am using only spheres, where the cubes in yours bounced a lot differently because of the way they were rotated. The only physics object in my game are the players. I’m just working on a simple racing game that most likely will never be released, it’s more of a learning tool. I can upload a video of the current state of it though, I think it looks pretty good.