All Collections
Multiplayer
Custom Multiplayer Server with Unity
Custom Multiplayer Server with Unity

This article explains how to deploy a Unity dedicated server as a brainCloud Custom Server.

Jason Liang avatar
Written by Jason Liang
Updated over a week ago

Make a game

Create a simple scene with a Server GameObject and a client GameObject. When building for Linux with the Server Build option:

You should be able to test if you are running a dedicated. Using the following code:

if (Application.isBatchMode)
{
    Debug.Log("This is dedicated server");
    ... create your Server GameObject
}
else
{
    Debug.Log("This is client");
    ... create your Client GameObject
}

TCP Sockets

You can use whatever networking library you desired. For brainCloud customer servers, it's doesn't matter. For Simplicity in this example, we are going to use TCP
sockets.

Server

In the Server GameObject, put the following code. This example TCP code is taken from Microsoft's .NET documentation.

async void StartServerAsync()
{
    await Task.Run(async () =>
    {
        TcpListener server = null;

        try
        {
            // Set the TcpListener on port 7777.
            Int32 port = 7777;
            server = new TcpListener(port);

            // Start listening for client requests.
            server.Start();

            // Buffer for reading data
            Byte[] bytes = new Byte[256];
            String data = null;

            Debug.Log("Waiting for a connection... ");

            // Perform a blocking call to accept requests.
            // You could also use server.AcceptSocket() here.
            TcpClient client = server.AcceptTcpClient();            
            Debug.Log("Connected!");

            data = null;

            // Get a stream object for reading and writing
            NetworkStream stream = client.GetStream();

            int i;

            // Loop to receive all the data sent by the client.
            while ((i = stream.Read(bytes, 0, bytes.Length)) != 0)
            {  
                // Translate data bytes to a ASCII string.
                data = System.Text.Encoding.ASCII.GetString(bytes, 0, i);
                Debug.Log("Received: " + data);

                // Process the data sent by the client.
                data = data.ToUpper();

                byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);

                // Send back a response.
                stream.Write(msg, 0, msg.Length);
                Debug.Log("Sent: " + data);            
            }

            // Shutdown and end connection
            client.Close();
        }
        catch(SocketException e)
        {
            Debug.Log("SocketException: " + e);
        }
        finally
        {
            // Stop listening for new clients.
            server.Stop();
        }
    });
}

The steps above are as follow:

  1. Create a TCP socket

  2. Listen on port 7777

  3. Accept new client connection

  4. Read data from client

  5. Send data back to client

  6. Close server

Client

In the Client GameObject, use the following TCP code:

void ConnectToServer()
{
    try
    {
        // Create a TcpClient.
        // Note, for this client to work you need to have a TcpServer
        // connected to the same address as specified by the server, port
        // combination.
        Debug.Log("Connect to: " + _address + ":" + _port);
        TcpClient client = new TcpClient(_address, _port);

        // Translate a message into ASCII and store it as a Byte array.
        string message = "Hello World!";
        Byte[] data = System.Text.Encoding.ASCII.GetBytes(message);        

        // Get a client stream for reading and writing.
        //  Stream stream = client.GetStream();

        NetworkStream stream = client.GetStream();

        // Send the message to the connected TcpServer.
        stream.Write(data, 0, data.Length);

        Debug.Log("Sent: " + message);

        // Receive the TcpServer.response.

        // Buffer to store the response bytes.
        data = new Byte[256];

        // String to store the response ASCII representation.
        String responseData = String.Empty;

        // Read the first batch of the TcpServer response bytes.
        Int32 bytes = stream.Read(data, 0, data.Length);
        responseData = System.Text.Encoding.ASCII.GetString(data, 0, bytes);
        Debug.Log("Received: " + responseData);      

        // Close everything.
        stream.Close();        
        client.Close();
    }
    catch (ArgumentNullException e)
    {
        Debug.Log("ArgumentNullException: " + e);
    }
    catch (SocketException e)
    {
        Debug.Log("SocketException: " + e);
    }
}

The steps above are as follow:

  1. Create a TCP socket to the specified _address and _port.

  2. Send a message on the socket "Hello World!"

  3. Read the message back from the server. Should match "Hello World!"

  4. Disconnect the socket.

Test TCP socket

At this point, you should be able to build the dedicated server, run it then connect to it from Unity Client using _address = "127.0.0.1" and _port = 7777. You should build the dedicated in Windows for this test because you will run on the same machine. (WSL will not connect with the same localhost.)

