Lesson 9: Matchmaking & Lobby Systems

Great work on combat systems! Now players can fight, but they need a way to find each other and prepare for battle. Matchmaking and lobby systems are the bridge between players wanting to play and actual gameplay. A well-designed matchmaking system ensures players find games quickly, while lobbies provide space for preparation, team formation, and social interaction before matches begin.

In this lesson, you'll learn how to build matchmaking systems that connect players efficiently, create lobby interfaces for pre-game preparation, implement team formation, and design queue systems that ensure fair and balanced matches. By the end, players will be able to find games and prepare for battle seamlessly.

What You'll Learn

By the end of this lesson, you'll be able to:

  • Design matchmaking systems that efficiently connect players
  • Create lobby interfaces for pre-game preparation and social interaction
  • Implement team formation and squad management
  • Build queue systems that match players based on skill and preferences
  • Handle matchmaking edge cases like disconnections and failed matches
  • Design player-ready systems to ensure all players are prepared
  • Implement region-based matching for optimal network performance
  • Create matchmaking analytics to monitor and improve the system

Why This Matters

Matchmaking and lobby systems enable:

  • Player Connection - Players can find games and other players easily
  • Fair Matches - Skill-based matching creates balanced gameplay
  • Social Interaction - Lobbies provide space for team coordination
  • Smooth Experience - Well-designed systems reduce wait times
  • Player Retention - Good matchmaking keeps players coming back
  • Scalability - Efficient systems handle growing player bases

Prerequisites

Before starting this lesson, make sure you have:

  • Completed all previous lessons in this course
  • Understanding of multiplayer networking (from Lesson 6)
  • Basic knowledge of UI systems
  • Familiarity with Unreal Engine's online subsystem
  • Understanding of player data and statistics

Step 1: Understanding Matchmaking Requirements

Before building matchmaking, understand what your game needs.

Matchmaking Goals

Define clear goals for your matchmaking system:

Primary Goals:

  • Fast Matchmaking - Players find games quickly (under 2 minutes)
  • Balanced Matches - Similar skill levels for competitive play
  • Full Lobbies - Matches start with maximum players
  • Regional Matching - Low latency connections

Secondary Goals:

  • Team Balancing - Fair team distribution
  • Preference Matching - Consider player preferences
  • Reconnection - Handle disconnections gracefully
  • Queue Management - Manage different game modes

Matchmaking Metrics

Track these metrics to measure success:

  • Average Queue Time - How long players wait
  • Match Quality - Skill balance in matches
  • Fill Rate - Percentage of full matches
  • Region Distribution - Geographic spread of players
  • Success Rate - Percentage of successful matches

Step 2: Design Matchmaking Architecture

Plan your matchmaking system architecture before implementation.

Matchmaking Flow

Design the player journey:

  1. Player Joins Queue - Player selects game mode and enters queue
  2. Matchmaking Search - System searches for suitable players
  3. Match Found - Players are grouped into a match
  4. Lobby Creation - Lobby is created for the match
  5. Player Preparation - Players customize loadouts, form teams
  6. Ready Check - All players confirm readiness
  7. Match Start - Game begins when all players ready

Matchmaking Components

Break matchmaking into components:

Queue Manager:

  • Manages player queues for different game modes
  • Tracks queue times and player counts
  • Handles queue priorities

Matchmaker:

  • Searches for suitable players
  • Applies matching criteria (skill, region, preferences)
  • Forms matches when criteria met

Lobby Manager:

  • Creates and manages game lobbies
  • Handles player joining/leaving
  • Manages lobby state

Session Manager:

  • Tracks active game sessions
  • Handles match transitions
  • Manages server allocation

Step 3: Implement Basic Queue System

Start with a simple queue system that matches players.

Queue Data Structure

Design queue data structures:

// Queue entry for a player
USTRUCT()
struct FQueueEntry
{
    GENERATED_BODY()

