How to setup server relay to only specific peers?

Godot Version

4.2.2

Question

Hi all,

I’m trying to develop simple MMO server with Godot High-level Multiplayer.

On my game, game world is separated by multiple “zones”, and a player can only interact with the players on the same zone. So if the zone and player is like…

Zone 1 - Player a,b,c
Zone 2 - Player x,y,z

“Player a” can interact with b and c, not with x,y,z.

On server side, I’d like to keep game state for every zones, and want to relay MultiplayerSynchronizer or RPC from players to only on same zone. (i.e. I want to keep server_relay as true)
So if “player x” modify some synced variable, it should be delivered to y and z, not to a,b,c.
However, it seems like SceneMultiplayer relay all messages to every connected players…

So, how can I relay messages to only specific sets of players?

There are a few options depending on exactly what you need to do.

Firstly, MultiplayerSynchronizer has various properties and methods related to the “visibility” of the synchronizer. You can set up a visibility filter function to automatically update the visibility based on which players are nearby, which is good practice for when players can be separated by large distances.

Also, when calling @rpc functions, you can use .rpc_id() to specify which peer will receive the message.

Lastly, you can create an instance of SceneMultiplayer for each Zone, and assign it to the Zone’s root node path using SceneTree.set_multiplayer. Then, when a player loads into that Zone, their Zone root node should connect to the server’s matching Zone. This does mean that the Zones will need to each manage their own multiplayer, but it allows you to totally isolate them from each other. Also, a parallel, always-connected multiplayer session can be used to coordinate Zone changes between the client and server.

The above techniques are certainly necessary to make the high-level multiplayer work for an MMO. But you may run into scaling problems when you have more than a few hundred players. Eventually you’ll need to make heavy use of threading in order to run things like physics across several Zones at once.

You should also make some consideration towards splitting your Zones into separate server processes entirely. This ensures total isolation of each Zone, and will scale across multiple server machines if you need that in the future. Obviously though, this is a much more advanced topic, and usually requires some higher-level orchestration outside of the game’s process. But at a certain player count, it’ll be almost necessary.

1 Like

Thanks very much for helping reply.

  1. Distributed server
    Well, actually I’m developing for research purpose, to evaluate network usage of dedicated server on MMO game. So, separating server instance is not the option for now.

  2. Multiple SceneMultiplayer
    => Yeah, this looks good enough and I believe it should works. But needs lots of code change on my sample game, so I would like to avoid it if possible…

  3. Setting up for RPC and MultiplayerSynchronizer
    => For rpc, yes, this is simple enough with .rpc_id(1) to server , then server can decide which peer to be relayed.
    => For MultiplayerSynchronizer, it’s bit tricky I guess… I dived into the engine codes and it looks like it relays sync packet regardless of visibility.

For sending, SceneReplicationInterface::_send_sync() → _send_raw() → SceneMultiplayer::send_command(), and send_command has SYS_COMMAND_RELAY…

Error SceneMultiplayer::send_command(int p_to, const uint8_t *p_packet, int p_packet_len) {
	if (server_relay && get_unique_id() != 1 && p_to != 1 && multiplayer_peer->is_server_relay_supported()) {
		// Send relay packet.
		relay_buffer->seek(0);
		relay_buffer->put_u8(NETWORK_COMMAND_SYS);
		relay_buffer->put_u8(SYS_COMMAND_RELAY);
		relay_buffer->put_32(p_to); // Set the destination.
		relay_buffer->put_data(p_packet, p_packet_len);
		multiplayer_peer->set_target_peer(1);
		const Vector<uint8_t> data = relay_buffer->get_data_array();
		return _send(data.ptr(), relay_buffer->get_position());
	}
	if (p_to > 0) {
		ERR_FAIL_COND_V(!connected_peers.has(p_to), ERR_BUG);
		multiplayer_peer->set_target_peer(p_to);
		return _send(p_packet, p_packet_len);
	} else {
		for (const int &pid : connected_peers) {
			if (p_to && pid == -p_to) {
				continue;
			}
			multiplayer_peer->set_target_peer(pid);
			_send(p_packet, p_packet_len);
		}
		return OK;
	}
}

While on receiving… SceneMultiplayer::_process_sys() takes the packet and relay without considering visibility

void SceneMultiplayer::_process_sys(int p_from, const uint8_t *p_packet, int p_packet_len, MultiplayerPeer::TransferMode p_mode, int p_channel) {
        ...

case SYS_COMMAND_RELAY: {
			ERR_FAIL_COND(!server_relay || !multiplayer_peer->is_server_relay_supported());
			ERR_FAIL_COND(p_packet_len < SYS_CMD_SIZE + 1);
			const uint8_t *packet = p_packet + SYS_CMD_SIZE;
			int len = p_packet_len - SYS_CMD_SIZE;
			bool should_process = false;
			if (get_unique_id() == 1) { // I am the server.
				// The requested target might have disconnected while the packet was in transit.
				if (unlikely(peer > 0 && !connected_peers.has(peer))) {
					return;
				}
				// Send relay packet.
				relay_buffer->seek(0);
				relay_buffer->put_u8(NETWORK_COMMAND_SYS);
				relay_buffer->put_u8(SYS_COMMAND_RELAY);
				relay_buffer->put_32(p_from); // Set the source.
				relay_buffer->put_data(packet, len);
				const Vector<uint8_t> data = relay_buffer->get_data_array();
				multiplayer_peer->set_transfer_mode(p_mode);
				multiplayer_peer->set_transfer_channel(p_channel);
				if (peer > 0) {
					// Single destination.
					multiplayer_peer->set_target_peer(peer);
					_send(data.ptr(), relay_buffer->get_position());
				} else {
					// Multiple destinations.
					for (const int &P : connected_peers) {
						// Not to sender, nor excluded.
						if (P == p_from || P == -peer) {
							continue;
						}
						multiplayer_peer->set_target_peer(P);
						_send(data.ptr(), relay_buffer->get_position());
					}
					if (peer != -1) {
						// The server is one of the targets, process the packet with sender as source.
						should_process = true;
						peer = p_from;
					}
				}

Although this packet would be ignored due to visibility on client side, I’d like to prevent server to send this packet to players on other zone. Any ideas? Or do I make some mistake on here?

Relay packets are only used in client-to-client communication for client-server architectures. These packets shouldn’t need to get filtered based on visibility on the server side, since they should already be filtered by the sender.

Also, the visibility of a node path can differ on the server vs the client, so even if the server decides a node isn’t visible to a certain client, a client peer which is an authority for that node may decide otherwise.

I’m not too familiar with the internals for this stuff, but it looks like SceneReplicationInterface keeps a list of MultiplayerSynchronizer nodes to sync to each peer (PeerInfo.sync_nodes in the code), which is always kept updated based on visibility filters.

For an MMO, it might be wise to disable relay entirely using SceneMultiplayer.server_relay. If this is disabled, client peers will only be able to send packets to the server, and not other clients, so this will probably change your architecture a bit.

1 Like

My bad. I was mistakenly manage visibility on client side as you guessed. Managing it properly works as expected - relay sync packets only to players on the same zone.

And for the engine internal, it does check the visibility right before the _send_sync() which I was missed on previous comment.