Integrate traditional and role-based matchmaking with the lobby system.
PlayFlow’s matchmaking system supports both traditional symmetric matchmaking (like Fortnite, Counter-Strike) and advanced role-based asymmetric matchmaking (like League of Legends, Overwatch, Dead by Daylight). The system seamlessly integrates with lobbies to provide automated matching based on skill, region, roles, and custom criteria.
For role-based modes, also specify your role preferences:
public class RoleBasedMatchmaking : MonoBehaviour{ // Single specific role (MOBA player who only plays one lane) public void SetSpecificRole(string role, int playerMMR) { var matchmakingData = new Dictionary<string, object> { { "role", role }, // e.g., "jungle", "mid", "support" { "mmr", playerMMR }, { "regions", new List<string> { "us-east", "us-west" } } }; PlayFlowLobbyManagerV2.Instance.UpdatePlayerState(matchmakingData); } // Priority array of roles (flexible fill player) public void SetFlexibleRoles(int playerMMR) { var matchmakingData = new Dictionary<string, object> { { "role", new List<string> { "support", "adc", "mid" } }, // Preference order { "mmr", playerMMR }, { "regions", new List<string> { "us-east", "eu-west" } } }; PlayFlowLobbyManagerV2.Instance.UpdatePlayerState(matchmakingData); } // Spectator role (excluded from matchmaking) public void SetSpectatorRole() { var matchmakingData = new Dictionary<string, object> { { "role", "Spectator" }, // Will be excluded from team requirements { "regions", new List<string> { "us-east" } } }; PlayFlowLobbyManagerV2.Instance.UpdatePlayerState(matchmakingData); }}
Once the player’s data is set, the lobby host can start the search by calling FindMatch. The mode parameter must match the name of the matchmaking mode you created in the dashboard.
Unity
Unreal Engine
Godot
public class MatchmakingStarter : MonoBehaviour{ // Traditional matchmaking (Competitive5v5, Duels, BattleRoyale, etc.) public void StartTraditionalSearch(string mode = "Competitive5v5") { var manager = PlayFlowLobbyManagerV2.Instance; if (!manager.IsInLobby || !manager.IsHost) { Debug.LogWarning("Must be the host of a lobby to start matchmaking."); return; } manager.FindMatch(mode, onSuccess: (lobby) => { Debug.Log($"Successfully entered the '{lobby.matchmaking.mode}' queue."); // Update your UI to show a "searching..." state }, onError: (error) => { Debug.LogError($"Failed to start matchmaking: {error}"); } ); } // Role-based matchmaking (RankedRoleQueue, AsymmetricHorror, etc.) public void StartRoleBasedSearch(string mode = "RankedRoleQueue") { var manager = PlayFlowLobbyManagerV2.Instance; if (!manager.IsInLobby || !manager.IsHost) { Debug.LogWarning("Must be the host of a lobby to start matchmaking."); return; } // Ensure all players have set their roles var currentLobby = manager.CurrentLobby; bool allPlayersHaveRoles = true; foreach (var playerId in currentLobby.players) { if (currentLobby.lobbyStateRealTime.TryGetValue(playerId, out var playerState)) { if (!playerState.ContainsKey("role")) { Debug.LogWarning($"Player {playerId} has not set their role!"); allPlayersHaveRoles = false; } } } if (!allPlayersHaveRoles) { Debug.LogError("All players must set their role before matchmaking!"); return; } manager.FindMatch(mode, onSuccess: (lobby) => { Debug.Log($"Entered role-based queue: {lobby.matchmaking.mode}"); // Show searching UI with role information }, onError: (error) => { Debug.LogError($"Failed to start role-based matchmaking: {error}"); } ); }}
After starting the search, subscribe to events to drive your UI.
Event
When it fires
OnMatchmakingStarted
Lobby entered the queue (status → in_queue)
OnQueueStats
Every ~10s while queueing — live wait-time telemetry
OnMatchAwaitingConfirmation
Match proposal pending (only if matchConfirmation.enabled)
OnMatchConfirmed
This lobby accepted; waiting on others
OnMatchDeclined
Someone declined or timed out — back to waiting
OnMatchFound
Match committed, server launching
OnMatchRunning
Server is ready to accept connections
OnMatchServerDetailsReady
Full port list (for multi-port games)
OnMatchmakingCancelled
Host called CancelMatchmaking()
OnMatchmakingTimeout
Queue timed out without finding a match
Unity
Unreal Engine
Godot
void Start(){ var events = PlayFlowLobbyManagerV2.Instance.Events; events.OnMatchmakingStarted.AddListener(HandleMatchmakingStarted); events.OnQueueStats.AddListener(HandleQueueStats); events.OnMatchFound.AddListener(HandleMatchFound); events.OnMatchRunning.AddListener(HandleMatchRunning); events.OnMatchServerDetailsReady.AddListener(HandleServerDetailsReady); events.OnMatchmakingCancelled.AddListener(HandleMatchmakingCancelled); events.OnMatchmakingTimeout.AddListener(HandleMatchmakingTimeout);}private void HandleMatchmakingStarted(Lobby lobby){ Debug.Log($"In queue for mode: {lobby.matchmaking.mode}"); // Show "Searching for match…" UI}private void HandleQueueStats(QueueStats stats){ // Update your UI: "42 players searching · ~12s avg wait" Debug.Log($"{stats.playersSearching} searching, {stats.avgWaitSeconds:F0}s avg wait");}private void HandleMatchFound(Lobby lobby){ Debug.Log("Match committed! Server launching…"); // Update UI to "Connecting…"}private void HandleMatchRunning(ConnectionInfo connectionInfo){ // Quick-access for single-port games MyNetworkManager.Connect(connectionInfo.Ip, connectionInfo.Port);}private void HandleServerDetailsReady(List<PortMappingInfo> portMappings){ // Preferred: look up by the port name you defined in the dashboard var lobby = PlayFlowLobbyManagerV2.Instance.CurrentLobby; if (lobby.TryGetPort("game_udp", out var port)) { Debug.Log($"Connecting to {port.host}:{port.external_port}"); MyNetworkManager.Connect(port.host, port.external_port); }}private void HandleMatchmakingCancelled(Lobby lobby) { /* back to lobby */ }private void HandleMatchmakingTimeout(Lobby lobby) { /* show "no match found" */ }
If the matchmaking mode has matchConfirmation.enabled: true in the dashboard, matches pause at the match_found status waiting for every lobby to explicitly accept. Opt in by subscribing to three extra events and exposing accept/decline buttons.
void Start(){ var events = PlayFlowLobbyManagerV2.Instance.Events; events.OnMatchAwaitingConfirmation.AddListener(HandleAwaitingConfirmation); events.OnMatchConfirmed.AddListener(_ => Debug.Log("You accepted. Waiting on other players…")); events.OnMatchDeclined.AddListener(_ => Debug.Log("Match cancelled — re-queue when ready."));}void HandleAwaitingConfirmation(Lobby lobby){ // Show your "Accept Match" dialog var deadlineIso = lobby.matchmaking.confirmation.deadline; Debug.Log($"Match found — accept before {deadlineIso}");}public void OnAcceptClicked() => PlayFlowLobbyManagerV2.Instance.ConfirmMatch();public void OnDeclineClicked() => PlayFlowLobbyManagerV2.Instance.DeclineMatch();
If any player declines or the deadline expires, every participating lobby returns to waiting (not back to in_queue) — players re-queue manually when ready. This matches how CS2 and League of Legends handle match acceptance.
The host can cancel the matchmaking search at any time by calling CancelMatchmaking.
Unity
Unreal Engine
Godot
public void CancelSearch(){ var manager = PlayFlowLobbyManagerV2.Instance; if (!manager.IsHost || manager.CurrentLobby?.status != "in_queue") { return; // Can only cancel if you are the host and in the queue } manager.CancelMatchmaking( onSuccess: (lobby) => { Debug.Log("Successfully cancelled matchmaking."); }, onError: (error) => { Debug.LogError($"Failed to cancel matchmaking: {error}"); } );}
The role-based matchmaking system uses an intelligent priority-based algorithm to assign players to roles. Roles can be provided as either a single string or a string array for flexibility.
Key Principle: Players with fewer role options get priority assignment to ensure everyone gets a suitable role. This is NOT random - it’s deterministic and fair.
1. Specificity First
Players with fewer role options are assigned before flexible players:
// Assignment priority (highest to lowest):Player A: ["tank"] // 1 option - HIGHEST PRIORITYPlayer B: ["tank", "healer"] // 2 options - medium priorityPlayer C: ["tank", "healer", "dps"] // 3 options - lowest priority// Player A gets first choice, then B, then C
2. Preference Order Matters
When assigned, the system tries roles left-to-right in your array:
// Player's preference array:["support", "adc", "mid"]// System tries:// 1st: Can they be support? → If yes, assigned!// 2nd: If not, can they be adc? → If yes, assigned!// 3rd: If not, can they be mid? → If yes, assigned!
3. Tie-Breaking
When players have identical preferences, first-come-first-served applies:
// Both want the same roles:Player1: ["support", "adc"] // Joined lobby firstPlayer2: ["support", "adc"] // Joined lobby second// Result:Player1 → Gets "support" (first preference)Player2 → Gets "adc" (support taken, gets second choice)
public class RolePreferenceManager : MonoBehaviour{ // Specific player - highest priority but risky public void SetOneRoleOnly(string role) { var data = new Dictionary<string, object> { { "role", role } // Single string }; PlayFlowLobbyManagerV2.Instance.UpdatePlayerState(data); } // Flexible player - lower priority but guarantees match public void SetMultipleRoles(List<string> roles) { var data = new Dictionary<string, object> { { "role", roles } // String array in preference order }; PlayFlowLobbyManagerV2.Instance.UpdatePlayerState(data); } // Ultra-flexible - can play anything public void SetFillPlayer() { var allRoles = new List<string> { "top", "jungle", "mid", "adc", "support" }; var data = new Dictionary<string, object> { { "role", allRoles } }; PlayFlowLobbyManagerV2.Instance.UpdatePlayerState(data); }}
Edge Case: If too many players are inflexible (single role only) and want the same role, the match will fail. Always encourage some flexibility in your player base!
Spectators are excluded from team requirements but still receive game server access for observation.
public class SpectatorManager : MonoBehaviour{ public void JoinAsSpectator() { // Set spectator role var spectatorData = new Dictionary<string, object> { { "role", "Spectator" }, { "regions", new List<string> { "us-east" } } }; PlayFlowLobbyManagerV2.Instance.UpdatePlayerState(spectatorData); } private void OnMatchFound(Lobby lobby) { // Spectators still get notified and can connect if (IsSpectator()) { Debug.Log("Joined match as spectator!"); // Connect to server in spectator mode } }}
Players can specify multiple acceptable regions. The system finds matches where players have overlapping regions.
// Specify regions in order of preferencevar regionList = new List<string> { "us-east", "us-west", "eu-west" };UpdatePlayerState(new Dictionary<string, object> { { "regions", regionList }});
The lobby-based approach naturally supports parties. Friends can join the same lobby and queue together:
public class PartyManager : MonoBehaviour{ public void CreatePartyLobby() { // Create lobby for your party PlayFlowLobbyManagerV2.Instance.CreateLobby( name: "My Party", maxPlayers: 4, // Your party size isPrivate: true, onSuccess: (lobby) => { Debug.Log($"Party lobby created! Share code: {lobby.code}"); } ); } public void QueueWithParty(string mode) { // Everyone in the lobby queues together PlayFlowLobbyManagerV2.Instance.FindMatch(mode); }}