    UPROPERTY()
    FString PlayerID;

    UPROPERTY()
    float SkillRating;

    UPROPERTY()
    FString Region;

    UPROPERTY()
    float QueueTime;

    UPROPERTY()
    TArray<FString> PreferredTeammates;
};

// Queue manager
UCLASS()
class ABattleRoyaleGameMode : public AGameModeBase
{
    GENERATED_BODY()

public:
    // Add player to queue
    UFUNCTION(BlueprintCallable)
    void JoinQueue(const FString& PlayerID, float SkillRating, const FString& Region);

    // Remove player from queue
    UFUNCTION(BlueprintCallable)
    void LeaveQueue(const FString& PlayerID);

    // Matchmaking tick
    virtual void Tick(float DeltaTime) override;

private:
    UPROPERTY()
    TArray<FQueueEntry> Queue;

    UPROPERTY()
    int32 TargetPlayersPerMatch = 100;

    // Matchmaking logic
    void ProcessMatchmaking();
    bool CanMatchPlayers(const FQueueEntry& Player1, const FQueueEntry& Player2);
    void CreateMatch(const TArray<FQueueEntry>& MatchedPlayers);
};

Basic Matching Logic

Implement simple matching:

void ABattleRoyaleGameMode::ProcessMatchmaking()
{
    // Sort queue by skill rating
    Queue.Sort([](const FQueueEntry& A, const FQueueEntry& B) {
        return A.SkillRating < B.SkillRating;
    });

    // Try to form matches
    TArray<FQueueEntry> CurrentMatch;

    for (const FQueueEntry& Entry : Queue)
    {
        // Check if player can join current match
        if (CurrentMatch.Num() < TargetPlayersPerMatch)
        {
            // Check skill range (within 200 points)
            bool CanJoin = true;
            if (CurrentMatch.Num() > 0)
            {
                float MinSkill = CurrentMatch[0].SkillRating;
                float MaxSkill = CurrentMatch.Last().SkillRating;

                if (FMath::Abs(Entry.SkillRating - MinSkill) > 200 ||
                    FMath::Abs(Entry.SkillRating - MaxSkill) > 200)
                {
                    CanJoin = false;
                }
            }

            // Check region (prefer same region)
            if (CurrentMatch.Num() > 0 && Entry.Region != CurrentMatch[0].Region)
            {
                // Allow different regions if queue is getting long
                if (Entry.QueueTime < 60.0f) // Less than 60 seconds
                {
                    CanJoin = false;
                }
            }

            if (CanJoin)
            {
                CurrentMatch.Add(Entry);
            }
        }

        // Create match when full
        if (CurrentMatch.Num() >= TargetPlayersPerMatch)
        {
            CreateMatch(CurrentMatch);
            CurrentMatch.Empty();
        }
    }
}

Step 4: Create Lobby System

Build lobbies where players prepare before matches.

Lobby Structure

Design lobby data and management:

USTRUCT()
struct FLobbyPlayer
{
    GENERATED_BODY()

    UPROPERTY()
    FString PlayerID;

    UPROPERTY()
    FString PlayerName;

    UPROPERTY()
    bool bIsReady = false;

    UPROPERTY()
    int32 TeamIndex = -1;

    UPROPERTY()
    TArray<FString> Loadout;
};

UCLASS()
class ALobbyGameMode : public AGameModeBase
{
    GENERATED_BODY()

public:
    // Join lobby
    UFUNCTION(BlueprintCallable)
    void JoinLobby(const FString& PlayerID, const FString& PlayerName);

    // Leave lobby
    UFUNCTION(BlueprintCallable)
    void LeaveLobby(const FString& PlayerID);

    // Set player ready
    UFUNCTION(BlueprintCallable)
    void SetPlayerReady(const FString& PlayerID, bool bReady);

