Having trouble with my RPC jump function causing the player to jump multiple times at high ping

Godot Version

4.3

Question and Background

Hello again. I’m still working on the MVP (minimum viable product) of the third-person shooter I’m developing.

I’m having an issue with my jump function. At high ping/packet loss the jump will trigger multiple times as the player touches the ground.

For example, at optimal network conditions: (0-50 ping/<1% packet loss)

  • The player pressed the jump input.
  • If the player is on the ground, jump.

At suboptimal network conditions: (75-150 ping/>1% packet loss)

  • The player pressed the jump input.
  • If the player is on the ground, jump.
  • Once the player model touches the ground again, they will jump without the player pressing the input. This can happen 1-2 times before the player stops automatically jumping.

While I will need to provide background on how my game’s networking system currently works, I’ll only include the functions that are relevant to the question.

Working Example Code

My game uses a dedicated client-server model. Which means the server is the definitive source of truth.

The player code and the player input code are separated. With the server having authority over the player and the client having authority over the player’s input.

Let’s looks at some code that works at high ping: The movement code.

First, the client sends their movement input and sends it to the server if authorized to.

@rpc("any_peer","call_local","reliable",0)
func client_move() -> void:
	var Server_Authorized = false
	
	if Player_ID == str(Client_Input.get_multiplayer_authority()):
		server_move.rpc(Input.get_vector("move_right", "move_left", "move_back", "move_forward"), true)

Then, the server converts that input into game logic.

@rpc("any_peer","call_local","unreliable",0)
func server_move(input : Vector2,  Server_Authorized : bool) -> void:
	if Server_Authorized == false: return
	if Client_Input.is_multiplayer_authority() == false: return
	
	var Movement_Direction = Camera_Rotation.basis * Vector3(input.x, 0, input.y).normalized()
	
	velocity.x = Movement_Direction.x * Movement_Speed
	velocity.z = Movement_Direction.z * Movement_Speed
	
	sync_player_position.rpc(self.global_position)

Server authorization IS NOT done via the @RPC arguments. It is done via server sided checks that aren’t currently implemented. Plus, since the server has authority over the main player script at all times, setting the @RPC to authority or call_remote is redundant and prints an error.

After the movement input is sent from the client and processed by the server RPC, it is then triggered by another RPC function that moves the player instead of a Multiplayer Sync node.

@rpc("any_peer","call_remote","unreliable_ordered",0)
func sync_player_position(pos : Vector3) -> void:
	self.global_position = pos

The reason I don’t use a multiplayer sync node is because I need fine tuned control over what RPCs get sent and how. Plus, the sync node is made for Peer-to-peer, which my game is not.

My client_move function is also called every physics frame. (This has a good chance of being changed later)

func _physics_process(delta) -> void:
	
	client_move.rpc()
	
	apply_gravity(delta)
	
	move_and_slide()

Now here’s the problem code for my jump function(s)

Client script jump signal emission:

func _process(delta : float) -> void:
	
	#Client Jump Input
	if Input.is_action_just_pressed("jump") \
	and is_multiplayer_authority():
		Client_Jump.emit()

Player script client jump function:

@rpc("any_peer","call_local","reliable",0)
func client_jump() -> void:
	var Server_Authorized = false
	
	if Player_ID == str(Client_Input.get_multiplayer_authority()):
		Server_Authorized = true
		server_jump.rpc(Server_Authorized)
	else:
		Server_Authorized = false
		return

Player script server jump function:

@rpc("any_peer","call_local","reliable",0)
func server_jump(Server_Authorized) -> void:
	if Server_Authorized == false: return
	
	if is_on_floor():
		velocity.y = Jump_Force

I’m not sure why this jump issue is occurring. I’d like to know your theories and potential solutions.

Are you doing any kind of client side prediction? Seems like it’s jumping locally on the client, starting to fall down, and then an older server update arrives from when the player was still in the air.

1 Like

Nope. I haven’t gotten to implementing that yet. That could be the issue; I’ll look into it.

The only thing that stands out to me is your use of “reliable” RPCs. It sounds like your doing stress tests and a reliable RPC needs confirmation that a packet was received called ACK for acknowledge.

So if the jump RPC arrives and the ACK is lost the client will send the jump rpc again, and will continue to do so until it gets a corresponding ACK back.

Rule of thumb reliable RPC (also considered as TCP ) is bad for realtime networking. It’s okay for, one off, important stuff but not for input.

For mitigating poor network behavior you need to implement server reconciliation. There is an GDC talk about how Overwatch implements theirs let me see if I can find it.

2 Likes

Here is a presentation on what overwatch does. You may have to skip around to find out how they do input.(Net code section at 00:22:15)

Also I’m curious to know what you used to stress the network. I haven’t gotten that far to look for a test setup.

1 Like

Thank you so much. I just woke up so, It’ll take me a bit to find a solution.

There’s some easy options for adding latency to localhost, netem for linux and I think people use clumsy for windows.

2 Likes

I use clumsy over my local network.

1 Like

Alright, I found the solution.

Turns out, sending a signal to jump from my client input script was causing the issue. I made it so that the client_jump function is called within the main player script. Then, having the client input script provide authority.

func _physics_process(delta) -> void:
	
	client_move.rpc()
	
	if Input.is_action_just_pressed("jump") \
	and Client_Input.is_multiplayer_authority():
		client_jump()
	
	apply_gravity(delta)
	
	move_and_slide()
@rpc("any_peer","call_local","unreliable",0)
func client_jump() -> void:
	var Server_Authorized = false
	
	if Player_ID == str(Client_Input.get_multiplayer_authority()):
		Server_Authorized = true
		server_jump.rpc(Server_Authorized)
	else:
		Server_Authorized = false
		return
@rpc("any_peer","call_local","unreliable",0)
func server_jump(Server_Authorized) -> void:
	if Server_Authorized == false: return
	if Client_Input.is_multiplayer_authority() == false: return
	
	if is_on_floor():
		velocity.y = Jump_Force

I’ll take note of this for all future inputs. I’ll also test out the reliability argument more and watch that Overwatch networking GDC talk.

Edit: Almost forgot to mention that I switched the transfer_mode argument from reliable to unreliable for performance take. Thank you @pennyloafers.

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