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.
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
Joinya lo cubren.
Quién hace qué
| Actor | Qué hace en esta página |
|---|---|
player | crea y cancela su propio ticket, y entra como parte de una party |
match-organizer | declara las colas del matchmaker y la ampliación de criterios; lee los resultados de ubicación |
backend-service | fija criterios de confianza antes del encolado; ejecuta las decisiones de un matchmaker externo |
En resumen
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)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.// client — one call for the common case
var seat = await playserv.Matchmaking.Find("ranked-duo");
var room = await playserv.Rooms.Join(seat);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"),
]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[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:
[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 tTodavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.
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.
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.
| Parte | Qué es | Quién la cree |
|---|---|---|
autodescripción | las propiedades declaradas del participante: puntuación, modo, idioma, mapa elegido | nadie, sin una verificación: es lo que afirma quien llama |
requisito | un predicado que deben cumplir los demás | la 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.
| Declara | Qué es |
|---|---|
propiedades | por 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 lista | un 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 requisitos | un 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 predicados | el 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. |
reciprocidad | si se acepta una lista en la que A acepta a B pero B no acepta a A. No hay valor predeterminado |
vida útil del ticket | pasado ese tiempo, el ticket pasa a expired con un evento |
resultado | RoomPlacement: 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.
| Siempre | Qué es |
|---|---|
un ticket vivo por participante y por cola | un segundo es un conflicto, no una segunda solicitud: lee el que ya existe |
el motivo de un emparejamiento | es 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 vencimiento | es 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 resultado | llega por suscripción, no por consulta periódica: el cliente mantiene un único ticket y nunca vuelve a preguntar |
matched | es 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
expiredcon 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.