    // Start match when all ready
    UFUNCTION(BlueprintCallable)
    void CheckAllPlayersReady();

private:
    UPROPERTY()
    TArray<FLobbyPlayer> LobbyPlayers;

    UPROPERTY()
    int32 MaxPlayers = 100;

    UPROPERTY()
    float LobbyTimer = 60.0f; // 60 second lobby timer

    // Lobby management
    void UpdateLobbyUI();
    void StartMatch();
    void HandlePlayerDisconnect(const FString& PlayerID);
};

Lobby UI

Create lobby interface for players:

UCLASS()
class ULobbyWidget : public UUserWidget
{
    GENERATED_BODY()

public:
    // Update player list
    UFUNCTION(BlueprintImplementableEvent)
    void UpdatePlayerList(const TArray<FLobbyPlayer>& Players);

    // Update ready status
    UFUNCTION(BlueprintImplementableEvent)
    void UpdateReadyStatus(int32 ReadyCount, int32 TotalPlayers);

    // Update lobby timer
    UFUNCTION(BlueprintImplementableEvent)
    void UpdateTimer(float RemainingTime);

    // Show match starting countdown
    UFUNCTION(BlueprintImplementableEvent)
    void ShowMatchStarting(int32 Countdown);
};

Step 5: Implement Skill-Based Matching

Match players based on skill level for balanced games.

Skill Rating System

Implement skill rating:

USTRUCT()
struct FPlayerStats
{
    GENERATED_BODY()

    UPROPERTY()
    float SkillRating = 1000.0f; // Starting rating

    UPROPERTY()
    int32 Wins = 0;

    UPROPERTY()
    int32 Kills = 0;

    UPROPERTY()
    int32 Deaths = 0;

    UPROPERTY()
    float AveragePlacement = 50.0f; // Average placement (1-100)

    // Calculate skill rating
    void UpdateSkillRating(int32 Placement, int32 Kills, bool bWon)
    {
        // Elo-like rating system
        float ExpectedScore = CalculateExpectedScore(SkillRating);
        float ActualScore = CalculateActualScore(Placement, Kills, bWon);

        float RatingChange = 32.0f * (ActualScore - ExpectedScore);
        SkillRating += RatingChange;

        // Update stats
        if (bWon) Wins++;
        Kills += Kills;
        Deaths++;
        AveragePlacement = (AveragePlacement + Placement) / 2.0f;
    }

private:
    float CalculateExpectedScore(float Rating)
    {
        // Simplified expected score calculation
        return Rating / 2000.0f;
    }

    float CalculateActualScore(int32 Placement, int32 Kills, bool bWon)
    {
        float Score = (101.0f - Placement) / 100.0f; // Placement score
        Score += Kills * 0.1f; // Kill bonus
        if (bWon) Score += 0.5f; // Win bonus
        return FMath::Clamp(Score, 0.0f, 1.0f);
    }
};

Skill-Based Matching

Match players with similar skill:

bool ABattleRoyaleGameMode::CanMatchPlayers(const FQueueEntry& Player1, const FQueueEntry& Player2)
{
    // Skill range check
    float SkillDifference = FMath::Abs(Player1.SkillRating - Player2.SkillRating);

    // Expand skill range based on queue time
    float MaxSkillDifference = 200.0f; // Base range
    MaxSkillDifference += Player1.QueueTime * 2.0f; // Expand over time
    MaxSkillDifference += Player2.QueueTime * 2.0f;

    if (SkillDifference > MaxSkillDifference)
    {
        return false;
    }

    // Region preference
    if (Player1.Region == Player2.Region)
    {
        return true; // Same region, good match
    }

    // Different regions okay if queue time is long
    if (Player1.QueueTime > 30.0f || Player2.QueueTime > 30.0f)
    {
        return true;
    }

    return false;
}

Step 6: Build Team Formation System

Allow players to form teams and squads.

Team Management

Implement team formation:

USTRUCT()
struct FTeam
{
    GENERATED_BODY()

