Aller au contenu principal
Le jeu en direct

Cette page n’est pas encore traduite : la version anglaise est affichée.

Matchmaking

Amener un joueur dans la bonne salle. Les tickets décrivent le joueur et filtrent les autres. Le matchmaker détermine un placement, réserve une place, puis le trafic de jeu va directement à la salle.

Le matchmaker n’intervient qu’une fois, pour décider où vous devez aller. Il ne se trouve pas sur le chemin du match : il produit un placement et une réservation de place (seat) limitée dans le temps, et dès l’arrivée le trafic de jeu va droit à la salle. Une file d’attente chargée ne ralentit donc jamais une partie.

a ticket — a player, or a party entering wholeyour pre-enqueue hook — stamps trusted criteriathe queue — the requirement widens rung by rungexpired — an outcome, not an errormatched — atomicallyRoomPlacement — a room and reserved seatsRosterSet — the roster only, no roomthe room lifetime passeda simultaneous gamean asynchronous gamethe player joins; the matchmaker leaves the path
Le parcours d’un ticket : complété côté serveur, élargi palier par palier, apparié de façon atomique, puis remis à une salle que le matchmaker quitte ensuite

Quand l’utiliser

  • Vous devez acheminer les joueurs vers des salles selon des critères déclarés (mode, région, rang), et non au moyen d’une liste de lobbies faite maison.
  • Les critères d’appariement doivent venir des données de la plateforme, pas de ce qu’affirme le client : inscrivez le rang dans le hook qui précède la mise en file.
  • Les files doivent s’élargir avec le temps, côté serveur, pendant que le client garde un seul ticket et n’interroge jamais le serveur.
  • Les groupes de joueurs (party) doivent arriver ensemble dans le même match : un groupe entre en entier ou pas du tout.
  • Vous utilisez un matchmaker externe et avez seulement besoin que sa décision aboutisse à un placement et à une réservation de place.
  • Inutile quand les joueurs choisissent eux-mêmes leur session : la liste des salles et Join suffisent.

Qui fait quoi

ActeurCe qu’il fait sur cette page
playercrée et annule son propre ticket, et entre en tant que membre d’un groupe
match-organizerdéclare les files du matchmaker et leur assouplissement ; lit les résultats de placement
backend-serviceinscrit des critères de confiance avant la mise en file ; applique les décisions d’un matchmaker externe

En bref

Trouver un match : un seul appel Find renvoie une place réservée à rejoindre
// client — one call for the common case
var seat = await playserv.Matchmaking.Find("ranked-duo");
var room = await playserv.Rooms.Join(seat);
// client — one call for the common case
const seat = await playserv.matchmaking.find('ranked-duo');
const room = await playserv.rooms.join(seat);
# client — one call for the common case
seat = await playserv.matchmaking.find("ranked-duo")
room = await playserv.rooms.join(seat)
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

