Powrót do Bloga

Inżynieria hybrydowego matchmakingu: Jak podział kolejek w Call of Duty ujawnia architekturę współczesnego matchmakingu w grach

Opublikowano 24 lipca 2026
Inżynieria hybrydowego matchmakingu: Jak podział kolejek w Call of Duty ujawnia architekturę współczesnego matchmakingu w grach

W skrócie

Wdrożenie podziału kolejek matchmakingowych w grach wieloosobowych wymaga zaawansowanej architektury backendowej zdolnej do radzenia sobie z rywalizacją o blokady, opóźnieniami sieciowymi oraz fragmentacją graczy. Kluczem do sukcesu jest optymalizacja trilematu obejmującego opóźnienie (RTT), wariancję umiejętności (ΔMMR) oraz czas oczekiwania w kolejce (T_queue) poprzez algorytmy dynamicznego rozszerzania reguł. Platformy takie jak horizOn eliminują konieczność samodzielnego budowania skomplikowanej infrastruktury, dostarczając gotowe, wysoce wydajne rozwiązania do matchmakingu i orkiestracji serwerów dedykowanych.

Gdy Activision ogłosiło, że Call of Duty: Black Ops 7 rozdzieli społeczność graczy na trzy osobne kolejki matchmakingu — Skill-Based Matchmaking (SBMM), klasyczną Connection-Based oraz Hybrid — sprawiło to, że wieloletnia dyskusja z zakresu inżynierii backendowej znalazła się w centrum uwagi. W przypadku tytułów rywalizacyjnych decyzja o tym, jak łączyć graczy w pary, to nie tylko kwestia game designu; to złożony problem z zakresu architektury matchmakingu w grach (game matchmaking architecture), który balansuje submilisekundowe ograniczenia sieciowe, matematyczną wariancję umiejętności, fragmentację poola graczy oraz koszty chmury obliczeniowej.

Podział systemu matchmakingu na wiele osobnych kolejek wydaje się z pozoru prostą funkcją dostosowaną do preferencji graczy. W rzeczywistości podwaja lub potraja to narzut inżynieryjny na infrastrukturę backendową. Kiedy dzielisz jednocześnie aktywnych graczy (CCU) na osobne kolejki, gęstość ticketów gwałtownie spada, czas oczekiwania w kolejce (queue time) rośnie wykładniczo w regionach o niskiej gęstości, a algorytmy alokacji serwerów zmagają się ze zwiększoną rotacją (churn).

W tym artykule przeanalizujemy techniczne kompromisy między SBMM a matchmakingiem zorientowanym na połączenie (connection-first), dokonamy sekcji matematyki stojącej za dynamicznym rozszerzaniem kolejki hybrydowej, przeanalizujemy kod backendowy C# do przetwarzania ticketów oraz zbadamy, jak budować elastyczne poole matchmakingowe, które skalują się bezproblemowo.


Nienaruszalne trilema matchmakingu

Każda nowoczesna architektura matchmakingu w grach musi rozwiązać problem optymalizacji z ograniczeniami związany z trzema konkurującymi zmiennymi:

  1. Opóźnienie (RTT): Czas podróży pakietu w obie strony (round-trip time) między klientem gracza a zaalokowaną instancją dedicated servera (mierzony w milisekundach).
  2. Różnica umiejętności ($\Delta$MMR): Matematyczna przepaść w odzwierciedleniu umiejętności (MMR, Elo lub TrueSkill) między graczami w danej lobby.
  3. Czas w kolejce ($T_{queue}$): Całkowity czas, jaki gracz spędza na bezczynnym oczekiwaniu w kolejce, zanim prawidłowy ticket meczowy przejdzie w aktywną alokację serwera.
                    Latency (RTT)
                      /       \
                     /         \
                    /   Ideal   \
                   /    Match    \
                  /               \
Skill Delta (ΔMMR) --------------- Queue Duration (T_queue)

