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.

Matchmaking Types

Traditional/Symmetric Matchmaking

Used for games where all players have equal capabilities and teams are symmetrical. Examples include:
  • Battle Royale (Fortnite, PUBG): 100 players, last one standing
  • Team Deathmatch (Counter-Strike, Valorant): 5v5 competitive matches
  • Arena Duels: 1v1 or 2v2 ranked matches
  • Large-scale Battles: 32v32 or 64v64 team warfare

Role-Based/Asymmetric Matchmaking

Used for games where players have distinct roles with unique abilities and responsibilities. Examples include:
  • MOBA Games (League of Legends, Dota 2): Support, Tank, DPS roles with team composition requirements
  • Hero Shooters (Overwatch, Paladins): Role queue with tanks, healers, and damage dealers
  • Asymmetric Horror (Dead by Daylight): 4 survivors vs 1 killer
  • Social Deduction (Among Us, Werewolf): Different roles with hidden information

How Matchmaking Works

In PlayFlow, matchmaking always works through lobbies:
  1. A player creates a lobby (for just themself, or for a party of friends)
  2. Players set their matchmaking data (skill, region, roles)
  3. The lobby host starts the search
  4. The system finds compatible lobbies based on your configuration
  5. When a match is found, a game server automatically starts for all players
This lobby-based approach naturally supports party matchmaking (friends queuing together) while maintaining team cohesion.

Dashboard Configuration

Before writing code, you must configure your matchmaking rules in the PlayFlow Dashboard.
  1. Go to your project’s ConfigurationLobbies tab
  2. Create or edit a lobby configuration
  3. Click the Matchmaking tab
  4. Add and configure matchmaking modes

Traditional Configuration Examples

For tactical shooters like Counter-Strike or Valorant:
{
  "Competitive5v5": {
    "teams": 2,
    "playersPerTeam": 5,
    "timeout": 120,
    "allowBackfill": false,
    "matchmakingRules": [
      {
        "type": "difference",
        "field": "mmr",
        "maxDifference": 200
      },
      {
        "type": "intersection",
        "field": "regions"
      }
    ]
  }
}

Role-Based Configuration Examples

For games like League of Legends with strict role requirements:
{
  "RankedRoleQueue": {
    "teams": 2,
    "playersPerTeam": 5,
    "minPlayersPerTeam": 5,
    "maxPartySize": 2,
    "excludedFromQueue": ["Spectator", "Coach"],
    "teamComposition": {
      "team1": {
        "top": 1,
        "jungle": 1,
        "mid": 1,
        "adc": 1,
        "support": 1
      },
      "team2": {
        "top": 1,
        "jungle": 1,
        "mid": 1,
        "adc": 1,
        "support": 1
      }
    },
    "timeout": 90,
    "matchmakingRules": [
      {
        "type": "difference",
        "field": "mmr",
        "maxDifference": 150
      }
    ]
  }
}

Implementation Guide

Here are the steps to implement matchmaking in your game.

Step 1: Set Player Matchmaking Data

Players must set their matchmaking attributes before searching. This includes MMR, regions, and roles (for role-based modes).

Traditional Matchmaking Data

For traditional modes, set MMR and acceptable regions:
using System.Collections.Generic;
using PlayFlow;
using UnityEngine;

public class TraditionalMatchmaking : MonoBehaviour
{
    public void SetTraditionalMatchmakingData(int playerMMR)
    {
        var matchmakingData = new Dictionary<string, object>
        {
            { "mmr", playerMMR },
            { "regions", new List<string> { "us-west", "us-east", "eu-west" } }
        };

        PlayFlowLobbyManagerV2.Instance.UpdatePlayerState(matchmakingData,
            onSuccess: (lobby) => {
                Debug.Log("Player matchmaking data has been set.");
            },
            onError: (error) => {
                Debug.LogError($"Could not set matchmaking data: {error}");
            }
        );
    }
}

Role-Based Matchmaking Data

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.
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.matchmakingMode}' queue.");
                Debug.Log($"Matchmaking ticket: {lobby.matchmakingTicketId}");
                // 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.matchmakingMode}");
                // Show searching UI with role information
            },
            onError: (error) => {
                Debug.LogError($"Failed to start role-based matchmaking: {error}");
            }
        );
    }
}

Step 3: Handle Matchmaking Events

After starting the search, you need to listen for events to know what happens next.
  • OnMatchmakingStarted: Confirms you have entered the queue
  • OnMatchFound: The most important event! This fires when a match is found
  • OnMatchmakingCancelled: Fires if you cancel the search
  • OnMatchRunning: Fires when the server is ready with connection details
  • OnMatchServerDetailsReady: Provides full server details including team assignments
using Newtonsoft.Json;

