Need help to communicate with the local player script via the client's network script

Godot Version

4.3

Background

So, I’m still refactoring my netcode, but I’ve made considerable progress. Before I ask my question, I need to detail how my codebase works.

So I’m developing a multiplayer, PvP, third-person shooter that uses a dedicated server architecture.

The project operates like this:

  1. Game Boots (with or without the “dedicated_server” feature from the export presets)
  2. The main scene loads. It contains the game world, network manager(s), all players, and more.
  3. The network_check script runs in the NetworkCheck node. This node checks if the project has the “dedicated_server” feature. Deletes the opposite network manager and renames the surviving manager’s node to a generic NetworkManager in order for the client and server to communicate with one another. Lastly, it deletes itself to save memory. (I have this in place so I can develop from one project. It’s much easier than having 2 separate ones)
func _ready() -> void:
	
	#INFO If the file is a dedicated server:
	if OS.has_feature("dedicated_server"):
		delete_for_server()
		delete_self()
	
	#INFO If the file isn't a dedicated server:
	else:
		delete_for_client()
		delete_self()

---------------------------------------------------------

func delete_for_server() -> void:
	Client_Manager.queue_free()
	Server_Manager.name = "NetworkManager"

func delete_for_client() -> void:
	Server_Manager.queue_free()
	Client_Manager.name = "NetworkManager"

func delete_self() -> void:
	queue_free()
  1. Once this is done, the server will create itself (at localhost for testing) and the client(s) will automatically join (for testing).

Now the client can send an rpc_id(1) to the server and vice versa at any time to communicate.

Intention

What I want for this system is the following:

  • Client sends input data via their network manager.
  • Server receives that input data and queues it for the next network tick.
  • The network tick happens, making the server’s network manager trigger a client network manager RPC that connects to their player script. (This is the issue)
  • The client’s player script then performs that action.

Question

How do I communicate between the client’s network manager and the client’s local player script?

I’ve tried and consider some options already (Some of these could still work, but I’m not sure):

  • I tried using signals, as the client’s player script is local. Meaning I can use the RPC chains to lead back to it.

The issue I’ve encountered was trying to connect the signal. Since the client’s manager node’s original name is ClientManager, that then gets changed to NetworkManager, it makes it so the player script can’t find the node needed for a signal connection or the .connect keyword is invalid.

Connecting the signal the other way around has the same issue. The player’s node name is their multiplayer ID. (This is required to identify players for loading, unloading, and more)

This method can still work. It might be due to when the player script is loaded (after the server detects a client joined).

  • I considered using server RPCs. But that doesn’t work, as the player node’s name needs to be their ID and I still don’t have the client connected to their player as well.

I could use some help untangling the logic of this issue. Any suggestions are very appreciated.

I have mentioned this in the past, regarding data oriented and action oriented rpcs

Client Pseudo code

#action oriented
func input(event)
  If event jump and pressed
    Jump.rpc()

# data oriented
var jump : bool = false
func input(event)
  if event jump
    jump = event.pressed

func process(delta)
  If jump:
    jump()
  player_input.rpc(jump, ...)

This doesnt solve your issue but i prefer getting all the player input state in a data oriented fashion and send it all in one packet.

But in your case you would want to setup an api for player to give input and accept position/velocities.

That wasn’t really my question. I haven’t gotten to that part, yet.

I can send the data to the server and back just fine. I just need to communicate between the client and their local player.

Right, but what are you communicating? Design come from the data that is being handled.

Another thing is all clients will have a network manager so if the data path is like this

Client: player → input → network manager → server
Server: input_rpc → player_id.get_node.input.set

Server Tick
Server: for all players_ids → get player node → get_position → rpc client state → client
Client: network manager → set player position

The server will have to manage all players, but it is unclear how the remote puppets will be controlled.

1 Like

@pennyloafers In theory, the flow goes like this:

  1. Client enters input via the client network manager.
  2. This input is sent to the server network manager.
  3. (This is not implemented) The server queues that input for the next server tick.
  4. (This is the main thing) On that server tick, the server network manager sends an rpc_id to the client network manager, that THEN triggers a signal for the clients local player script, which then gets replicated to the other clients.