Możesz łatwo zoptymalizować dwie z tych zmiennych kosztem trzeciej:

  • Niskie opóźnienie + Niska różnica umiejętności: Skutkuje długim czasem oczekiwania w kolejce, ponieważ silnik musi szukać rzadkich, idealnie dobranych pod kątem umiejętności graczy, którzy mieszkają blisko tego samego centrum danych w chmurze (datacenter).
  • Krótki czas w kolejce + Niska różnica umiejętności: Skutkuje wysokim opóźnieniem (lagiem), ponieważ matchmaker musi rozszerzyć promień wyszukiwania geograficznego na cały świat, aby znaleźć przeciwników o równym poziomie.
  • Krótki czas w kolejce + Niskie opóźnienie: Skutkuje dużą wariancją umiejętności (klasyczne doświadczenie meczów publicznych typu "connection-first"), ponieważ matchmaker natychmiast dobiera najbliższych dostępnych klientów, niezależnie od ich statystyk.

Gdy tytuł taki jak Black Ops 7 wprowadza trzy osobne tryby kolejek, zmusza leżącą u podstaw architekturę matchmakingu do utrzymywania trzech równoległych pętli ewaluacji reguł na zdefragmentowanych poolach pamięci.

Jeśli liczba jednocześnie zalogowanych użytkowników (CCU) w określonym regionie — na przykład w Ameryce Południowej o 4 rano — spadnie poniżej 2000 aktywnych graczy, podział tych graczy na trzy osobne poole zmniejsza lokalną gęstość w każdej kolejce do zaledwie kilkuset osób. W rezultacie kolejki typu connection-first nie potrafią znaleźć dedicated serverów o niskim pingu, a kolejki SBMM utykają w miejscu bez końca.


Dyskurs SBMM vs. Ping-First vs. Kolejki hybrydowe

Aby zbudować backend zdolny do obsługi milionów ticketów meczowych, musisz najpierw zrozumieć, jak każdy model architektoniczny działa pod maską.

1. Architektura oparta na połączeniu (Ping-First)

W silniku ping-first macierze umiejętności są całkowicie drugorzędne lub pomijane. Głównym celem jest minimalizacja degradacji sieci (jitter, utrata pakietów, wysokie RTT).

  • Testowanie pingu klienta (Client Ping Probing): Po wejściu do kolejki klient gry wysyła pakiety ping ICMP lub UDP do serii regionalnych bramek brzegowych (e.g., us-east-1, eu-central-1, ap-southeast-1).
  • Generowanie wektora pingu: Klient tworzy wektor opóźnień: [ us-east: 24ms, us-west: 78ms, eu-central: 142ms ] i dołącza go do payloadu matchmakingowego.
  • Indeksowanie przestrzenne: Matchmaker grupuje graczy ściśle według haszy regionów na podstawie akceptowalnych progów opóźnienia (e.g., $RTT < 50ms$).

Ponieważ to topologia sieci dyktuje tworzenie meczu, koszyki wyszukiwania graczy są przewidywalne, co pozwala na rozwiązywanie ticketów w czasie $O(1)$ przy użyciu prostych kolejek przestrzennych FIFO (First-In, First-Out).

2. Architektura Skill-Based Matchmaking (SBMM)

SBMM stawia na pierwszym miejscu sprawiedliwość meczu, modelując umiejętności graczy za pomocą wielowymiarowych rozkładów Gaussa (np. TrueSkill 2) lub zmodyfikowanych wariantów Elo. Kluczowe dane wejściowe obejmują stosunek wygranych do przegranych (win/loss ratio), zabójstw do zgonów (K/D ratio), obrażenia na minutę oraz niedawną trajektorię wyników.

  • Ewaluacja odległości: Matchmaker oblicza odległość euklidesową lub Mahalanobisa między wektorami kandydatów w $N$-wymiarowej przestrzeni umiejętności.
  • Koszt sortowania: Matchmakery nie mogą polegać na prostych kolejkach FIFO. Muszą utrzymywać posortowane zbiory lub drzewa przestrzenne (np. KD-trees) profili ticketów, aby szybko znajdować kandydatów w granicach akceptowalnej wariancji umiejętności $\sigma$.
  • Zwężanie przestrzeni wyszukiwania: Wraz ze wzrostem poziomu umiejętności (np. top 0.5% graczy) pula kwalifikujących się kandydatów drastcznie maleje. Zmusza to system do przetrzymywania ticketów w nieskończoność lub powolnego łagodzenia parametrów surowości wymagań.

