Skip to main content

Custom Multiplayer Server in Unity with RSM

How to set up a Room Server Manager(RSM) to host room servers for your brainCloud App

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

A Room Server Manager in brainCloud is a custom service responsible for handling the hosting, allocation, and lifecycle management of Room Servers used for multiplayer experiences. Instead of relying on brainCloud’s default room server infrastructure, you can specify a URL that points to your own Room Server Manager server, which listens for room server requests from brainCloud and gives you full control over how Room Servers are provisioned, configured, launched, and terminated -- enabling more flexibility and customization for your game’s multiplayer server.

This article will walk you through the basic steps to set up a Room Server Manager in brainCloud with a Node.js Express RSM example.

Prerequisite: This tutorial uses the Room Server Docker image and client app created in the "Custom Multiplayer Server with Unity" guide.

However, you can use any multiplayer server image and client app that integrates with the brainCloud Lobby feature, whichever is most convenient for you to follow along.

Step 1: Expose your local RSM server using a public URL and configure it as the base URL for your brainCloud RSM integration.

Use a tool like ngrok to temporarily expose your local RSM server to the internet. In this example, we'll expose the local port 3000.

This tutorial runs the RSM locally. If your RSM is already hosted on a server with a public URL, or you've set up a public URL that forwards to your local server, you can skip this step. Just configure that public URL as the base URL for your brainCloud RSM server.

ngrok http 3000

Copy the forwarding public URL from the result.

Paste it into your brainCloud RSM Base Url field.

Step 2: Create a Node.js application to serve as your RSM.

Create a new project folder, add a rms.js file, paste the following code into it, and update the Configuration section with your server details.

// === Imports ===
import brainclouds2s from 'brainclouds2s';
import express from 'express';
import { publicIpv4 } from 'public-ip';
import os from 'os';
import { Docker } from 'docker-cli-js';

// === Configuration ===
const ids = {
appId: "<your app Id>",
serverName: "<your server name>",
serverSecret: "<your server secret>",
url: "api.braincloudservers.com",
dockerImage: "jasonbitheads/cmsu:1.3",
ports: {
http: 3000,
docker: 7777,
RSRange: [9310, 9320]
}
};

// === Globals ===
let globals = {
publicIP: "",
internalIP: "",
s2sContext: null
};

// === Logger ===
function log(message) {
console.log(`${(new Date()).toLocaleTimeString()} | ${message}`);
}

// === Room Server Manager ===
let REPORT_INTERVAL_SEC = 30;
let ASSIGN_TO_LAUNCH_TIMEOUT_SEC = 60;
let roomServers = [];
let possiblePorts = [];
let docker = new Docker();
for (let i = ids.ports.RSRange[0]; i <= ids.ports.RSRange[1]; ++i) {
possiblePorts.push(i);
}

setInterval(() => {
if (roomServers.length === 0) return;
log("--- Room servers report ---");
roomServers.forEach(roomServer => {
log(` ${roomServer.room.id}:${roomServer.port} spawned:${roomServer.spawned}`);
});
}, REPORT_INTERVAL_SEC * 1000);

const RoomServerManager = {
createRoomServer(room) {
let availPorts = possiblePorts.filter(
port => !roomServers.find(rs => rs.port === port)
);
if (availPorts.length === 0) {
log("Can't create room, no available ports");
return null;
}
let roomServer = {
spawned: false,
room: room,
port: availPorts[0],
timeoutHandle: null
};

roomServers.push(roomServer);

roomServer.timeoutHandle = setTimeout(() => {
log(`TIMEOUT: Room ${room.id} never launched after ${ASSIGN_TO_LAUNCH_TIMEOUT_SEC} seconds`);
RoomServerManager.removeRoomServer(roomServer);
}, ASSIGN_TO_LAUNCH_TIMEOUT_SEC * 1000);

return roomServer;
},

launchRoomServer(roomServer) {
if (roomServer.spawned) return false;

try {
let room = roomServer.room;

docker.command(`run -p ${roomServer.port}:${ids.ports.docker} -e SERVER_HOST=${ids.url} -e APP_ID=${ids.appId} -e SERVER_SECRET=${ids.serverSecret} -e SERVER_NAME=${ids.serverName} -e LOBBY_ID=${room.id} ${ids.dockerImage}`)
.then(() => {
log(`<-- ROOM TERMINATED: ${room.id}`);
RoomServerManager.removeRoomServer(roomServer);
})
.catch(error => {
log(`Error: ${error}`);
log(`<-- ROOM TERMINATED WITH ERROR: ${room.id}`);
RoomServerManager.removeRoomServer(roomServer);
});

roomServer.spawned = true;
if (roomServer.timeoutHandle) {
clearTimeout(roomServer.timeoutHandle);
roomServer.timeoutHandle = null;
}
} catch (e) {
log(`Failed to launch room ${room.id}, ${e}`);
RoomServerManager.removeRoomServer(roomServer);
return false;
}

return true;
},

getRoomServer(roomId) {
return roomServers.find(rs => rs.room.id === roomId);
},

removeRoomServer(roomServerToRemove) {
roomServers = roomServers.filter(rs => rs !== roomServerToRemove);
}
};