Client input → client network manager → rpc_id(1) → server network manager → rpc_id(multiplayer.get_remote_sender_id()) → client network manager → signal → local player script → the actual game action → rpc → replicate to all clients. (This excludes interpolation and server-side validation)

I intend for the server to track player position, velocity, etc, to see if that action was possible (Server-side validation). And have the actual tick be delayed (for interpolation)

(Including those) → server tracks player position, velocity, etc → validate action → interpolate position → repeat

I hope that made sense.

It is clear

I think this inverts authority. As it seems the client player is responsible for broadcasting the player state. The server should be doing this. It could allow the player to cheat by setting its own position where ever it wanted.

The server should take player input and play the outcome on the server with the server player instances. Then the server player instance will then broadcast the state to all clients player instances.

Clients should only have authority over input and the server has everything else. This is the central authority architecture.

1 Like

Well then, how should the server connect to the client’s player?

I saw you put player_id.get_node.input.set.

Could I trigger input events from the server network manager via: multiplayer.get_remote_sender_id().player.action_event? (That was just some pseudocode)

Edit: You know, now that I’ve typed it out, that makes so much sense. When the player is instantiated, it’s renamed to match the joining client’s id before getting put in the scene tree. So maybe I could trigger the client’s player script events using it. (and checking for the correct client by comparing the client’s sending ID with the player node’s name)

I can respond in more detail but i want to investigate something with Godot first. In some sense you want a centralized location to make packet decisions (for future client prediction and server reconciliation).

From what you are trying to do it could happen in the multiplayer api. But ive never looked to deeply into extending that part of godot to understand whats possible. So let me do that and i will report back on if its an option, and then help with the network manager organization.

1 Like

Thank you. I really appreciate it. :+1:

okay, well the MultiplayerApiExtension is limited. It can monitor outgoing RPCs, but that is about it.

So I’m reviewing your code that you last sent me.

So I think would make a signal from player input to your network manager, or a static function in the client network script.

# ClientManager
@onready var Player_Spawner : MultiplayerSpawner = $"../PlayerSpawner"

# put this some where
Player_Spawner.spawn_function = custom_spawn

func custom_spawn(id:int):
	var Player_Ref := Player.instantiate()
	Player_Ref.name = str(id)
	if id == multiplayer.get_unique_id():
 	  	Player_Ref.new_input.connect(_on_player_input)
	return Player_Ref

func _on_player_new_input(input):
  #implemetnation

the signal name can be whatever

1 Like

There’s 2 potential solutions I can try now. First is the signal, second is a server .player call. I’ll try both and report back.

Also, happy 2025!

Status update: The server .event.rpc() works. I did it by saving the instantiated player in a higher scope (from function to the server script). Then, calling the player’s RPC events when the client calls them.

This is in the server network manager.

var Perm_Player_Ref

func load_player(id) -> void:
	if not multiplayer.is_server(): return
	
	if has_node(str(id)): 
		print("A player with ID ", id, " already exists.")
		return
	
	var Player_Ref := Player.instantiate()
	Player_Ref.name = str(id)
	Player_Ref.Client_Input = "NetworkManager"
	Player_Container.add_child(Player_Ref, true) #Triggers Multiplayer Spawner
	Perm_Player_Ref = Player_Ref

It’s quite stuttery right now, but I bet it can be smoothed out when I implement it better (id checking) and add more features.

I’m not marking this as the solution yet, because I want to try out the method you suggested. After I do that, I’ll edit this comment with an update.

Edit: Ok Penny, I understand how your implementation works. The client manager uses the multiplayer spawner as a reference. Then, attaches player information like their id. Lastly, it… connects… input???

There are quite a few issues with this method.

  1. I’m planning to replace the multiplayer spawner with a custom RPC implementation. Same as I did for the multiplayer synchronizer.
  2. This RPC implementation is also done by the server network manager. Absolutely positively NOT the client.

So, method 1 was the solution. I can now have an online communication network between the client, the server, and the player. There’s still a lot more work to do, but I know that path forward. Thanks for your help @pennyloafers!