Godot P2P Physics Networking

Godot Version

4.4.1

Question

I found a solution to user @suero 's p2p networking issue, but I am a new user so made a newpost to respond to him

So I am very new to all of this grander godot documentaion and community stuff so I dont know if this is good form but I was so excited after finally working out a solution to @suero 's issue, and finding a solution to one of my own. Hopefully this gets to him and @pennyloafers for all his help.

For more background info look to this post

So I have been poking around with what you have made here and I think I have found the solution

The synchronizer that penny provided works great for plain rigidbodies like balls or boxes, but for rigidbody character controllers it gets more complicated, but still works.

Add this to the penny synchronizer. The server should be the authority on all of these rigidbody synchronisers. If the client attempts to set positions things get wacky

func _enter_tree() → void:
set_multiplayer_authority(1)

Then on your player add another synchroniser and add export vars of all your player inputs. we will then synchronize those inputs. This synchroniser should also have the player be the authority not the server. to make sure this is the case add this to your player script if it isnt already.

func _enter_tree() → void:
set_multiplayer_authority(name.to_int())

This should set up all children as the same authority except for the loafer synchronizer we set earlier.

Next wherever you have your player control stuff (mine is in integrate_forces if that ends up being important), add an if else like this

if is_multiplayer_authority():
input = (Controls) ## Grabs and sets the synced inputs to controls
synced_input = input ##sets the sync input for the synchroniser
character_controller_logic(input) ##Apply character control logic
else:
input = synced_input ##sets the input to what the synchroniser is setting
character_controller_logic(input) ##Apply character control logic

Now I dont really know what im doing, so this could potentially be causing huge issues I cant really see at the moment, but im tired and have been beating my head against the desk for hours learning multiplayer stuff for the first time, and physics is what I love so I NEEDED buttery network physics, and making these changes fixed it.

My hypothesis is this structure causes the server to do all the “Real” physics calculations while allowing the client to also run its physics separately, and in combination with the ring buffer that penny created, this really smooths out the physics at least in the little local host testing I have done. I made a similar synchroniser to his but it had really bad jitter. The ring buffer though did the trick so I cant thank you enough penny I would never have figured out all that without you. :smiley:

heres a modified version for Rigidbody2Ds as well if anyone needs it. Only differences are typing things for rigidbody2d’s, the enter tree func, and changing the exported vars into a single array I got annoyed with setting 5 vars to sync over and over

class_name RigidBody2DSynchronizer
extends MultiplayerSynchronizer

##Make sure to connect the synchronized signal to on_synchronized()

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

##Make sure to sync these properties
@export_group(“Needs to be synced”)
enum {SYNC_POS,SYNC_LVEL,SYNC_ROT,SYNC_AVEL,SYNC_FRAME}
@export var sync_data : Array = [Vector2.ZERO, Vector2.ZERO, 0.0, 0.0, 0]

var ring_buffer : RingBuffer2D = RingBuffer2D.new()

var last_frame = -1
var set_num = 0

##For the default rigidbody synch the server should be the one calling the shots
func _enter_tree() → void:
set_multiplayer_authority(1)

func _exit_tree():
ring_buffer.free()

#copy state to array
func get_state(state : PhysicsDirectBodyState2D ):
sync_data[SYNC_POS] = state.transform.origin
sync_data[SYNC_LVEL] = state.linear_velocity
sync_data[SYNC_ROT] = state.transform.get_rotation()
sync_data[SYNC_AVEL] = state.angular_velocity

#copy array to state
func set_state(state : PhysicsDirectBodyState2D, data:Array ):
state.transform.origin = data[SYNC_POS]
state.linear_velocity = data[SYNC_LVEL]
state.transform.x = Vector2.RIGHT.rotated(data[SYNC_ROT])
state.transform.y = Vector2.DOWN.rotated(data[SYNC_ROT])
state.angular_velocity = data[SYNC_AVEL]

func get_physics_body_info():

server copy for sync

get_state( body_state )

func set_physics_body_info():

client rpc set from server

var data :Array = ring_buffer.remove()
while data.is_empty():
return
set_state( body_state, data )

func _physics_process(_delta):

if is_multiplayer_authority():
sync_data[SYNC_FRAME] += 1
get_physics_body_info()
else:
set_physics_body_info()

make sure to wire the “synchronized” signal to this function

func _on_synchronized():
if is_previouse_frame():
return
ring_buffer.add([
sync_data[SYNC_POS],
sync_data[SYNC_LVEL],
sync_data[SYNC_ROT],
sync_data[SYNC_AVEL],
])

func is_previouse_frame() → bool:
if sync_data[SYNC_FRAME] <= last_frame:
#print(“previous frame %d %d” % [sync_frame, last_frame] )
return true
else:
last_frame = sync_data[SYNC_FRAME]
return false

class RingBuffer2D extends Object:
const SAFETY:int = 1
const CAPACITY:int = 4 + SAFETY
var buf:Array[Array]
var head:int = 0
var tail:int = 0

func _init():
buf.resize(CAPACITY)

func add(frame:Array):
if _increment(head) == tail: # full
_comsume_extra()
if is_low():
_produce_extra(frame)
buf[head]=frame
head = _increment(head)

func _comsume_extra():
#print( “RingBuffer: consume_extra”)
var next_index = _increment(tail)
buf[next_index] = _interpolate(buf[tail], buf[next_index],0.5)
tail = next_index

func _produce_extra(frame:Array):
#print(“RingBuffer: produce_extra”)
var first_frame = _interpolate(buf[tail],frame, 0.33) # assume only one frame exists tail should point at it
var second_frame = _interpolate(buf[tail],frame, 0.66) # assume only one frame exists tail should point at it
buf[head]=first_frame
head = _increment(head)
buf[head] = second_frame
head = _increment(head)

func _interpolate(from:Array, to:Array, percentage:float) → Array:
var frame:Array = [
from[SYNC_POS].lerp(to[SYNC_POS], percentage),
from[SYNC_LVEL].lerp(to[SYNC_LVEL], percentage),
lerp_angle(from[SYNC_ROT], to[SYNC_ROT], percentage),
lerp_angle(from[SYNC_AVEL],to[SYNC_AVEL],percentage)
]
return frame

func _increment(index:int)->int:
index += 1
if index == CAPACITY: # avoid modulus
index = 0
return index

func remove() → Array:
var frame : Array = buf[tail]
if is_empty() or is_low():
frame =
else:
tail = _increment(tail)
return frame

func is_empty() → bool:
return tail == head

func is_low() → bool:
return _increment(tail) == head

3 Likes

Nice, i still haven’t gotten around to improving that original script yet, but have leveled up on reading more of Godot’s net code. But if i ever get around to it, it will happen here. (It is still pretty much the same as you found it, except i think i removed the interpolation from the ring buffer, because it can, and probably should, be done elsewhere. TBD…).

This was my attempt at state-synchronization and If you want to learn more about what inspired it there is a good GDC talk from the guy who wrote the articles.

In the video he talks about snapshot interpolation being the better/easier option when bandwidth is good, dont remember particularly why, and another challenge is Godot’s Multiplayer API doesnt group individual packets into one large packet. So there are some timing issues to possible contend with if you have multiple nodes interacting with each other. Also if you have studied netfox they have some caveats with physics objects which i think only comes into play if you have rollback? But it will be a more robust alternative if you dont plan to continue developing your own solution.

1 Like