Godot: Multiplayer: joining an ongoing game and dealing with RPCs

Created on 14 Feb 2018  路  11Comments  路  Source: godotengine/godot

Godot version:
3.0

OS/device including version:
Windows 10

Issue description:
Hello all.
I'm a new Godot user and I'm experimenting the multiplayer API.

I didn't make a lobby like in most of the demos. So the server is always running, and clients connect to it.
When a client connects, a new entity is created for him and authority is granted.
This entity have some sync vars.
So far so good.

But when a second client connects, it immediately receives the state sync of the previous client entity even before the entities are created in this new client, resulting in errors...

Is there a way to ignore incoming packets if the client is in a "joining game" state?
Or buffer the packets and deliver to the target entity when it's instantiated?

Steps to reproduce:
I've made a little separation of concerns so far, so there is a MasterNetwork scene and a SlaveNetwork scene... Some files were divided in BaseClass/MasterClass/SlaveClass also.

I don't know if this is the best approach for Godot multiplayer development, but it's working so far. I would appreciate some feedback on this. Let me know if I'm not being Godotish, in terms of using the engine the way it's meant to be used.

Download the project, open the server and client projects with 3 different Godot editors. (I use symlinks to keep them synchronized).
Don't forget to change the remote debugger port for each editor.
Play them all.

Thanks!

Minimal reproduction project:
godot_network.zip

archived feature proposal network

Most helpful comment

Just found this issue thanks to Github recommending it to me via "similar issues" box.

Our game is also affected by this. We could disable any synchronization RPCs until all players are ready and that would fix it, but players can join game at any time. So while they are loading resources & populating their scenes, they receive lots of RPC synchronization packets for yet unknown nodes (because client is still not fully ready). Thise yields hundreds of RPC errors that freeze client during lag (it eventually unfreezes on Linux, but renders "Program not responding" on Windows).

This could be solved by adding an API method to Node class to (temporarily) fully disable RPC for this node and all its children. So every RPC call would simply need to walk down the requested node path and check if every node in path has RPC enabled (which should be true by defalut for backwards compatibility).

So for example, if an incoming RPC call for method foo in node /root/spam/eggs is received, the engine should first check if /root, /root/spam and /root/spam/eggs all have RPC enabled. If any of them hasn't, then this RPC call should not be handled and an error should not be risen.

I think this process is pretty trivial and should take no more than few hundred CPU ticks. It's basically just fetching parent nodes until the root of the tree and checking their flag.

If anyone thinks this makes sense, I'd love to volunteer to contribute on this & implement something like:

func _ready():
    $players_root.set_rpc_enabled(false)
    get_tree().multiplayer = MultiplayerWhatever.new()
    # Communicate with server by using RPC from other nodes:
    $config.rpc('marco')

puppet func polo():
    # Re-enable RPC on $players_root
    $players_root.set_rpc_enabled(true)

I think this can currently be hacked together by iterating through methods and overriding their RPC behavior via rpc_config, but changing RPC for the entire node would really be handy.

All 11 comments

I'd like to throw in on having the same issue. I've tried a number of approaches to minimize the errors when you have players joining persistent games and have been partially successful but still get at least some network errors. Specifically last night I ran a server with 40 networked bots and the startup errors were enough to crash some clients as they connected.

Overall the new multiplayer API is working GREAT and this is the only thing I think needs some work.

I think what might help would be more general way to control when players start/stop receiving RPC's from "global" broadcasts. One method that seems intuitive to me would be have different groups you can add network_id's to which determines which RPC's they'll get. This may also be useful in larger games where you'd like only players on a certain part of the map to get the updates.

Just a note that I think the new system that enables multiple servers/clients for 3.1 provides a mechanism that can resolve this problem, though I've not tested it out.

I made my own system that calls 'rpc_id' on all clients who are ready to receive updates on given node.
I call my own function instead of 'rpc'.
Server keps track of clients and nodes they are interested in and clients register nodes on server when they are ready to receive updates for them.
Clients can register/unregister whenever they want.
Server keeps info in dictionary, keys are NodePaths and values are lists of clients' IDs.

Otherwise my console would be flooded with rpc error messages.

Hi all,

Just completed a search before logging something for this issue, so count me as a +1. I did however, type up some ideas so I'm pasting them below. Not only would this be useful for syncing game states but authentication.

Currently, most of Godot's high-level API is targeted at Lobby joining and synchronise starts. What I'm proposing is that there is a way to put a connection in to a unsynced or not ready state so that it doesn't receive global RPC calls.
It would still, allow the client to be able to send and receive directly to/from the server connection to report status or otherwise authenticate. This would be the default state and logic on the server would ultimately be responsible for taking the client out of this state. RSET should automatically sync when the client comes out of this state.