3. Dynamiczna architektura hybrydowa

Zamiast zmuszać graczy do sztywno skonfigurowanych wyborów kolejek, nowoczesne produkcyjne backendy często wdrażają model Dynamic Hybrid. W tej konfiguracji każdy ticket meczowy rozpoczyna pracę z rygorystycznymi ograniczeniami SBMM i niskiego opóźnienia. W miarę wzrostu $T_{queue}$ funkcja wygaszania reguł (rule decay) stale rozszerza dopuszczalną różnicę umiejętności ($\Delta MMR$) i limit opóźnienia ($RTT_{max}$)...

$$\Delta MMR_{allowed}(t) = \Delta MMR_{base} + \alpha \cdot t^{\gamma}$$

$$RTT_{allowed}(t) = \min\left(RTT_{max_cap}, RTT_{base} + \beta \cdot \lfloor t / \Delta t_{step} \rfloor\right)$$

Gdzie $\alpha$ i $\beta$ to współczynniki rozszerzania, $\gamma$ kontroluje stromość krzywej wykładniczej, a $t$ to upływający czas oczekiwania w kolejce w sekundach.

Dzięki wykorzystaniu dynamicznego łagodzenia kryteriów zapobiegasz nieskończonemu zawieszaniu się w kolejce, jednocześnie utrzymując wysoką jakość meczów w okresach szczytowego CCU.


Implementacja kodu: Silnik dynamicznego rozszerzania reguł

Poniżej znajduje się przetestowana w bojach implementacja silnika dynamicznego rozszerzania reguł matchmakingu w C#. Usługa ta ocenia przychodzące tickety pod kątem aktywnych pooli kolejek, obliczając w czasie rzeczywistym macierze kompatybilności pingu oraz dynamiczne limity wariancji MMR.

using System;
using System.Collections.Generic;
using System.Linq;

namespace Horizon.Matchmaking.Engine
{
    public class MatchmakingTicket
    {
        public string TicketId { get; set; } = Guid.NewGuid().ToString();
        public string PlayerId { get; set; }
        public double SkillRating { get; set; } // Elo/MMR representation
        public Dictionary<string, int> RegionalPingMap { get; set; } = new(); // e.g., "us-east": 28
        public DateTime EnqueuedAtUtc { get; set; }
    }

    public class DynamicMatchRules
    {
        public double MaxAllowedMmrDelta { get; set; }
        public int MaxAllowedPingMs { get; set; }
    }

    public class MatchmakingEvaluator
    {
        private const double BaseMmrDelta = 50.0;
        private const double MaxMmrCap = 600.0;
        private const int BasePingMs = 35;
        private const int AbsoluteMaxPingMs = 180;
        private const double PingStepIntervalSeconds = 4.0;

        /// <summary>
        /// Calculates the expanded criteria for a ticket based on elapsed queue time.
        /// </summary>
        public DynamicMatchRules GetRelaxedRules(MatchmakingTicket ticket, DateTime currentUtc)
        {
            double elapsedTime = (currentUtc - ticket.EnqueuedAtUtc).TotalSeconds;

            // Exponential expansion for skill tolerance to ensure high-skill players eventually match
            double mmrExpansion = Math.Pow(elapsedTime, 1.35) * 4.5;
            double calculatedMmrDelta = Math.Min(MaxMmrCap, BaseMmrDelta + mmrExpansion);

            // Step-wise discrete expansion for latency tolerance (prevents constant server hopping)
            int pingSteps = (int)Math.Floor(elapsedTime / PingStepIntervalSeconds);
            int calculatedPingLimit = Math.Min(AbsoluteMaxPingMs, BasePingMs + (pingSteps * 15));

            return new DynamicMatchRules
            {
                MaxAllowedMmrDelta = calculatedMmrDelta,
                MaxAllowedPingMs = calculatedPingLimit
            };
        }

