I don't understand how RPC works

Godot Version

Godot Engine 4.3

Question

Hello! I am making an multiplayer project and have stumbled upon serious issues regarding rpc functions. My game works like that client → server. Instead of creating two different projects - one for client and another one for the server - I’ve made one project. When I need to do only server specific things in the main_game node I do check like this if multiplayer.is_server(). Seems simple, but not when it comes to the rpc functions.

Let’s say I want to enable specific ui for ALL players connected to the server. Request comes from the server to all players.
Each player have it’s own Control node for a GUI with a script attached to it. The script have @rpc function declarations such as @rpc(“reliable”) func enable_screen_ui():.

Previously, I thought that I would just call from the server the rpc() method like so rpc(“enable_screen_ui”) but it doesn’t work because this would only work if @rpc function declaration would be in that same script.

So then I’ve decided to just go through all players → get their Control node → get their callable method and use rpc() on it. Like so: player.player_hud.enable_screen_ui.rpc().


Now I have multiple questions.

  1. This does not work for me at all… why? Player is authority of player_hud and calling from server script (the one in the main_game scene that has authority of 1) the player’s rpc function just does not work.

  2. if I go through all players what would be difference between player.player_hud.enable_screen_ui.rpc() and player.player_hud.enable_screen_ui.rpc_id(network_id)? Both would tecnically call that rpc function on that player because we’re using .rpc only on the player’s existing function.

  3. What is the purpose of having @rpc declared function on the same script and then calling rpc()? At first I thought that main purpose of rpcs is to communicate between server and client but I don’t see any sense in this.
    I assume it wa made for the case when you have two projects for one game. Server project have that script with @rpc declared function and calls rpc(). And client project have script with @rpc declared but with client specific implementation and it would be called. I am not sure about this, but atleast it makes sense to me… but unfortunately this is not what I really need.

  4. When to use "any_peer” on @rpc? I mean as far as I understand when instead of "any_peer” it’s "authority” then only that peer that have this @rpc declaration can call it. And when "any_peer” any peer can call it.
    At first I thought that would be useful for client → client communication. Player can call peer of another player using rpc_id. But that thought was back then when I thought I could call rpc_id in the script that had no @rpc declaration in it.
    So, technically if I wanted to either client → client or client → server then in any way I would need to set "any_peer” to all @rpc declarations. I am probably horribly wrong on this. Please help…

  5. When to use "call_local” on @rpc? Supposedly, I do player.cool_func.rpc_id(network_id); #network id is player’s what would happen with "call_local”? I mean probably nothing but uh still is there any difference?
    I mean it’s probably needed again when you have two different projects for client and server and you could from server project do rpc_id(network_id, “cool_func”) and if I’ve had that @rpc declaration on server it would be called on the server and on the client because I have "call_local” on it. Makes sense to me. But what about when I have one single project for both server and client?

To be honest, I really don’t want to create two different projects for client and server. I want to have one project that have two export settings as both for client and server because this is more convenient and I can include specific resources that each build should have (e.g. server build should NOT have textures).

And the worst part this thing just does not work. I’ve did everything and my attempts to do player.coolr_func.rpc_id(network_id) did not help me.

The only thing I’ve understood from the documentation that rpc calls should be on same NodePath is that it’s meant when you have two projects for client and server.
Because, technically on single project if player is it’s own scene that have Control node with a script that have @rpc declaration then server should also somehow have that same declaration on the LITERAL player’s OWN SCENE. SO it would be like 1 @rpc declaration just in one script just for both server and client.

I literally don’t understand and I probably miss so many things. Plaese help me. I am literally unable to go further. The documentation is literally no help. It has good definitions that makes sense but at the same time it does not answer all the occuring questions.

Don’t make two different projects for client and server, that’s just going to make things harder. One project is 100% the intended way to use Godot’s high level multiplayer.

You are correct that RPCs need to be on Nodes with the same NodePath in order to send data to the correct function. In addition, they need to be in the same script. I think you understand all that.

I don’t think your HUD should have RPCs going to it. Instead, I think the HUD should do HUD things (ui, local input). There should be a separate node with authority 1 (on ALL game instances) that receives game updates from the server. Then you can just add any updates you want to that Node. If you’re using the HUD for player inputs you can even make a separate input Node that has the client’s authority.

I also wonder if this RPC is necessary. If I had to guess, enabling the screen ui comes after some event like starting a match or entering the game. If there is even one other RPC that could be combined with the enable_screen_ui RPC underneath a “match_started” (or some other event) signal you should use a single RPC to trigger that signal and eliminate the two other RPCs. Network code kinda sucks, it adds complexity and makes everything it touches vulnerable to failure because of poor internet connection. You save yourself trouble by reducing the number of RPCs.

2 Likes

Would you consider sharing a git repo with your code in, or the mentioned code as snippets in this topic? I feel like I can answer the questions you’ve defined (1-5), but I would much prefer it if my answers could demonstrate their logic through a correction of your code so you can see, plainly, how the code is meant to look. Your English is good but not perfect so I think showing correct code with descriptions on the side is a better way forward (as it is for most coding-related problems).


