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.
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
Joinjá cobrem esse caso.
Quem faz o quê
| Ator | O que faz nesta página |
|---|---|
player | cria e cancela o próprio ticket e entra como parte de uma party |
match-organizer | declara as filas do matchmaker e a flexibilização dos critérios; lê os resultados de alocação |
backend-service | grava critérios confiáveis antes da entrada na fila; executa as decisões de um matchmaker externo |
Resumo
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)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.// 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 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"),
]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[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:
[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 tAinda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.
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.
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.
| Parte | O que é | Quem acredita nela |
|---|---|---|
autodescrição | as propriedades declaradas do participante — rating, modo, idioma, mapa escolhido | ninguém, sem verificação: é o que quem chama afirma |
requisito | um predicado que os demais precisam satisfazer | a 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.
| Declara | O que é |
|---|---|
propriedades | por nome e tipo. Uma propriedade não declarada aqui é recusada no ticket como falha de validação, em vez de ser ignorada |
tamanho da lista | um 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 requisitos | um 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 predicados | a 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. |
reciprocidade | se é aceitável uma lista em que A aceita B, mas B não aceita A. Não há padrão |
tempo de vida do ticket | depois dele, o ticket passa para expired, com um evento |
resultado | RoomPlacement — 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.
| Sempre | O que é |
|---|---|
um ticket ativo por participante por fila | um 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 resultado | chega 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.