// client — one call for the common case
Client->Matchmaking->Of<FRankedDuo>()->Tickets->Create(FPSTicketClaim{ .Mode = TEXT("duo") },
    TPSOnResult<FPSTicket*>::CreateWeakLambda(this, [this](const TPSResult<FPSTicket*>& TicketResult)
    {
        if (!TicketResult.HasValue()) { return; }
        // the seat arrives as the ticket's outcome
        TPSSubscription Placement = TicketResult.Value()->Subscribe([this](const FPSSeat& Seat)
        {
            Client->Rooms->Join(Seat,
                TPSOnResult<FPSRoom*>::CreateWeakLambda(this, [this](const TPSResult<FPSRoom*>& JoinResult)
                {
                    if (!JoinResult.HasValue()) { return; }
                    EnterMatch(JoinResult.Value());
                }));
        });
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — fonctionne tel quel dans Unity avec les types générés
// client — one call for the common case
var seat = await playserv.Matchmaking.Find("ranked-duo");
var room = await playserv.Rooms.Join(seat);
La file ranked-duo déclarée : filtres réciproques et échelle d’assouplissement en deux paliers
[Matchmaker("ranked-duo")]
public static class RankedDuo
{
    public static Size Size = Size.Exactly(4, multiple: 2);
    public static string Filter = "mode == 'duo' && region == self.region";
    public static Relax[] Relax =
    {
        Relax.After(15.Seconds(), "abs(rank - self.rank) < 300"),
        Relax.After(45.Seconds(), "abs(rank - self.rank) < 800"),
    };
}
@Matchmaker('ranked-duo')
export class RankedDuo {
  static size = Size.exactly(4, { multiple: 2 });
  static filter = "mode == 'duo' && region == self.region";
  static relax = [
    Relax.after(seconds(15), 'abs(rank - self.rank) < 300'),
    Relax.after(seconds(45), 'abs(rank - self.rank) < 800'),
  ];
}
@matchmaker("ranked-duo")
class RankedDuo:
    size = Size.exactly(4, multiple=2)
    filter = "mode == 'duo' && region == self.region"
    relax = [
        Relax.after(seconds(15), "abs(rank - self.rank) < 300"),
        Relax.after(seconds(45), "abs(rank - self.rank) < 800"),
    ]
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

USTRUCT(PSMatchmaker = (Name = "ranked-duo", Size = "Exactly:4", Multiple = 2,
                       Filter = "mode == 'duo' && region == self.region"))
struct FRankedDuo
{
    GENERATED_BODY()

    UPROPERTY(PSRelax = (After = "15s", Filter = "abs(rank - self.rank) < 300")) FPSRelax First;
    UPROPERTY(PSRelax = (After = "45s", Filter = "abs(rank - self.rank) < 800")) FPSRelax Second;
};
// declarations compile into the same pushed model — playserv push from the UE project or CI
Unity C# is the same C# API here — la même classe de modèle compile dans Unity, sur la base 2021.3 — aucune syntaxe C# 12 ici
[Matchmaker("ranked-duo")]
public static class RankedDuo
{
    public static Size Size = Size.Exactly(4, multiple: 2);
    public static string Filter = "mode == 'duo' && region == self.region";
    public static Relax[] Relax =
    {
        Relax.After(15.Seconds(), "abs(rank - self.rank) < 300"),
        Relax.After(45.Seconds(), "abs(rank - self.rank) < 800"),
    };
}

Les critères qu’on ne peut pas confier au client sont inscrits dans le hook qui précède la mise en file :

Le hook qui précède la mise en file inscrit le rang d’après les données de la plateforme, pas d’après ce qu’affirme le client
[Before(Matchmaking.Enqueue)]                     // the server has the last word
public static async Task<Ticket> StampRank(Ticket t)
{
    var rows = await PlayServ.Leaderboards.ForOwners("ranked", new[] { t.Player });
    t.Properties["rank"] = rows[0].Rank;          // the row carries its rank in the full table
    return t;
}
// the server has the last word
export const stampRank = before(Matchmaking.enqueue, async (t: Ticket) => {
  const rows = await PlayServ.leaderboards.forOwners('ranked', [t.player]);
  t.properties.rank = rows[0].rank;               // the row carries its rank in the full table
  return t;
});
@before(matchmaking.enqueue)                      # the server has the last word
async def stamp_rank(t: Ticket) -> Ticket:
    rows = await playserv.leaderboards.for_owners("ranked", [t.player])
    t.properties["rank"] = rows[0].rank           # the row carries its rank in the full table
    return t
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

S’exécute hors du moteur

Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python — le client Unreal se contente d’appeler Find ci-dessus. Des fonctions écrites dans le moteur sont prévues : graphes Blueprint et C++ (traduit vers une cible de script validée par la plateforme). Les unes comme les autres sont analysées et publiées comme toute déclaration ; l’exécution reste sur la plateforme.

S’exécute hors du moteur

Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python — le client Unity se contente d’appeler Find ci-dessus.

Cette lecture est la lecture par liste de propriétaires de Leaderboards — celle qu’utilise une cohorte d’amis —, et chaque ligne renvoyée porte le rang de son propriétaire dans le classement (leaderboard) complet. Le hook peut demander la ligne de quelqu’un d’autre parce que le prédicat du classement l’autorise pour une fonction cloud ; une session joueur qui pose la même question n’obtient que la sienne.

Le modèle

Ce que porte un ticket.

PartieCe que c’estQui s’y fie
autodescriptionles propriétés déclarées du participant — cote, mode, langue, carte choisiepersonne sans vérification : c’est ce qu’affirme l’appelant
exigenceun prédicat que les autres doivent satisfairela plateforme, puisque c’est elle qui l’applique

Le participant d’un ticket est un acteur ou un groupe : un groupe entre en entier, et c’est précisément cela, un groupe de joueurs. Son ticket est indivisible : le groupe entre tout entier dans une composition (roster), ou pas du tout.

Ce que déclare un type de file.

DéclareCe que c’est
propriétéspar nom et par type. Une propriété qui n’est pas déclarée ici est refusée dans un ticket comme échec de validation, au lieu d’être ignorée
taille de la compositionun minimum, un maximum et un pas de compatibilité — le multiple auquel une composition est acceptable, pour que des équipes « de cinq » veuillent bien dire cinq, et non n’importe quel nombre entre deux et dix
échelle d'exigencesun ensemble ordonné de prédicats assortis de fenêtres : chaque palier est une exigence plus large et un délai au-delà duquel le matchmaking passe au suivant. L’assouplissement est une déclaration, jamais une logique arbitraire dans un handler
le langage de prédicatsle même que partout ailleurs, et son vocabulaire inclut les propres propriétés du ticket : « une cote à ±100 de la mienne » peut s’exprimer.
réciprocitési une composition est acceptable lorsque A accepte B mais que B n’accepte pas A. Il n’y a pas de valeur par défaut
durée de vie du ticketau-delà, le ticket passe à expired avec un événement
résultatRoomPlacement — une référence de salle (room) et des réservations dans cette salle, pour un jeu en simultané ; ou RosterSet — la composition seule, sans salle ni réservation, pour un jeu asynchrone où l’adversaire est hors ligne

Les états d’un ticket. created → queued → matched · cancelled · expired, les trois derniers étant terminaux.

ToujoursCe que c’est
un seul ticket actif par participant et par fileun second est un conflit, pas une seconde candidature : lisez celui qui existe
le motif d'un appariementest observable : il figure dans l’événement de matchmaking et dans l’historique. Pour les algorithmes fournis, c’est le palier de l’échelle ; une implémentation de remplacement peut ne pas avoir de paliers, et le motif est alors une valeur opaque qu’elle déclare — mais il y en a toujours un
l'expirationest un résultat, pas une erreur : « aucune composition n’a pu être réunie dans le délai déclaré » est une issue normale, livrée comme résultat du ticket
le résultatarrive par abonnement, pas par interrogation périodique : le client garde un seul ticket et ne redemande jamais
matchedest atomique : pour RoomPlacement, soit la composition est appariée et chaque participant détient une réservation, soit les tickets restent dans la file. Pour RosterSet, le résultat atomique est la composition seule

Perdre la connexion annule le ticket

Un ticket est une candidature pour jouer maintenant : une déconnexion l’annule donc. Apparier un joueur absent dégraderait la composition pour tous les autres.

Erreurs

  • Un second ticket dans la même file est un conflit ; ne réessayez pas, lisez le ticket existant.
  • Une propriété non déclarée, ou une exigence qui en nomme une, est un échec de validation — et non un oubli silencieux qui ressurgirait plus tard sous la forme « aucun adversaire trouvé ».
  • Une file suspendue répond unavailable, pas forbidden : les droits de l’appelant sont intacts et la situation est temporaire ; il faut donc réessayer en espaçant les tentatives.
  • Une salle impossible à créer pour le résultat répond de même unavailable, à réessayer en espaçant les tentatives.
  • Une réservation qui échoue est un conflit qui mérite un nouvel essai : le ticket reste dans la file.
  • Un ticket introuvable ou appartenant à quelqu’un d’autre, comme un acteur qu’un prédicat n’admet pas dans la file, répondent not found : un refus ne révèle ni le ticket ni la file.

Limites

Les chiffres viendront avec le chapitre sur les limites de la plateforme ; ce qui se passe à chaque plafond, lui, est déjà fixé.

  • Tickets dans une file : la création est refusée comme conflit, et les tickets existants ne sont pas évincés pour faire de la place.
  • La durée de vie d’un ticket : un passage à expired avec un événement.
  • Paliers de l’échelle : une déclaration qui en compte trop est refusée dès la déclaration.
  • Taille du groupe dans un ticket : le ticket est refusé comme échec de validation.
  • Propriétés déclarées par type de file : refusées dès la déclaration.
  • Rythme de création de tickets : un refus pour limite de débit, avec une échéance.
  • Rétention de l’historique de matchmaking : passée la période déclarée, une entrée n’est plus lisible.