        /// <summary>
        /// Evaluates whether two tickets can be paired into a match session.
        /// </summary>
        public bool CanMatchTickets(MatchmakingTicket ticketA, MatchmakingTicket ticketB, DateTime currentUtc, out string selectedRegion)
        {
            selectedRegion = null;

            DynamicMatchRules rulesA = GetRelaxedRules(ticketA, currentUtc);
            DynamicMatchRules rulesB = GetRelaxedRules(ticketB, currentUtc);

            // 1. Evaluate Skill Delta
            double actualMmrDelta = Math.Abs(ticketA.SkillRating - ticketB.SkillRating);
            if (actualMmrDelta > rulesA.MaxAllowedMmrDelta || actualMmrDelta > rulesB.MaxAllowedMmrDelta)
            {
                return false; // Skill variance too wide for current queue age
            }

            // 2. Evaluate Common Datacenter Ping Compatibility
            int lowestCombinedPing = int.MaxValue;

            foreach (var (region, pingA) in ticketA.RegionalPingMap)
            {
                if (ticketB.RegionalPingMap.TryGetValue(region, out int pingB))
                {
                    // Match must satisfy BOTH players' dynamic ping limits
                    if (pingA <= rulesA.MaxAllowedPingMs && pingB <= rulesB.MaxAllowedPingMs)
                    {
                        int combinedPing = pingA + pingB;
                        if (combinedPing < lowestCombinedPing)
                        {
                            lowestCombinedPing = combinedPing;
                            selectedRegion = region;
                        }
                    }
                }
            }

            return selectedRegion != null;
        }
    }
}

Kluczowe aspekty techniczne tej implementacji:

  • Asymetryczne spełnianie reguł: Metoda sprawdza, czy ograniczenia obu graczy są respektowane (pingA <= rulesA.MaxAllowedPingMs && pingB <= rulesB.MaxAllowedPingMs). Nowy gracz będący w kolejce od 2 sekund nie zostanie wciągnięty na serwer z pingiem 150 ms tylko dlatego, że drugi gracz czeka już od 90 sekund.
  • Dyskretne stopniowanie pingu: Progi opóźnienia rozszerzają się przy użyciu dyskretnych kroków czasowych (PingStepIntervalSeconds), a nie ciągłych krzywych. Pozwala to uniknąć niepotrzebnego ponownego przypisywania routerów brzegowych przy pętli każdego ticka.
  • Iloczyn przestrzenny: Parowanie opiera się na iloczynie kluczy słownika na mapach regionalnych, wybierając centrum danych o najniższym łącznym opóźnieniu w obie strony (RTT).

Wyzwania infrastruktury backendowej: Współbieżność, blokady (Lock Contention) i alokacja Dedicated Serverów

Pisanie algorytmów meczowych w izolacji jest proste. Prawdziwe trudności inżynieryjne pojawiają się, gdy uruchamiasz ten system w rozproszonych klastrach węzłów (node clusters) obsługujących setki tysięcy jednoczesnych ticketów.

[Player Clients]
       │ (WebSockets / Low Latency)
       ▼
[Ingress Load Balancers]
       │
       ▼
[Distributed Ticket Pool (e.g., Redis Cluster / Memory Grid)]
       │
  ┌────┴────────────────────────┬────────────────────────┐
  ▼                             ▼                        ▼
