DarthCucumber
DarthCucumber

Reputation: 71

Paddle and Ball Sync Problem in Multiplayer Air Hockey in Godot

The explaination is going to be a bit lengthy, please bear with it and any help is appreciated

About the Game:

Creating a multiplayer air hockey type game with a paddle and a ball.

Elements of game

  1. a paddle (KinematicBody2d) which is dragged by the mouse
  2. Ball (RigidBody2d) that just stays and waits to be hit by the paddle. It's physics is handled by the engine in both instances.

Objective:

Replicate exact paddle movement and collision between instances.

Approach:

Sending paddle data from Instance 1 to Instance 2. Consuming data in Instance 2 to mimic paddle movement.

Sending data: using websockets and JavaScript bridge. Let's treat that as blackbox for now.

How the paddle drag logic works (Instance 1)?

func handle_drag_input(delta):
    var current_pos = global_position
        var target_pos = get_global_mouse_position()
    if is_selected:
        var velocity = calc_velocity(target_pos, current_pos, delta)
                
                # disabling infinite impuse
        move_and_slide(velocity,Vector2.ZERO,false,4,PI/4,false) 
                apply_impulse(100)

        player_state = {"paddle":{"velocity":velocity, "pos":current_pos}, 
                                "mouse":{"pos":target_pos,"active":true}}
    else:
        player_state = {"paddle":{"velocity":Vector2.ZERO, "pos":current_pos}, 
                                "mouse":{"pos":target_pos,"active":false}}

func _input(event: InputEvent):
    if event is InputEventMouseButton:
        if event.button_index == BUTTON_LEFT and not event.is_pressed():
            is_selected = false

func _on_Player_input_event(viewport, event, shape_idx):
    if Input.is_action_just_pressed('click'):
        is_selected = true

func calc_velocity(target_pos, current_pos, delta):
    var new_pos = global_position.linear_interpolate(target_pos, 500 * delta)
    var velocity = new_pos - current_pos
    return velocity

func apply_impulse(push):
    for index in get_slide_count():
        var collision = get_slide_collision(index)
        if collision.collider.is_in_group("bodies"):
            collision.collider.apply_central_impulse(-collision.normal * push)

the handle_drag_input is being called inside _physcis_process and it works fine.

As you can see the data I'm sending from Instance1:

{"paddle":{"velocity":velocity, "pos":current_pos}, 
                                "mouse":{"pos":target_pos,"active":false}}

How the paddle data consuming logic works (Instance 2)?

The paddle of Instance2 takes the data and consumes it in a similar way inside _physics_process() :


func _physics_process(delta):
        var mouse_active = player_state.mouse.active #from instance 1
        var mouse_pos = player_state.mouse.pos #from instance 1
        var current_pos = player_state.paddle.pos #from instance 1

        var velocity = calc_velocity(mouse_pos,current_pos,delta)

        # to make sure the paddle is in Same position as in Instance 1
    if not is_initial_position_used:
        self.global_position = player_state.paddle.pos
        is_initial_position_used = true

        if mouse_active:
            # disabling infinite impuse
        move_and_slide(velocity,Vector2.ZERO,false,4,PI/4,false) 
            apply_impulse(100)

The problem

Ball physics in Instance 2 occasionally behave unpredictably compared to Instance 1. even after the padddle velocity, impulse are same as that of paddle on Instance1.

I am able to mimic the exact movement of the Paddle of instance 1 however the ball is the issue.

What have I tried so far?

  1. Possibly the force applied to the ball different in both instances? Sol: disabled infinite impulse and added fixed impulse apply_impulse(100) in both.

  2. Possibly the velocity and position of the paddles may differ? Sol: sending mouse position and paddle position of Instance 1 and calculating velocity at Instance 2 to make sure it is same as that of Instance 1

Questions

  1. How do I track down the exact issue?
  2. Is there a role of collision shape in the inconsistency (as in the collision contact points)?
  3. What am I doing wrong here?

Any other approach is also welcomed.. Any help is appreciated. I have been stuck in this for a while now. Thank you.

Upvotes: 1

Views: 99

Answers (1)

Robert Carroll
Robert Carroll

Reputation: 11

  1. How do I track down the exact issue?

    This is more of a general coding question, but if you had no where else to start, you could track down the issue by finding every point in the code where the ball's position changes. Try to understand exactly what's happening at each of these points. If that doesn't tell you how the positions are changing differently, try printing a time stamp, followed by all of the input values before the change, followed by the position after the change. This will let you see which input values differ between instances at the time when the ball's position goes out of sync. Whichever input values differ are likely responsible. You would then repeat this entire process recursively for each input variable until you find out where and how the problem is originating.

  2. Is there a role of collision shape in the inconsistency (as in the collision contact points)?

    Yes but this isn't the root of the issue. If the positions of the ball or paddle are already slightly different between instances, then the ball will bounce off at a slightly different angle, which makes the initial difference in position even worse, especially for small objects where the difference in angles will be greater.

  3. What am I doing wrong here?

    The main reason the problem occurs is because the ball's(and paddle's) velocity rely on delta(the time between updates), which is not going to be the same between instances. If the velocity is off, the positions will be off unless you correct them like you do with the paddle. Even small differences can compound over time, especially when collisions happen at different angles. Sending the delta of one instance to another to simulate the first isn't a reasonable solution because it's unnecessarily complicated, it would require a perfect network in order to stay in sync, and you could still end up with differences to the different ways floating point numbers are handled.

    The basic structure of how you should keep positions roughly the same between instances is to have the "server"(probably Instance 1) hold a "true state" of positions and velocities which it sends to the "client(s)", and have the client(s) send change requests to the server. It's genrally good to keep the client change requests as limitted as possible: ideally, only send player mouse inputs and make the server process how those inputs affect the state of the world.

    There are many ways to handle it after this point. An easy way is to force the client to match the server state whenever it gets a new packet(This is what you're already doing with the paddle. Instance 2 behaves like a client, matching the "true" paddle position from Instance 1. You can do the same thing for the ball and the "player2 paddle") If you want to make it even easier, you can let the client handle it's own player state directly(for example by let Instance 2 control the "true" position of a "player2 paddle" and send that position to be matched by Instance 1 in the same way the ball and "player1 paddle" are handled in the other direction).

    This way has a ton of problems and is very dependent on good network conditions to be close to acceptable, but it can be a good starting point. This article goes into more detail on some better strategies you can implement.

Upvotes: 1

Related Questions