[v3.4.1-beta] MatchmakingService

MatchmakingService

Current Version: V3.4.1-beta
Github. Asset. Uncopylocked hub/receiver game.

In case it wasn’t clear to everyone, which it obviously isn’t. This module is in beta and has no guarantee of working under all conditions. It is being actively developed there are DEFINITELY game breaking bugs and issues that have not been found.

Basically,

Use this module at your own risk until it’s out of beta

When it’s out of beta, the releases will be more stable.

PLEASE READ THE EDITS TO THE CURRENT SOLUTION BEFORE USING THIS IN PRODUCTION.

MatchmakingService is a way to easily make games that involve matchmaking. It utilizes the new MemoryStoreService for blazing fast execution speed. MatchmakingService is as easy to use as:

(On your hub server where players queue from)

-- Obtain the service
local MatchmakingService = require(7567983240).GetSingleton()

-- Set the game place
MatchmakingService:SetGamePlace(7584483307)

-- Queue players (you can call QueuePlayer from anywhere)
game.Players.PlayerAdded:Connect(function(p)
	MatchmakingService:QueuePlayer(p, "ranked")
end)

game.Players.PlayerRemoving:Connect(function(p)
	MatchmakingService:RemovePlayerFromQueue(p)
end)

for i, p in ipairs(game.Players:GetPlayers()) do
	MatchmakingService:QueuePlayer(p, "ranked")
end

On the game where players are teleported to:

local MatchmakingService = require(7567983240).GetSingleton()

-- Tell the service this is a game server
MatchmakingService:SetIsGameServer(true)

local t1 = {}
local t2 = {}
-- Basic start function
function Start()
	print("Started")
	MatchmakingService:StartGame(_G.gameId)
	-- Simple teams.
	local p = game.Players:GetPlayers()
	table.insert(t1, p[1])
	table.insert(t2, p[2])
end

-- YOU MUST CALL UpdateRatings BEFORE THE GAME IS CLOSED. YOU CANNOT PUT THIS IN BindToClose!
function EndGame(winner)
	MatchmakingService:UpdateRatings(t1, t2, _G.ratingType, winner)
	for i, v in ipairs(game.Players:GetPlayers()) do
		-- You can teleport them back to the hub here, I just kick them
		v:Kick()
	end
end

game.Players.PlayerAdded:Connect(function(player)
	local joinData = player:GetJoinData()
	if _G.gameId == nil and joinData then
		-- Global so its accessible from other scripts if it needs to be.
		_G.gameId = joinData.TeleportData.gameCode
		_G.ratingType = joinData.TeleportData.ratingType
	end
	if #game.Players:GetPlayers() >= 2 then
		Start()
	end
end)

game.Players.PlayerRemoving:Connect(function(player)
	MatchmakingService:RemovePlayerFromGame(player, _G.gameId)
end)

-- THIS IS EXTREMELY IMPORTANT
game:BindToClose(function()
	MatchmakingService:RemoveGame(_G.gameId)
end)

Small note before we start

Due to the lack of tools that MemoryQueue allows, current this version of MatchmakingService solely uses different SortedMaps which are slightly less tuned for this process. If/when MemoryQueue gets more tools that allow us to reliably manage it, then I will write this all utilizing MemoryQueues where possible.

How does it work?

MatchmakingService utilizes the new MemoryStoreService for cross-game ephemeral memory storage. This storage is kind of like the RAM in your computer or phone, except it’s not exactly the same. There’s a great article on it here which describes more of the technical details if you’re interested in it. MemoryStores have a much higher throughput potential than other services which makes them great for queuing items that will be quickly removed or changed. It has overwrite protections as well! Basically MemoryStores are the best way to make a system like this (it will be even better when they give us more tools to manage a MemoryQueue which will speed this up even more).

Basically, however, one server will manage making matches across the entire queue. Think of this server as the centralized handler (I was thinking of ways to have this be completely random access, but it gets a little messy, though has potential to happen in the future). When a player queues for a specific skill level they will only be matched up against a certain number of people in the same skill level. The number that are put into the game depends on what you set it to (read the docs below!).

Users are in a first-in-first-out queue. This means that the first players to queue are the first players to get into a game. When a game is created it can either be joinable or not. By default it is still joinable if the game hasn’t started and the server isn’t full, the MatchmakingService will prioritize these existing games before trying to make new ones. When a game starts, by default, the server will be locked and no new joiners are permitted.

Documentation

Using the MatchmakingService is easy, the source itself has documentation on everything you’d need, however it will be written here as well.

Correctly obtaining the MatchmakingService

