P2P Networked Physics Inaccuracy. Seeking Help

Godot Version

Godot 4.2.1

Question

Greetings everyone,

I’m currently developing a P2P multiplayer game in Godot utilizing WebRTC. I have successfully set up most of the multiplayer synchronization and authority. In my test scene, I have two connected peers and a ball (all of them being 2D RigidBodies), with the ball controlled by the main/host peer.

The issue I’m facing is occasional jittering of the ball’s position, which seems to be related to the correct_error function. I got this script from a helpful user on this forum and adapted it for a RigidBody2D (special thanks to @pennyloafers). The script pulls the physics object on the main peer, gathers position, rotation, and velocities into an array, and syncs that to the client. The client receives the array and updates its local physics object in reverse, extracting from the array and placing it onto the physics server object.

I’m creating this topic because I’m somewhat confused about what steps I should take to address this issue. The solutions I’ve attempted so far haven’t produced satisfactory results. Should I implement interpolations in my script? What steps can I take to resolve this?

Demonstration

The larger screen represents the host, which operates normally without jittering. However, jittering and inconsistencies are noticeable in the smaller one.

My Script

BallSynchronizer.gd

extends MultiplayerSynchronizer
class_name PhysicsSynchronizer

@export var sync_bstate_array : Array = \
	[0, Vector2.ZERO, Vector2.ZERO, Vector2.ZERO]

@onready var sync_object : RigidBody2D = get_node(root_path)
@onready var body_state : PhysicsDirectBodyState2D = \
	PhysicsServer2D.body_get_direct_state( sync_object.get_rid() )

var frame : int = 0
var last_frame : int = 0

enum { 
	FRAME,
	ORIGIN,
	LIN_VEL,
	ANG_VEL,
}


#copy state to array
func get_state( state, array ):
	array[ORIGIN] = state.transform.origin
	array[LIN_VEL] = state.linear_velocity
	array[ANG_VEL] = state.angular_velocity


#copy array to state
func set_state( array, state ):
	state.transform.origin = state.transform.origin.lerp(array[ORIGIN], 0.5)
	state.linear_velocity = state.linear_velocity.lerp(array[LIN_VEL], 0.5)
	state.angular_velocity = lerpf(state.angular_velocity, array[ANG_VEL], 0.5)


func get_physics_body_info():
	# server copy for sync
	get_state( body_state, sync_bstate_array )


func set_physics_body_info():
	# client rpc set from server
	set_state( sync_bstate_array, body_state )


func _physics_process(_delta):
	if is_multiplayer_authority() and sync_object.visible:
		frame += 1
		sync_bstate_array[FRAME] = frame
		get_physics_body_info()


# make sure to wire the "synchronized" signal to this function
func _on_synchronized():
	correct_error()
	# is this necessary?
	if is_previous_frame():
		return
	set_physics_body_info()

#  very basic network jitter reduction
func correct_error():
	var diff :Vector2= body_state.transform.origin - sync_bstate_array[ORIGIN]
	# correct minor error, but snap to incoming state if too far from reality
	if diff.length() < 10:
		sync_bstate_array[ORIGIN] = body_state.transform.origin.lerp(sync_bstate_array[ORIGIN], 0)


func is_previous_frame() -> bool:
	if sync_bstate_array[FRAME] <= last_frame:
		return true
	else:
		last_frame = sync_bstate_array[FRAME]
		return false

Synchronizer Settings

Screenshot_20240130_212322

Screenshot_20240130_212254

1 Like

Nice, I like the progression. I have a similar issue on my end with the same script and I plan to make a 2.0 version of this. But with my schedule… it will be a month or two.

I think we are at the whims of the cpu to make sure we get a game update every frame. But we are probably sometimes getting a game state at an inconsistent rate. Greater or less then a 16ms (60fps) frames rate. Meaning there is a “network” jitter caused by inconsistent packet handling, even on the localhost.

One remedy could be to buffer incoming game states and then on the client physics cycle apply the oldest game state. Making sure that the updates happen on the 16ms mark. The draw back is that we add latency to the client for every frame we buffer.

This is an extreme change…

I will admit the correct error is basic, and it is lerping position every frame, so we should try to get away from that. We should let the local physics do it’s thing and make corrections smarter.

Maybe we could say only start correcting if greater then 10, and don’t assign the sync states if less then 10 units of error. Just let the local physics work.

Just so you know networking is hard… If I have a chance to improve the script myself I’ll let you know how it goes.

Did you by chance give this article a read?

1 Like