Godot 3D Multiplayer Simplified

FloffahFloffah
8 min read

All over the internet, you can find resources on creating online co-op multiplayer games in Godot. I continuously see the same comments from people misunderstanding some pieces of the system as there is very little detailed documentation or online support for them. I have managed to get it working in Godot, so I'll attempt to fill in the gaps by explaining all the moving parts and the nice structures that Godot provides for creating multiplayer experiences.

I have a GitHub repository with all the code in this guide. Note that it’s built for Godot 4.4. https://github.com/Floffah/godot-multiplayer-example

Implementation Details

If you follow this guide, there are a few things to note about my design choices here.

  • This article assumes a basic understanding of how Godot works.

  • I use C# for crucial player and server code (but the GDScript equivalent code is similar)

  • This structure does not consider anticheat mechanisms as nothing is validated on the server. This is perfectly fine for co-op games, think REPO or Lethal Company, but for more complicated MMO games you might want to implement server validation. An official Godot guide dips its toes into that a bit.

Important Pieces

MultiplayerSpawner

This is a built-in node type in Godot that allows hosts and clients to sync packed scenes whenever they are spawned. This is useful for changing the level for all peers without manually switching it on each client, or for spawning the correct number of players on each peer. You have to pass this a root node (parent of all spawned packed scenes) and a list of assets (packed scenes to spawn). If a host adds a player node as a child of the spawner’s root node, all other peers will immediately see it too. It is worth noting that this also spawns existing nodes for a new peer joining after they were created.

MultiplayerSynchronizer

This is another built-in node in Godot that allows the host or a peer to synchronise the values of set node properties with every other peer. This is useful for syncing the position of a player or any other data we need.

Multiplayer Authority

When a global multiplayer peer is set in Godot, every node can have an authority ID. This defines which peer (including the server) has control over the node, preventing other peers from manipulating it. By default the SetMultiplayerAuthority method applies to all children too, unless you pass false as the second parameter.

The two previously explained nodes are also affected by authority. MultiplayerSynchronizer syncs the values of properties from the authority to all other peers. Similarly, MultiplayerSpawner synchronises spawned nodes from the authority to all other peers.

The multiplayer authority field itself isn’t synchronised to my knowledge, so if you need to give explicit control over something to a specific peer, you must do it on all peers (including the host).

The Game

I will use a simple main menu to start:

Connect these two buttons to methods similar to the ones shown below. These functions provide simple host and connect logic, along with replacing the main menu with the first level scene.

    public void ConnectToServer() {
        var peer = new ENetMultiplayerPeer();
        peer.CreateClient("localhost", 7777);

        Multiplayer.MultiplayerPeer = peer;

        GD.Print("Connected to server at " + address + ":" + port);

        GetTree().ChangeSceneToFile("res://scenes/game.tscn");
    }

    public void StartServer() {
        var peer = new ENetMultiplayerPeer();
        peer.CreateServer(7777);

        Multiplayer.MultiplayerPeer = peer;

        GetTree().ChangeSceneToFile("res://scenes/game.tscn");
    }

Now, the structure of network communication will be:

  • The host (server) spawns all players using a MultiplayerSpawner to sync to all peers

  • Each player controls their own input and movement. Their position is synced to other peers via a MultiplayerSynchronizer node

Let’s start by creating the player. You can use anything you want, I used a rectangle 😄. I won’t provide the player movement code, but you can find an example in the GitHub repo if you need help. Start by creating a player script like this:

using Godot;
using System;

public partial class Player : CharacterBody3D {
    [Export] public int PlayerId; // Acts as the multiplayer peer (& authority) ID for the player

    [Export] public Camera3D Camera { get; set; }; // You can set this using the Godot editor

    public override void _EnterTree() {
        // Parse the ID from the name
        var nameParts = Name.ToString().Split("_");
        if (nameParts.Length > 1) {
            PlayerId = int.Parse(nameParts[1]);
        }

        // The authority ID must be set in EnterTree so that its available to the MultiplayerSynchronizer when it expects it. Note that this function runs on every peer
        SetMultiplayerAuthority(PlayerId);
    }

    public override void _Ready() {
        if (PlayerId == Multiplayer.GetUniqueId()) {
            Camera.SetCurrent(true);
            SetPhysicsProcess(true); // Only the correct peer should have control over it's player. This isn't required, but prevents unnecessary processing on clients that can't move the player
        } else {
            SetPhysicsProcess(false);
        }
    }