// === HTTP Server ===
function readPOSTData(request, callback) {
let body = "";
request.on('data', data => {
body += data;
if (body.length > 1e6) request.connection.destroy();
});
request.on('end', () => {
log(" headers: " + JSON.stringify(request.headers));
log(" body: " + body);
callback(body);
});
}

function cancelRoom(roomId, msg) {
brainclouds2s.request(globals.s2sContext, {
service: "lobby",
operation: "SYS_ROOM_CANCELLED",
data: {
lobbyId: roomId,
msg: msg,
details: {}
}
});
}

function respond(res, code, msg) {
res.writeHead(code, { 'Content-Type': 'text/plain' });
res.write(msg);
res.end();
}

function onRequestRoomServer(req, res) {
log("Incoming /requestRoomServer:");
readPOSTData(req, data => {
let room = JSON.parse(data);
let roomServer = RoomServerManager.createRoomServer(room);
if (!roomServer) {
respond(res, 400, "bad request, failed to create room server");
return;
}

if (RoomServerManager.launchRoomServer(roomServer)) {
respond(res, 200, JSON.stringify({
connectInfo: {
address: globals.internalIP,
ports: {
"7777/tcp": roomServer.port
}
}
}));
} else {
respond(res, 500, "Failed to launch room server");
}
});
}

function startHttpServer() {
const app = express();
// this is the only endpoint brainCloud will send request to
app.post('/requestRoomServer', onRequestRoomServer);

const port = ids.ports.http;
app.listen(port);
log(`HTTP server listening on port ${port}`);
}

// === Get Local IP Address ===
function getLocalIPAddress() {
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
if (iface.family === 'IPv4' && !iface.internal) {
return iface.address;
}
}
}
return '127.0.0.1';
}

// === Main ===
globals.s2sContext = brainclouds2s.init(
ids.appId,
ids.serverName,
ids.serverSecret,
ids.url
);
brainclouds2s.setLogEnabled(globals.s2sContext, true);

globals.publicIP = await publicIpv4();
log("Public IP: " + globals.publicIP);
globals.internalIP = getLocalIPAddress();
log("Internal IP: " + globals.internalIP);
startHttpServer();

Initialize it as an ES module using npm.

npm init

Install these dependencies.

npm install brainclouds2s docker-cli-js public-ip express

Step 3: Initiate a test run

Before testing your RSM, make sure the Docker daemon is running on the same machine as your RSM and that the required Docker image is already available locally.

Note: the image for this example will be the one we built from the "Custom Multiplayer Server with Unity" tutorial.

Run the RSM server file rsm.js from your terminal.

node rsm.js

Next, run the client app from the Unity Editor.

If everything is set up correctly, your RSM (listening on port 3000) should start logging activity to the console and launching the room server via the local Docker container in response to a brainCloud request.

You can monitor the requests from brainCloud to your RSM on your local machine.

Below is an example of a typical post request payload from brainCloud to RSM when a matchmaking is completed.

{
"id": "13469:CustomGame:68",
"lobbyType": "CustomGame",
"state": "starting",
"rating": 0,
"ownerCxId": "13469:48844593-6e01-464f-bc13-565ee536a46b:nfe42u48a7d6ogk7a0i2lsn6b8",
"lobbyTypeDef": {
"lobbyTypeId": "CustomGame",
"teams": {
"all": {
"minUsers": 1,
"maxUsers": 8,
"autoAssign": true,
"code": "all"
}
},
"rules": {
"allowEarlyStartWithoutMax": false,
"forceOnTimeStartWithoutReady": true,
"allowJoinInProgress": false,
"onTimeStartSecs": 1,
"disbandOnStart": true,
"everyReadyMinPercent": 50,
"everyReadyMinNum": 1,
"earliestStartSecs": 1,
"tooLateSecs": 300
},
"desc": "for example -- custom multiplayer server with unity"
},
"settings": {},
"version": 1,
"connectData": {},
"timetable": {
"createdAt": 1748055133044,
"early": 1748055134044,
"onTime": 1748055134044,
"tooLate": 1748055433044,
"dropDead": 1748056033044,
"ignoreDropDeadUntil": 0
},
"cRegions": [],
"round": 1,
"isRoomReady": false,
"keepAliveRateSeconds": 0,
"isAvailable": true,
"shardId": 0,
"legacyLobbyOwnerEnabled": true,
"owner": "48844593-6e01-464f-bc13-565ee536a46b",
"numMembers": 1,
"members": [
{
"profileId": "48844593-6e01-464f-bc13-565ee536a46b",
"name": "",
"pic": "",
"rating": 0,
"team": "all",
"isReady": true,
"extra": {},
"passcode": "b15f65",
"ipAddress": "174.116.76.186",
"cxId": "13469:48844593-6e01-464f-bc13-565ee536a46b:nfe42u48a7d6ogk7a0i2lsn6b8"
}
]
}

Your RSM server endpoint needs to provide the connectInfo data to brainCloud, which will then relay it to the lobby members. For example, address, ports like below.

Did this answer your question?