How to make a Server Browser/List

This tutorial covers on how to make a Public & Private Server Browser, meaning:

  • Public Server Browser: Players can view all public servers and choose which one to join
  • Private Server Browser: Players can create & join private servers, viewable by everyone
    This tutorial only covers the basics, more advanced features for private servers such as invite-only, ban list, creator control over the server, etc have to be made by you.

NOTE: This is ONLY a tutorial, you may use pieces of the tutorial’s code, however some parts are in blank for you to fill in using your game’s systems or systems you have to create yourself (such as notifying a user when a problem occurs and adding a filter to server name)

There may be mistakes in the code or hard-to-read sections, if you stumble upon one of them, please comment in this post
This tutorial uses @prooheckcp’s CountryService module to get the flag decal of certain countries


Making a Public Server Browser

Setup

For this tutorial, this will be the UI (you can make your own), which should be enough:



Next, put @prooheckcp’s CountryService module somewhere, in this tutorial I will place it in ReplicatedStorage
image
At last, add 1 Remote Event and 1 Remote Function to ReplicatedStorage, you can name them whatever you want or place them inside folders if you want.
image

Server Script

Now we get to the Server Script, this will handle the server’s communication to the client and also add the current server to the server list (if it’s a public server). To start, add the variables:

--Variables
local MemoryStoreService = game:GetService("MemoryStoreService")
local Players = game:GetService("Players")
local TeleportService = game:GetService("TeleportService")
local Remote = game.ReplicatedStorage.RemoteFunction --Make sure this is the one in your game!
local Event = game.ReplicatedStorage.RemoteEvent --Make sure this is the one in your game!
local SortedMap = MemoryStoreService:GetSortedMap("PublicServerList")

local PlayerCooldowns = {}

We will also need this helper function:

function RetryRequest(Object:Instance, Function:string, ...)
	local MaxRetries = 5 --Change this to whatever you prefer
	local Cooldown = 1
	local Tries = 0

	local ErrorMessage = "Unknown Error"

	while Tries <= MaxRetries do
		Tries += 1
		local Result = {pcall(Object[Function], Object, ...)}
		if Result[1] then
			--Request resulted in success!
			return unpack(Result)
		else
			ErrorMessage = Result[2] or ErrorMessage
		end
		task.wait(Cooldown)
	end

	return false, ErrorMessage
end

Now we start with the server logic, we need to store a variable ‘ServerList’ for later use, while updating it every once in a while, and when we update it, we also let all the clients know:

local ServerList = {}
task.spawn(function()
	while true do
		local Success, NewServerList = RetryRequest(SortedMap, "GetRangeAsync", Enum.SortDirection.Ascending, 50)
		if Success then
			ServerList = NewServerList
			Event:FireAllClients("NewServerList", NewServerList)
		else
			warn("Failed to get server list: "..NewServerList)
		end
		task.wait(10) --Update the server list every 10 seconds
	end
end)

Next we need to handle the client to server communication, this will handle requests when the client wants to get the server list, quick join and join a specific server, while also applying rate-limits for safety:

Remote.OnServerInvoke = function(Player:Player, Action, Data)

	local UserId = Player.UserId

	--Rate-limit the player to only allow requests every 5 seconds
	local LastRequest = PlayerCooldowns[UserId] or 0
	local Now = tick()
	if Now - LastRequest < 5 then
		return false, "Rate-limited"
	end
	PlayerCooldowns[UserId] = Now

	if Action == "GetServerList" then
		return true, ServerList

	elseif Action == "JoinServer" then

		local JobId = Data

		--Verify if the server is in the server list
		for _, Server in ServerList do
			if Server.key == JobId then

				--Server is indeed in the list!
				local Options = Instance.new("TeleportOptions")
				Options.ServerInstanceId = JobId

				RetryRequest(TeleportService, "TeleportAsync", game.PlaceId, {Player}, Options)

				return true
			end
		end

		return false, "Server not found"

	elseif Action == "QuickJoin" then
		
		RetryRequest(TeleportService, "TeleportAsync", game.PlaceId, {Player})
		return true

	end