    UPROPERTY()
    int32 TeamID;

    UPROPERTY()
    TArray<FString> PlayerIDs;

    UPROPERTY()
    FString TeamName;

    UPROPERTY()
    FLinearColor TeamColor;

    bool IsFull() const { return PlayerIDs.Num() >= 4; } // Max 4 players per team
    void AddPlayer(const FString& PlayerID) { PlayerIDs.AddUnique(PlayerID); }
    void RemovePlayer(const FString& PlayerID) { PlayerIDs.Remove(PlayerID); }
};

UCLASS()
class ALobbyGameMode : public AGameModeBase
{
    GENERATED_BODY()

public:
    // Create team
    UFUNCTION(BlueprintCallable)
    int32 CreateTeam(const FString& TeamName);

    // Join team
    UFUNCTION(BlueprintCallable)
    bool JoinTeam(const FString& PlayerID, int32 TeamID);

    // Leave team
    UFUNCTION(BlueprintCallable)
    void LeaveTeam(const FString& PlayerID);

    // Auto-assign teams
    UFUNCTION(BlueprintCallable)
    void AutoAssignTeams();

private:
    UPROPERTY()
    TArray<FTeam> Teams;

    UPROPERTY()
    int32 NextTeamID = 1;

    // Team balancing
    void BalanceTeams();
    float CalculateTeamSkill(const FTeam& Team);
};

Team Balancing

Balance teams by skill:

void ALobbyGameMode::AutoAssignTeams()
{
    // Get all unassigned players
    TArray<FLobbyPlayer> UnassignedPlayers;
    for (FLobbyPlayer& Player : LobbyPlayers)
    {
        if (Player.TeamIndex == -1)
        {
            UnassignedPlayers.Add(Player);
        }
    }

    // Sort by skill (highest first)
    UnassignedPlayers.Sort([](const FLobbyPlayer& A, const FLobbyPlayer& B) {
        // Get player stats and sort by skill rating
        return GetPlayerSkillRating(A.PlayerID) > GetPlayerSkillRating(B.PlayerID);
    });

    // Distribute players across teams
    int32 TeamIndex = 0;
    for (FLobbyPlayer& Player : UnassignedPlayers)
    {
        // Ensure teams exist
        while (Teams.Num() <= TeamIndex)
        {
            FTeam NewTeam;
            NewTeam.TeamID = NextTeamID++;
            NewTeam.TeamName = FString::Printf(TEXT("Team %d"), NewTeam.TeamID);
            Teams.Add(NewTeam);
        }

        // Add to team
        Teams[TeamIndex].AddPlayer(Player.PlayerID);
        Player.TeamIndex = TeamIndex;

        // Round-robin assignment
        TeamIndex = (TeamIndex + 1) % Teams.Num();
    }

    BalanceTeams();
}

void ALobbyGameMode::BalanceTeams()
{
    // Calculate team skills
    TArray<float> TeamSkills;
    for (const FTeam& Team : Teams)
    {
        TeamSkills.Add(CalculateTeamSkill(Team));
    }

    // Find teams that need balancing
    float AverageSkill = 0.0f;
    for (float Skill : TeamSkills)
    {
        AverageSkill += Skill;
    }
    AverageSkill /= TeamSkills.Num();

    // Move players to balance (simplified)
    // In production, use more sophisticated balancing algorithms
}

Step 7: Handle Matchmaking Edge Cases

Prepare for common matchmaking problems.

Player Disconnection

Handle players leaving during matchmaking:

void ABattleRoyaleGameMode::HandlePlayerDisconnect(const FString& PlayerID)
{
    // Remove from queue
    Queue.RemoveAll([PlayerID](const FQueueEntry& Entry) {
        return Entry.PlayerID == PlayerID;
    });

    // Remove from lobby
    LobbyPlayers.RemoveAll([PlayerID](const FLobbyPlayer& Player) {
        return Player.PlayerID == PlayerID;
    });

    // Remove from teams
    for (FTeam& Team : Teams)
    {
        Team.RemovePlayer(PlayerID);
    }

    // Check if match can still proceed
    if (LobbyPlayers.Num() < MinPlayersRequired)
    {
        // Cancel match or find replacement players
        HandleInsufficientPlayers();
    }
}

