Unveiling Godot 4's Multiplayer Magic: Setting Up Your Dedicated Server in C#

Unveiling Godot 4's Multiplayer Magic: Setting Up Your Dedicated Server in C#

Welcome back, fellow Godot enthusiasts! In this instalment, we're diving into the realm of multiplayer game development with Godot 4. Buckle up as we explore the steps to set up a dedicated multiplayer server using C#. Don't worry; we'll keep it concise, aiming for a quick 5-minute read.

Why a Dedicated Server?

Before we dive in, let's briefly address why you might opt for a dedicated server. Dedicated servers provide a centralized, authoritative source of truth for your multiplayer game. They handle the game's logic, ensuring consistency across all connected clients and offering a robust solution for multiplayer experiences.

Prerequisites: Godot 4 and .NET

Ensure you have Godot 4 installed, and .NET support is configured, as we covered in our previous posts. If not, check out the quick setup guide to get yourself up to speed.

Setting Up the Dedicated Server

  1. Create a new Godot project for your dedicated server.

  2. Create a New Scene multiplayer.tscn:

  3. Add a new node of type Control and call it Multiplayer.

    • Each node in Godot has a multiplayer property, which is a reference to the MultiplayerAPI instance configured for it by the scene tree.

    • We will use Godot’s high-level MultiplayerAPI to implement our server logic.

  4. Add a C# Script MultiplayerController.cs and attach it to your scene.

  5. Add basic configuration for your server:

    • Create a new private variable for _port, _ip and _maylayerCount

    • (Optional) export all variables to make them visible in the Godot 4 Editor

    using Godot;
    using System;
    using System.Linq;

    public partial class MultiplayerController : Control
    {
        [Export]
        private int _port = 8910;
        [Export]
        private string _ip = "127.0.0.1";
        [Export]
        private int _maxPlayerCount = 2;

        ...
    }
  1. Initialize Networking:

    • Create a new private variable _peer to store a reference to the peer

    • Create a new method HostGame(), which will contain all the necessary logic to initialize a peer as a server and listen for client connections.

    • Create new methods PlayerConnected() and PlayerDisconnected() and set them as event handlers for the respective events of Multiplayer. These events will be triggered for all connected peers

    • Both EventHandlers take the userId as a parameter, so we simply print it for now.

    • Override Godot’s _Ready() method, which is called when the node enters the scene tree for the first time, to set the EventHandlers and start hosting the game by calling the HostGame() method.

    using Godot;
    using System;
    using System.Linq;

    public partial class MultiplayerController : Control
    {
        [Export]
        private int _port = 8910;
        [Export]
        private string _ip = "127.0.0.1";
        [Export]
        private int _maxPlayerCount = 2;

        private ENetMultiplayerPeer _peer;

        public override void _Ready()
        {
            GDPrint.Print("<<< START SERVER >>>");
            Multiplayer.PeerConnected += PlayerConnected;
            Multiplayer.PeerDisconnected += PlayerDisconnected;

            HostGame();
            this.Hide();
        }

        private void HostGame()
        {
            _peer = new ENetMultiplayerPeer();
            var status = _peer.CreateServer(_port, _maxPlayerCount);
            if (status != Error.Ok)
            {
                GDPrint.PrintErr("Server could not be created:");
                GDPrint.PrintErr($"Port: {_port}");
                return;
            }

            _peer.Host.Compress(ENetConnection.CompressionMode.RangeCoder);
            Multiplayer.MultiplayerPeer = _peer;
            GDPrint.Print("Server started SUCCESSFULLY.");
            GDPrint.Print("Waiting for players to connect ...");
        }

        private void PlayerConnected(long id)
        {
            GDPrint.Print($"Player <{id}> connected.");
        }

        private void PlayerDisconnected(long id)
        {
            GDPrint.Print($"Player <{id}> disconected.");
        }
    }
  1. Run the Server:

    • Run your dedicated server scene.

    • You'll now have a dedicated server running, waiting for client connections.

Setting up the Client

