Skip to main content

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

This article will guide you through the key steps to deploy a Unity Linux server as a brainCloud room server, complete with a practical example.

Step 1: Create a Unity project

Create a new Unity project and add a new scene.

  • Name it something like MasinScene.unity

  • Save it in your Assets/Scenes/ folder

Create an empty GameManager GameObject.

Create a folder called Scripts and create a script EntryPoint.cs under the folder, then attach the script to GameManager GameObject.

Add the following code to your EntryPoint.cs script.

using UnityEngine;

public class EntryPoint : MonoBehaviour
{
public GameObject serverPrefab;
public GameObject clientPrefab;

void Awake()
{
if (Application.isBatchMode)
{
Debug.Log("Running as Dedicated Server");
Instantiate(serverPrefab);
}
else
{
Debug.Log("Running as Client");
Instantiate(clientPrefab);
}
}
}

Create a folder called Prefabs and create two prefabs (Server.prefab, Client.prefab) under this folder, then assign them to the EntryPoint.cs script.

Create a script named Server.cs under Scripts folder and attach it to Server.prefab. Then copy the code below into it.

using UnityEngine;
using System.Net.Sockets;
using System.Net;
using System;
using System.Text;
using System.Threading.Tasks;

public class ServerBehaviour : MonoBehaviour
{
void Awake()
{
StartServerAsync();
}

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

try
{
int port = 7777;
server = new TcpListener(IPAddress.Any, port);
server.Start();

byte[] bytes = new byte[256];
string data = null;

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

TcpClient client = server.AcceptTcpClient();
Debug.Log("Connected!");

NetworkStream stream = client.GetStream();

int i;
while ((i = stream.Read(bytes, 0, bytes.Length)) != 0)
{
data = Encoding.ASCII.GetString(bytes, 0, i);
Debug.Log("Received: " + data);

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

stream.Write(msg, 0, msg.Length);
Debug.Log("Sent: " + data);
}

client.Close();
}
catch (SocketException e)
{
Debug.Log("SocketException: " + e);
}
finally
{
server?.Stop();
}
});
}
}

Create a script named Clinet.cs under Scripts folder and attach it to Client.prefab. Then copy the code below into it.

using UnityEngine;
using System.Net.Sockets;
using System.Text;