Failed Match Creation

Handle match creation failures:

void ABattleRoyaleGameMode::CreateMatch(const TArray<FQueueEntry>& MatchedPlayers)
{
    // Try to allocate server
    FString ServerURL = AllocateGameServer();

    if (ServerURL.IsEmpty())
    {
        // Server allocation failed
        HandleMatchCreationFailure(MatchedPlayers);
        return;
    }

    // Create lobby
    ALobbyGameMode* Lobby = CreateLobby(MatchedPlayers);

    if (!Lobby)
    {
        // Lobby creation failed
        HandleMatchCreationFailure(MatchedPlayers);
        return;
    }

    // Send players to lobby
    for (const FQueueEntry& Entry : MatchedPlayers)
    {
        SendPlayerToLobby(Entry.PlayerID, Lobby->GetLobbyID());
    }

    // Remove from queue
    for (const FQueueEntry& Entry : MatchedPlayers)
    {
        LeaveQueue(Entry.PlayerID);
    }
}

void ABattleRoyaleGameMode::HandleMatchCreationFailure(const TArray<FQueueEntry>& MatchedPlayers)
{
    // Return players to queue with priority
    for (const FQueueEntry& Entry : MatchedPlayers)
    {
        // Add priority boost
        FQueueEntry PriorityEntry = Entry;
        PriorityEntry.QueueTime = 0.0f; // Reset queue time
        Queue.Insert(PriorityEntry, 0); // Add to front of queue
    }

    // Notify players
    NotifyPlayersMatchFailed(MatchedPlayers);
}

Region-Based Matching

Match players by region for better latency:

FString ABattleRoyaleGameMode::GetPlayerRegion(const FString& PlayerID)
{
    // Get player's IP or location
    FString PlayerIP = GetPlayerIP(PlayerID);

    // Determine region from IP (simplified)
    // In production, use geolocation service
    if (PlayerIP.StartsWith("192.168.") || PlayerIP.StartsWith("10."))
    {
        return "US-East"; // Example
    }

    return "US-West"; // Default
}

bool ABattleRoyaleGameMode::IsRegionCompatible(const FString& Region1, const FString& Region2)
{
    // Same region always compatible
    if (Region1 == Region2)
    {
        return true;
    }

    // Adjacent regions sometimes compatible
    TMap<FString, TArray<FString>> AdjacentRegions;
    AdjacentRegions.Add("US-East", {"US-West", "EU"});
    AdjacentRegions.Add("US-West", {"US-East", "Asia"});

    if (AdjacentRegions.Contains(Region1))
    {
        return AdjacentRegions[Region1].Contains(Region2);
    }

    return false;
}

Step 8: Create Lobby UI

Design intuitive lobby interface for players.

Lobby Widget Design

Create lobby UI components:

UCLASS()
class ULobbyWidget : public UUserWidget
{
    GENERATED_BODY()

public:
    virtual void NativeConstruct() override;

    // Player list display
    UPROPERTY(meta = (BindWidget))
    class UScrollBox* PlayerListBox;

    // Ready button
    UPROPERTY(meta = (BindWidget))
    class UButton* ReadyButton;

    // Team selection
    UPROPERTY(meta = (BindWidget))
    class UComboBoxString* TeamSelection;

    // Lobby timer
    UPROPERTY(meta = (BindWidget))
    class UTextBlock* TimerText;

    // Ready count
    UPROPERTY(meta = (BindWidget))
    class UTextBlock* ReadyCountText;

    UFUNCTION()
    void OnReadyButtonClicked();