end

We are almost done! Now, if the current server is a public server, we need to add it to the server list. So let’s start with this check:

if game.PrivateServerId ~= "" then
	return --End script here if it's a private/VIP server
end

Then let’s get the country and region of the server, we will use this later to let players know where each server is located:

--Get the server location
local Country = game.LocalizationService.SystemLocaleId:sub(4,5):upper() or "??"
local Region = "Unknown"
local City = "Unknown"
local HTTP = game:GetService("HttpService")

local Success, Body = RetryRequest(HTTP, "GetAsync", "http://ip-api.com/json/")
if Success then
	Body = HTTP:JSONDecode(Body)
	Country = Body.countryCode or Country
	Region = Body.regionName or Region
	City = Body.city or City
end
--If HTTPService is disabled or the HTTP request fails, Region and City will default to "Unknown"
```
Then let's set the JobId variable:
```
local JobId = game.JobId
if game:GetService("RunService"):IsStudio() then
	JobId = tostring(math.random(1000,100000000))
	--Studio doesn't have JobId
end
```
Now that we have everything necessary, we need to make the function that adds the serverdata (Location data, PlayerList, etc) to the serverlist in MemoryStorageService:
```
--Function to add the server to the server list
local function Update ()

	local PlayerList = {}

	for _, Player in Players:GetPlayers() do
		table.insert(PlayerList, Player.UserId)
	end

	local Success, Message = RetryRequest(SortedMap, "SetAsync", JobId, {
		Country = Country,
		Region = Region,
		City = City,
		Age = time(),
		Players = PlayerList
	}, 20)
	if not Success then
		warn("Failed to update server: "..Message)
	end

	return Success
end
```
Everytime we run the function Update(), the current server is added/updated to the serverlist for 20 more seconds, next we need to add the loop that is actually gonna keep updating the server so that it doesn't expire and disappear from the serverlist, then when the server shuts down we remove it from the server list so that it doesn't stay there as a ghost server for 20 seconds:
```
local Closing = false

--Loop to update the server constantly
task.spawn(function()
	while not Closing do
		Update()
		task.wait(10)
	end
end)