public class ClientBehaviour : MonoBehaviour
{
string _address = "127.0.0.1";
int _port = 7777;

void Start()
{
ConnectToServer();
}

void ConnectToServer()
{
try
{
TcpClient client = new TcpClient(_address, _port);
string message = "Hello World!";
Byte[] data = Encoding.ASCII.GetBytes(message);

NetworkStream stream = client.GetStream();
stream.Write(data, 0, data.Length);
Debug.Log("Sent: " + message);

data = new Byte[256];
string responseData = string.Empty;
int bytes = stream.Read(data, 0, data.Length);
responseData = Encoding.ASCII.GetString(data, 0, bytes);
Debug.Log("Received: " + responseData);

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

Note: If you haven't already, delete the Server and Client objects from the scene -- these will be instantiated dynamically at runtime.

Step 2: Test the above code with a local build

At this point, you should be able to build a test dedicated server, run it, then connect to it from Unity Client using _address = "127.0.0.1" and _port = 7777.

Create a dedicated server build that can run on your current local platform from the Unity Editor. For this example, we'll build a macOS version.

Note: For this test, do not build for the Linux Server platform yet. If you're on a Windows machine, create a Windows build instead, since you'll be running both the server and client on the same machine. (WSL cannot connect to the same localhost as the host system.)

Run the server build in batchmode via your local terminal.

cd path/to/MyServerBuild.app/Contents/MacOS
./MyServerBuild -batchmode -nographics -logFile ./server.log

Next, run the client from the Unity Editor, and check the logs on both the server and client side -- you should see that they successfully establish a connection and communicate as expected.

If this doesn't work, review the steps above carefully and correct any mistakes before proceeding.

Step 3: Add brainCloud S2S to your dedicated server

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 in 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 time to time, just like we do in the brainCloud SDK.

void Update()
{
_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
}

Now that the server code is complete, you can build it using the Linux Server platform in the Unity Editor. It will generate a yourlinuxserverbuildname.x86_64 file along with yourlinuxserverbuildname_Data folder.

Step 4: Create a Docker image for your dedicated server and configure it as a brainCloud room server

Create a Dockerfile with the code below under the same folder as your build file.

FROM debian:bullseye-slim

# Create app directory
WORKDIR /app

# Copy files and folders
COPY . .

# Install only necessary runtime dependencies
RUN apt-get update -y && apt-get install -y \
libcurl4-openssl-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/*

# Expose default Unity port (customize if needed)
EXPOSE 7777/tcp

# Run your build
ENTRYPOINT ["./linuxServerBuild.x86_64"]

Build the Docker image using this command from your terminal under the same Dockerfile folder.

docker build -t jasonbitheads/cmsu:1.3 .

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

Run the Docker container locally using this command.

docker run -p 7777:7777 --name cmsu3 jasonbitheads/cmsu:1.3

Here, we're binding the container's internal port 7777 to the external port 7777 and assigning the container the name cmus3 for easy reference later. You should see an S2S failure log—this is expected, as we haven’t provided the environment variables that brainCloud typically injects. The error is caught, and the Docker container stops as a result.

Note: brainCloud will pass these environment variables by default for any room server it spins up. Below is an example list of environment variables from a launched server.

// passed by default from brainCloud
APP_ID = 13469
CONTAINER_EXPOSED_PORTS = 7777/tcp,7777/udp
EXTERNAL_IP = 3.99.186.192
EXTERNAL_PORTS = 9001/tcp,9001/udp
HOSTNAME = 029ec2d7bc98
LOBBY_ID = 13469:CustomGame:15
SERVER_NAME = CMSU
SERVER_HOST = api.braincloudservers.com
SERVER_PORT = 443
SERVER_SECRET = 99ef5e85-9a99-4140-8b0d-292b4b13807d

// from server itself
HOME = /root
PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

// from brainCloud server settings of portal server page
PACKET_LOG_ENABLED = true
CONNECTION_TIMEOUT = 240

You can always add more if you need, through the Server Settings page on the brainCloud portal.

If you want to debug a server running inside a local Docker container, you can set up a Room Server Manager (RSM) and configure brainCloud to communicate with your locally hosted RSM. For detailed instructions, refer to this article.

Push the image you built to your Docker Hub repository.

docker push jasonbitheads/cmsu:1.3

Set up room server on brainCloud portal

Adjust the configuration based on your game's size and complexity. In this example, we’ve set the session timeout to 30 minutes to ensure the server doesn’t persist unnecessarily. Since our simple implementation doesn’t include logic to shut down the server if no players connect, be aware that you can always terminate the server manually via the Server Monitor page to conserve resources.

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

Make sure your app lobby service is enabled, then create a lobby with the settings below, and bind the correct server you created above to this lobby.

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.

Step 5: Adjust your Client.cs script and initiate a test run.

The server part is completed at this point.

Follow the instructions here on how to set up 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 the application, or showing a popup.

void OnFailed(int status, int reasonCode, string jsonError, object cbObject)
{
Debug.Log("Client brainCloud Call Error: " + jsonError);
}

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

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

Once RTT enabled, find a lobby. The name of the lobby here should match the one you've set in 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>(),
true,
new Dictionary<string, object>(),
"all",
new Dictionary<string, object>(),
null,
null,
OnFailed,
null
);
}

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 server. The first time the server is launched, if none are warm, it could take up to a minute or two.

Monitor the lobby matchmaking process via Matchmaking Monitor.

Monitor the server status via Server Monitor.

Monitor the server logs from Recent Error Logs page with Info checkbox selected

You can also monitor the server logs via your Slack app if you have integrated it with brainCloud

Did this answer your question?