If this doesn't work, you need to review the steps above and fix the mistakes before moving forward.

Note: Make sure to call StartServerAsync from Awake. Remove this call after the test.

brainCloud S2S

Install the brainCloud S2S (Server to Server) plugin, found here: https://github.com/getbraincloud/brainclouds2s-csharp

Follow the installation steps. This is required because you might need to do some initialization, level generation, etc. before accepting new connections. Our dedicated server needs to talk to brainCloud to communicate when it is ready.

In your Server GameObject, add the following members:

BrainCloudS2S _s2s;
string _lobbyId;

The first is our S2S session. The second is the Lobby ID for the current game. We need this when telling brainCloud that our Lobby is ready. That information is passed to us through environment variables on the docker container.

Put the following code in the Awake call:

void Awake()
{
    // Load environment variables passed in by brainCloud to our container
    var appId = Environment.GetEnvironmentVariable("APP_ID");
    var serverName = Environment.GetEnvironmentVariable("SERVER_NAME");
    var secret = Environment.GetEnvironmentVariable("SERVER_SECRET");
    _lobbyId = Environment.GetEnvironmentVariable("LOBBY_ID");

    // Initialize S2S library and enable log
    _s2s = new BrainCloudS2S();
    _s2s.Init(appId, serverName, secret);
    _s2s.LoggingEnabled = true;

    // Send request to get the lobby data. This will tell us who we are
    // expecting to get connection from and their passcode.
    Dictionary<string, object> request = new Dictionary<string, object>
    {
        { "service", "lobby" },
        { "operation", "GET_LOBBY_DATA" },
        { "data", new Dictionary<string, object>
        {
            { "lobbyId", _lobbyId }
        }}
    };
    _s2s.Request(request, OnLobbyData);
}

Next, we need to update S2S from to time just like we do in the brainCloud SDK:

void Update()
{
    if (_s2s != null)
    {
        _s2s.RunCallbacks();
    }

We then need to implement the OnLobbyData function we passed as callback above. This will be called once we receive the Lobby data which contains members to be connected, their passcode, information about the server type, etc.

void OnLobbyData(string responseString)
{
    Dictionary<string, object> response =    
        JsonReader.Deserialize<Dictionary<string, object>>(responseString);
    int status = (int)response["status"];
    if (status != 200)
    {
        // Something went wrong
        Application.Quit(1);
        return;
    }

    // Start the server asynchronously
    StartServerAsync();

    // Tell brainCloud we are ready to accept connections
    Dictionary<string, object> request = new Dictionary<string, object>
    {
        { "service", "lobby" },
        { "operation", "SYS_ROOM_READY" },
        { "data", new Dictionary<string, object>
        {
            { "lobbyId", _lobbyId }
        }}
    };
    _s2s.Request(request, OnRoomReady);
}

Before telling brainCloud that our server is ready, we are starting it by calling our previously implemented socket code StartServerAsync().

In OnRoomReady, we just check for error and then destroy the S2S context because we won't need it anymore.

void OnRoomReady(string responseString)
{
    Dictionary<string, object> response =    
        JsonReader.Deserialize<Dictionary<string, object>>(responseString);
    int status = (int)response["status"];
    if (status != 200)
    {
        // Something went wrong
        Application.Quit(1);
        return;
    }

    _s2s = null; // We will not need this anymore
}

Build Linux

From File -> Build Settings -> Do a Linux Build with the "Server Build" option.

Docker

We now create a docker image with our built dedicated server.

  1. Create an account and a repository for your game on https://hub.docker.com/

  2. Install docker daemon. https://www.docker.com/

  3. Go to your package dedicated folder, and create the following DockerFile:

FROM debian:latest

# Create an app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# Copy files to the app directory
COPY . /usr/src/app

# Unity is trying to find the file /etc/ssl/certs/ca-certificates.crt
# By installing libcurl4-openssl-dev it seems to install that file for us.
RUN apt-get update -y --fix-missing
RUN apt-get install -y libcurl4-openssl-dev

# Ports (This does nothing, documentation purpose only)
EXPOSE 7777/tcp

# Execute
ENTRYPOINT ["./server.x86_64"]
CMD []

4. Build the docker image using this command: 

docker build -t repository:tag .

Your repository and tag are usually in the form of team/gamename:1.0.

5. Run the container locally using this command:

docker run -p 7777:7777 --name gamename team/gamename:1.0

Here we are binding the internal container port 7777 to the external port, 7777. We are also giving the container a name, gamename, so we can refer to it later. You should see S2S failure log. This is normal, we didn't pass-in environment variable. Make sure the error was caught and the docker container stopped.

6. You can manually stop a local container:

docker stop gamename

7. When all good, push the image to docker hub.

docker push team/gamename:1.0

It might ask you to log in to your docker hub user.

Lobby Setup in Portal

For those following steps, I have used a game named "ue4example". You will see that from now on instead of "gamename".

In brainCloud portal, create a new custom server. Fill in those settings:

Tweak the configuration depending on your game size and complexity. We've put 2 minutes session time here so the server doesn't stick around. In our simple example, we didn't code any behavior to stop the server if no one connects.

Go in the Regions tab and configure at least one region.

Enable lobbies under Multiplayer -> Lobbies

Create a lobby with those settings:

Those last settings will start the game as soon as 1 player joins the lobby. This is so we can test quickly. You will want to tweak those to allow more players.

Client brainCloud

The server part is completed.

Follow instructions here on how to setup brainCloud in Unity:
https://github.com/getbraincloud/braincloud-csharp/blob/master/README.md

All of the following code will be in your Client MonoBehaviour.
We'll be using brainCloud and the included JsonFx for parsing json:

using BrainCloud;
using BrainCloud.JsonFx.Json;

Member variables we will need:

BrainCloudWrapper _bc = null;
string _address;
int _port;

In Awake(), add the brainCloud component.

void Awake()
{
    _bc = gameObject.AddComponent<BrainCloudWrapper>();
    _bc.Init();
    _bc.Client.EnableLogging(true);
    _bc.AuthenticateAnonymous(OnAuthenticated, OnFailed);
}

The OnFailed method will be a general failure call. Handle error as you wish in there by logging, quitting application, showing a popup.

void OnFailed(int status, int reasonCode, string jsonError, object cbObject)
{
    ...
}

Once authenticated, register for Lobby events into RTT then enable RTT.

void OnAuthenticated(string jsonResponse, object cbObject)
{
    _bc.RTTService.RegisterRTTLobbyCallback(OnLobbyEvent);
    _bc.RTTService.EnableRTT(RTTConnectionType.WEBSOCKET,
                            OnRTTEnabled, OnFailed);
}

Once RTT enabled, find a lobby. The name of the lobby here should match the one you've set into the Lobbies section on the portal. We don't pass a success callback here. The Lobby events will happen through RTT.

void OnRTTEnabled(string jsonResponse, object cbObject)
{
    var algo = new Dictionary<string, object>();
    algo["strategy"] = "ranged-absolute";
    algo["alignment"] = "center";
    List<int> ranges = new List<int>();
    ranges.Add(1000);
    algo["ranges"] = ranges;
    _bc.LobbyService.FindOrCreateLobby(
        "CustomGame", 0, 1, algo,
        new Dictionary<string, object>(), 0, true,
        new Dictionary<string, object>(), "all",
        new Dictionary<string, object>(), null, null, OnFailed);
}

Finally, our RTT Lobby events:

void OnLobbyEvent(string json)
{
    var response = JsonReader.Deserialize<Dictionary<string, object>>(json);
    var data = response["data"] as Dictionary<string, object>;

    switch (response["operation"] as string)
    {
        case "DISBANDED":
            var reason = data["reason"]
                as Dictionary<string, object>;
            var reasonCode = (int)reason["code"];
            if (reasonCode == ReasonCodes.RTT_ROOM_READY)
                ConnectToServer();
            else
                OnFailed(0, 0, "DISBANDED != RTT_ROOM_READY", null);
            break;

        // ROOM_READY contains information on how to connect to the
        // relay server.
        case "ROOM_READY":
            _message += "Room Ready\n";

            var connectData = data["connectData"]
                as Dictionary<string, object>;
            var ports = connectData["ports"]
                as Dictionary<string, object>;

            _address = (string)connectData["address"];
            _port = (int)ports["7777/tcp"];
            break;
    }
}

If everything goes well, you should now be able to  run the Unity Client, and join the dedicated. The first time the server is launched if none are warm, it could take up to a minute or two.

Did this answer your question?