[Worker Node 1]           [Worker Node 2]          [Worker Node 3]
  │                             │                        │
  └────┬────────────────────────┴────────────────────────┘
       │ (Atomic Claim / Lua Mutex Lock)
       ▼
[Server Orchestration API] ──► Spin up Agones / Fleet Instances

1. Rywalizacja o blokady w rozproszonym poolu ticketów (Distributed Ticket Lock Contention)

Gdy wiele równoległych procesów matchmakera skanuje tę samą centralną pulę ticketów, nieuchronnie dochodzi do wyścigów (race conditions). Dwa osobne wątki robocze (worker threads) mogą jednocześnie oceniać Ticket #1042 i próbować połączyć go w dwie zupełnie różne grupy/lobby.

Aby to rozwiązać, deweloperzy muszą wykonywać atomowe przejęcia blokad (atomic lock claims) za pomocą prymitywów rozproszonych (takich jak skrypty Lua w Redis lub operacje atomowe w gridzie pamięci) przed wyemitowaniem przydziału do meczu. Jeśli ticket jest zablokowany przez innego workera, wątek natychmiast zwalnia swój stan i cofa operację.

2. Komunikacja w czasie rzeczywistym o wysokiej częstotliwości

Aktualizacje stanu matchmakingu nie mogą polegać na standardowym odpytywaniu HTTP (HTTP polling) bez marnowania ogromnych zasobów obliczeniowych na bezużyteczne nawiązywanie połączeń (handshakes). Aby informować klientów o szacowanym czasie w kolejce i dynamicznym wyszukiwaniu pingu, backendy muszą utrzymywać trwałe, dwukierunkowe połączenia WebSockets lub długożyjące strumienie gRPC.

Jeśli obecnie polegasz na nieefektywnych pętlach odpytywania do obsługi stanu gry wieloosobowej, sprawdź nasz przewodnik o tym, jak zrezygnavać z odpytywania HTTP na rzecz WebSockets w Unreal Engine w backendach czasu rzeczywistego.

3. Uzgodnienia alokacji serwerów i provisioning floty

Dobrani gracze to tylko połowa sukcesu. Gdy powstanie prawidłowa grupa ticketów:

  1. Matchmaker kontaktuje się z orkiestratorem dedicated serverów (np. Agones, autorskie kontrolery Kubernetes).
  2. Czysta instancja serwera gry musi zostać przejęta lub zaalokowana w wybranym centrum danych w rygorystycznym limitowanym czasie (zazwyczaj $< 1500ms$).
  3. Serwer uruchamia się, binduje swój port nasłuchowy UDP i zwraca payload z adresem IP/Port.
  4. Matchmaker wysyła szczegóły połączenia do WebSockets wszystkich klientów.

Ręczne budowanie rozproszonych pooli ticketów, obsługa rywalizacji o blokady (lock contention), zarządzanie regionalnymi klastrami socketów oraz orkiestracja cyklu życia dedicated serverów wymaga miesięcy pracy nad infrastrukturą.

W tym miejscu horizOn eliminuje ogromny narzut inżynieryjny. Zamiast składać magazyny ticketów w Redis, pisać własne wrappery na klastry Agones i zarządzać skryptami skalowania floty serwerów, horizOn zapewnia w pełni zarządzane, charakteryzujące się ultra-niskim opóźnieniem kolejki matchmakingowe oraz orkiestrację floty serwerów od ręki. Ty piszesz reguły matchmakingu, a horizOn bezproblemowo obsługuje globalną dystrybucję, atomowe blokowanie ticketów i automatyczną alokację serwerów.


5 dobrych praktyk przy budowaniu nowoczesnej architektury matchmakingu w grach

Niezależnie od tego, czy tworzysz rywalizacyjną strzelankę o wysokiej stawce, czy niezależną grę zręcznościową, postępuj zgodnie z tymi sprawdzonymi w praktyce zasadami architektonicznymi:

1. Wymagaj wektorów pingu od klienta przed przesłaniem ticketu

