How to set a custom multiplayer in Godot 4.0

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Mudderhino

Previously, I would set the node-specific custom_multiplayer with self.set_custom_multiplayer(multiplayerAPI) .

How is this done in Godot 4 though?

:bust_in_silhouette: Reply From: BoxyLlama

I’m only just figuring it out myself, but this is how I believe it’s done as of Beta 1.

3.x:

var network := NetworkedMultiplayerENet.new()
var gateway := SceneMultiplayer.new()

func start_server():
    network.create_server(1910, 100)
    set_custom_multiplayer(gateway)
    custom_multiplayer.set_root_node(self)
    custom_multiplayer.set_network_peer(network)

4.x:

var network := NetworkedMultiplayerENet.new()
var gateway := SceneMultiplayer.new()

func start_server():
    network.create_server(1910, 100)
    get_tree().set_multiplayer(gateway, self.get_path())
    multiplayer.set_multiplayer_peer(network)

Thanks for pointing me in the right direction!

BadicalExtreme | 2022-09-25 00:15

:bust_in_silhouette: Reply From: BadicalExtreme

I had been having issues with the v4 Custom Multiplayer and not getting RPC functions to work, but BoxyLlama’s answer below helped me out a bunch with the second critical parameter node path of the get_tree().set_multiplayer(gateway, self.get_path()) line.

To expand on this for everyone’s benefit after I did a bunch of experimentation, here’s what I’ve learned. The sample scenario below is separate client and server projects, however it should apply to combined projects as well. The server has an Autoload singleton called ServerCustom and the client has one called ClientCustom (notice they can be different node names). Start the server up first, then the client which should auto-connect, then call an RPC function that flows through the server and back to the client over several calls that should be self explanatory by name.

Server code (ServerCustom.gd):

extends Node

var server_custom = ENetMultiplayerPeer.new()
var multiplayer_api : MultiplayerAPI

var port = 8888
var max_peers = 5

func _ready():
	print("Custom Server _ready() Entered")

	server_custom.peer_connected.connect(_on_peer_connected)
	server_custom.peer_disconnected.connect(_on_peer_disconnected)
	multiplayer_api = MultiplayerAPI.create_default_interface()
	server_custom.create_server(port, max_peers)

	get_tree().set_multiplayer(multiplayer_api, ServerCustom.get_path())
	# can use "/root/ServerCustom" or self.get_path()
	multiplayer_api.multiplayer_peer = server_custom

func _process(_delta: float) -> void:
	if multiplayer_api.has_multiplayer_peer():
		multiplayer_api.poll()

func _on_peer_connected(peer_id):
	print("Custom Server _on_peer_connected, peer_id: {0}".format([peer_id]))
	await get_tree().create_timer(1).timeout
	print("Custom Peers: {0}".format([multiplayer.get_peers()]))

func _on_peer_disconnected(peer_id):
	print("Custom Server _on_peer_disconnected, peer_id: {0}".format([peer_id]))

@rpc(any_peer) 
func rpc_server_custom():
	var peer_id = multiplayer.get_remote_sender_id() # even custom uses default "multiplayer" calls
	print("rpc_server_custom, peer_id: {0}".format([peer_id]))
	rpc_server_custom_response(peer_id)

@rpc 
func rpc_server_custom_response(peer_id, test_var1 : String = "party like it's", test_var2 : int = 1999):
	print("rpc_server_custom_response to peer_id : {0}".format([peer_id]))
	rpc_server_custom_response.rpc_id(peer_id, test_var1, test_var2)

Client Code (ClientCustom.gd):

extends Node

var client_custom = ENetMultiplayerPeer.new()
var multiplayer_api : MultiplayerAPI

var address = "127.0.0.1"
var port = 8888

func _ready():
	print("Custom Client _ready() Entered")

	client_custom.connection_succeeded.connect(_on_connection_succeeded)
	client_custom.connection_failed.connect(_on_connection_failed)

	client_custom.create_client(address, port)
	multiplayer_api = MultiplayerAPI.create_default_interface()
	get_tree().set_multiplayer(multiplayer_api, ClientCustom.get_path()) 
	multiplayer_api.multiplayer_peer = client_custom

	print("Custom ClientUnique ID: {0}".format([multiplayer_api.get_unique_id()]))

func _process(_delta: float) -> void:
	if multiplayer_api.has_multiplayer_peer():
		multiplayer_api.poll()

func _on_connection_succeeded():
	print("Custom Client _on_connection_succeeded")
	await get_tree().create_timer(1).timeout
	print("Custom Peers: {0}".format([multiplayer.get_peers()]))
	rpc_server_custom()

func _on_connection_failed():
	print("Custom Client _on_connection_failed")


@rpc 
func rpc_server_custom():
	print("Custom Client rpc_server_custom")
	print("Custom Peers: {0}".format([multiplayer.get_peers()]))
	rpc_server_custom.rpc() # this works (NO MORE STRINGS!)

@rpc(authority) 
func rpc_server_custom_response(test_var1, test_var2):
	print("Custom Client rpc_server_custom_response: {0} {1}".format(
		[test_var1, test_var2]))

A few Notes:

  • For CUSTOM multiplayer, it does not appear that node name/paths must match but they do need to be non-default so you’re not sharing RPC functions in the same path as default. My tester projects had both default multiplayer AND the custom multiplayer autoload singletons above, and both worked fine together and kept the correct peer IDs straight between who was calling what. The key was to keep all default RPC function definitions in a separate Node path as well as specify the path default multiplayer should use.
  • var scene_multiplayer = SceneMultiplayer.new()
    SceneMultiplayer is the default implementation class for MultiplayerAPI. The biggest difference is that you can’t new() up the MultiplayerAPI, you must call the create_deafalt_interface() in the example, but either works with the above pattern if you switch it out for SceneMultiplayer instead
  • Notice that multiplayer.get_peers() and multiplayer.get_remote_sender_id() are the same even if you’r’e using custom or using both default and custom, it keeps them straight internally based on your server path definitions. If you try something like CustomServer.multiplayer_api.get_remote_sender_id() it always returns 0 even during RPC calls
  • For DEFAULT multiplayer, I first thought you had to have the full /root/Path/to/Node be exactly the same on both sides for all RPC functions to work, but that’s not actually required as long as you set the get_tree().set_multiplayer(multiplayer, "/root/Path/to/Node") appropriately for where your default RPC functions will be defined.