Mastering Photon Realtime Matchmaking: Custom Rooms, Lobbies & Regional Load‑Balancing
Learn how to set up custom room properties, use Photon Realtime’s lobby system, and balance latency by selecting the nearest region. A step‑by‑step guide with Unity code snippets and trade‑off insights.
21 Sept 2025, 18:50 UTC

Problem: Pairing Players Fast and Fair
When you launch a multiplayer title, the first thing players notice is how quickly they find a game that matches their skill, game mode, and location. If matchmaking is slow or uneven, users drop out. Photon Realtime offers a turnkey solution, but its true power comes from customizing room properties, leveraging the lobby for discovery, and selecting the nearest region.
Thesis: Use Photon’s Built‑In Tools, Not a Custom Server
Instead of building your own matchmaking logic, let Photon handle the heavy lifting and expose the hooks you need to control game mode, player count, and latency. The combination of RoomOptions, PhotonNetwork.JoinRandomRoom, the lobby API, and the RegionSelector gives you a concise, scalable workflow.
1. Custom Room Properties & RoomOptions
RoomOptions lets you set:
- MaxPlayers – the maximum number of participants.
- IsVisible – whether the room appears in the lobby.
- CustomProperties – a dictionary of key/value pairs that persist across the session.
Example: create a “Deathmatch” room that only accepts 8 players and records the map name.
// Unity – Photon Realtime 4.x
using Photon.Pun;
using Photon.Realtime;
public class Matchmaker : MonoBehaviourPunCallbacks
{
public void CreateDeathmatchRoom(string map)
{
var options = new RoomOptions
{
MaxPlayers = 8,
IsVisible = true,
CustomRoomProperties = new Hashtable {{"mode", "deathmatch"}, {"map", map}}
};
PhotonNetwork.CreateRoom(null, options, null); // null name = random
}
}
After creation, any client can read PhotonNetwork.CurrentRoom.CustomProperties["map"] to know the map.
2. Joining with Filters – The Lobby System
Instead of iterating through every room, call JoinRandomRoom with a RoomOptions filter:
public void JoinBestDeathmatch()
{
var options = new JoinRoomParams
{
RoomOptions = new RoomOptions { MaxPlayers = 8 },
ExpectedUsers = null,
Lobby = Lobby.DefaultLobby
};
options.CustomRoomProperties = new Hashtable {{"mode", "deathmatch"}};
PhotonNetwork.JoinRandomRoom(options);
}
If no matching room exists, the callback OnJoinRandomFailed triggers, where you can fallback to creating a new room.
3. Regional Load Balancing with RegionSelector
Photon runs servers in multiple geographic clusters. Use RegionSelector to automatically pick the closest region based on ping:
public void ConnectToBestRegion(string appId)
{
var regionSelector = new RegionSelector(appId, new RegionHandler());
regionSelector.Start();
}
class RegionHandler : IPhotonHandler
{
public void OnEvent(EventData photonEvent) { /* not used */ }
public void OnOperationResponse(OperationResponse response) { /* not used */ }
public void OnStatusChanged(StatusCode statusCode)
{
if (statusCode == StatusCode.Connect)
{
Debug.Log("Connected to " + PhotonNetwork.ServerAddress);
Debug.Log("Ping: " + PhotonNetwork.GetPing());
}
}
}
Run PhotonNetwork.GetPing() after connection to verify latency. Switching the selector to a different region (e.g., "eu-west") should reduce ping for European players.
Worked Example: Full Flow
Below is a minimal Unity MonoBehaviour that:
- Connects to the nearest region.
- Creates a deathmatch room with a custom map.
- Attempts to join a random deathmatch room if one exists.
public class QuickMatch : MonoBehaviourPunCallbacks
{
private void Start()
{
PhotonNetwork.AutomaticallySyncScene = true;
PhotonNetwork.ConnectUsingSettings();
}
public override void OnConnectedToMaster()
{
// Prefer a regional selector – omitted for brevity.
JoinBestDeathmatch();
}
void JoinBestDeathmatch()
{
var filter = new Hashtable {{"mode", "deathmatch"}};
PhotonNetwork.JoinRandomRoom(new JoinRoomParams { CustomRoomProperties = filter });
}
public override void OnJoinRandomFailed(short returnCode, string message)
{
CreateDeathmatchRoom("Arena1");
}
void CreateDeathmatchRoom(string map)
{
var options = new RoomOptions
{
MaxPlayers = 8,
CustomRoomProperties = new Hashtable {{"mode", "deathmatch"}, {"map", map}}
};
PhotonNetwork.CreateRoom(null, options, null);
}
public override void OnJoinedRoom()
{
Debug.Log("Joined room " + PhotonNetwork.CurrentRoom.Name);
Debug.Log("Map: " + PhotonNetwork.CurrentRoom.CustomProperties["map"]);
Debug.Log("Ping: " + PhotonNetwork.GetPing());
}
}
Run two instances of the client. One will create the room; the other will join it. Verify that PhotonNetwork.CurrentRoom.CustomProperties contains the map and that the ping is low for each region.
Trade‑Offs & Limitations
- Metadata Size – Every custom property is sent to all clients in the room. Excessive keys (hundreds) can inflate bandwidth and memory usage.
- Server Load vs. Client Filters – The matchmaking algorithm is server‑side; client‑side filters are only applied after a room is returned. If the server is overloaded, you may still receive rooms that don’t match your criteria.
- Region Switching – Changing regions mid‑game forces a reconnect, which can disrupt gameplay. Plan region selection at startup.
Actionable Checklist
- Define key custom properties (e.g., mode, map) and keep the dictionary lean.
- Use
RoomOptions.IsVisibleto hide matchmaking‑only rooms from the lobby. - Implement
RegionSelectorbefore connecting to reduce latency. - Handle
OnJoinRandomFailedgracefully to create a new room. - Periodically monitor
PhotonNetwork.GetPing()and room load in the Photon Dashboard.
By following these patterns, you can deliver a responsive, scalable matchmaking experience without reinventing the wheel.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.