Setting up the client follows the same logic. Please setup a new project with a main scene, that contains a Join button.

  1. Create basic MultiplayerController:

    • Create a new private variable _playerId to store the playerId once e successfully creates a connection to the server
    using Godot;
    using System;
    using System.Linq;

    public partial class MultiplayerController : Control
    {
        [Export]
        private int _port = 8910;
        [Export]
        private string _ip = "127.0.0.1";

        private ENetMultiplayerPeer _peer;
        private int _playerId;

        public override void _Ready()
        {
            Multiplayer.PeerConnected += PlayerConnected;
            Multiplayer.PeerDisconnected += PlayerDisconnected;
        }

        private void PlayerConnected(long id)
        {
            GDPrint.Print(_playerId, $"Player <{id}> connected.");
        }

        private void PlayerDisconnected(long id)
        {
            GDPrint.Print(_playerId, $"Player <${id}> disconnected.");
        }
    }
  1. Initialize Networking

    • Create new methods ConnectionSuccessful() and ConnectionFailed() and set them as event handlers for the respective events of Multiplayer. These events will only be triggered for the current client.

    • Add a new ConnectToServer() method which contains all the necessary logic to initialize a peer as a client and connect to the server.

    • (Optional) Add a new OnJoinPressed() method and connect it to a Join button in your Godot UI. You can use this button to trigger connecting to the server

    using Godot;
    using System;
    using System.Linq;

    public partial class MultiplayerController : Control
    {
        [Export]
        private int _port = 8910;
        [Export]
        private string _ip = "127.0.0.1";

        private ENetMultiplayerPeer _peer;
        private int _playerId;

        public override void _Ready()
        {
            Multiplayer.PeerConnected += PlayerConnected;
            Multiplayer.PeerDisconnected += PlayerDisconnected;
            Multiplayer.ConnectedToServer += ConnectionSuccessful;
            Multiplayer.ConnectionFailed += ConnectionFailed;
        }

        private void ConnectionFailed()
        {
            GDPrint.Print("Connection FAILED.");
            GDPrint.Print("Could not connect to server.");
        }

        private void ConnectionSuccessful()
        {
            GDPrint.Print("Connection SUCCESSFULL.");

            _playerId = Multiplayer.GetUniqueId();

            GDPrint.Print(_playerId, "Sending player information to server.");
            GDPrint.Print(_playerId, $"Id: {_playerId}");

            RpcId(1, "SendPlayerInformation", _playerId);
        }

        private void PlayerConnected(long id)
        {
            GDPrint.Print(_playerId, $"Player <{id}> connected.");
        }

        private void PlayerDisconnected(long id)
        {
            GDPrint.Print(_playerId, $"Player <${id}> disconnected.");
        }

        public void ConnectToServer()
        {
            _peer = new ENetMultiplayerPeer();
            var status = _peer.CreateClient(_ip, _port);
            if (status != Error.Ok)
            {
                GDPrint.PrintErr("Creating client FAILED.");
                return;
            }

            _peer.Host.Compress(ENetConnection.CompressionMode.RangeCoder);
            Multiplayer.MultiplayerPeer = _peer;
        }

        public void OnJoinPressed()
        {
            ConnectToServer();
        }
    }

Compression Types

The selection of the appropriate compression type depends on the nature of your game and the characteristics of the data being transmitted. Experiment with different compression modes to find the right balance between compression ratio, computational overhead, and network bandwidth.

The compression mode must be set to the same value on both the server and all its clients. Clients will fail to connect if the compression mode set on the client differs from the one set on the server.

For more in-depth information you can take a look at the following article:

Decoding Data Efficiency: A Comprehensive Guide to Compression Types in Godot Multiplayer Networking

GDPrint

GDPrint is just a small wrapper around GD.Print(), to be able to easily add additional information like timestamps or unique player IDs to print statements.

using System;
using Godot;

public static class GDPrint
{
    public static string GetTimestamp()
    {
        return DateTime.Now.ToString("dd.MM.yyyy-HH:mm:ss");
    }

    public static void Print(string message)
    {
        GD.Print($"{GetTimestamp()}: {message}");
    }

    public static void Print(int id, string message)
    {
        GD.Print($"{GetTimestamp()} - <{id}>: {message}");
    }

    public static void PrintErr(string message)
    {
        GD.PrintErr($"{GetTimestamp()}: {message}");
    }

    public static void PrintErr(int id, string message)
    {
        GD.PrintErr($"{GetTimestamp()} - <{id}>: {message}");
    }
}

Testing Locally:

  1. Run your Server Project:

    • Check the console output to see if we are listening for player connections.
  2. Run your Client Project:

    • Hit the Join button in your Godot UI

    • Check the console output to see if we successfully connected to the server.

References

For more in-depth information check the following links:

  1. Basics of Multiplayer in Godot 4! C# Godot Basics! - by FinePointCGI

  2. Basics-of-Multiplayer-Lan-Tutorial-CSharp

  3. Godot Networking Documentation

  4. Example Server Project

  5. Example Client Project

  6. Decoding Data Efficiency: A Comprehensive Guide to Compression Types in Godot Multiplayer Networking

Conclusion:

Congratulations! You've set up a dedicated multiplayer server in Godot 4 using C#. In just a few minutes, you've unlocked the potential for robust, scalable multiplayer experiences. Stay tuned for more deep dives into Godot's capabilities in our upcoming posts.

Happy coding and multiplayer gaming