Examples of use:

  1. This is useful for pausing network flow during scene changes, until the client reports that it has completed loading the world sync state before it's ready to receive commands.
  2. Allows the client to authenticate with the server before world state data is transmitted to said client. If the user doesn't authenticate before a time threshold it could be kicked without being on the network, due to the server sending RPC commands to it.

Just found this issue thanks to Github recommending it to me via "similar issues" box.

Our game is also affected by this. We could disable any synchronization RPCs until all players are ready and that would fix it, but players can join game at any time. So while they are loading resources & populating their scenes, they receive lots of RPC synchronization packets for yet unknown nodes (because client is still not fully ready). Thise yields hundreds of RPC errors that freeze client during lag (it eventually unfreezes on Linux, but renders "Program not responding" on Windows).

This could be solved by adding an API method to Node class to (temporarily) fully disable RPC for this node and all its children. So every RPC call would simply need to walk down the requested node path and check if every node in path has RPC enabled (which should be true by defalut for backwards compatibility).

So for example, if an incoming RPC call for method foo in node /root/spam/eggs is received, the engine should first check if /root, /root/spam and /root/spam/eggs all have RPC enabled. If any of them hasn't, then this RPC call should not be handled and an error should not be risen.

I think this process is pretty trivial and should take no more than few hundred CPU ticks. It's basically just fetching parent nodes until the root of the tree and checking their flag.

If anyone thinks this makes sense, I'd love to volunteer to contribute on this & implement something like:

func _ready():
    $players_root.set_rpc_enabled(false)
    get_tree().multiplayer = MultiplayerWhatever.new()
    # Communicate with server by using RPC from other nodes:
    $config.rpc('marco')

puppet func polo():
    # Re-enable RPC on $players_root
    $players_root.set_rpc_enabled(true)

I think this can currently be hacked together by iterating through methods and overriding their RPC behavior via rpc_config, but changing RPC for the entire node would really be handy.

I think this can currently be hacked together by iterating through methods and overriding their RPC behavior via rpc_config, but changing RPC for the entire node would really be handy.

This seems like a very sensible solution. Have you been testing it out recently?

I think a nice solution as mentioned above is to be able to disable 'global' RPC calls for a client except for direct ones (rpc_id, etc.) from the server and maybe other peers.
I supposed being able to disable RPC for specific nodes would help, but I'm not sure how useful that is for more complex multiplayer games with a load of nodes to sync.

+1

Got pretty confused. Just this week we started a project that is affected by the same problem...
Only for testing we have some kind of a server. Here 250 cubes are just moving around and syncing position with rset.

At any time a new client can join. In our approach he sends a rpc "login" request. Server logs the new client in and sends him the existing cubes and their current position per rpc_id.
Just yesterday we tested this not inside a local network but over the internet and it seems inside our local network this just worked as a result of pure luck.

I too think the best solution here would be to have a way to exclude a specific peer from receiving global rpc / rset calls. this way new peers are connected and maybe allways end up in some kind of "connected-but-not-ready" state and only after the server received an "I'm ready now!" call from the newly joined peer, it also receives the global rpc calls.

This way you could build a typical client - server kind of game. client logs in -> receives the date -> is ready -> and from now on the syncing can happen without any errors.

I have a really naive question -- is it possible to rejoin "as" a specific network_id? As in, imagine if I have a server Z, and two clients with network_ids / RPC IDs of Foo and Bar (pretend these are long numbers)
If Foo disconnects, it will rejoin with a network_id of Qux, and we'll run into this chestnut:

ERROR: get_node: Node not found: Game/Qux.
   At: scene/main/node.cpp:1381.
ERROR: _process_get_node: Failed to get cached path from RPC: Game/Qux.

and we really just want the old Foo network_id to be associated with all the new RPC requests. I don't know very much, but would this address the issues discussed, and if so, is there a way to achieve this hack in 3.2.3 (i.e., not changing existing engine)?

@ZackingIt I think your question is mostly off topic here, since even a reconnecting player would probably not have all the nodes loaded that it should. That said...

All players will know other players' network id. So to reconnect as a "previous network_id", you should assign a separate "primary id" with a secret (like user/pass) that lets the player confirm, on reconnect, that they are the only one that should be that primary id. Then the network id really only represents the current connection for that player's primary id.

Hope this helps, but maybe try the Discord server for more help.

Closing in favor of https://github.com/godotengine/godot-proposals/issues/75, as feature proposals are now tracked in the Godot proposals repository.

Was this page helpful?
0 / 5 - 0 ratings