    UFUNCTION()
    void OnTeamSelectionChanged(FString SelectedItem, ESelectInfo::Type SelectionType);

    // Update lobby display
    void UpdateLobbyDisplay(const TArray<FLobbyPlayer>& Players, int32 ReadyCount, float RemainingTime);
};

Lobby UI Implementation

Implement lobby UI functionality:

void ULobbyWidget::NativeConstruct()
{
    Super::NativeConstruct();

    if (ReadyButton)
    {
        ReadyButton->OnClicked.AddDynamic(this, &ULobbyWidget::OnReadyButtonClicked);
    }

    if (TeamSelection)
    {
        TeamSelection->OnSelectionChanged.AddDynamic(this, &ULobbyWidget::OnTeamSelectionChanged);
    }
}

void ULobbyWidget::OnReadyButtonClicked()
{
    // Toggle ready state
    bool bCurrentReady = GetPlayerReadyState();
    SetPlayerReady(!bCurrentReady);

    // Update button text
    if (ReadyButton)
    {
        ReadyButton->SetIsEnabled(!bCurrentReady);
    }
}

void ULobbyWidget::UpdateLobbyDisplay(const TArray<FLobbyPlayer>& Players, int32 ReadyCount, float RemainingTime)
{
    // Update player list
    if (PlayerListBox)
    {
        PlayerListBox->ClearChildren();

        for (const FLobbyPlayer& Player : Players)
        {
            UPlayerListEntryWidget* EntryWidget = CreateWidget<UPlayerListEntryWidget>(this, PlayerEntryWidgetClass);
            EntryWidget->SetPlayerData(Player);
            PlayerListBox->AddChild(EntryWidget);
        }
    }

    // Update ready count
    if (ReadyCountText)
    {
        ReadyCountText->SetText(FText::FromString(FString::Printf(TEXT("%d / %d Ready"), ReadyCount, Players.Num())));
    }

    // Update timer
    if (TimerText)
    {
        int32 Minutes = FMath::FloorToInt(RemainingTime / 60.0f);
        int32 Seconds = FMath::FloorToInt(RemainingTime) % 60;
        TimerText->SetText(FText::FromString(FString::Printf(TEXT("%02d:%02d"), Minutes, Seconds)));
    }
}

Step 9: Implement Ready System

Ensure all players are prepared before match starts.

Ready Check Logic

Implement ready system:

void ALobbyGameMode::SetPlayerReady(const FString& PlayerID, bool bReady)
{
    for (FLobbyPlayer& Player : LobbyPlayers)
    {
        if (Player.PlayerID == PlayerID)
        {
            Player.bIsReady = bReady;
            break;
        }
    }

    // Update UI
    UpdateLobbyUI();

    // Check if all ready
    CheckAllPlayersReady();
}

void ALobbyGameMode::CheckAllPlayersReady()
{
    int32 ReadyCount = 0;
    for (const FLobbyPlayer& Player : LobbyPlayers)
    {
        if (Player.bIsReady)
        {
            ReadyCount++;
        }
    }

    // All players ready
    if (ReadyCount == LobbyPlayers.Num() && LobbyPlayers.Num() >= MinPlayersRequired)
    {
        // Start countdown
        StartMatchCountdown();
    }
    else if (LobbyTimer <= 0.0f && LobbyPlayers.Num() >= MinPlayersRequired)
    {
        // Timer expired, start anyway
        StartMatch();
    }
}

void ALobbyGameMode::StartMatchCountdown()
{
    // 10 second countdown
    GetWorldTimerManager().SetTimer(CountdownTimer, this, &ALobbyGameMode::OnCountdownTick, 1.0f, true);
    CountdownSeconds = 10;
}