Nigdy nie polegaj na geolokalizacji IP klienta do określania odległości do centrów danych. Bazy danych Geo-IP słyną z niedokładności w routingu brzegowym i ignorują przeciążenia dostawców Internetu (ISP) w czasie rzeczywistym. Zawsze wymagaj, aby silnik gry na klincie mierzył bezpośrednie opóźnienie za pomocą sond UDP ping do wszystkich punktów końcowych zanim wywoła endpoint dodania do kolejki.

2. Chroń system przed fragmentacją poola graczy

Unikaj tworzenia osobnych kolejek dla mniej istotnych trybów gry, chyba że liczba aktywnych graczy wyraźnie to uzasadnia. Podział społeczności na tryby gry, preferencje map i kolejki o różnym stopniu rygorystyczności matchmakingu potęguje degradację poola. Jeśli aktywne CCU na kolejkę spadnie poniżej 1000 graczy w danym regionie, przełącz się automatycznie na dynamiczny tryb awaryjny (fallback) z jedną kolejką.

3. Odseparuj provisioning serwerów od ewaluacji meczów

Upewnij się, że wątki matchmakera działają asynchronicznie względem warstwy orkiestracji floty. Nigdy nie blokuj pętli workera matchmakera w oczekiwaniu na uruchomienie instancji serwera wirtualnego. Używaj nieblokujących kolejek wiadomości pub/sub do żądania alokacji serwera i dostarczania danych połączeniowych, gdy instancje zgłoszą gotowość (healthy status).

4. Optymalizuj koszty bezczynnych serwerów dzięki inteligentnej hibernacji

Zapotrzebowanie na matchmaking rośnie nieprzewidywalnie w godzinach szczytu i gwałtownie spada poza nimi. Pozostawienie setek pustych instancji serwerów gry marnuje znaczne przychody operacyjne. Wdróż wzorce dynamicznego rozgrzewania floty (warm-up) oraz hibernacji serwerów. Aby poznać głęboką analizę techniczną optymalizacji zużycia bezczynnych serwerów, przeczytaj nasz wpis o architekturze serwerów zero-waste i propozycji optymalizacji serwerów w Fortnite.

5. Przeprowadzaj benchmarki netcode'u i replikacji przy wysokim opóźnieniu

Nawet najbardziej zaawansowana architektura matchmakingu sporadycznie połączy graczy z umiarkowanym opóźnieniem ($100-120ms$) w godzinach poza szczytem. Upewnij się, że netcode Twojego serwera wykorzystuje solidną predykcję po stronie klienta (client prediction), kompensację lagów (lag compensation) oraz rekoncyliację stanu (state reconciliation), aby łagodnie znosić opóźnienia pakietów. Jeśli pozycje graczy rwie lub skaczą podczas meczów z wysokim pingiem, zapoznaj się z naszym poradnikiem jak naprawić desynchronizację pozycji gracza w Unreal Engine w trybie multiplayer.


Podsumowanie i kolejne kroki

Decyzja Activision o zaoferowaniu w Call of Duty: Black Ops 7 wyraźnych kolejek SBMM, Connection-First oraz Hybrid pokazuje, jak kluczowa dla satysfakcji graczy jest architektura matchmakingu. Jednak rozdzielenie kolejek wymaga ogromnej gęstości graczy, niskopoziomowych sieci socketowych o niskim opóźnieniu, algorytmów dynamicznego wygaszania reguł oraz atomowej obsługi ticketów.

Jeśli budujesz swoją następną grę multiplayer, nie marnuj miesięcy na pisanie od zera infrastruktury backendowej, serwerów socketowych i logiki alokacji floty. Dowiedz się, jak horizOn umożliwia deweloperom wdrażanie skalowalnych backendów do gier multiplayer, zautomatyzowanego matchmakingu i orkiestracji serwerów w kilka minut. Wypróbuj horizOn za darmo już dziś lub przejrzyj dokumentację horizOn, aby przyspieszyć prace nad backendem.