#P2P Networked Physics Inaccuracy. Seeking Help

13 messages · Page 1 of 1 (latest)

outer obsidian
#

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.

https://cdn.discordapp.com/attachments/1072329733083242607/1202047759851724891/simplescreenrecorder-2024-01-30_21.26.45.webm

#

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
#

( I had to split it in parts bcuz discord won't let me send it fully. )

#
odd cosmos
#

When the client receives a state from the server, that state is old, it's from the past. The client needs to not just set itself to that state (snap (which means setting to a past state)), but also replay up to current time (unsnap / predict / no longer be in the past). This involves having a player input buffer (client side) that you use to replay actions. The state the client receives needs a number indicating the last player input that the server received at that time (client sends this with their input), so the client knows how far back it needs to go in the input buffer and replay.

#

In addition, I recommend not smoothing your updates wtih the correction function, since it does not make sense to extrapolate from a made up lerped state.

#

You can however smooth the visuals to hide small snaps.

#

Note that unlike the player, the ball does not have an associated input buffer, so it's just predicted like normal. Unlike players (which can change directions at any time from an external noise generator (the human)), simple physics bodies are predictable and so that can work out well.

#

Other players (on the client) are usually interpolated (with extrapolation if they are still waiting on the next update to arrive when the inteportation is already done) because they are not predictable and so if you just extrapolated them like a simple physics body, it would be constantly snapping (incorrect prediction). This can still work if the players can't change direction too quickly and you have some visual smoothing, but for something like an FPS where players can instantaneously change velocity, it won't work well (unless the server packet send rate is really high and ping is low enough and player move speed is low enough (visual smoothing can cover it)).

outer obsidian
#

@odd cosmos tysm for answering and so much info, I'll review it all when I can later on