Physics Networking: How can I properly do it to my 2D game?

Godot Version

4.2 Stable

Question

Hello, guys. So, I’m developing a Top-Down 2D multiplayer game, I’m currently using a dedicated server to create the network.

My biggest problem right now is about “Physics Networking”, I’m facing a bad time with RigidBody2D, because it constantly gets out of sync with the another client since I’m relaying on the both clients to get all the physics.

I first thought in doing all the calcs in the Godot Server with the own engine, but after some googling, I found some ppl telling that it can turn into a expensive server with some delay.

So I’m a bit confunsed on what to do, if I should keep in the way of using clients to do all the physics, if I shouldn’t be so scared (bcuz of the costs of mantaining it or the scalability or capacity ) of doing it all the calcs in my own server.

My game is pretty simple, so I like the idea of an easy solution like relaying on the clients to do the calcs. If anyone has any good solution to keep the calcs with the clients and to solve unsynced glitches please tell me, I’ll leave some recordings of my game and some scripts.

P.S: I know that I’m getting these unsync bugs bcuz I’m not syncing the position, but only the velocity. When I try to manually set the RigidBody2D position I get unexpected physics behaviour, that’s why I’m only using velocity rn and not only position.

Video Demonstrations

First demo: https://streamable.com/4oer58

Second Demo:

Some Code

Some server code:


# world_state = {intPlayerId: {T: unixSystemTime, P: rigidBodyGlobalPosition, V: rigidBodyVelocity}}

@rpc("any_peer")
func ReceivePlayerState(player_state):

    var player_id = multiplayer.get_remote_sender_id()

    if players_states.has(player_id):
        if players_states[player_id]["T"] < player_state["T"]:
            players_states[player_id] = player_state
    else:
        players_states[player_id] = player_state

func SendWorldState(world_state):
    rpc_id(0, 'ReceiveWorldState', world_state)

I have separate scenes for players, one for the main player and another for connected players.

Main Player code to Sync:

extends RigidBody2D

@export var _move_speed: float = 620
var player_state

func _ready():
    pass

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

func _physics_process(delta):
    _move_speed = 620
    move(delta)
    set_player_state()
    pass

func move(delta: float) -> void:
    var _direction: Vector2 = Vector2(
        Input.get_action_strength("move_right") - Input.get_action_strength("move_left"),
        Input.get_action_strength("move_down") - Input.get_action_strength("move_up")
    )

    if _direction != Vector2.ZERO:
        apply_central_impulse(_direction.normalized() * _move_speed * delta)
        
    if Input.is_action_pressed('ability'):
        player_border.texture = border_spritesheet['ability']
        apply_central_impulse(Vector2(_direction.normalized()) * dash_power)

func set_player_state():
    player_state = {"T": Time.get_unix_time_from_system(), "V": linear_velocity, "P": global_position}
    Server.SendPlayerState(player_state)
    pass

Code for the template_player scene ( other players that arent the main one ):

extends RigidBody2D

var client_player_state

# Called when the node enters the scene tree for the first time.
func _ready():
    pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
    pass

func _integrate_forces(state):
    if client_player_state:
        var t:Transform2D = state.transform
        t.origin = global_position
        state.set_transform(t)
        state.linear_velocity = client_player_state['V']
    
func MovePlayer(player_state) -> void:
    client_player_state = player_state
3 Likes

default Godot physics are not deterministic. That means if you give a fixed input to multiple godot physics clients, you will not get an expected output. This is a problem all online games have (unless you use something like SGPhysics2D or Rapier )

The solution is quite simple. Only the host does the physics, and its physics’ results override to all clients. So there is essentially only 1 physics simulation. What the clients receive is the result, aka what position they should have.

tl;dr: Only the host has physics, the clients receive the positions (not the velocities)

The above will result in jittery movement if you apply it as-is, so every game has a simple snapshot system, which essentially stores the last 3 snapshots (last 3 physics states received from the host), and lerps between them. That’s how you get smooth motion, even though there is latency.

Related link: Source Multiplayer Networking - Valve Developer Community

4 Likes

id Software pretty much perfected this (client-inputs + authorative server) for Quake (in 3D):

see