void ALobbyGameMode::OnCountdownTick()
{
    CountdownSeconds--;

    // Update UI
    if (LobbyWidget)
    {
        LobbyWidget->ShowMatchStarting(CountdownSeconds);
    }

    if (CountdownSeconds <= 0)
    {
        GetWorldTimerManager().ClearTimer(CountdownTimer);
        StartMatch();
    }
}

Step 10: Matchmaking Analytics

Track matchmaking performance to improve the system.

Analytics Tracking

Implement analytics:

USTRUCT()
struct FMatchmakingMetrics
{
    GENERATED_BODY()

    UPROPERTY()
    float AverageQueueTime = 0.0f;

    UPROPERTY()
    int32 TotalMatchesCreated = 0;

    UPROPERTY()
    int32 FailedMatches = 0;

    UPROPERTY()
    float AverageSkillDifference = 0.0f;

    UPROPERTY()
    TMap<FString, int32> RegionDistribution;

    // Record matchmaking event
    void RecordMatchCreated(const TArray<FQueueEntry>& Players, float QueueTime)
    {
        TotalMatchesCreated++;

        // Calculate average queue time
        float TotalQueueTime = 0.0f;
        for (const FQueueEntry& Entry : Players)
        {
            TotalQueueTime += Entry.QueueTime;
        }
        AverageQueueTime = (AverageQueueTime + (TotalQueueTime / Players.Num())) / 2.0f;

        // Calculate skill difference
        float MinSkill = Players[0].SkillRating;
        float MaxSkill = Players[0].SkillRating;
        for (const FQueueEntry& Entry : Players)
        {
            MinSkill = FMath::Min(MinSkill, Entry.SkillRating);
            MaxSkill = FMath::Max(MaxSkill, Entry.SkillRating);
        }
        float SkillDiff = MaxSkill - MinSkill;
        AverageSkillDifference = (AverageSkillDifference + SkillDiff) / 2.0f;

        // Track regions
        for (const FQueueEntry& Entry : Players)
        {
            RegionDistribution.FindOrAdd(Entry.Region)++;
        }
    }
};

Mini Challenge: Build Matchmaking System

Create a basic matchmaking system:

  1. Implement queue system - Players can join and leave queue
  2. Create matching logic - Match players based on skill and region
  3. Build lobby - Create lobby when match found
  4. Add ready system - Players confirm readiness
  5. Start match - Transition to game when all ready

Success Criteria:

  • Players can join queue
  • Matches form within 2 minutes
  • Lobby displays all players
  • Ready system works correctly
  • Match starts when all ready

Pro Tips

Tip 1: Expand Matchmaking Criteria Over Time

Start with strict matching, then expand criteria as queue time increases:

float GetMaxSkillDifference(float QueueTime)
{
    // Expand skill range over time
    float BaseRange = 200.0f;
    float ExpandedRange = BaseRange + (QueueTime * 2.0f);
    return FMath::Min(ExpandedRange, 1000.0f); // Cap at 1000
}

Tip 2: Prioritize Full Matches

Prefer creating full matches over partial matches:

// Wait for full match if queue is filling quickly
if (Queue.Num() > TargetPlayersPerMatch * 0.8f)
{
    // Wait a bit longer for full match
    return;
}

Tip 3: Handle Peak Times

Adjust matchmaking for peak player counts:

int32 GetTargetPlayersPerMatch()
{
    int32 QueueSize = Queue.Num();

    // Reduce match size during peak times to start matches faster
    if (QueueSize > 500)
    {
        return 80; // Smaller matches during peak
    }

    return 100; // Normal match size
}

Tip 4: Implement Match Cancellation

Allow players to cancel matchmaking:

UFUNCTION(BlueprintCallable)
void CancelMatchmaking(const FString& PlayerID)
{
    LeaveQueue(PlayerID);
    NotifyPlayerMatchmakingCancelled(PlayerID);
}

Tip 5: Add Matchmaking Preferences

Let players set preferences:

USTRUCT()
struct FMatchmakingPreferences
{
    GENERATED_BODY()