MatchmakingService provides a top-level singleton to avoid accidentally creating multiple MatchmakingServices in one game server. I recommend requiring it from the asset id like so:

local MatchmakingService = require(7567983240).GetSingleton()

This makes it easy to stay up to date, but it isn’t necessary.

Changing settings

You can choose to change the properties themselves, or you can use the setters. It doesn’t matter which you use, but the setters are documented as follows:

Setting the update interval

By default, MatchmakingService will try to find matches or teleport players to their matches every 3 seconds. You can change this for any number of reasons like performance, but I recommend the 1-3 second range. Changing it is simple:

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:SetMatchmakingInterval(1) -- Sets the update interval to 1 second

Setting the player range

The player range is the number min and max players allowed in a server. It uses roblox’s NumberRange to achive this easier. The default minimum players is 6 and the default maximum is 10. The max cannot be above 100 players. Changing it requires making a new NumberRange which isn’t hard to do:

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:SetPlayerRange(NumberRange.new(1, 10)) -- Sets the minimum players to 1 and the maximum players to 10.

Setting the game place

Setting the game place is necessary as it tells the MatchmakingService what place it should teleport players to when a game is found. These servers are private and not joinable when created through MatchmakingService unless you use the TeleportService, which MatchmakingService does internally. You must set this to a place that’s in the same universe as the place where players are teleported from. Here’s an example:

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:SetGamePlace(7563846691) -- Sets the game place to 7563846691.

Denoting a server as a game server

You will need to require the MatchmakingService in your game servers as well. You don’t want your game servers running the matchmaking loop unnecessarily so denoting them as a game server will prevent the matchmaking from running. This is a simple boolean value and defaults to false:

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:SetIsGameServer(true) -- Denotes this server as a game server.

Setting the starting rating

As of v2.0.0-beta, MatchmakingService uses an implementation of glicko-2 for rating purposes. You can set the starting rating like so (default is 0, negative rating does exist):

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:SetStartingRating(1000) -- Sets the starting rating to 1000.

Two additional glicko-2 initalizers

I won’t go into detail what these two initializers do as you shouldn’t modify them unless you know how glicko-2 works and want to set up very specific starting conditions.

The first one is starting rating deviation:

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:SetStartingDeviation(0.08314) -- Sets the starting deviation to 0.08314.

The next is volatility:

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:SetStartingVolatility(0.6) -- Sets the starting volatility to 0.6.

I do not recommend changing them from their default values.

Setting the max skill disparity in parties

By default all party members must be within 50 rating points of all other party members. You may want to change this if you want parties to be of more or less similar skill.

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:SetMaxPartySkillGap(100) -- Sets the max skill disparity of parties to 100 rating points.

Getting/Setting ratings

While I do not recommend ever setting ratings directly, you may want to get ratings for any number of reasons. MatchmakingService exposes both operations.

How glicko-2 objects work

A glicko-2 object is guaranteed to have these three properies: Rating, RD (Rating deviation), and Vol (Volatility). The main thing you would ever access is the Rating property. This object may also contain a Score property, but that has to do with how ratings are updated and it’s unlikely you will ever see it.

Getting a glicko-2 object

One of a player’s glicko-2 object(s) can be retrieved like so (if it does not exist it will be created and defaulted):

local MatchmakingService = require(7567983240).GetSingleton()
local g = MatchmakingService:GetPlayerGlicko(player, "ranked") -- Gets the player's ranked glicko-2 object.
print(g.Rating)

You can access the properties described above when you retrieve the object. Changing these values will have no effect.

Setting a player’s glicko-2 object

As I stated above I do not recommend using this, but if you want to set specific ratings the option is available for you. This must be a glicko-2 object.

local MatchmakingService = require(7567983240).GetSingleton()
 MatchmakingService:SetPlayerGlicko(player, "ranked", glicko2Object) -- Sets the player's ranked glicko-2 object.

Updating a player’s rating after a game

Currently MatchmakingService supports games that have 2 teams with any number of players on each team. As described in the example handler script, you must update the players’ ratings before the game is closed. It must execute before BindToClose, this means that it cannot be put in BindToClose. A simple way to handle this is make a basic game ender:

-- YOU MUST CALL UpdateRatings BEFORE THE GAME IS CLOSED. YOU CANNOT PUT THIS IN BindToClose!
function EndGame()
	MatchmakingService:UpdateRatings(t1, t2, _G.ratingType, winner)
	for i, v in ipairs(game.Players:GetPlayers()) do
		-- You can teleport them back to the hub here, I just kick them
		v:Kick()
	end
end

