Multiplayer client instance cannot load the game properly

Godot Version

v4.2.1.stable.official [b09f793f5]

Question

I am trying to implement multiplayer to this terrific demo of a tactics game by ramaureirac and using FinePointCGI’s multiplayer tutorials on Youtube as reference.

I changed nothing on the base project, immediately implemented basic multiplayer configuration according to the tutorial (host, join & start game via RPC). For some reason, the instance that isn’t the host is always unable to load the game properly. It throws an error because the tile’s global transform doesn’t exist when it tries to center characters on their positions; then proceeds to hang (not responding window) and so I just stop the project from Godot. The host’s instance is completely fine.

Here is the additional script that I attached to the multiplayer controller node:

extends Control

@export var address = "127.0.0.1"
@export var port = 8910
var peer

func _ready():
	multiplayer.peer_connected.connect(player_connected)
	multiplayer.peer_disconnected.connect(player_disconnected)
	multiplayer.connected_to_server.connect(connected_to_server)
	multiplayer.connection_failed.connect(connection_failed)

@rpc("any_peer", "call_local")
func start_game():
	var scene = load("res://assets/tscn/test_level.tscn").instantiate()
	get_tree().root.add_child(scene)
	self.hide()

func player_connected(id):
	print("Player connected " + str(id))

func player_disconnected(id):
	print("Player disconnected " + str(id))

func connected_to_server():
	print("Connected to server")

func connection_failed():
	print("Connection failed")

func _on_host_button_up():
	peer = ENetMultiplayerPeer.new()
	var error = peer.create_server(port, 2)
	if error != OK:
		print("Cannot host: " + str(error))
		return
	peer.get_host().compress(ENetConnection.COMPRESS_RANGE_CODER)
	multiplayer.set_multiplayer_peer(peer)
	print("Waiting for players")

func _on_join_button_up():
	peer = ENetMultiplayerPeer.new()
	peer.create_client(address, port)
	peer.get_host().compress(ENetConnection.COMPRESS_RANGE_CODER)
	multiplayer.set_multiplayer_peer(peer)

func _on_start_button_up():
	start_game.rpc()

I know that the machine itself is not the problem because I can run 2 instances of the base game without going through the multiplayer stuff with no issues.

Could anyone please give some advice? Thank you so much!

Can you add the error?

If you have any other code that checks to see if it is the server or authority over a node before continuing, the client will not run that specific code. This could lead to initialization issues if the client is pressing start game first.

Without knowing much about the rest of the code I would highly suggest that the host be the one to start the game first. Since it is an RPC it will trigger the same function on the client. If the client has access to that button to start_game I would remove/hide it from use, and make the RPC authority only.

I watched his Godot 4 multiplayer vid and looked at the source code.

If you haven’t changed anything other then adding this new server node. I’m not sure what could go wrong without the error code. But I would assume it has to deal with something in the original code, or maybe how you instantiate the game.

If I understand the error correctly, it’s because the tile itself hasn’t been instantiated properly therefore it doesn’t have a global_transform.

If you haven’t changed anything other then adding this new server node.

Yes this is correct.

But I would assume it has to deal with something in the original code

I’m sure it’s not with the original code, 1) because I’m able to run it on its own, even two instances of it without multiplayer; and 2) because even when I run it with multiplayer, one of the two instances works without issues.

or maybe how you instantiate the game.

I’m inclined to think this as well, however I’m not sure what else can be done with how I instantiate other than what I have provided above… I added a screenshot of the error to @HexY’s reply, maybe that would help?

I’m looking at the code, and I see that the pawn.gd is associated with a scene. And the get_tile() function directly calls a $Tile node within the pawn scene. This “Tile” node is a raycast3d object and the get_tile function returns $Tile.get_collider() in which get_collider can return null!

I’m looking at why it may not be colliding, and in the code there seems to be a lot of calls to load, building the arena. I’m wondering when the RPC happens there is a lot of stress to build the level by loading many resources for the first time, for both host and client.

I’m not aware of any differed threading for loading resouces, but it could be an explanation for the get_collider call to return null during a _process cycle, if the tile is still being loaded when get_tile is called.

I would suggest doing one thing, change the pawn code to handle a null return from get_tile() function.

func adjust_to_center():
  var tile = get_tile()
  if tile == null:
    return
   move_direction = tile.global_transform.origin - global_transform.origin
   ...

other options include moving the adjust_to_center call stack to a physics cycle, (may not work), or change all load calls to known and static file paths to preload. (This could help if loading is causing issue, but still may not fix the issue.)

in your second image of the client It definitely seems like the tiles don’t exist when the issue happens.

oh one last option, the test_level.tscn is the original main scene and it has the player and enemy pawns as child nodes at the end of the sceme list so they will be built first as the order goes bottom up in the tree. so my last suggestion would be to put the arena node at the bottom of the scene list so it will be loaded first.

1 Like

The first suggestion to add a check if tile is null did the trick!

I stand corrected about the issue not being with the original code. This has taught me to never rule out a possibility unless explicitly confirmed.

Thank you so much!

1 Like

This is just some ponderings I’m having for you going forward.

That null check should work, but it could be hard to debug later when you start to change things, and you will need to change things. If a pawn somehow got lost, or isn’t in the right place, this fix, as is, will be hard to trace.

You could add a print statement whenever a pawn isn’t colliding with a tile easily and that may be enough… but one benefit to the issue (and drawback), is that it did fail and it let you know something wasn’t completely expected by the code. (But the drawback is that it was at runtime).

I can’t readily provide a solution without expending more of my time, but I wanted to share my thoughts.

Anyway, I think your project is cool, and I like this approach to retrofit existing projects. I hope to see a version of it with multiplayer in the future!

Reach out to me if you have more multiplayer questions, (or I will just keep a watch in the forums). I may not have all the answers as I’m still learning, but I’m working on a multiplayer game myself so maybe I can answer yours!