Jittery RigidBody movement on client side

Godot Version

4.1.1

Question

Hey everyone!
Working on my first multiplayer project which is a basic soccer game, the problem i have drives me crazy, it might have a simple solution but i cant seem to find it. Below there is a video which shows my problem, there is no code involved, its just two collision shape colliding with each other and pushing the ball away, i use a multiplayer synchronizer to sync the ball rotation, position etc everything works fine on the host side (Top Left window) but on the client side (Bottom Right) well its obvious whats the problem is (I increased the Replication Intervall of the sync node by 0.1 to make the jitter more visible, if i set it back to 0 it still happens but way less annoying).
Video

1 Like

There could be a few issues.

One physics run on a separate thread and modifying the node position on the main thread will cause race conditions. This is why you see the jitter as the two threads battle to put the ball where they think it should be.

Two your network topology.
Doing a peer-to-peer physics based game requires complex client side predictions, or authority swapping of physics nodes. It may not even be possible.

Doing a central authority server is a better way to go for physics based games. As there is only a single truth to the physics state. You can add client side prediction to make it feel more responsive on the client side but the host has the final say of where they are and what happens physically. To do this you should only be sending input data to the host and the host will move the player object while the client receives the update to where they moved to. There is an inherent delay and that is why client side predictions can be helpful.

I will put a follow up post with a script I use for my physics based game.

1 Like

This script will pull the physics object on the server, collect the position rotation and velocities into an array and sync that to the client.

the client will receive the array and update its local physics object in reverse, extracting from the array and placing it on to the physics server object.

extends MultiplayerSynchronizer
class_name PhysicsSynchronizer
@export var sync_bstate_array : Array = \
	[0, Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO]

@onready var sync_object : RigidBody3D = get_node(root_path)
@onready var body_state : PhysicsDirectBodyState3D = \
	PhysicsServer3D.body_get_direct_state( sync_object.get_rid() )

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

enum { 
	FRAME,
	ORIGIN,
	QUAT, # the quaternion is used for an optimized rotation state
	LIN_VEL,
	ANG_VEL,
}


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


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


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_previouse_frame():
		return
	set_physics_body_info()

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

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

it requires one property to sync

and one signal connected to itself
image

2 Likes

Thank you so much! it feels so much smoother, amazing, makes me so happy and finally able to continue working on other stuff

Just as an fyi, this script isn’t perfect and you may start running into issues in real world network situations.

The far as I have tested it, it can work okay from a coffee shop 4 blocks away from my server.

If you want to get deeper into it, this script was inspired by reading these articles.

He explains a few different styles and then promotes his own style which is two physics simulations one authority. The client can run his own physics, but if it deviates to much from the authority state it should snap the object back.

He has very good articles. 100% recommend giving them a read.

Good luck and Have fun!

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.