Field Notes
Roblox2023-11-19

Cross Server Network Matchmaking - Roblox Tutorial

A unified tutorial on locating and displaying a server's region using a RESTful API. Posted on the DevForum in November 2023.

TutorialWoodxCross-server

Many experiences/games have a cross-server matchmaking system based on game modes or simply matchmaking across servers.

You need to know these before we start

  1. Data store, key, and value in data.
  2. Knowing what a proximity prompt is and remote events/functions.
  3. Client- and server-side differential replication.

Let’s begin!

Step 1

Make your GUI and the proximity prompt or click detector based on how you want players to join the queue.

Step 2

Alright, since you have done your front-end work or GUI and trigger action It’s time to make them functional.

Now, we should tell the client to show the GUI on top when a player joins the queue. How can we do this? We use a remote event and communicate between the server and client; it’s that easy!

Now add a remote event, name it whatever you want, and add a script in ServerScriptService

Step 3

Now fire the remote and receive the remote from the client Add a script in the place where you are triggering the action (ProximityPrompt or ClickDetector, etc).

If you used a ClickDetector:

local clickdetector = script.Parent -- make sure you keep this correct
clickdetector.MouseClick:Connect(function(pl)
	game.ReplicatedStorage.ConfirmQueueEvent:FireClient(plr,true) -- your firing to the player, and here true means your queue is confirmed; it will tell the client that the queue is confirmed
end)

If you used ProximityPrompt:

local proxprompt = script.Parent
proxprompt.Triggered:Connect(function(plr)
	game.ReplicatedStorage.ConfirmQueueEvent:FireClient(plr,true) -- your firing to the player, and here true means your says confirm queue is true, it means it will tell the client that queue is confirmed
end)

Now you are firing successfully when the player triggers the function As that's done, we make the GUI appear when the event is received by the client.

game.ReplicatedStorage.ConfirmQueueEvent.OnClientEvent:Connect(function(confirmed)
	if confirmed == true then
		script.Parent.Frame.Visible = true
	else
		script.Parent.Frame.Visible = false
	end
end)

⚠️ In case the queue got cancelled due to an error or any reason in the main script later, we can fire this event again with false so the GUI will be hidden.

Here is the boolean we fired along with the event before; check it out.

  1. Change the Visible property of your GUI to false.
  2. Run it and see it work in action for now.

Congrats, you finished about 50% of this tutorial successfully.

Step 4

Now comes the main part; you should make it work right, shouldn’t you? So the player should join the queue when the event is triggered; along with showing the UI, it should add you to the queue.

Add a bindable event in the replicated storage and name it whatever you want, as usual.

Always remember we can fire any data while firing a remote event/function or bindable event/function; it can be a 2-letter word or even a big dictionary of data Now we will tell the main script, which handles the matchmaking process, to add the player

Add this 1 line below or above the function where you fire the ConfirmQueueEvent to the player.

game.ReplicatedStorage.JoinQueueEvent:Fire(plr,"GameMode1")

Now you can receive it from the main script.

Step 5

Making the core script for this matchmaking process We start by defining some variables for the script

local mss = game:GetService("MemoryStoreService") local ms1 = mss:GetSortedMap("GameMode1") local serverid = game.JobId Workaround (How does this process work?) Now imagine it as a clothing rack. When you put new clothing on, you keep it at the front. When you add another piece, it moves to the front too, and the old clothing moves back and forth, like stacking one piece of bread on top of another.

Similarly, you add a value to this MemoryStoreService and then it will be at first, and slowly it will you will be adding more data and its position in the queue will go back, so what we do is, we read the players in the queue from the back, it means oldest player is first to get in the queue, so this will avoid a lot of time wasting for the person, and imagine a lot of players play your game, and if you do last player is first to get in queue, imagine some player missed out, and new players keep on coming, and until the new players joining queue stops that player won’t get the queue, so to avoid this chaos we do first player is first in the queue, aka descending order based system.

Now, when you add players to the queue, confirm that the queue should teleport them to the round or another place/game. You cannot do it from every server; imagine there are 100 servers, and the queue gets confirmed on 100 servers. Only 1 server will create a new server instance, and the rest will teleport the players, if any, in their local server.

In other words, only one server will manage this queue confirmation process. For example, if each host managed each mode, this would avoid a lot of work on a single server and provide an organized process.

Also, one thing is, the player can join the queue only once, so we keep an attribute such as IsInQueue for the player if he/she joins the queue, and before adding a player to the queue, we check if they are in the queue already by seeing if they have this attribute simply.

