Matchmaking Guide
Getting into a room to play with (or against) someone is very easy with Photon.
There are basically three approaches:
Either tell the server to find a matching room, follow a friend into her room, or fetch a list of rooms to let the user pick one.
All three variants are supported by Photon and you can even roll your own.
We think, for most games it's best to use a quick and simple matchmaking, so we suggest to use Random Matchmaking and maybe filters for skills, levels and such.
Matchmaking Checklist
If you run into issues matching players, here is a quick checklist:
C#
EnterRoomParams enterRoomParams = new EnterRoomParams();
enterRoomParams.ExpectedUsers = expectedUsers;
// create room example
loadBalancingClient.OpCreateRoom(enterRoomParams);
// join room example
loadBalancingClient.OpJoinRoom(enterRoomParams);
// join or create room example
loadBalancingClient.OpJoinOrCreateRoom(enterRoomParams);
// join random room example
OpJoinRandomRoomParams opJoinRandomRoomParams = new OpJoinRandomRoomParams();
opJoinRandomRoomParams.ExpectedUsers = expectedUsers;
loadBalancingClient.OpJoinRandomRoom(opJoinRandomRoomParams);
When you know someone should join, pass an array of UserIDs.
For JoinRandomRoom
, the server will attempt to find a room with enough slots for you and your expected players (plus all active and expected players already in the room).
The server will update clients in a room with the current expectedUsers
, should they change.
You can update the list of expected users inside a room (add or remove one or more users), this is done via a well known room property.
(In C# SDKs, you can get and set Room.ExpectedUsers
).
To support Slot Reservation, you need to enable publishing UserIDs inside rooms.
Example Use Case: Teams Matchmaking
You can use this to support teams in matchmaking.
The leader of a team does the actual matchmaking.
He/She can join a room and reserve slots for all members:
Try to find a random room:
C#
OpJoinRandomRoomParams opJoinRandomRoomParams = new OpJoinRandomRoomParams();
opJoinRandomRoomParams.ExpectedUsers = teamMembersUserIds;
loadBalancingClient.OpJoinRandomRoom(opJoinRandomRoomParams);
Create a new one if none found:
C#
EnterRoomParams enterRoomParams = new EnterRoomParams();
enterRoomParams.ExpectedUsers = teamMembersUserIds;
loadBalancingClient.OpCreateRoom(enterRoomParams);
The others don't have to do any matchmaking but instead repeatedly call ('periodic poll', every few frames/(milli)seconds):
C#
loadBalancingClient.OpFindFriends(new string[1]{ leaderUserId });
When the leader arrives in a room, the FindFriends
operation will reveal that room's name and everyone can join it:
C#
EnterRoomParams enterRoomParams = new EnterRoomParams();
enterRoomParams.RoomName = roomNameWhereTheLeaderIs;
loadBalancingClient.OpJoinRoom(enterRoomParams);
Lobbies
Photon is organizing your rooms in so called "lobbies".
So all rooms belong to lobbies.
Lobbies are identified using their name and type.
The name can be any string, however there are only 3 types of lobbies: Default, SQL and Async.
Each one has a unique capability which suits specific use cases.
All applications start with a preexisting lobby: The Default Lobby.
Most applications won't need other lobbies.
However, clients can create other lobbies on the fly.
Lobbies begin to exist when you specify a new lobby definition in operation requests: JoinLobby
, CreateRoom
or JoinOrCreateRoom
.
Like rooms, lobbies can be joined and you can leave them.
In a lobby, the clients only get the room list of that lobby when applicable.
Nothing else.
There is no way to communicate with others in a lobby.
When a client is joined to a lobby and tries to create (or JoinOrCreate
) a room without explicitly setting a lobby, if the creation succeeds/happens, the room will be added to the currently joined lobby.
When a client is not joined to a lobby and tries to create (or JoinOrCreate
) a room without explicitly setting a lobby, if the creation succeeds/happens, the room will be added to the default lobby.
When a client is joined to a lobby and tries to create (or JoinOrCreate
) a room by explicitly setting a lobby, if the creation succeeds/happens:
- if the lobby name is null or empty: the room will be added to the currently joined lobby.
This means you cannot create rooms in the default lobby when you are joined to a custom/different one. - if the lobby name is not null nor empty: the room will be added to the lobby specified by the room creation request.
When a client is joined to a lobby and tries to join a random room without explicitly setting a lobby, the server will look for the room in the currently joined lobby.
When a client is not joined to a lobby and tries to join a random room without explicitly setting a lobby, the server will look for the room in the default lobby.
When a client is joined to a lobby and tries to join a random room a room by explicitly setting a lobby:
- if the lobby name is null or empty: the server will look for the room in the currently joined lobby.
This means you cannot join random rooms in the default lobby when you are joined to a custom/different one. - if the lobby name is not null nor empty: the server will look for the room in the lobby specified by the room creation request.
When a client is joined to a lobby and wants to switch to a different one, you can call JoinLobby directly and no need to leave the first one by calling LeaveLobby explicitly.
Default Lobby Type
The most suited type for synchronous random matchmaking.
Probably the less sophisticated and most used type.
While joined to a default lobby type, the client will receive periodic room list updates.
When the client joins a lobby of default type, it instantly gets an initial list of available rooms.
After that the client will receive periodic room list updates.
The list is sorted using two criteria: open or closed, full or not.
So the list is composed of three groups, in this order:
- first group: open and not full (joinable).
- second group: full but not closed (not joinable).
- third group: closed (not joinable, could be full or not).
In each group, entries do not have any particular order (random).
The list of rooms (or rooms' updates) is also limited in number, see Lobby Limits.
C#
using Photon.Realtime;
using System.Collections.Generic;
public class RoomListCachingExample : ILobbyCallbacks, IConnectionCallbacks
{
private TypedLobby customLobby = new TypedLobby("customLobby", LobbyType.Default);
private LoadBalancingClient loadBalancingClient;
private Dictionary<string, RoomInfo> cachedRoomList = new Dictionary<string, RoomInfo>();
public void JoinLobby()
{
loadBalancingClient.JoinLobby(customLobby);
}
private void UpdateCachedRoomList(List<RoomInfo> roomList)
{
for(int i=0; i<roomList.Count; i++)
{
RoomInfo info = roomList[i];
if (info.RemovedFromList)
{
cachedRoomList.Remove(info.Name);
}
else
{
cachedRoomList[info.Name] = info;
}
}
}
// do not forget to register callbacks via loadBalancingClient.AddCallbackTarget
// also deregister via loadBalancingClient.RemoveCallbackTarget
#region ILobbyCallbacks
void ILobbyCallbacks.OnJoinedLobby()
{
cachedRoomList.Clear();
}
void ILobbyCallbacks.OnLeftLobby()
{
cachedRoomList.Clear();
}
void ILobbyCallbacks.OnRoomListUpdate(List<RoomInfo> roomList)
{
// here you get the response, empty list if no rooms found
UpdateCachedRoomList(roomList);
}
// [..] Other callbacks implementations are stripped out for brevity, they are empty in this case as not used.
#endif
#region IConnectionCallbacks
void IConnectionCallbacks.OnDisconnected(DisconnectCause cause)
{
cachedRoomList.Clear();
}
// [..] Other callbacks implementations are stripped out for brevity, they are empty in this case as not used.
#endregion
}
The Default Lobby
It has a null
name and its type is Default Lobby Type.
In C# SDKs, it's defined in TypedLobby.Default
.
The default lobby's name is reserved:
only the default lobby can have a null
name, all other lobbies need to have a name string that is not null nor empty.
If you use a string empty or null as a lobby name it will point to the default lobby nomatter the type specified.
Recommended Flow
We encourage everyone to skip joining lobbies unless abolutely necessary.
If needed, when you want rooms to be added to specific or custom lobbies, the client can specify the lobby when creating new rooms.
Joining lobbies of default type will get you the list of rooms, but it's not useful in most cases:
- there is no difference in terms of ping between the entries of the list
- usually players are looking for a quick match
- receiving rooms list adds an extra delay and consumes traffic
- a long list with too much information can have a bad effect on the user experience
Instead, to give your players more control over the matchmaking, use filters for random matchmaking.
Multiple lobbies can still be useful, as they are also used in (server-side) random matchmaking and you could make use of lobby statistics.
SQL Lobby Type
In SQL lobby type, string filters in JoinRandomRoom
replace the default expected lobby properties.
Also, in SQL lobby type, only one MatchmakingMode is supported: FillRoom
(default, 0).
Besides "Custom Room Listing" replaces the automatic periodic rooms listing which exists only in the default lobby type.
This lobby type adds a more elaborate matchmaking filtering which could be used for a server-side skill-based matchmaking that's completely client-driven.
Internally, SQL lobbies save rooms in a SQLite table with up to 10 special "SQL filtering properties".
The naming of those SQL properties is fixed as: "C0", "C1" up to "C9".
Only integer-typed and string-typed values are allowed and once a value was assigned to any column in a specific lobby, this column is locked to values of that type.
Despite the static naming, clients have to define which ones are needed in the lobby.
Be careful as SQL properties are case sensitive when you define them as lobby properties or set their values but are not case sensitive inside SQL filters.
You can still use custom room properties other than the SQL properties, visible or invisible to the lobby, during room creation or after joining it.
Those will not be used for matchmaking however.
Queries can be sent in JoinRandomRoom
operation.
The filtering queries are basically SQL WHERE conditions based on the "C0" .. "C9" values.
Find the list of all SQLite supported operators and how to use them here.
Take into consideration the excluded keywords.
Example:
C#
using Photon.Realtime;
using System.Collections.Generic;
using Hashtable = ExitGames.Client.Photon.Hashtable;
public class RandomMatchmakingExample : IMatchmakingCallbacks
{
public const string ELO_PROP_KEY = "C0";
public const string MAP_PROP_KEY = "C1";
private TypedLobby sqlLobby = new TypedLobby("customSqlLobby", LobbyType.SqlLobby);
private LoadBalancingClient loadBalancingClient;
private void CreateRoom()
{
RoomOptions roomOptions = new RoomOptions();
roomOptions.CustomRoomProperties = new Hashtable { { ELO_PROP_KEY, 400 }, { MAP_PROP_KEY, "Map3" } };
roomOptions.CustomRoomPropertiesForLobby = { ELO_PROP_KEY, MAP_PROP_KEY }; // makes "C0" and "C1" available in the lobby
EnterRoomParams enterRoomParams = new EnterRoomParams();
enterRoomParams.RoomOptions = roomOptions;
enterRoomParams.Lobby = sqlLobby;
loadBalancingClient.OpCreateRoom(enterRoomParams);
}
private void JoinRandomRoom()
{
string sqlLobbyFilter = "C0 BETWEEN 345 AND 475 AND C1 = 'Map2'";
//string sqlLobbyFilter = "C0 > 345 AND C0 < 475 AND (C1 = 'Map2' OR C1 = \"Map3\")";
//string sqlLobbyFilter = "C0 >= 345 AND C0 <= 475 AND C1 IN ('Map1', 'Map2', 'Map3')";
OpJoinRandomRoomParams opJoinRandomRoomParams = new OpJoinRandomRoomParams();
opJoinRandomRoomParams.SqlLobbyFilter = sqlLobbyFilter;
loadBalancingClient.OpJoinRandomRoom(opJoinRandomRoomParams);
}
// do not forget to register callbacks via loadBalancingClient.AddCallbackTarget
// also deregister via loadBalancingClient.RemoveCallbackTarget
#region IMatchmakingCallbacks
void IMatchmakingCallbacks.OnJoinRandomFailed(short returnCode, string message)
{
CreateRoom();
}
void IMatchmakingCallbacks.OnCreateRoomFailed(short returnCode, string message)
{
Debug.LogErrorFormat("Room creation failed with error code {0} and error message {1}", returnCode, message);
}
void IMatchmakingCallbacks.OnJoinedRoom()
{
// joined a room successfully, both JoinRandomRoom or CreateRoom lead here on success
}
// [..] Other callbacks implementations are stripped out for brevity, they are empty in this case as not used.
#endif
}
Chained Filters
You can send up to 3 comma separated filters at once in a single JoinRandomRoom
operation.
These are called chained filters.
Photon servers will try to use the filters in order.
A room will be joined if any of the filters matches a room.
Otherwise a NoMatchFound error will be returned to the client.
Chained filters could help save matchmaking requests and speed up its process.
It could be useful especially for skill-based matchmaking where you need to 'relax' the filter after failed attempt.
Possible filters string formats:
- 1 (min) filter value:
{filter1}
(or{filter1};
) - 2 filter values:
{filter1};{filter2}
(or{filter1};{filter2};
) - 3 (max) filter values:
{filter1};{filter2};{filter3}
(or{filter1};{filter2};{filter3};
)
Examples:
C0 BETWEEN 345 AND 475
C0 BETWEEN 345 AND 475;C0 BETWEEN 475 AND 575
C0 BETWEEN 345 AND 475;C0 BETWEEN 475 AND 575;C0 >= 575
Custom Room Listing
Client can also request a custom list of rooms from an SqlLobby using SQL-like queries.
This method will return up to 100 rooms that fit the conditions.
The returned rooms are joinable (i.e. open and not full) and visible.
C#
using Photon.Realtime;
using System.Collections.Generic;
public class GetCustomRoomListExample : ILobbyCallbacks
{
private TypedLobby sqlLobby = new TypedLobby("customSqlLobby", LobbyType.SqlLobby);
public void GetCustomRoomList(string sqlLobbyFilter)
{
loadBalancingClient.OpGetGameList(sqlLobby, sqlLobbyFilter);
}
// do not forget to register callbacks via loadBalancingClient.AddCallbackTarget
// also deregister via loadBalancingClient.RemoveCallbackTarget
#region ILobbyCallbacks
void ILobbyCallbacks.OnRoomListUpdate(List<RoomInfo> roomList)
{
// here you get the response, empty list if no rooms found
}
// [..] Other callbacks implementations are stripped out for brevity, they are empty in this case as not used.
#endif
}
Skill-based Matchmaking
You can use lobbies of the SQL-type to implement your own skill-based matchmaking.
First of all, each room gets a fixed skill that players should have to join it.
This value should not change, or else it will basically invalidate any matching the players in it did before.
As usual, players should try to get into a room by JoinRandomRoom
.
The filter should be based on the user's skill.
The client can easily filter for rooms of "skill +/- X".
JoinRandomRoom
will get a response as usual but if it didn't find a match right away, the client should wait a few seconds and then try again.
You can do as many or few requests as you like.
If you use SQL lobby type, you could make use of Chained Filters.
Best of all: The client can begin to relax the filter rule over time.
It's important to relax the filters after a moment.
Granted: A room might be joined by a player with not-so-well-fitting skill but obviously no other room was a better fit and it's better to play with someone.
You can define a max deviation and a timeout.
If no room was found, this client has to open a new room with the skill this user has.
Then it has to wait for others doing the same.
Obviously, this workflow might take some time when few rooms are available.
You can rescue your players by checking the "application stats" which tell you how many rooms are available.
See Matchmaking For Low CCU.
You can adjust the filters and the timing for "less than 100 rooms" and use different settings for "100 to 1000 rooms" and again for "even more".
Excluded SQL Keywords
SQL filters will not accept the following keywords:
- ALTER
- CREATE
- DELETE
- DROP
- EXEC
- EXECUTE
- INSERT
- INSERT INTO
- MERGE
- SELECT
- UPDATE
- UNION
- UNION ALL
If you use any of these words in the SQL filter string the corresponding operation will fail.
Asynchronous Random Lobby Type
This lobby is similar to the default lobby type with two major differences:
- Room entries stay in the lobby list (available for matchmaking) for one hour after they are removed from game servers.
Rooms need to be visible and open to be considered in the asynchronous matchmaking. - Rooms lists are not sent to clients.
Lobby Types Comparison
LobbyType | Periodic Rooms List Updates | SQL Filter | Max Players Filter | Custom Room Properties Filter | Matchmaking Modes | Removed Rooms Entries TTL (minutes) |
---|---|---|---|---|---|---|
Default | 0 | |||||
SQL | 0 | |||||
Asynchronous | 60 |
Matchmaking For Low CCU
For really good matchmaking, a game needs a couple hundred players online.
With less players online, it will become harder to find a worthy opponent and at some point it makes sense to just accept almost any match.
You have to take this into account when you build a more elaborate matchmaking on the client side.
To do so, the Photon Master Server provides the count of connected users, rooms and players (in a room), so you can adjust the client-driven matchmaking at runtime.
The number of rooms should be a good, generic indicator of how busy the game currently is.
You could obviously also fine tune the matchmaking by on how many players are not in a room.
Whoever is not in a room might be looking for one.
For example, you could define a low CCU situation as less than 20 rooms.
So, if the count of rooms is below 20, your clients use no filtering and instead run the Quick Match routine.
Testing Matchmaking Early In Development
If you are testing matchmaking early in development phase and try to join a random room from two clients at about the same time, there is a chance both clients end up on different rooms: this happens because join random room will not return a match for both clients and each one will probably create a new room as none found.
So this is expected and OK.
To avoid this use JoinRandomOrCreateRoom (see Quick Match) instead of JoinRandomRoom then CreateRoom.
Otherwise, a possible workaround (for development purposes only) would be to add a random delay before (or after) attempting to join a room or retry again.
You could also listen for application or lobby statistics to make sure a room at least exists or has been created.
Other Matchmaking Options
If you want to roll your own matchmaking, make sure that most of that is done server side.
The clients don't have perfect information about how full rooms are as the room-list update frequency from server to client is low (~1..2 seconds).
When you have thousands of players, several will send their "join" requests at the very same time.
If a room gets full quickly, your players will frequently fail to join rooms and matchmaking will take longer and longer.
On the other hand, the server can distribute players perfectly, preferring almost full rooms and respecting your filtering.
What you could do is to make matchmaking external to Photon, via HTTP based web service maybe and then use Photon to create and join rooms (or one call to JoinOrCreate
).
Such matchmaking service could make use of (combined with) Photon's "native HTTP modules" (Custom Authentication / WebRPCs / WebHooks) or even a custom plugin to report rooms availability to your web service.
Things to consider in matchmaking (keywords): Region (and Cluster when applicable), AppVersion, AppId, UserId, RoomName/GameId, Auth Cookie (Custom Auth), URL tags (WebHooks), etc.
Another option is to modify LoadBalancing server application, MasterServer specifically, the matchmaking part.
This option is for self-hosted only of course.
That said, using a room as 'lobby' or a 'matchmaking place', before the actual gameplay starts, is in most cases not a good idea for popular games.
Lobby Limits
Photon has the following lobbies related default limits:
- Maximum number of lobbies per application: 10000.
- Maximum number of room list entries in GameList events (initial list when you join lobbies of type Default): 500.
- Maximum number of updated rooms entries in GameListUpdate events (when joined to lobbies of type Default): 500.
This limit does not account for removed rooms entries (corresponding to rooms no longer visible or simply gone). - Maximum number of room list entries in GetGameList operation response (SQL Lobby): 100.
Notes:
In lobby v2 only the number of initial/updated room entries sent to the clients joined to the same respective lobby of default type, in GameList or GameListUpdate events are limited to 500.
For GameList event, the length of the received array by the client will not exceed 500.
For GameListUpdate event, the length of the received array by the client may exceed 500 as we do not limit the number of removed rooms entries (which are also sent here), only the updated ones.
The new limits in Lobbies v2 do not affect anything else server-side: the rooms still exist in the lobby.
We only limit what we broadcast to clients joined to lobbies for bandwidth reasons and to not overwhelm clients which could be limited in specs.
And 500 is quite a big number, no player would scroll to see the full list.
Besides, in theory, a client that stays long enough in the lobby may end up with the full list of rooms available on the server because the server adds all updates to a queue and send them in batches of max length 500.
Of course, if a room exists in the lobby and does not change for a while the client may miss it.
So on the servers, there is no limit of the number of rooms per lobby.
FindFriends, CreateRoom, JoinRoom or JoinOrCreateRoom are not affected by the move to Lobbies v2 and are not limited, meaning clients can create room indefinitely or join or find friends in rooms not sent to the client in the lobbies list updates.