What is this?
Hello there, this is my matchmaking code to replicate how Project Sekai does their matchmaking. This is useful for making games where a game with multiple players have little to no data being sent (like a rhythm game, only points and players are stored).
How does Project Sekai do their matchmaking? What data is sent?
When a player joins a multiplayer match, the player either makes a room or joins one. Stickers (emotes replicated to all clients) and votes (for what song to play) can be sent as data. The code has been made so either can be added easily.
How does this code work?
-
Requests
Requests are sent from a client. The client can be the host itself. When a client sends a message, it has an identifier attached to it from the AddIdentifier function. The identifier shows what client sent the message. The recipient shows where it should go to. For example, stickers can be sent to individual (except the host) or all players by using the recipient function. It could go to the host which then replicates it to all players. Scores can be sent using requests too. -
Events
Events can only be sent by the server. It just sends an event to all clients or just 1 client.
Important note: Clients are on the behalf of the server. The host is not really a host because it can host itself, more like it is a primary client that happens to respond to others. That’s why the IsHost identifier tag can be set to false, even if the UserId is the UserId of the host. The host will treat it as another request and will not treat it like a host response if IsHost is set to false.
Now, here is the code for people who want to modify it for their own use or just want to look at it for fun:
Module in Libraries.WebSocketCreator
local WebSocketCreator = {}
local HttpService = game:GetService("HttpService")
local MessagingService = game:GetService("MessagingService")
function WebSocketCreator.Create(Name)
local WebSocket = {}
function WebSocket.GetReciever()
local Reciever = {}
Reciever.CallbackFunctions = {}
function Reciever.Initialize(self, Functions)
self.CallbackFunctions = Functions
MessagingService:SubscribeAsync(Name, function(Message)
for _, Function in pairs(self.CallbackFunctions) do
local DataTable = HttpService:JSONDecode(Message.Data)
Function(DataTable)
end
end)
end
function Reciever.GetCallbackFunctions(self)
return self.CallbackFunctions
end
function Reciever.SetCallbackFunctions(self, Functions)
self.CallbackFunctions = Functions
end
return Reciever
end
function WebSocket.GetSender()
local Sender = {}
function Sender.Initialize(self)
--Sender Initialization Is Not Required It Is Just There To Prevent Confusion
end
function Sender.SendDataTable(self, Data)
local Serialized = HttpService:JSONEncode(Data)
while true do
local A, B = pcall(function()
MessagingService:PublishAsync(Name, Serialized)
end)
if A == true then
print("Request sent.")
break
else
print("Request failed. Retrying...")
end
task.wait(1)
end
end
return Sender
end
return WebSocket
end
return WebSocketCreator
Matchmaking Server Runner
local WebSocketCreator = require(game:GetService("ServerScriptService").Libraries.WebSocketCreator)
function AddIdentifier(Identification, Target, IsHost, OriginalData)
local NewData = {}
if IsHost then
NewData = {
Identity = Identification,
Recipient = Target,
IsHost = IsHost,
Response = OriginalData,
}
else
NewData = {
Identity = Identification,
Recipient = Target,
IsHost = IsHost,
Request = OriginalData,
}
end
return NewData
end
function MatchNewReserve(Amount, UserIDRequester)
--Matchmaking
--Initialization
local WebSocket = WebSocketCreator.Create(tostring(UserIDRequester))
local Sender = WebSocket.GetSender()
local Reciever = WebSocket.GetReciever()
function CurrentlyNotRecieving(Data)
if Data.Identity == UserIDRequester then
return
end
if not (Data.Recipient == UserIDRequester) then
return
end
Sender:SendDataTable(AddIdentifier(UserIDRequester, Data.Identity, true, {
Type = "Soft-Failure",
Response = "Currently not recieving."
}))
end
Reciever:Initialize({CurrentlyNotRecieving})
--Wait for [Amount] of players
local Players = {UserIDRequester}
function RecievePlayer(Data)
if Data.Identity == UserIDRequester then
return
end
if not (Data.Recipient == UserIDRequester) then
return
end
if not (Data.Request.Type == "JoinRequest") then
return
end
if #Players >= Amount then
Sender:SendDataTable(AddIdentifier(UserIDRequester, Data.Identity, true, {
Type = "Failure",
Response = "Too many players!"
}))
return
end
local AlreadyInGame = table.find(Players, Data.Identity, 1)
if not AlreadyInGame then
table.insert(Players, Data.Identity)
Sender:SendDataTable(AddIdentifier(UserIDRequester, Data.Identity, true, {
Type = "Success",
Response = "Match joined."
}))
--Join Signal
Sender:SendDataTable(AddIdentifier(UserIDRequester, -100, true, {
Type = "Event",
Response = {
Event = "Join",
Data = {
Identity = tostring(Data.Identity)
}
}
}))
return
else
Sender:SendDataTable(AddIdentifier(UserIDRequester, Data.Identity, true, {
Type = "Failure",
Response = "Already in game!"
}))
return
end
end
Reciever:SetCallbackFunctions({RecievePlayer, print})
repeat task.wait() until #Players >= Amount
Reciever:SetCallbackFunctions({CurrentlyNotRecieving, print})
local PlayerCounter = 0
local Temp = {}
for _, UserID in pairs(Players) do
PlayerCounter = PlayerCounter + 1
if PlayerCounter <= Amount then
table.insert(Temp, UserID)
else
Sender:SendDataTable(AddIdentifier(UserIDRequester, UserID, true, {
Type = "Failure",
Response = "Too many players!"
}))
end
end
Players = Temp
print("Reserved Players: "..tostring(#Players))
print(Players)
task.wait(3)
Sender:SendDataTable(AddIdentifier(UserIDRequester, -100, true, {
Type = "Event",
Response = {
Event = "AllReady",
Data = {}
}
}))
task.wait(3)
Sender:SendDataTable(AddIdentifier(UserIDRequester, -100, true, {
Type = "Event",
Response = {
Event = "VoteStart",
Data = {}
}
}))
--Voting
local Votes = {}
function RecieveVote(Data)
if not (Data.Recipient == UserIDRequester) then
return
end
if Data.IsHost == true then
return
end
if not (Data.Request.Type == "Vote") then
return
end
Votes[Data.Identity] = Data.Request.Data.Vote
Sender:SendDataTable(AddIdentifier(UserIDRequester, Data.Identity, true, {
Type = "Success",
Response = "Vote recieved."
}))
end
local TimerVote = 20
Reciever:SetCallbackFunctions({RecieveVote, print})
repeat task.wait(1) TimerVote = TimerVote - 1
until ((#Votes == Amount) or (TimerVote <= 0))
Reciever:SetCallbackFunctions({CurrentlyNotRecieving, print})
print("Voting completed. Votes: "..tostring(#Votes))
print(Votes)
Sender:SendDataTable(AddIdentifier(UserIDRequester, -100, true, {
Type = "Event",
Response = {
Event = "VoteEnded",
Data = {
Winner = Votes[Players[math.random(1,Amount)]]
}
}
}))
-- NonVisualReadyUp
Sender:SendDataTable(AddIdentifier(UserIDRequester, -100, true, {
Type = "Event",
Response = {
Event = "NonVisualReady",
Data = {}
}
}))
local ReadyList = {}
function RecieveReadySignalNonVisual(Data)
if Data.IsHost == true then
return
end
if not (Data.Recipient == UserIDRequester) then
return
end
if not (Data.Request.Type == "NonVisualReadyUp") then
return
end
ReadyList[Data.Identity] = true
Sender:SendDataTable(AddIdentifier(UserIDRequester, Data.Identity, true, {
Type = "Success",
Response = "Ready signal recieved."
}))
end
Reciever:SetCallbackFunctions({RecieveReadySignalNonVisual, print})
local TimeForNonVisualReady = 20
repeat task.wait(1) TimeForNonVisualReady = TimeForNonVisualReady - 1 until ((TimeForNonVisualReady <= 0) or (#ReadyList == Amount))
if not (#ReadyList == Amount) then
Sender:SendDataTable(AddIdentifier(UserIDRequester, -100, true, {
Type = "Event",
Response = {
Event = "DisbandedRoom",
Data = {}
}
}))
print("Room disbanded.")
return
end
print("Minigame started.")
end
MatchNewReserve(10, 1)
Test Matchmaking Script
local WebSocketCreator = require(game:GetService("ServerScriptService").Libraries.WebSocketCreator)
task.wait(5)
function AddIdentifier(Identification, Target, IsHost, OriginalData)
local NewData = {}
if IsHost then
NewData = {
Identity = Identification,
Recipient = Target,
IsHost = IsHost,
Response = OriginalData,
}
else
NewData = {
Identity = Identification,
Recipient = Target,
IsHost = IsHost,
Request = OriginalData,
}
end
return NewData
end
local WebSocket = WebSocketCreator.Create(tostring(1))
function CreateClient(ID)
local ClientReturned = {}
local TestSend = WebSocket.GetSender()
local TestRecieve = WebSocket.GetReciever()
TestRecieve:Initialize({})
ClientReturned.SendRequest = function(Type1, Data1, WaitTime)
local Response = nil
local TimeResponse = WaitTime*10
TestRecieve:SetCallbackFunctions({
function(Data)
if (Data.IsHost == false) then
return
end
if (Data.Response.Type == "Failure") or (Data.Response.Type == "Success") then
Response = Data.Response
end
end,
})
TestSend:SendDataTable(AddIdentifier(ID, 1, false, {
Type = Type1,
Data = Data1
}))
repeat
task.wait(0.1)
TimeResponse = TimeResponse - 1
until (TimeResponse <= 0) or Response
return Response
end
ClientReturned.ConnectEvent = function(Event, Function)
TestRecieve:SetCallbackFunctions({
function(Data)
if (Data.IsHost == false) then
return
end
if not ((Data.Recipient == ID) or (Data.Recipient == -100))then
return
end
if (Data.Response.Type == "Event") then
if Data.Response.Response.Event == Event then
Function(Data.Response.Response.Data)
end
end
end,
})
end
return ClientReturned
end
local Clients = {
CreateClient(1),
CreateClient(2),
CreateClient(3),
CreateClient(4),
CreateClient(5),
CreateClient(6),
CreateClient(7),
CreateClient(8),
CreateClient(9),
CreateClient(10),
}
task.wait(5)
for i, Client in pairs(Clients) do
spawn(function()
Client.SendRequest("JoinRequest", {}, 10)
end)
task.wait(0.1)
end
Clients[1].ConnectEvent("VoteStart", function()
task.wait(3)
local Votes = {"A", "B", "C", "D", "E"}
for i, Client in pairs(Clients) do
spawn(function()
Client.SendRequest("Vote", {Vote = Votes[math.random(1,5)]}, 10)
end)
task.wait(0.1)
end
Clients[1].ConnectEvent("NonVisualReady", function()
task.wait(3)
for i, Client in pairs(Clients) do
spawn(function()
Client.SendRequest("NonVisualReadyUp", {}, 10)
end)
task.wait(0.1)
end
end)
end)