To break this down I will go over the parameters of this method:
MatchmakingService:UpdateRatings(t1, t2, ratingType, winner) takes the two teams (t1 and t2), these are tables of players. Following the second team is the rating type, this is passed in the teleport data of players, in the example script it’s set to _G.ratingType. Finally we have the winner. This is either 0, 1, or 2. If the game is a draw, the value should be 0, if team one won then you should pass 1. And obviously if team 2 won you pass 2.

This method updates, and then saves player ratings for that specific rating type.

Getting player info

Getting a player’s info can be useful mainly to check if they’re in a party.

local MatchmakingService = require(7567983240).GetSingleton()
local info = MatchmakingService:GetPlayerInfo(player)

The info returned is a dictionary (it may be nil). The main thing you’ll use this for is parties. If the player is in a party, then info.party will be a table of all of the player ids of the players in their party including the players own id.

Getting a player’s party

If a player is partied, you can get all the players in their party like so:

local MatchmakingService = require(7567983240).GetSingleton()
local party = MatchmakingService:GetPlayerParty(player)

If they aren’t in a party, this will return nil. If they are in a party, this will return a table of all of the player ids of the players in their party including their own id.

Managing the queue

Most of the functions of MatchmakingService are for internal use, but are exposed if you want to directly manage the queue yourself. All of the methods shown here have an equivalent id variant that accepts player ids instead of player objects. These exist for convenience. For example QueuePlayer is the same as QueuePlayerId, except QueuePlayer takes a player and QueuePlayerId takes a user id.

Enough with that though here’s all the methods that MatchmakingService provides to manage your queue.

Getting a specific queue

Queue might be important to you for any number of reasons. MatchmakingService exposes a helpful method to get the queue of a specific rating type:

local MatchmakingService = require(7567983240).GetSingleton()
local queues = MatchmakingService:GetQueue(ratingType)

queues is a dictionary that would look like this:

{
	[poolOne] = {userId, userId2, userId3, ...};
	[poolTwo] = {userId, userId2, userId3, ...};
	...;
}

The pool is a rounded rating. It’s internally stored as a string, so if you want to perform mathematical expressions on it make sure you convert it to a number. So if you want to get the queue of a specific rating in a specific pool, say 500 rating: MatchmakingService:GetQueue(ratingType)["500"] would get you that queue.

Getting queue counts

You may want to get a count of players in the queue. This can be accomplished like so:

local MatchmakingService = require(7567983240).GetSingleton()
local perRating, totalCount = MatchmakingService:GetQueueCounts()

perRating is a table of {ratingType=count} and totalCount is the sum of those.

Listening to when a player is added to or removed from the queue

You may want to listen to exactly when a player is added to or removed from a queue. This is more performant than running GetQueueCounts because it does not make any api calls. MatchmakingService uses a custom signal class to achive this with very little overhead:

local MatchmakingService = require(7567983240).GetSingleton()

MatchmakingService.PlayerAddedToQueue:Connect(function(plr, glicko, ratingType, skillPool)
	print(plr, glicko, ratingType, skillPool)
end)

MatchmakingService.PlayerRemovedFromQueue:Connect(function(plr, ratingType, skillPool)
	print(plr, ratingType, skillPool)
end)

In PlayerAddedToQueue, the player’s user id is passed along with their glicko object, the rating type their queued for and the skill pool they were put in (which is their rating rounded to the nearest 10).

PlayerRemovedFromQueue is similar but it does not pass their glicko object. If you need it you can still use GetPlayerGlicko.

Adding a player to the queue

Queuing a player is simple:

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:QueuePlayer(player, "ranked") -- Queues the player in the ranked queue pool. Ranked can be any string you want. Think of them as different game modes or queue types. For example in League of Legends you have blind, draft, and ranked. All 3 of these game types use a separate rating behind the scenes.

For now, a rating type must be provided, but in future versions the default will be “none” which will have no rating nor will it have skill-based match making.

Adding a party to the queue

Parties can be added to the queue and are ensured that they all get into the same game when a game is found. This can also be useful for forcing teams. As of v2.2.0-beta, there is no party parity which means there may be parties matched against all solo players. I may add this in the future if it doesn’t prove to be too complex.

You can add a party to the queue like so:

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:QueueParty(players, ratingType)

You can use RemovePlayersFromQueueId(GetPlayerParty(player)) to dequeue a party.

Removing a player from queue

Removing a player is also incredibly simple:

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:RemovePlayerFromQueue(player) -- Removes the player from the queue.

Removing multiple players from the queue

If you handle your own matchmaking you may want to remove multiple users from the queue at the same time which is more efficient than running 10 update operations. This method does that for you and takes a table of players:

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:RemovePlayersFromQueue(players) -- Removes the players from the queue.

