Saltar al contenido principal
El juego en vivo

Esta página aún no está traducida: se muestra la versión en inglés.

Matchmaking

Lleva a un jugador a la sala correcta. Los tickets describen al jugador y filtran a los demás. El matchmaker resuelve una ubicación, reserva un lugar y, a partir de ahí, el tráfico del juego va directo a la sala.

El matchmaker está en el camino una sola vez, para decidir dónde te corresponde jugar. No está en el camino de la partida: su resultado es una ubicación y una reserva de lugar (seat) con tiempo limitado, y desde el ingreso en adelante el tráfico del juego va directo a la sala. Por eso una cola saturada nunca se convierte en un juego saturado.

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
El recorrido de un ticket: se completa en el servidor, se amplía escalón por escalón, se empareja de forma atómica y se entrega a una sala, que el matchmaker después deja

Cuándo usarlo

  • Necesitas llevar a los jugadores (player) a salas según criterios declarados (modo, región, posición), no con una lista de lobbies hecha a mano.
  • Los criterios de emparejamiento (matchmaking) tienen que salir de los datos de la plataforma, no de lo que dice el cliente: fija la posición en el hook previo al encolado.
  • Las colas deben ampliarse con el tiempo del lado del servidor, mientras el cliente mantiene un único ticket y nunca consulta de nuevo.
  • Las parties tienen que caer juntas en la misma partida: un grupo entra completo o no entra.
  • Usas un matchmaker externo y solo necesitas que su decisión termine en una ubicación más una reserva de lugar.
  • No lo necesitas si los jugadores eligen la sesión por su cuenta. El explorador de salas y Join ya lo cubren.

Quién hace qué

ActorQué hace en esta página
playercrea y cancela su propio ticket, y entra como parte de una party
match-organizerdeclara las colas del matchmaker y la ampliación de criterios; lee los resultados de ubicación
backend-servicefija criterios de confianza antes del encolado; ejecuta las decisiones de un matchmaker externo

En resumen

Buscar una partida: una sola llamada a Find devuelve un lugar reservado para entrar
// 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)
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

// 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 — funciona tal cual en Unity con los tipos generados
// client — one call for the common case
var seat = await playserv.Matchmaking.Find("ranked-duo");
var room = await playserv.Rooms.Join(seat);
La cola ranked-duo declarada: filtros mutuos y una escalera de ampliación de dos escalones
[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"),
    ]
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

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 misma clase de plantilla compila en Unity, sobre la base 2021.3; aquí no hay sintaxis de C# 12
[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"),
    };
}

Los criterios que no se le pueden confiar al cliente se fijan en el hook previo al encolado:

El hook previo al encolado fija la posición con datos de la plataforma, no con lo que dice el cliente
[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
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; el cliente de Unreal solo llama a Find, como se ve más arriba. Están previstas funciones escritas desde el motor: grafos de Blueprint y C++ (traducido a un script que valida la plataforma). Ambos se analizan y se suben como cualquier declaración; la ejecución sigue en la plataforma.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; el cliente de Unity solo llama a Find, como se ve más arriba.

Es la lectura por lista de dueños de Leaderboards — la misma que usa una cohorte de amigos —, y cada fila que devuelve trae la posición de ese dueño en la tabla completa. El hook puede pedir la fila de otra persona porque el predicado de la tabla le permite hacerlo a una función en la nube; una sesión de jugador que hace la misma pregunta solo recibe la suya.

El modelo

Qué lleva un ticket.

ParteQué esQuién la cree
autodescripciónlas propiedades declaradas del participante: puntuación, modo, idioma, mapa elegidonadie, sin una verificación: es lo que afirma quien llama
requisitoun predicado que deben cumplir los demásla plataforma, porque es quien lo aplica

El participante de un ticket es un actor o un grupo: un grupo entra completo, y eso es una party. Su ticket es indivisible: el grupo entra entero en una lista de participantes (roster) o no entra.

Qué declara un tipo de cola.

DeclaraQué es
propiedadespor nombre y tipo. Una propiedad que no está declarada aquí se rechaza en un ticket como falla de validación, en lugar de ignorarse
tamaño de la listaun mínimo, un máximo y un paso de compatibilidad: el múltiplo con el que una lista es aceptable, para que equipos “de cinco” signifique cinco y no cualquier número entre dos y diez
escalera de requisitosun conjunto ordenado de predicados con ventanas: cada escalón es un requisito más amplio y un tiempo tras el cual el emparejamiento pasa al siguiente. La ampliación es una declaración, nunca lógica arbitraria en un handler
el lenguaje de predicadosel mismo que usa todo lo demás, y su vocabulario incluye las propiedades propias del ticket: “una puntuación a ±100 de la mía” se puede expresar.
reciprocidadsi se acepta una lista en la que A acepta a B pero B no acepta a A. No hay valor predeterminado
vida útil del ticketpasado ese tiempo, el ticket pasa a expired con un evento
resultadoRoomPlacement: una referencia a una sala (room) más reservas en ella, para un juego simultáneo; o RosterSet: solo la lista, sin sala ni reservas, para uno asíncrono en el que el rival está desconectado

Los estados de un ticket. created → queued → matched · cancelled · expired; los tres últimos son terminales.

SiempreQué es
un ticket vivo por participante y por colaun segundo es un conflicto, no una segunda solicitud: lee el que ya existe
el motivo de un emparejamientoes observable: llega al evento de emparejamiento y al historial. En los algoritmos incluidos es el escalón de la escalera; una implementación que los reemplace puede no tener escalones, y entonces el motivo es un valor opaco que ella misma declara, pero siempre hay uno
el vencimientoes un resultado, no un error: “no se formó una lista en el tiempo declarado” es una conclusión normal que se entrega como resultado del ticket
el resultadollega por suscripción, no por consulta periódica: el cliente mantiene un único ticket y nunca vuelve a preguntar
matchedes atómico: con RoomPlacement, o la lista queda emparejada y cada participante tiene una reserva, o los tickets siguen en la cola. Con RosterSet, el resultado atómico es solo la lista

Perder la conexión cancela el ticket

Un ticket es una solicitud para jugar ahora, así que una desconexión lo cancela. Emparejar a un jugador ausente empeoraría la lista para todos los demás.

Errores

  • Un segundo ticket en la misma cola es un conflicto; no lo repitas, lee el ticket existente.
  • Una propiedad no declarada, o un requisito que nombra una, es una falla de validación, no algo que se ignora en silencio y que más tarde aparecería como “no se encontraron rivales”.
  • La cola está suspendida responde unavailable, no forbidden: los derechos de quien llama siguen intactos y la situación es temporal, así que lo correcto es reintentar con backoff.
  • No se puede crear la sala para el resultado también responde unavailable, con backoff.
  • La reserva falló es un conflicto que vale la pena reintentar: el ticket sigue en la cola.
  • Un ticket que no se encuentra o que es de otro, y un actor al que un predicado no admite en la cola, responden ambos not found, así que un rechazo no revela ni el ticket ni la cola.

Límites

Las cifras llegarán con el capítulo de límites de la plataforma; lo que pasa en cada tope ya está definido.

  • Tickets en una cola: la creación se rechaza como conflicto, y los tickets existentes no se expulsan para hacer lugar.
  • La vida útil de un ticket: una transición a expired con un evento.
  • Escalones de la escalera: una declaración con demasiados se rechaza al declararla.
  • Tamaño del grupo en un ticket: el ticket se rechaza como falla de validación.
  • Propiedades declaradas por tipo de cola: se rechazan al declararlas.
  • Frecuencia de creación de tickets: un rechazo por límite de frecuencia, con un plazo.
  • Retención del historial de emparejamiento: pasado el período declarado, una entrada ya no se puede leer.