I hope you understand where I’m coming from, and that you can provide the relevant code somehow.

I believe, I have found a solution. Now I am able to call .rpc() on a player’s script @rpc declared function. My problem was unrelated to networking at all it’s just I didn’t add the player to the game.

Yet, I still have many questions regarding how rpcs work.


@gaboot

The reason why player have a Control node with a script attached to it that has all @rpc declarations is because it controls only player’s ui elements.

Example, Control node’s script:

@rpc("any_peer","reliable")
func enable_ui() -> void:
	screen_ui.show();

@rpc("any_peer","reliable")
func show_status_ui(message : String) -> void:
	screen_ui.get_node("status").text = message;

# and so on

Server’s script:

# enable ui for all players e.g. on round stop players should have specific gui enabled.
func enable_ui() -> void:
	# players is an Array[int] containing network_ids;
	# player_instances is an Dictionary containing added players;

	for i in range(0,players):
		# consider that player's script have @export variable player_hud pointing to his Control node.
		player_instances[i].player_hud.rpc_id(i); # i is network_id;

P.S.: By the way it’s interesting how you can specify the "reliable” part. Does this mean that by default the @rpcs are "unreliable”? What is even the purpose of this? Shouldn’t ALL rpcs be reliable?

I mean it’s literally important e.g. for communication between client → server or server → client.

Let’s say player killed another player. Player A fired rpc on Player B to take_damage. And Player B realized his health is below or equal zero and calls rpc function to the server to register that.

In this case it’s client → client then client → server. But I geniunely don’t know how a client would call server’s rpc lol. Perhaps, player would call get_root() and find the server’s node with a script that have @rpc server function declaration. So it would be like this: get_root().get_node(“main_game”).get_node(“server_node”).player_killed.rpc(player_name, killed_by_player_name).

And that part I also don’t understand. What would be difference if I just did .player_killed.rpc() instead of .player_killed.rpc_id(1)? I mean the server’s network_id is 1. And rpc (how I understand) is supposed to “replicate”… for all clients…? Why would clients need to know something that it’s only server’s business? What even rpc would do? It’s not like clients also have that @rpc declaration on their own scripts. I literally don’t understand.


@Sweatix Thank you for your response. I will think about sharing a git repo but not right now…

I hope the code provided above would help you kind of understand what I mean.

By the way, quick unrelated question.

Do you guys know by any chance why - when I set from server’s script a position to instantiated PackedScene and then add_child… that same instantiate scene (which is now node) have its position set to Vector3(0,0,0).

What I mean is that I add a specific model (NOT player model) from the server script and set the specific position but it seems like MultiplayerSpawner just overrides the spawned node position to the default values.

In the debugger it’s clearly visible that on the server the positon is correct but on the clients it’s zero…

Can you please uh explain what do you mean by “they need to be in the same script”?

I mean let’s say Player has connected to the game and his model was added. The script’s path where @rpc function declaration is would be this: root/main_game/players_spawn/204354839. (all players have their names set to the network_ids).

Then if I wanted to declare a server version of that same @rpc function then… tecnhically if NodePath should be the same… then… the server’s declared function would be also at root/main_game/players_spawn/204354839. Why documentation even specified that there should be the same NodePath if the function would be technically on the same script?

Is the Godot Engine’s networking hard or I am stupid?

Not that hard :D. I think you jumped in to a complicated project without a lot of previous experience. Always going to be slow going at the start.

To answer specific questions:

reliable RPCs consume extra bandwidth and (a very small amount) of CPU time. reliable RPCs are also ordered, so clients can’t run RPC 2 until they run RPC 1. Ordering means that if every RPC was reliable they would grind to a halt under bad network conditions and packet loss would essentially freeze the game. Strategy/turn based games might actually make everything reliable but real time stuff like position updates should probably always be unreliable.

If Player B has to send something to the server there should be a Node that has Player B as authority. Also, if Player B already controls getting killed then the player Node can just use @rpc to send that update to everyone instead of routing it through a separate server function.
Instead of using long get_node functions you should consider using an @export var. If you want to get a Node in a different scene you might want something like Service Locator · Decoupling Patterns · Game Programming Patterns or some kind of “dependency injection”. This is a large topic that’s very important for large/complicated software so if you read stuff on the internet that says you need to define static interfaces and use integrations tests or something like that you shouldn’t worry about it :p.

By “they need to be in the same script” I was reminding you that RPCs function declarations need to be the same on both the sender and receiver. Technically, player 204354839 can still send and receive RPCs even if different game instances attach different scripts to Node 20435839 as long as the different scripts both define ALL of the @rpc functions and these functions have the same parameters. However, I strongly recommend just using the same script on all game instances.

Networking is complicated, and multiplayer networking is even more complicated, so don’t worry about it seeming hard. It IS hard.