void Start()
{
    var events = PlayFlowLobbyManagerV2.Instance.Events;

    events.OnMatchmakingStarted.AddListener(HandleMatchmakingStarted);
    events.OnMatchFound.AddListener(HandleMatchFound);
    events.OnMatchRunning.AddListener(HandleMatchRunning);
    events.OnMatchServerDetailsReady.AddListener(HandleServerDetailsReady);
    events.OnMatchmakingCancelled.AddListener(HandleMatchmakingCancelled);
}

private void HandleMatchmakingStarted(Lobby lobby)
{
    Debug.Log($"Entered queue for mode: {lobby.matchmakingMode}");
    Debug.Log($"Ticket ID: {lobby.matchmakingTicketId}");
}

private void HandleMatchFound(Lobby lobby)
{
    Debug.Log("Match has been found! A server is being prepared.");

    // For role-based matches, you can access matchmaking data
    if (lobby.matchmakingData != null)
    {
        Debug.Log($"Match data: {JsonConvert.SerializeObject(lobby.matchmakingData)}");
    }

    // Update your UI to "Connecting..." state
}

private void HandleMatchRunning(ConnectionInfo connectionInfo)
{
    Debug.Log($"Server ready at {connectionInfo.Ip}:{connectionInfo.Port}");

    // Connect your game client
    MyNetworkManager.Connect(connectionInfo.Ip, connectionInfo.Port);
}

private void HandleServerDetailsReady(List<PortMappingInfo> portMappings)
{
    Debug.Log("Full server details received.");

    var currentLobby = PlayFlowLobbyManagerV2.Instance.CurrentLobby;
    if (currentLobby?.gameServer?.custom_data != null)
    {
        var customData = currentLobby.gameServer.custom_data;

        // Access team information for role-based matches
        if (customData.ContainsKey("teams"))
        {
            var teams = customData["teams"] as List<object>;
            foreach (dynamic team in teams)
            {
                Debug.Log($"Team {team.team_id}:");
                foreach (dynamic lobbyInfo in team.lobbies)
                {
                    foreach (var playerEntry in lobbyInfo.player_states)
                    {
                        string playerId = playerEntry.Key;
                        dynamic playerState = playerEntry.Value;
                        Debug.Log($"  Player {playerId}: Role = {playerState.role}");
                    }
                }
            }
        }

        // Check which team you're on
        var myPlayerId = PlayFlowLobbyManagerV2.Instance.PlayerId;
        Debug.Log($"My player ID: {myPlayerId}");
    }
}

private void HandleMatchmakingCancelled(Lobby lobby)
{
    Debug.Log("Matchmaking was cancelled.");
    // Return to lobby screen
}
The host can cancel the matchmaking search at any time by calling CancelMatchmaking.
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}");
        }
    );
}

Advanced Features

Role Assignment Algorithm

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.

How Priority Assignment Works

1. Specificity First Players with fewer role options are assigned before flexible players:
// Assignment priority (highest to lowest):
Player A: ["tank"]                    // 1 option - HIGHEST PRIORITY
Player B: ["tank", "healer"]          // 2 options - medium priority
Player 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 first
Player2: ["support", "adc"]  // Joined lobby second

// Result:
Player1Gets "support" (first preference)
Player2Gets "adc" (support taken, gets second choice)

Real-World Example

For a MOBA team needing specific roles:
// Team needs: 1 top, 1 jungle, 1 mid, 1 adc, 1 support

// Players with their preferences:
Player1: ["mid", "top", "support"]     // Flexible
Player2: ["jungle"]                    // One-trick (gets priority!)
Player3: ["support", "adc"]            // Support main
Player4: ["mid"]                       // Mid only
Player5: ["top", "mid", "jungle"]      // Fill player

// Assignment result:
Player2jungle (most specific, only wants jungle)
Player4mid (most specific for mid)
Player3support (first preference available)
Player5top (mid/jungle taken, gets top)
Player1adc (forced to fill, but match succeeds!)

Setting Role Preferences in Code

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!

Spectator Support

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
        }
    }
}

Regional Matchmaking

Players can specify multiple acceptable regions. The system finds matches where players have overlapping regions.
// Specify regions in order of preference
var regionList = new List<string> { "us-east", "us-west", "eu-west" };
UpdatePlayerState(new Dictionary<string, object> {
    { "regions", regionList }
});

Party Support

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.inviteCode}");
            }
        );
    }

    public void QueueWithParty(string mode)
    {
        // Everyone in the lobby queues together
        PlayFlowLobbyManagerV2.Instance.FindMatch(mode);
    }
}

Troubleshooting

Common Issues

  • Verify all players have set required matchmaking data (MMR, regions, roles)
  • Check that players have overlapping regions in their regions field
  • For role-based modes, ensure you have the exact roles needed for team composition
  • Check dashboard configuration timeout settings
Ensure spectator roles are listed in the excludedFromQueue array in your dashboard configuration:
"excludedFromQueue": ["Spectator", "Observer"]
  • Players with single specific roles are prioritized over flexible players
  • Ensure role names match exactly with dashboard configuration
  • Check that total players match team requirements

Next Steps