Pular para o conteúdo principal
O jogo ao vivo

Esta página ainda não foi traduzida: mostrando a versão em inglês.

Matchmaking

Coloque o jogador na sala certa. Os tickets descrevem o jogador e filtram os demais. O matchmaker decide uma alocação e reserva uma vaga; daí em diante, o tráfego do jogo vai direto para a sala.

O matchmaker entra no caminho uma vez, para decidir onde você deve jogar. Ele não fica no caminho da partida: o resultado dele é uma alocação (placement) e uma reserva de vaga com prazo, e, a partir da entrada, o tráfego do jogo vai direto para a sala. Por isso, uma fila congestionada nunca congestiona o jogo.

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
O caminho de um ticket: critérios gravados no servidor, requisito ampliado degrau a degrau, pareamento atômico e entrega a uma sala, da qual o matchmaker então sai

Quando usar

  • Você precisa encaminhar jogadores para salas por critérios declarados (modo, região, posição no ranking), e não por uma lista de lobbies feita à mão.
  • Os critérios da partida precisam vir dos dados da plataforma, e não do que o cliente declara: grave a posição no ranking no hook de pré-entrada na fila.
  • As filas devem se ampliar com o tempo, no servidor, enquanto o cliente mantém um único ticket e nunca faz polling.
  • Uma party precisa cair junta na mesma partida: o grupo entra inteiro ou não entra.
  • Você usa um matchmaker externo e só precisa que a decisão dele termine em alocação + reserva de vaga (seat).
  • Dispensável quando os próprios jogadores escolhem a sessão. A navegação de salas e o Join já cobrem esse caso.

Quem faz o quê

AtorO que faz nesta página
playercria e cancela o próprio ticket e entra como parte de uma party
match-organizerdeclara as filas do matchmaker e a flexibilização dos critérios; lê os resultados de alocação
backend-servicegrava critérios confiáveis antes da entrada na fila; executa as decisões de um matchmaker externo

Resumo

Encontrar uma partida: uma chamada a Find devolve uma vaga reservada 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)
Em breve — Go

Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.

// 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 — roda sem alterações na Unity com os tipos gerados
// client — one call for the common case
var seat = await playserv.Matchmaking.Find("ranked-duo");
var room = await playserv.Rooms.Join(seat);
A fila ranked-duo declarada: filtros mútuos e uma escada de flexibilização em dois degraus
[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"),
    ]
Em breve — Go

Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.

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 — a mesma classe de modelo compila na Unity, na base 2021.3 — sem sintaxe do C# 12 aqui
[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"),
    };
}

Os critérios que não podem ficar a cargo do cliente são gravados no hook de pré-entrada na fila:

