Can you use GodotSteam + Netfox? (connection + syncing)?

Godot Version

4.5

Question

I’m working on a 3D multiplayer game, and I’m getting hung up on RigidBodies acting jittery when moving around. I am using GodotSteam for Steam connectivity and MultiplayerSynchronizers. I tried replacing the synchronizers with custom RPCs and saw no significant improvement. The same jitter can be seen when testing locally with ENet.

On the client side, if an object is loosely on the ground and is pushed by a player walking into it, it has a jitter as it slides on the ground. The cars of other players also appear slightly jittery as people drive around. (server/host owns physics objects and players own their individual cars)

I think the issue boils down to network latency and I need to learn prediction/rollback. The different posts and forums I’ve found suggest using Netfox rather than making my own custom system, but I’ve only found 1 post talking about GodotSteam and Netfox in combination, so I just wanted to see if anyone has tried this and can verify what this commenter said:

“netfox provides a RollbackSynchronizer node, which handles synchronization with rollback logic and effectively takes over (some of) the responsibilities of the MultiplayerSynchronizer. The GodotSteam MultiplayerPeer is responsible for establishing and maintaining a connection. So yes, you can connect players over Steam, and then use netfox for synchronization and game logic.”

Thank you in advance for any help/guidance :slight_smile:

I’m not sure it’s latency. I think the clients are simulating physics on those bodies while the host’s updates keep overwriting them, and the two fight every sync. Try freezing the RigidBodies on non authority peers so they only receive state, and see if the ground jitter stops.

It seems to be working most of the time now. Perhaps the jitter is coming from the capsule shape of the player colliding with it, trying to push it into the floor?

I apologize for the formatting, I’m new to the forums and tried copy/pasting

@rpc(“any_peer”, “call_local”, “reliable”)
func execute_drop() → void:
held = false
hold_target = null
if is_multiplayer_authority():
freeze = false
print(“freeze false”)
else:
freeze = true
print(“freeze true”)
collision_shape.disabled = false
mesh_instance.visible = true
linear_velocity *= 0.4
angular_velocity *= 0.4

Try watching the same push on the host’s window. If the host looks smooth and only clients jitter, the physics is fine and what you’re seeing is the raw sync rate, state arrives at network tick intervals while rendering runs at 60+, so the object visibly steps between updates. The fix for that is interpolation on the receiving side, either lerp the received transform yourself instead of snapping to it, or since you’re already eyeing netfox, its TickInterpolator node does exactly this and pairs with the RollbackSynchronizer.

If the host jitters too, then yes it’s the capsule grinding the object into the floor during the push, and that’s plain physics tuning, a bit of friction/damping or a small collision margin adjustment, nothing to do with networking.

On the drop code, one thing worth changing: every peer runs that RPC, and your else branch freezes it on non authority peers, good, but authority sets freeze = false and then everyone applies the velocity dampen at the bottom. The non authority peers are dampening velocities on a frozen body, harmless but dead code, and if authority ever changes mid flight the stale values bite. Keeping all physics writes inside the is_multiplayer_authority() branch is the cleaner habit.

It is perfectly smooth on the host. I think I got it to about 99% smoothness now

# network smoothing variables
var velocity_weight: float = 0.85
var transform_weight: float = 0.02
var max_correction_frames: int = 2
var frames_since_target: int = 999
var target_pos: Vector3
var target_basis: Basis
var target_lin_vel: Vector3
var target_ang_vel: Vector3

func _physics_process(delta: float) → void:

if held and hold_target and is_multiplayer_authority():
	var follow_speed := 20.0
	global_position = global_position.lerp(hold_target.global_position, follow_speed * delta)
	global_basis = global_basis.slerp(hold_target.global_basis, follow_speed * delta)

if is_multiplayer_authority():
	broadcast_physics_state()

— network smoothing ----------------------------------------------------------

func _integrate_forces(state: PhysicsDirectBodyState3D) → void:
if !is_multiplayer_authority() and !freeze:
apply_client_corrections(state)

func broadcast_physics_state():
recieve_physics_state.rpc(global_position, global_basis, linear_velocity, angular_velocity)

@rpc(“authority”, “call_remote”, “unreliable_ordered”)
func recieve_physics_state(pos: Vector3, rot_basis: Basis, lin_vel: Vector3, ang_vel: Vector3) → void:
target_pos = pos
target_basis = rot_basis
target_lin_vel = lin_vel
target_ang_vel = ang_vel

frames_since_target = 0

func apply_client_corrections(state):
if frames_since_target < max_correction_frames:

	state.linear_velocity = state.linear_velocity.lerp(target_lin_vel, velocity_weight)
	state.angular_velocity = state.angular_velocity.lerp(target_ang_vel, velocity_weight)
	
	state.transform.origin = state.transform.origin.lerp(target_pos, transform_weight)
	
	var current_quat := Quaternion(transform.basis)
	var target_quat := Quaternion(rot_basis_to_clean_quat(target_basis))
	var blended_quat := current_quat.slerp(target_quat, transform_weight)
	
	global_transform.basis = Basis(blended_quat)
	
	frames_since_target += 1

func rot_basis_to_clean_quat(b: Basis) → Quaternion:
return Quaternion(b.orthonormalized())

Ah nice!

One thing worth trying to see if it gets you 100% is inside apply_client_corrections you write position through state.transform.origin, which is the right way inside _integrate_forces, but the rotation goes through global_transform.basis directly. Mixing those two write paths in the same callback means the direct transform write and the physics state can fight each other, the state you hand back at the end of _integrate_forces is what the engine actually commits. Set the basis on the state too:

state.transform.basis = Basis(blended_quat)

Also drop the global_transform line.

Hey I just wanted to follow up on this and say this ended up being the ultimate solution.

I overcomplicated the matter as I tend to do lol