How do I send client input from the client to the server, then back to the client, with the client/server scripts being separate projects?

Godot Version

4.3

Question

This question is a more specific extension of this one:

@pennyloafers asked me to draft up a more specific question for my knowledge gap. And I am COMMITTED to figure networking out!

So, my specific issue is as follows:

  1. I need to send a packet from the client that contains an input as a string.
  2. Then, the sever (which is a separate project, not the same script) needs to read the information on that packet to determine how the client’s player character should respond.

The main issue is I don’t know how to send info from separate client/server projects.

My current implementation is as follows:

  1. Client and server script are in the same project, and are separated by an export preset. (This works)
  2. The server creates itself. (This works)
  3. The client joins the server when the client project is loaded. (This works)
  4. The server is the main authority over all nodes. (This works)
  5. The client receives their own player and unique ID. (This works)
  6. (In theory (I don’t have this part implemented)) The client sends input to the server from their client script. The server then receives this input info on it’s server script, checks if it’s a valid action, then performs that action on the client’s player script. (I have NO IDEA how to do this)

Any example code to implement this functionality is appreciated. I don’t need an exact implementation, I just need to understand how to communicate between client and server using RPCs.

So how the SceneMultiplayer for RPCs works is with two pieces of infomation. A nodepath and an rpc method. /root/path/to/MyNode:<rpc_method>. When a node introduces an RPC method it gets cached in the SceneMultiplayer RPC interface. So its important for the declaration of the methods to be the same.

So as long as the nodepath exists, on the client and server and the rpc declaration match, you can send an rpc between peers while each has their own script to handle the call in any fashion they want.


Server

Extends Node
class_name ServerAPI

@rpc("any_peer", "call_remote", "reliable")
func my_relay_rpc(data:string):
  print("hello im the server")
  my_relay_rpc.rpc_id(multiplayer.get_remote_sender_id(), "Hello %s this is server replying" % [data])

Client

Extends Node
class_name ClientAPI

# rpc declaration must match server script, but definition can be different.
@rpc("any_peer", "call_remote", "reliable")
func my_relay_rpc(data:String):
  print(data)

func _on_button_press():
    my_relay_rpc.rpc_id(1, "steve")
  

It will be easier if you reuse function names and rpc configurations. The SceneMultiplayer needs these to be able to format the rpc on either end and they will need to match.

Now when you apply these scripts you will have to do it programmatically

(Some pseudo code )

const rpc_node_name = "RPCNode" # must be same for each peer for nodepath routing

func select_peer_type():
  If peer is dedicated server:
     var node = ServerAPI.new()
     node.name = rpc_node_name
     add_child(node)
  If peer is client
     var node = ClientAPI.new()
     node.name = rpc_node_name
     add_child(node)

In the end both server and client have the same nodepath and rpc method

/root/path/to/RPCNode:my_relay_rpc

1 Like

Now as far as this approach is concerned it will ad a considerable delay to client/input feel. You will want the client to move locally now and have the server validate the input and send back the correct position.

I will advocate for a data first approach vs an action first approach.

I would typically handle input in its own node that is authed to the client. The input is compressed into a bit mask for discrete inputs (e.g. wasd, jumping, shooting) and individual vectors for analog input (“joysticks” ).

Then on character scripts, there is a input function to update input, which can parse the bit-data and vectors for actual movement and actions.

So the input node calls a local rpc with all the input data and is copied via rpc to the remote character input api.

When the server gets the input and acts on it, the server will send back the character position, velocities. Snapping the remote client character to the authorized position.

This may get a little strange if the network is bad as you will walk then get snapped back, but this is where client side prediction and server side reconciliation comes into play.

And now that input data is in one rpc it can be buffered easily for those practices.

(This behaviors is actually what the MultiplayerSynchronizers do, but can be replicated with RPCs.)

1 Like

Thank you so much for this response.

It’s going to take time for me to wrap my head around this, pseudocode my systems, and then refactor my netcode for the 3rd time.

@Demetrius_Dixon i will share my input code and rewrite it in a rpc format. In a near future reply to hopefully solidify my ideas.

Another thing i hate doing is writing duplicate code. I know that you are making an MMO(?) so you may want to obfuscate some server code, but for many world entities and objects i think they should share code. It just makes things very complicated to have different definitions and it typically requires you to write the same code twice, or jump into inheritance hell.

I think for server code you may want to obfuscate is a lot of the business logic. How do you auth clients, data collection, etc.

@pennyloafers
I’m making a PvP third-person arena shooter. 16 players max on small-medium size maps.

I’m not insane enough to make an MMO!!!

Input


PlayerInput Script
extends Node
class_name PlayerInput

# this input node will translate all player input into a bit mask for efficent
# transmission. It will also parse any directional input into appropriate
# vectors. These inputs will be passed to the character.

# bit mask for input
enum {
	NONE 	= 0x00,
	MOVING	= 0x01,
	FIRE 	= 0x02,
	JUMP 	= 0x04,
}

@export var move_dir := Vector2.ZERO
@export var aim_dir := Vector3.FORWARD
@export var input : int = 0