O hook de pré-entrada na fila grava a posição no ranking a partir dos dados da plataforma, e não do que o cliente declara
[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
Em breve — Go

Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.

Executa fora da engine

Um hook é uma cloud function: roda na plataforma, não na engine. Escreva-o em C#, TypeScript ou Python — o cliente Unreal só chama o Find acima. Estão previstas funções escritas na própria engine: grafos de Blueprint e C++ (traduzido para um script que a plataforma valida). As duas formas são analisadas e publicadas como qualquer declaração; a execução continua na plataforma.

Executa fora da engine

Um hook é uma cloud function: roda na plataforma, não na engine. Escreva-o em C#, TypeScript ou Python — o cliente Unity só chama o Find acima.

É a leitura por lista de donos do módulo Leaderboards — a mesma que um grupo de amigos usa —, e cada linha retornada traz a posição daquele dono no ranking completo. O hook pode pedir a linha de outra pessoa porque o predicado do ranking permite isso a uma cloud function; uma sessão de jogador que faça a mesma pergunta recebe só a própria linha.

O modelo

O que um ticket carrega.

ParteO que éQuem acredita nela
autodescriçãoas propriedades declaradas do participante — rating, modo, idioma, mapa escolhidoninguém, sem verificação: é o que quem chama afirma
requisitoum predicado que os demais precisam satisfazera plataforma, porque é ela quem o aplica

O participante de um ticket é um ator ou um grupo: o grupo entra inteiro, e é isso que é uma party. O ticket dele é indivisível: o grupo entra inteiro na lista de participantes (roster) ou não entra.

O que um tipo de fila declara.

DeclaraO que é
propriedadespor nome e tipo. Uma propriedade não declarada aqui é recusada no ticket como falha de validação, em vez de ser ignorada
tamanho da listaum mínimo, um máximo e um passo de compatibilidade — o múltiplo em que uma lista é aceitável, para que times “de cinco” signifiquem cinco, e não qualquer número entre dois e dez
escada de requisitosum conjunto ordenado de predicados com janelas: cada degrau é um requisito mais amplo e um tempo depois do qual o matchmaking passa ao próximo. A flexibilização é uma declaração, nunca lógica arbitrária em um handler
a linguagem de predicadosa mesma que todo o resto usa, e o vocabulário dela inclui as próprias propriedades do ticket — “um rating a ±100 do meu” pode ser expresso.
reciprocidadese é aceitável uma lista em que A aceita B, mas B não aceita A. Não há padrão
tempo de vida do ticketdepois dele, o ticket passa para expired, com um evento
resultadoRoomPlacement — uma referência de sala (room) mais as reservas nela, para um jogo simultâneo; ou RosterSet — só a lista de participantes, sem sala e sem reservas, para um jogo assíncrono em que o adversário está offline

Os estados de um ticket. created → queued → matched · cancelled · expired; os três últimos são terminais.

SempreO que é
um ticket ativo por participante por filaum segundo é conflito, e não uma segunda inscrição: leia o que já existe
o motivo de um pareamentoé observável: chega ao evento de matchmaking e ao histórico. Nos algoritmos que vêm com a plataforma, é o degrau da escada; uma implementação própria, que os substitua, pode não ter degraus, e aí o motivo é um valor opaco que ela declara — mas sempre existe um
expiraçãoé um desfecho, e não um erro: “a lista não se formou no tempo declarado” é uma conclusão normal, entregue como resultado do ticket
o resultadochega por assinatura (subscription), e não por polling: o cliente mantém um ticket e nunca pergunta de novo
matchedé atômico: em RoomPlacement, ou a lista se fecha e cada participante fica com uma reserva, ou os tickets continuam na fila. Em RosterSet, o resultado atômico é só a lista

Perder a conexão cancela o ticket

Um ticket é um pedido para jogar agora, então uma desconexão o cancela. Parear um jogador ausente pioraria a partida para todos os outros.

Erros

  • Um segundo ticket na mesma fila é conflito; não repita a chamada, leia o ticket existente.
  • Uma propriedade não declarada, ou um requisito que cite uma, é falha de validação — e não algo ignorado em silêncio, que depois apareceria como “nenhum adversário encontrado”.
  • Fila suspensa responde unavailable, e não forbidden: os direitos de quem chama continuam intactos e a situação é temporária, então o certo é tentar de novo com backoff.
  • Não foi possível criar a sala para o resultado também responde unavailable, com backoff.
  • A reserva falhou é um conflito que vale repetir: o ticket continua na fila.
  • Um ticket que não existe ou é de outra pessoa e um ator que um predicado não admite na fila respondem, ambos, not found; assim, a recusa não revela nem o ticket nem a fila.

Limites

Os números chegam com o capítulo de limites da plataforma; o que acontece em cada teto já está definido.

  • Tickets em uma fila: a criação é recusada como conflito, e os tickets existentes não são removidos para abrir espaço.
  • Tempo de vida de um ticket: transição para expired, com um evento.
  • Degraus da escada: uma declaração com degraus demais é recusada no momento da declaração.
  • Tamanho do grupo em um ticket: o ticket é recusado como falha de validação.
  • Propriedades declaradas por tipo de fila: recusadas no momento da declaração.
  • Taxa de criação de tickets: recusa por rate limit, com prazo.
  • Retenção do histórico de matchmaking: passado o período declarado, a entrada não pode mais ser lida.