--When the server shuts down...
game:BindToClose(function()
	Closing = true
	RetryRequest(SortedMap, "RemoveAsync", JobId) --Remove from server list
end)
```
The server script is finished! Next, we need to do the client-side logic
Local Script

This ClientScript will be placed in: UI (ScreenGui) → MainFrame → HERE
First set the variables:

--Variables
local Players = game:GetService("Players")
local Replicated = game.ReplicatedStorage
local CountryService = require(Replicated:WaitForChild("CountryService"))
local Remote = Replicated:WaitForChild("RemoteFunction")
local Event = Replicated:WaitForChild("RemoteEvent")

local MainFrame = script.Parent
local UI = MainFrame.Parent
local Close = MainFrame.Close
local QuickJoin = MainFrame.QuickJoin
local List = MainFrame.List
local Template = List.Template

Then make the code that destroys (or disables) the UI when the X button gets clicked:

--Close button
Close.MouseButton1Click:Connect(function()
	UI:Destroy()
end)

Then the code for the quick join button, quick joining will just throw the player into a random public server that roblox decides it’s best:

--Quick join button
QuickJoin.MouseButton1Click:Connect(function()
	local Success, Msg = Remote:InvokeServer("QuickJoin")
	if not Success then
		return warn("Client failed to quick join: "..Msg)
	end
end)

Now we will make a function that creates each server in the server list as an UI element every time it gets refreshed:

--Refresh the server list
local function Refresh(ServerList)
	--Get the server list from the server
	if not ServerList then
		local Success, Received = Remote:InvokeServer("GetServerList")
		if not Success then
			return warn("Client failed to get server list: "..Received)
		end
		ServerList = Received
	end
	
	--Delete existing items in the server list
	for _,Object in List:GetChildren() do
		if Object:IsA("Frame") and Object.Name ~= "Template" then
			Object:Destroy()
		end
	end
	
	--Add the new servers in the server list
	for _, Item in ServerList do
		local JobId = Item.key
		local ServerData = Item.value
		
		local Frame = Template:Clone()
		Frame.Name = JobId
		
		--Get country image
		local CountryData = CountryService:GetCountryByCode(ServerData.Country)
		if CountryData then
			Frame.Country.Image = CountryData.Decal or ""
		end
		
		Frame.Location.Text = ServerData.Region..", "..ServerData.City
		
		--Create the player icons
		for i, UserId in ServerData.Players do
			local ImgLabel = Frame.PlayerList.Template:Clone()
			ImgLabel.Image = Players:GetUserThumbnailAsync(UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size48x48)
			ImgLabel.Visible = true
			ImgLabel.Name = UserId
			ImgLabel.Parent = Frame.PlayerList
			
			--Playerlist frame can only fit 8 items, if this is the seventh player then add a +n:
			if i == 7 and #ServerData.Players > 7 then
				local More = Frame.PlayerList.More:Clone()
				More.Text = "+"..(#ServerData.Players - 7)
				More.Parent = Frame.PlayerList
				More.Visible = true
			end
			
		end
		
		--Joining the server
		Frame.Join.MouseButton1Click:Connect(function()
			local Success, Msg = Remote:InvokeServer("JoinServer", JobId)
			if not Success then
				warn("Client failed to join server: "..Msg)
			end
		end)
		
		Frame.Visible = true
		Frame.Parent = List
	end
	
	return true
	
end

After that function, call it so that the serverlist is already made the moment the script runs:

Refresh()

Lastly, whenever we receive a new server list from the server, we need to refresh it in the client’s UI again:

--Event received from the server...
Event.OnClientEvent:Connect(function(Action, Data)
	if Action == "NewServerList" then
		Refresh(Data)
	end
end)

Place File for the Public Server Browser: PublicServerBrowser.rbxl (101.7 KB)


Making a Private Server Browser

Note: Since this is just a tutorial, it doesn’t have systems such as setting your private server to Invite-Only, banning players from your server and etc. You have to make those yourself using your game’s existing systems or a new system

Setup

For this tutorial, this will be the UI (you can make your own), which should be enough:



At last, add 1 Remote Event and 1 Remote Function to ReplicatedStorage, you can name them whatever you want or place them inside folders if you want.
image

Server Script

Now we get to the Server Script, this will handle the server’s communication to the client and also add the current server to the server list (if it’s a valid private server). To start, add the variables:

--Variables
local MemoryStoreService = game:GetService("MemoryStoreService")
local Players = game:GetService("Players")
local TeleportService = game:GetService("TeleportService")
local Remote = game.ReplicatedStorage.RemoteFunction
local Event = game.ReplicatedStorage.RemoteEvent
local SortedMap = MemoryStoreService:GetSortedMap("PrivateServerList")

local PlayerCooldowns = {}

We will also need this helper function:

function RetryRequest(Object:Instance, Function:string, ...)
	local MaxRetries = 5 --Change this to whatever you prefer
	local Cooldown = 1
	local Tries = 0

	local ErrorMessage = "Unknown Error"

	while Tries <= MaxRetries do
		Tries += 1
		local Result = {pcall(Object[Function], Object, ...)}
		if Result[1] then
			--Request resulted in success!
			return unpack(Result)
		else
			ErrorMessage = Result[2] or ErrorMessage
		end
		task.wait(Cooldown)
	end

	return false, ErrorMessage
end

Now we start with the server logic, we need to store a variable ‘ServerList’ for later use, while updating it every once in a while, and when we update it, we also let all the clients know:

local ServerList = {}
task.spawn(function()
	while true do
		local Success, NewServerList = RetryRequest(SortedMap, "GetRangeAsync", Enum.SortDirection.Ascending, 50)
		if Success then
			ServerList = NewServerList
			Event:FireAllClients("NewServerList", NewServerList)
		else
			warn("Failed to get server list: "..NewServerList)
		end
		task.wait(10) --Update the server list every 10 seconds
	end
end)

Next we need to handle the client to server communication, this will handle requests when the client wants to get the server list, join a specific server and create a server, while also applying rate-limits for safety:

--Connection between the client and server
Remote.OnServerInvoke = function(Player:Player, Action, Data)

	local UserId = Player.UserId

	--Rate-limit the player to only allow requests every 5 seconds
	local LastRequest = PlayerCooldowns[UserId] or 0
	local Now = tick()
	if Now - LastRequest < 5 then
		return false, "Rate-limited"
	end
	PlayerCooldowns[UserId] = Now

	if Action == "GetServerList" then
		return true, ServerList

	elseif Action == "JoinServer" then

		local JobId = Data
		
		if game.JobId == JobId then
			return false, "You're already in this server!"
		end

		--Verify if the server is in the server list
		for _, Server in ServerList do
			if Server.key == JobId then

				--Server is indeed in the list!
				local Options = Instance.new("TeleportOptions")
				Options.ReservedServerAccessCode = Server.value.AccessCode
				Options:SetTeleportData({
					ServerName = Server.value.ServerName,
					AccessCode = Server.value.AccessCode,
					Creator = Server.value.Creator
				})

				RetryRequest(TeleportService, "TeleportAsync", game.PlaceId, {Player}, Options)

				return true
			end
		end

		return false, "Server not found"

	elseif Action == "CreateServer" then
		
		--Security checks
		if not Data or type(Data) ~= "string" then
			return false, "Invalid data"
		end
		if #Data == 0 then
			return false, "Name too short"
		end
		if #Data > 50 then
			return false, "Name too long"
		end
		--[[------WARNING----------
		This code has no filtering for the server name.
		This is only a tutorial so you must implement the filter yourself.
		---------------------------]]
		
		local Success, AccessCode = RetryRequest(TeleportService, "ReserveServer", game.PlaceId)
		if not Success then
			return false, AccessCode
		end
		
		local Options = Instance.new("TeleportOptions")
		Options.ReservedServerAccessCode = AccessCode
		Options:SetTeleportData({
			ServerName = Data,
			AccessCode = AccessCode,
			Creator = UserId
		})
		
		RetryRequest(TeleportService, "TeleportAsync", game.PlaceId, {Player}, Options)
		return true

	end

end

Great! Now before we add the code that adds the current server to the server list, we need to add a check to see if the server currently running has been created by a player:

if game.PrivateServerId == "" or game.PrivateServerOwnerId ~= 0 then
	return --Stop the script if we are in a public or VIP server
end

local Player = Players:FindFirstChildOfClass("Player") or Players.PlayerAdded:Wait()
local Data = Player:GetJoinData()
if not Data or not Data.TeleportData then
	return --Stop the script if the player didn't join through a teleport
end

local ServerName = Data.TeleportData.ServerName
local AccessCode = Data.TeleportData.AccessCode
local Creator = Data.TeleportData.Creator

if not AccessCode or not ServerName or not Creator then
	return --Stop the script if the teleport data is invalid
end

Now that we have added the check, we need to make the function that adds the current server’s data (Name, Creator, PlayerList, etc) to the serverlist in MemoryStorageService:

local JobId = game.JobId

--Function to add the server to the server list
local function Update ()

	local PlayerList = {}

	for _, Player in Players:GetPlayers() do
		table.insert(PlayerList, Player.UserId)
	end

	local Success, Message = RetryRequest(SortedMap, "SetAsync", JobId, {
		Age = time(),
		Players = PlayerList,
		ServerName = ServerName,
		AccessCode = AccessCode,
		Creator = Creator
	}, 20)
	if not Success then
		warn("Failed to update server: "..Message)
	end

	return Success
end

Everytime we run the function Update(), the current server is added/updated to the serverlist for 20 more seconds, next we need to add the loop that is actually gonna keep updating the server so that it doesn’t expire and disappear from the serverlist, then when the server shuts down we remove it from the server list so that it doesn’t stay there as a ghost server for 20 seconds:

local Closing = false

--Loop to update the server constantly
task.spawn(function()
	while not Closing do
		Update()
		task.wait(10)
	end
end)

--When the server shuts down...
game:BindToClose(function()
	Closing = true
	RetryRequest(SortedMap, "RemoveAsync", JobId) --Remove from server list
end)
Local Script

This ClientScript will be placed in: UI (ScreenGui) → MainFrame → HERE
First set the variables:

--Variables
local Players = game:GetService("Players")
local Replicated = game.ReplicatedStorage
local Remote = Replicated:WaitForChild("RemoteFunction")
local Event = Replicated:WaitForChild("RemoteEvent")

local MainFrame = script.Parent
local UI = MainFrame.Parent
local Close = MainFrame.Close
local Create = MainFrame.Create
local NameInput = MainFrame.TextBox
local List = MainFrame.List
local Template = List.Template

Then the code for the close button that destroys (or disables) the UI:

--Close button
Close.MouseButton1Click:Connect(function()
	UI:Destroy()
end)

Then the code for creating a server:

--Create button
Create.MouseButton1Click:Connect(function()
	local Success, Msg = Remote:InvokeServer("CreateServer", NameInput.Text)
	if not Success then
		return warn("Client failed to create server: "..Msg)
	end
end)

Then the code for limiting the inputted server name to 50 characters:

--Limit the name to 50 characters
NameInput:GetPropertyChangedSignal("Text"):Connect(function()
	NameInput.Text = NameInput.Text:sub(1, 50)
end)

Now we will make a function that creates each server in the server list as an UI element every time it gets refreshed:

--Refresh the server list
local function Refresh(ServerList)
	--Get the server list from the server
	if not ServerList then
		local Success, Received = Remote:InvokeServer("GetServerList")
		if not Success then
			return warn("Client failed to get server list: "..Received)
		end
		ServerList = Received
	end
	
	--Delete existing items in the server list
	for _,Object in List:GetChildren() do
		if Object:IsA("Frame") and Object.Name ~= "Template" then
			Object:Destroy()
		end
	end
	
	--Add the new servers in the server list
	for _, Item in ServerList do
		local JobId = Item.key
		local ServerData = Item.value
		
		local Frame = Template:Clone()
		Frame.Name = JobId
		
		Frame.Profile.Image = Players:GetUserThumbnailAsync(ServerData.Creator, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size60x60)
		Frame.Title.Text = ServerData.ServerName
		
		--Create the player icons
		for i, UserId in ServerData.Players do
			local ImgLabel = Frame.PlayerList.Template:Clone()
			ImgLabel.Image = Players:GetUserThumbnailAsync(UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size48x48)
			ImgLabel.Visible = true
			ImgLabel.Name = UserId
			ImgLabel.Parent = Frame.PlayerList
			
			--Playerlist frame can only fit 8 items, if this is the seventh player then add a +n:
			if i == 8 and #ServerData.Players > 8 then
				local More = Frame.PlayerList.More:Clone()
				More.Text = "+"..(#ServerData.Players - 8)
				More.Parent = Frame.PlayerList
				More.Visible = true
			end
			
		end
		
		--Joining the server
		Frame.Join.MouseButton1Click:Connect(function()
			local Success, Msg = Remote:InvokeServer("JoinServer", JobId)
			if not Success then
				warn("Client failed to join server: "..Msg)
			end
		end)
		
		Frame.Visible = true
		Frame.Parent = List
	end
	
	return true
	
end

After that function, call it so that the serverlist is already made the moment the script runs:

Refresh()

Lastly, whenever we receive a new server list from the server, we need to refresh it in the client’s UI again:

--Event received from the server...
Event.OnClientEvent:Connect(function(Action, Data)
	if Action == "NewServerList" then
		Refresh(Data)
	end
end)

Place File for the Private Server Browser:
PrivateServerBrowser.rbxl (98.1 KB)


Note: Since the Places above are empty baseplates, the client loads instantly before the server can add itself to the serverlist and show in the UI, that’s why it takes 10 seconds to show in the server list

22 Likes

If you have any questions or problems, don’t be afraid to say it here!

This is an excellent tutorial. It will really help me for my game. Thank you!

1 Like

Cela mérite un prix! Merci énormément pour avoir prit le temps d’écrire ce sujet.

2 Likes