tldr: Client sends player inputs (incl position) to server while the PLAYER gets moved immediately by the clients engine (0ms lag, simple form of client prediction). The actual world state happens on the server and sent back to client.

Details from jfedor:
"What happens when the network packet containing the next snapshot is delayed or lost and the client runs out of states to interpolate between? Then it’s forced to do what it normally tries to avoid: extrapolate or guess where the objects will be if they keep moving the same way they’re currently moving. This might not be a big problem for objects that move in a straight line like rockets or that are only affected by gravity. But other players move in an unpredictable manner and because of that the game normally wants to interpolate and not extrapolate, even if it means artificially delaying everything by up to a single server frame in addition to whatever network lag is present.

The above description applies to all game objects except the most important one: the player. This case is special, because the player’s eyes are the first person camera through which we are seeing the game world. Any latency or lack of smoothness here would be much more noticeable than with other game objects. Which is why sending user input to the server, waiting for the server to process it and respond with the current player position and only then rendering the world using that delayed information isn’t a good option. Instead the Quake 3 client works as follows. In each frame it samples user input (how much the mouse was moved in which direction, what keys are pressed), constructs a user command, which is that user input translated into a form independent of the actual input method used. It then either sends that user command to the server immediately or stores it to be sent in one batch with future user commands from subsequent frames. The server will use those commands to perform the player movement and calculate the authoritative world state. But instead of waiting for the server to do that and send back a snapshot containing that information, the client also immediately performs the same player movement locally. In this case there’s no interpolation - the move commands are applied onto the latest snapshot received from the server and the results can be seen on the screen immediately. In a way this means that each player lives in the future on their own machine, when compared to the rest of the world, which is behind because of the network latency and the delay needed for interpolation. Applying user inputs on the client without waiting for a response from the server is called prediction."

Also note that your game is highly interactive (Player1<->Ball<->Player2), this needs a fast+high bandwidth server. id software circumvented the problem by letting players host their own games. Obv. this open the door for cheating.

6 Likes

Hello, thanks for the reply and suggestion! Most of ppl is telling me to just do all the physics on server and then replicate it through clients and trying to make it feel smoothier, for example, using the snapshots method you told and/or extra/interpolation ( may be the same thing xd ).

With this in mind, what should my player node be? Character2D? still RigidBody2D?

What do I need to research about in Godot/Game Development to learn to do some sort of client prediction? Should I consider determinist physics to make it?

Also, what if I wanted to create a p2p alternative to my game, it would be the same thing? is it dangerous to have this alternative, since, who sends the info is the main peer and some malicious actor can manipulate this info?

2 Likes

Only the server will have physics, so if you use Rigidbody2D for your player nodes and ball, then only the server will have Rigidbody2D children on the player nodes and ball.

Skim the links above, all 3 posted are great imo. Like spend 1 hour to “digest” the snapshot system.

For now, I suggest making a simple server/client snapshot system. Simple because the local player will not have local simulation like Quake 3 mentioned above. It will all be server-authoritative.

So each client’s input has a timestamp. The client will send this input not locally, but to the server. The server will then apply this input to the matching player’s node, do the physics, and send the new player’s position back to the player. So the local client can only change position by an RPC of the server. Do this, and reach this point, and you will see server-authoritative movement.

Once you do the above, you will see that the players will jitter once you increase ping/latency (localhost is 0). This is normal because you are basically teleporting the player nodes (or the ball) without smoothly moving it. It goes from X=0.1 to X=0.2, instead of going from 0.1 to 0.11, to 0.12 etc etc to 0.2. So once you receive the X=0.2, and you are at X=0.1, you must lerp to that X instead of teleporting. And you have concluded with a 2-state snapshot system, where the client holds the latest position and the current position :stuck_out_tongue:

If you made the above-mentioned snapshot system, you can simply choose a player to be your server, or a player to host a server. But if you want true p2p (why? no game does this even ones that claim to be p2p) you would need deterministic physics.

If your game doesn’t hit it big, or has no ranked mode/competitiveness, thinking about evil hackers is needless, especially at this early stage. A malicious actor needs to waste a week to accurately manipulate the game state, and I doubt he can do actually malicious things using Godot like downloading virus.exe. That said, all network models have flaws.

3 Likes