    public override void _PhysicsProcess(double delta) {
        // Movement code
    }
}

A glaring issue here is that we have to set the ID based on the name..

When we spawn the player on the server, we will create a player with a name like Player_1. Unfortunately, because the client peer needs control over the player to move it, we can’t synchronise the ID field from the server as it isn’t the authority over any other players than its own. Without parsing the name, the server would know the peer is the authority, but all other peers wouldn’t know, meaning only the host player is controllable from the host instance, and other peers are left with a frozen screen.

There are a few possible fixes here:

  1. We could let the host have initial authority over the player, and at some point, once the client is ready, we pass control over to it. In my experience, this way of doing it is challenging to get right, and I couldn’t.

  2. We could only capture the keyboard/controller inputs on the client and send them to the server via RPC or synchronisation, where movement is calculated. I don’t like this version as it will be really annoying to players if there is network latency. If you want to do it this way, the official Godot blog has a guide explaining it.

  3. It may also be possible to provide the MultiplayerSpawner with a custom spawn callback that applies the ID in a much less hacky way, but we won’t do this.

Create a player scene and attach the above script to the root node. Here is the structure we need:

There are a few key things to do here. Firstly, make sure we have a Camera3D node and a MultiplayerSynchronizer node. The synchroniser root should be set to the player node, and you should add position to the list of properties to sync. I put PlayerId here too, but you can ignore that because of the issues explained above. If you get it working and I am missing something, please let me know.

Also make sure you attach the Camera3D you created to the Camera property of Player.

Our players will now synchronise movements and each peer will have control over its own player. But what about actually spawning the player? Let’s do that next. On every level I create, I attach a script to the root node called MultiplayerLevel. This takes the responsibility of spawning the players. Before we create that though, lets create the level.

You can ignore everything else, but the crucial nodes here are MultiplayerSpawner, Players, and the root node with our script.

As for the player spawner, once you have created the Players node make sure to copy the settings I’ve set in MultiplayerSpawner on the right. Specifically:

  1. Set spawn path to Players, we will add player objects as a child of this node and they will be replicated to other peers

  2. Add your player scene to auto spawn list so Godot knows we want to sync our player

The final piece is server logic to handle spawning the players. Create the MultiplayerLevel script and use the following structure:

using Godot;

public partial class MultiplayerLevel : Node {
    public static string PLAYER_SCENE = "res://scenes/players/player.tscn";

    [Export]
    public Node3D PlayerSpawner { get; set; } = null;

    public override void _Ready() {
        // Only run on the server
        if (!Multiplayer.IsServer()) {
            return;
        }

        // Subscribe to peer events
        Multiplayer.PeerConnected += PlayerJoined;
        Multiplayer.PeerDisconnected += PlayerLeft;

        // Create all existing players in case this level is being loaded after users have joined (multi level games)
        foreach (var peerId in Multiplayer.GetPeers()) {
            PlayerJoined(peerId);
        }

        // Add the host player unless this is a godot server build
        if (!OS.HasFeature("dedicated_server")) {
            PlayerJoined(1);
        }
    }

    public void PlayerJoined(long id) {
        GD.Print("Peer " + id + " connected");

        var playerScene = GD.Load<PackedScene>(PLAYER_SCENE);
        var player = playerScene.Instantiate<Player>();

        player.PlayerId = (int)id;
        player.SetPosition( new Vector3(0, 2, 0));
        player.Name = "Player_" + id;
        player.SetMultiplayerAuthority((int) id); // Immediately give over control from the server to the peer. As this is not replicated, our Player script handles this on each peer

        PlayerSpawner.AddChild(player);
    }

    public void PlayerLeft(long id) {
        GD.Print("Peer " + id + " disconnected");

        // Remove them to avoid bad state
        var player = PlayerSpawner.GetNode("Player" + id) as Player;
        if (player != null) {
            player.QueueFree();
        }
    }
}

And make sure to set the player spawner in the Godot UI.

After implementing all this, if you run multiple game instances (Debug → Customize Run Instances), you should be able to do the following. The rectangles are the players.

If I have missed something, please let me know. In the meantime you can find a full runnable project here: https://github.com/Floffah/godot-multiplayer-example

0
Subscribe to my newsletter

Read articles from Floffah directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Floffah
Floffah

I am a full stack web developer from Scotland. I have some pretty cool projects going on over at my GitHub page. Check it out!