Adding a player to a game

You may want to add a player to a game if you’re doing your own matchmaking. A game id is a reserved teleport code to teleport a user to a reserved server. This is unique at all times. The third parameter (defaults to true) will tell the MatchmakingService to update the servers joinability (meaning if it’s full then close the server).

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:AddPlayerToGame(player, gameId, true) -- Adds the player to the game and updates its joinability status.

Adding multiple players to a game

You can add multiple players to a game in one update operation as well. It takes a table of players, like RemovePlayersFromQueue.

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:AddPlayersToGame(players, gameId, true) -- Adds the players to the game and updates its joinability status.

Removing a player from an existing game

If a player disconnects mid game you can remove them from it very simply:

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:RemovePlayerFromGame(player, gameId, true) -- Removes the player from the game and updates its joinability status.

Removing multiple players from an existing game

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:RemovePlayersFromGame(players, gameId, true) -- Removes the players from the game and updates its joinability status.

Starting a game and removing a game from memory

You should denote a game as started when it actually started. Removing a game from memory after it’s closed is very important and is incredibly simple to do.

You can choose whether or not the game is joinable after starting (default false) with the second parameter in StartGame:

local MatchmakingService = require(7567983240).GetSingleton()
MatchmakingService:StartGame(gameId, false) -- Starts the game and locks players from joining.

You can remove a game easily as well when the game server closes:

game:BindToClose(function()
  MatchmakingService:RemoveGame(gameId)
end)

In your server that players are teleported to I recommend using this script at a minimum, but feel free to add to it:

local MatchmakingService = require(7567983240).GetSingleton()

-- Tell the service this is a game server
MatchmakingService:SetIsGameServer(true)

local t1 = {}
local t2 = {}
-- Basic start function
function Start()
	print("Started")
	MatchmakingService:StartGame(_G.gameId)
	-- Simple teams.
	local p = game.Players:GetPlayers()
	table.insert(t1, p[1])
	table.insert(t2, p[2])
end

-- YOU MUST CALL UpdateRatings BEFORE THE GAME IS CLOSED. YOU CANNOT PUT THIS IN BindToClose!
function EndGame(winner)
	MatchmakingService:UpdateRatings(t1, t2, _G.ratingType, winner)
	for i, v in ipairs(game.Players:GetPlayers()) do
		-- You can teleport them back to the hub here, I just kick them
		v:Kick()
	end
end

game.Players.PlayerAdded:Connect(function(player)
	local joinData = player:GetJoinData()
	if _G.gameId == nil and joinData then
		-- Global so its accessible from other scripts if it needs to be.
		_G.gameId = joinData.TeleportData.gameCode
		_G.ratingType = joinData.TeleportData.ratingType
	end
	if #game.Players:GetPlayers() >= 2 then
		Start()
	end
end)

game.Players.PlayerRemoving:Connect(function(player)
	MatchmakingService:RemovePlayerFromGame(player, _G.gameId)
end)

-- THIS IS EXTREMELY IMPORTANT
game:BindToClose(function()
	MatchmakingService:RemoveGame(_G.gameId)
end)

If you put this script in your game server in ServerScriptService, it will handle setting game id and removing players from the game on disconnect and removing the game itself on close.

Future Plans

  • A “none” rating type Done with an option
  • Switch to MemoryQueues if/when we get more ways to manage it
  • Fully random access management that doesn’t use a central server
  • Party parity
  • Map support
  • An event that fires when a player is added to a match

Updates

I do plan to update this to fix bugs and add features when I have free time. I don’t get a lot of free time these days because of college and the game that I’m currently working on, but updates/fixes will release periodically.

132 Likes

This is awesome… Thank you :smiley:

()

5 Likes

Amazing resource and an epix use of the new memory service! 10/10! ;D

3 Likes

Work pretty well. I would use this as my Game Ideas

1 Like

You should check this out for the skill level things :slight_smile:

3 Likes

I was actually thinking about implementing that at a base level when I first saw it a few days ago. I’ll just have to read through how it works and think of a good way to implement it. Thank you for the suggestion though!

Here’s what I’m thinking:
I’ll make some sort of MatchmakingService:GetRating(plr, ratingType) where ratingType is a string of a defined type of rating (e.g. “ranked” or “normal”) that will default if not existing. This will allow you to have many separate ratings for different queues, much like most games already work where they have a rating for ranked and a rating for unranked. While this would be for internal use when queueing a player instead of providing a skill level, you provide a ratingType and it will get the data for you out of the back end (i.e. MatchmakingService:QueuePlayer(plr, skillLevel) becomes MatchmakingService:QueuePlayer(plr, ratingType)), you could still get their rating yourself if you want to display it to them. Instead of single queue pools for each skill level, they will be put into a pool in a range of levels that gets increasingly bigger the longer they’re in queue. I will also add a default ratingType of none that basically queues all the players that were queued with it in one queue with no rating taken into account and no rating is updated at the end, a pure non-rated game (most games still use some sort of rating in unrated gameplay, but I will provide the option to skip it completely).

