I have attempted to re-create the project described here: https://godotengine.org/article/multiplayer-in-godot-4-0-scene-replication/ but done in 2d to simplify things. My goal is to better understand the mechanics of how networking works in Godot.

I have spent many years coding multiplayer functionality in Unreal Engine so I have a few things to unlearn but I am determined to get this right. Currently, I believe I am having trouble with assigning authority.

Please see a demonstration of the problem here:
As you can see, I host the server on the left. The player moves fine.
I join with a client on the right. As the client moves, you can see the server stuttering. I'm guessing this is because the client believes the client has authority over the server's input. So it keeps trying to move the server in the same direction the client is moving. But then the "position" of the server is replicated so the server corrects the client, saying "No, you did not move me - I am still"
Then, when moving with the server, it seems like both are fighting for who has authority .. and the server wins but not without a struggle.

Here is the code for the project:

So.. time to dig into some code. I need to understand exactly what code is running where (client vs server)

main.cs
Handles setting up the connection, hosting and joining a server. When either a server is hosted or a client joins they create a new instance of the "TestLevel" scene.

level.cs (attached to the TestLevel scene)
This will set the server up to run "AddPlayer" when a player connects. "AddPlayer" creates a new instance of the "player" scene and sets its "PlayerId" before adding it to the TestLevel scene.

    private void AddPlayer(long playerId)
    {
        // Create a new instance of the player
        player newPlayer = PlayerScene.Instantiate<player>();
        
        // Assign the PlayerId and name
        newPlayer.PlayerId = ((int)playerId);
        newPlayer.Name = playerId.ToString();

        // Add the player to the Players container
        GetNode<Node2D>("Players").AddChild(newPlayer);
    }

The TestLevel scene has a MultiplayerSpawner in it which I know does some magic to ensure all peers have the same entities spawned in order to ensure they're kept in sync. The newPlayer.PlayerId = ((int)playerId); is the important bit of code here, I think.

player.cs
When assigning the PlayerId in the above code, this is the code in the player.cs that handles it

    [Export] public int PlayerId
    {
        get
        {
            return _playerId;
        }
        set
        {
            _playerId = value;
            GetNode<MultiplayerSynchronizer>("PlayerInput").SetMultiplayerAuthority(value);
        }
    }

So as soon as the PlayerId is assigned to this newly created player, SetMultiplayerAuthority is executed to set the authority of this newly created player's "PlayerInput" (a MultiplayerSynchronizer desscribed below) to be the PlayerId of the connected client.
The _Process of this code will look to grab the Direction from the PlayerInput and update the player's final Position based on the direction. This code differs from the tutorial code. The tutorial calculates the velocity and runs "move_and_slide()". I have no idea what that means, my guess is that it's a call to an internal function to calculate physics-based movement which I'm not using in my 2d version. But this may be a culprit for why my code isn't working properly.

player.tscn
PlayerInput has a root path of PlayerInput ... what does this mean? I have absolutely no idea. PlayerInput has the authority of the appropriate controlling player. It has a replicated property of "PlayerInput: Direction". So when the PlayerInput.cs (below) executes and updates the Direction, this property is replicated to all peers (right?).

PlayerInput.cs
Not much code here. The _EnterTree function runs SetProcess(GetMultiplayerAuthority() == Multiplayer.GetUniqueId()); to allow the _Process node to run if the client executing this code has the same ID as the one who has authority.
As a sanity test (I've needed a lot of sanity), I added some code in the _Process to update the player's name label with whether the current client has authority over the PlayerInput

    public override void _Process(double delta)
    {
        GetParent<player>().GetNode<Label>("NameLabel").Text = (GetMultiplayerAuthority() == Multiplayer.GetUniqueId()).ToString();
        Direction = Input.GetVector("move_left", "move_right", "move_up", "move_down");
    }

The result looks good
The result looks good
... but wait. As I did this test I realized something. I am updating the name label in the "_Process" ... which should only be running for the current client. In other words, the label should be "True" for the controlling client and blank (or, actually "Beans" as the default text - don't ask) for the other clients. Why is it "False"? Why is Process running in PlayerInput for clients who don't have authority? This tells me maybe the _EnterTree code is not functioning correctly. For a moment I thought "Maybe the _Process" had a chance to tick once or twice before the _EnterTree turned it off, so I added some code to generate and tack on a random number to the name and confirmed - it is definitely running on ALL peers.

And so, while typing this post, I found that running SetProcess in _EnterTree simply doesn't work well, for whatever reason. I changed _EnterTree to _Ready instead and now everything runs perfectly. I can't believe it was that simple.

In the end, I'm still left with a couple questions. And I figure I typed all this up, maybe it will help someone else with similar issues so I'll leave the contents above.

  1. What is the purpose of setting the "Root Path" of the PlayerInput MultiplayerSynchronizer to itself? I understand that the idea is for the client to only have authority over this single component of the player but does this actually do anything? And same with the ServerSynchronizer, the root path is set to the base "Player" node. What is the purpose?
  2. Running SetMultiplayerAuthority on the server to assign a client authority over a player works to set it on the server. How does the client know it has authority? Does the MultiplayerSpawner replicate this to the client? Somehow in the PlayerInput the client knows it has authority over that player and therefore can run _Process. But the value is set on the server. So either the MultiplayerSpawner handles it or there's just some extra magic that happens under the hood to tell the client it has authority.

I guess that's it. Would be very happy to continue discussion here about the nuances of Godot networking - the more I understand the more comfortable I'll feel using this stuff. Thanks for reading