# external nodes
@export var camera : Camera3D
@export var player_character : PlayerCharacter


signal input_updated(input_bits:int, movement_direction:Vector3, aim_direction:Vector3)

func _ready():
	var id : int = get_node(self.player_character).name.to_int()
	if id == 0:
		print(name, "player_character  node's name is not an int!!!!")
	else:
		set_auth_id(id)


# make sure client can send input
func set_auth_id(id : int):
	print(name, ": uid ", multiplayer.get_unique_id(), " setting auth to ", id)
	call_deferred("set_multiplayer_authority",id)

func _process(delta):
	update_aim_position()
	#rpc
	if is_multiplayer_authority():
		set_player_input.rpc_id(1, input, move_dir, aim_dir)


func update_aim_direction():
	self.aim_dir = camera.global_transform.basis.z

func _unhandled_input(event):
	if is_multiplayer_authority():
		if event.is_action("input_movement_left") \
		or event.is_action("input_movement_right") \
		or event.is_action("input_movement_forward") \
		or event.is_action("input_movement_backward"):
			pull_movement_direction_input()
		elif not event.is_echo():
			check_weapon_input(event)
			check_jump_input(event)


func pull_movement_direction_input() -> void:
	move_dir = Input.get_vector(
			"input_movement_left",
			"input_movement_right",
			"input_movement_forward",
			"input_movement_backward"
		)
	input = set_bit_mask( input, Vector2.ZERO < self.move_dir, MOVING)


func set_bit_mask(bit_field:int, bit_on:bool, bit:int) -> int :
	var output = bit_field | bit if bit_on else bit_field & ~bit
	return output


func check_weapon_input(event) -> void:
	if event.is_action("input_equipment_fire_weapon"):
		# buttons/key strength is always 1.0, test analog strength
		if event.get_action_strength("input_equipment_fire_weapon") > 0.2:
			input = set_bit_mask( input, true, FIRE)
		else:
			input = set_bit_mask( input, false, FIRE)


@rpc("authority", "call_local", "unreliable_ordered")
func set_player_input(input_bits:int, movement_direction:Vector2, aim_direction:Vector3):
	input_updated.emit(input_bits, move_direction, aim_direction)



Character


PlayerCharacter Script
class_name PlayerCharacter
extends CharaterBody3D
const SCENE_RES : = "res://src/character.tscn"
static func create_scene() -> PlayerCharacter:
	return preload(SCENE_RES).instantiate()

var weapon : Node3D = null
var move_direction : Vector2 = Vector2.ZERO
var look_direction : Vector3 = Vector3.ZERO
var state = STATE.OKAY
var input : int = 0


enum STATE { OKAY, DEAD }


func is_moving() -> bool :
	return input & MOVING


func _enter_tree():
	if is_remote():
		remove_puppet_nodes()


func is_remote()->bool:
	return not self.name == str(multiplayer.get_unique_id())


func remove_puppet_nodes():
	#TODO: if we can spectate we need these nodes below,
	# but should hide/switch from other instances of the players.
	if $Camera:
		$Camera.queue_free()
	if $HUD:
		$HUD.queue_free()

# signal from PlayerInput node
func _on_player_input_input_updated(input_bits, movement_direction, aim_direction):
	set_input(input_bits, movement_direction, aim_direction)


func set_input( input_bit_array : int, player_direction : Vector3, aim_direction : Vector3  ):
	# validate incoming input: normalize vectors
	if state != STATE.DEAD:
		move_direction = player_direction.normalized()
		look_direction = aim_direction.normalized()
		input = input_bit_array

func _process(_delta):
	dispatch_input()
	if is_multiplayer_authority():
		update_remote_peer.rpc(global_position, velocity, global_rotation, state)


func dispatch_input():
	$StrafeSystem.strafe_direction( move_direction )
	$CameraSystem.aim_direction( look_direction )
	$JumpSystem.user_input( input )
	set_fire( input )


func set_fire( user_input : int):
	if weapon != null:
		weapon.set_fire( user_input )
	elif user_input & PlayerInput.FIRE:
		print( name, ": no weapon equiped to fire" )

@rpc("authority", "call_remote", "unreliable_ordered") # The server is the authority so this should be trusted on client.
func update_remote_peer(pos:Vector3, vel:Vector3, rot:Vector3, st:int):
	global_position = pos
	velocity = vel
	global_rotation = rot
	state = st

details


This code has been heavily redacted and simplifed so there is probably errors. basically the tree looks like this

|-- 12345678 : PlayerCharacter (authed to server)
|   \-- PlayerInput (authed to player)

PlayerCharacter sends back server state and PlayerInput sends data to server to act, and acts on it locally.

PlayerInput is connected to PlayerCharacter via a signal, but could be a direct call during the RPC

I dont have any client/server prediction stuff yet, but there is only one rpc per class that can easily be buffered. you can also send the input back with the player character to figure out if an input was lost to the network.

The second benifit to this approach is we are not using “any_peer” and can trust that only the authority is sending the RPC.

1 Like

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