Feedback on this idea?

5 Likes

I’m about 70% done with my rewrite that involves adding a rating system into this service. Right now all necessary functions have been rewritten to accept these new rating values. There’s basically 2 new things, a rating type, think of this as ranked or normal (the gamemode essentailly), but it can be any string you want to call it, and a rating itself. For every rating type, a player is assigned a glicko-2 rating. So if you have 5 different rated queues you can have 5 different ratings for each queue if it’s a different gamemode. See figure below:

Glicko-2 is a pretty good rating system but it does have a flaw called volatility farming. I won’t go into detail about this flaw, but I will be looking into making some changes to glicko-2 to fix this, but it’s currently not something I’m worried about.

There will also be a queue that does not use any rating system. This will be done by using “none” as the rating. This queue will be entirely separate and have no ratings attached.

This update will not be backwards compatible with the current version.

1 Like

I just pushed version 2.0.0-beta out! I’ve updated the post with additional information and how the system works now. This version is not fully backwards compatible with the v1 versions. I will soon add the party system and the none rating type for non-skill based match making.

4 Likes

Considering the tragedy of skill-based matchmaking, I will be using this wonderful resource. However, I have one question (idk if it’s too dumb or not): is this using SBMM ( or is it using something different)? I’m asking because I have serious concerns about SBMM regarding players and I am planning to add to my game a ranked version of a new game mode I just added.

As of version 2.0.0-beta, there is skill-based match making using the glicko-2 rating system (glicko-2 is used in games like TF2, CSGO, Splatoon and more). Players are queued into a pool based on their rating and the service prioritizes getting players of a similar rating but does expand its search every now and then if it can’t find people of the same level. I will soon be adding a none rating type that will not use any skill based match making or rating system to pool them into a queue, but I am currently working on the party system.

4 Likes

Wow, sorry I didn’t see your messages but nice work on this! I’m definitely going to be using it on a future project as this simplifies things so much! I’d love to help you in some way, if there’s anything I can do.

Any testing you can do on a larger scale to ensure that it’s stable at more than 2 players would be amazing. I can only test with 2 players, so before I make an official release version I really want to make sure it’s stable.

3 Likes

Is there a way to get how many players are in a queue?

1 Like

Not currently I will add that in with the next update though! Thanks for the suggestion!

1 Like

2.1.0 is now out with MatchmakingService:GetQueueCounts()

2 Likes

Would it be best to constantly refresh it or is there a smarter way of updating it?

Im a little confused with what you mean. If you want I could add an event you can subscribe to in code to listen to when someone gets added or removed from the queue which would be significantly more performant and wouldn’t add to the rate limits.

1 Like

Version 2.2.0-beta has been released!
Changes (breaking):

  • None

Changes (non-breaking):

  • [Addition] MatchmakingService:SetMaxPartySkillGap(newMaxGap)
  • [Addition] MatchmakingService:GetPlayerInfo(player)
  • [Addition] MatchmakingService:QueueParty(players, ratingType)
  • [Addition] MatchmakingService:GetPlayerParty(player)
  • [Addition] MatchmakingService.PlayerAddedToQueue signal.
  • [Addition] MatchmakingService.PlayerRemovedFromQueue signal.
  • [Change] MatchmakingService:SetPlayerInfo now accepts a new argument, party, which is a table of player ids in their party including the player’s own id.

Fixes:

  • None

The party system did work with 2 players, but I would appreciate if someone could test it with more to make sure it works! Please remember this is a beta, bugs may exist!

Currently the signals are not global across servers, this is planned though!

1 Like

Does this service work across servers or is it just within one server?

As long as all of the games are in the same universe it will work between them all and queue is global.

Meaning they’re like this:
df907351cea63b77f62deac109f918ca674b38d6_2_690x453

My recommendation is a hub place where players can queue up (all hub server instances would be connected, so if you have say 5000 players across 100 servers, all 100 servers would share the same queue pool). At the moment, only one game place is allowed. The game place is where players are teleported when they find a game. In the teleport data you can get information like the game type, the game code, etc and generate a map based on that data. In the future I may look into allowing multiple game places for different maps, but for now you can clone a map into the workspace based on teleport data if you need multiple maps.