    UPROPERTY()
    bool bPreferSameRegion = true;

    UPROPERTY()
    int32 MaxQueueTime = 120; // 2 minutes

    UPROPERTY()
    bool bAllowSkillVariance = true;
};

Common Mistakes to Avoid

Mistake 1: Too Strict Matching

Problem: Players wait forever for perfect matches.

Solution: Expand matching criteria over time to ensure reasonable queue times.

Mistake 2: Ignoring Region

Problem: High latency matches frustrate players.

Solution: Prioritize regional matching, expand only when necessary.

Mistake 3: No Ready System

Problem: Matches start with unprepared players.

Solution: Implement ready check to ensure all players are prepared.

Mistake 4: Poor Lobby UX

Problem: Confusing lobby interface frustrates players.

Solution: Design clear, intuitive lobby UI with obvious actions.

Mistake 5: Not Handling Disconnections

Problem: Disconnected players break matchmaking.

Solution: Handle disconnections gracefully, find replacements or adjust match size.

Troubleshooting

Issue: Long Queue Times

Symptoms: Players wait too long for matches.

Solutions:

  • Expand skill matching range
  • Reduce required players per match
  • Combine different regions
  • Add bot players (if appropriate)

Issue: Unbalanced Matches

Symptoms: Matches have wide skill gaps.

Solutions:

  • Improve skill rating system
  • Tighter skill matching
  • Better team balancing
  • Track and analyze match quality

Issue: Lobby Not Starting

Symptoms: Lobby created but match doesn't start.

Solutions:

  • Check ready system logic
  • Verify minimum player count
  • Handle edge cases (disconnections, etc.)
  • Add timeout to force start

Issue: Region Mismatches

Symptoms: Players matched across regions with high latency.

Solutions:

  • Improve region detection
  • Stricter regional matching
  • Show region in lobby
  • Allow region selection

Key Takeaways

  • Matchmaking connects players efficiently and fairly
  • Lobby systems provide preparation space before matches
  • Skill-based matching creates balanced, competitive games
  • Team formation allows social interaction and coordination
  • Ready systems ensure all players are prepared
  • Region matching improves network performance
  • Analytics help improve matchmaking over time
  • Handle edge cases for robust matchmaking

Matchmaking and lobby systems are critical for multiplayer games. Well-designed systems create positive first impressions and keep players engaged. Start with simple systems and iterate based on player feedback and analytics.

What's Next?

Excellent work on matchmaking! In the next lesson, you'll learn how to:

  • Design UI/UX for battle royale games
  • Create intuitive interfaces for inventory, map, and HUD
  • Implement responsive design for different screen sizes
  • Optimize UI performance for smooth gameplay

Ready to design interfaces? Continue to Lesson 10: UI/UX Design & Implementation to learn how to create professional game interfaces.

Related Resources

FAQ

Q: How long should matchmaking take? A: Aim for under 2 minutes average. Expand matching criteria if queue times exceed this.

Q: Should I match by skill or randomly? A: Skill-based matching creates better games, but expand criteria over time to ensure reasonable queue times.

Q: How many players should be in a battle royale match? A: Typically 100 players, but you can adjust based on your game design and player count.

Q: What if not enough players are in queue? A: Options include reducing match size, using bots, expanding matching criteria, or showing estimated wait time.

Q: How do I handle players leaving during matchmaking? A: Remove them from queue, check if match can proceed, and find replacements if needed.

Q: Should I show queue position to players? A: Yes, showing position and estimated wait time improves player experience and reduces frustration.

Q: How do I test matchmaking with few players? A: Use bots, reduce match size, or create test accounts to simulate multiple players.

Q: What's the best way to balance teams? A: Distribute players by skill rating across teams, ensuring similar total team skill levels.


Ready to match players? Start by implementing a basic queue system, then add skill-based matching and lobby functionality. Your matchmaking system will evolve as you gather player feedback and analytics.