Now we add 2 more variables and a function:

local mss = game:GetService("MemoryStoreService")
local ms1 = mss:GetSortedMap("GameMode1")
local serverid = game.JobId
local isserverhost = false
local dss = game:GetService("DataStoreService")
local ds1 = dss:GetDataStore("HostServerData")

local function addplrtoqueue(plr,mode)
	if plr:GetAttribute("IsInQueue") ~= nil then
		return
	end
	plr:SetAttribute("IsInQueue")
	if mode == "GameMode1" then
		ms1:SetAsync(tostring(plr.UserId),game.JobId)
	end
end

game.ReplicatedStorage.JoinQueueEvent.Event:Connect(function(plr,gamemode)
	addplrtoqueue(plr,gamemode)
end)

We're gonna use UpdateAsync instead of SetAsync for being careful here; you might as well use SetAsync only.

Run this in the command bar:

game:GetService("DataStoreService"):GetDataStore("HostServerData"):SetAsync("HostData",{})

⚠️Note that if you have a different name for your DataStore, replace HostServerData with that name.

local mss = game:GetService("MemoryStoreService")
local ms1 = mss:GetSortedMap("GameMode1")
local ms = game:GetService("MessagingService")
local serverid = game.JobId
local isserverhost = false
local dss = game:GetService("DataStoreService")
local serverhostforwhatmode = ""
local ds1 = dss:GetDataStore("HostServerData")
local numberofplayersperround = 5 -- keep it as any number you want
local tps = game:GetService("TeleportService")

-- if you're teleporting a player to that place if they get confirmed
local placeid = 000 --

local dataretrived = ds1:GetAsync("HostData")

local function checkforconfirmedqueue(gamemode)
	if isserverhost == true then
		local playerlist = ms1:GetRangeAsync(Enum.SortDirection.Descending,5)
		if #playerlist == numberofplayersperround then
			local createdplaceid = tps:ReserveServer(placeid)
			local missingplayertable = {}
			local playerstobeteleported = {}
			for i,v in pairs(playerlist) do
				if game.Players:FindFirstChild(v) then
					table.insert(playerstobeteleported,game.Players:FindFirstChild(v))
				else
					table.insert(missingplayertable,v)
				end
			end
			tps:TeleportToPrivateServer(placeid,createdplaceid,playerstobeteleported)
			if #playerstobeteleported < numberofplayersperround then
				ms:PublishAsync("teleportplayer",{["ServerId"] = createdplaceid,["Players"] = missingplayertable})
			end
		end
	else
		if dataretrived[gamemode] == nil then
			isserverhost = true
			dataretrived[gamemode] = serverid
                        ds1:UpdateAsync("HostData",dataretrived)
			ms:PublishAsync("updatedata",dataretrived)
		end
	end
end

ms:SubscribeAsync("updatedata",function(message)
	dataretrived = message.Data or ds1:GetAsync("HostData")
        -- update the host data of servers when host is changed
end)

ms:SubscribeAsync("teleportplayer",function(message)
	local playerlist = message.Data["Players"]
	local serverid = message.Data["ServerId"]
	for i,v in pairs(playerlist) do
		if game.Players:FindFirstChild(v) then
                        -- if that player is in the game, then teleport him to the round
			tps:TeleportToPrivateServer(placeid,serverid,game.Players:FindFirstChild(v))
		end
	end
end)

local function addplrtoqueue(plr,mode)
	if plr:GetAttribute("IsInQueue") ~= nil then
		return
	end
	plr:SetAttribute("IsInQueue")
	if mode == "GameMode1" then
		ms1:SetAsync(plr.Name,game.JobId)
	end
        checkforconfirmedqueue(mode)
end

game.ReplicatedStorage.JoinQueueEvent.Event:Connect(function(plr,gamemode)
	addplrtoqueue(plr,gamemode)
end)
-- removing the server as host when server is closing
game.Players.PlayerRemoving:Connect(function(plr)
	if isserverhost == true and #game.Players:GetPlayers() <= 1 then
		dataretrived[serverhostforwhatmode] = nil
		ms:PublishAsync(dataretrived)
	end
        -- if player is in queue, remove him from the queue
        if plr:GetAttribute("IsInQueue") == true then
		ms1:RemoveAsync(plr.Name)
	end
end)

The script does the same in logical instructions.

You can always edit your script and match it to your game concept and system, and this is only a workaround for making a matchmaking system.