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.

Rooms

Uma sala é uma sessão de jogo, e para a plataforma não importa quem a hospeda. Uma só abstração cobre um servidor dedicado por partida, um grande mapa compartilhado dividido em camadas lógicas, uma sala hospedada por um cliente mestre e um minijogo hospedado no backend. Por dentro, a sala é responsabilidade da plataforma; você a controla de fora.

Quando usar

  • Seu jogo tem sessões (partidas, lobbies, dungeons, corridas), e alguém precisa cuidar do ciclo de vida, da participação e das reconexões de cada uma.
  • Você hospeda em servidores dedicados, no cliente mestre de um jogador ou no próprio backend, e os jogadores precisam ser encaminhados para lá.
  • Um único mapa compartilhado precisa rodar muitas sessões lógicas: camadas, cujo escopo o módulo Visibility define.
  • Jogadores entram no meio da sessão e precisam ver o estado atual: primeiro o estado da sala (room) no momento da chegada, depois o tráfego ao vivo.
  • Uma conexão perdida não pode custar a vaga (seat). O período de tolerância (grace window) do modelo — 45s em battle — retoma a mesma participação.
  • Dispensável quando o recurso é puramente requisição/resposta sobre registros. O módulo Data & Subscriptions já cobre esse caso.

Quem faz o quê

AtorO que faz nesta página
room-ownerregistra salas em qualquer ponto do processo; em uma instância de sala, usa a interface de administração daquela instância: altera a configuração ao vivo, expulsa, tranca, envia difusões (broadcast), descarta
entry-validatoraceita ou recusa pedidos de entrada com um código e um motivo
room-visitornavega pelas salas, entra com dados, reconecta dentro do período de tolerância, sai
spectatorentra sem disputar; recebe as difusões e o tráfego ao vivo
match-organizerreserva vagas que contam para a capacidade; a reserva expira no prazo do modelo (90s em battle)

Resumo

O modelo battle: capacidade, tick, tipo de host, um mapa nomeado e os dois prazos da vaga
[RoomTemplate("battle")]
public static class Battle
{
    public static int Capacity = 8;
    public static Tick Tick = Tick.Hz30;
    public static Host Host = Host.Backend;          // or DedicatedServer, MasterClient
    public static MapRef Map = Maps.Named("arena-caves-v3");
    public static Duration Grace = 45.Seconds();     // a dropped member keeps the seat this long
    public static Duration Reserve = 90.Seconds();   // a reserved seat is held this long
}
@RoomTemplate('battle')
export class Battle {
  static capacity = 8;
  static tick = Tick.hz30;
  static host = Host.backend;                // or Host.dedicatedServer, Host.masterClient
  static map = Maps.named('arena-caves-v3');
  static grace = seconds(45);                // a dropped member keeps the seat this long
  static reserve = seconds(90);              // a reserved seat is held this long
}
@room_template("battle")
class Battle:
    capacity = 8
    tick = Tick.HZ30
    host = Host.BACKEND                      # or Host.DEDICATED_SERVER, Host.MASTER_CLIENT
    map = maps.named("arena-caves-v3")
    grace = seconds(45)                      # a dropped member keeps the seat this long
    reserve = seconds(90)                    # a reserved seat is held this long
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(PSRoomTemplate = (Name = "battle", Capacity = 8, Tick = 30,
                         Host = "Backend",                  // or "DedicatedServer", "MasterClient"
                         Map = "arena-caves-v3",
                         Grace = "45s",                     // a dropped member keeps the seat
                         Reserve = "90s"))                  // a reserved seat is held
struct FBattle { GENERATED_BODY() };
// 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
[RoomTemplate("battle")]
public static class Battle
{
    public static int Capacity = 8;
    public static Tick Tick = Tick.Hz30;
    public static Host Host = Host.Backend;          // or DedicatedServer, MasterClient
    public static MapRef Map = Maps.Named("arena-caves-v3");
    public static Duration Grace = 45.Seconds();     // a dropped member keeps the seat this long
    public static Duration Reserve = 90.Seconds();   // a reserved seat is held this long
}

Onde quer que seja escrito, o modelo publicado ganha versão e pode ser editado no painel; assim, a equipe de live-ops reajusta um tipo de sala sem um novo deploy da engine. Hospedar as salas criadas a partir dele é a mesma interface, acessada por outro papel (role), e tanto um servidor dedicado Unreal quanto um cliente mestre dispõem dela inteira: registrar, hospedar várias por processo, alterar a configuração ao vivo, expulsar, publicar, descartar.

O hook entry-validator: jogadores banidos são barrados na porta, com um código e um motivo
[Before(Rooms.Entry, room: "battle")]                 // the entry-validator interface
public static Verdict ValidateEntry(EntryRequest entry) =>
    entry.Player.IsBanned
        ? Entry.Reject(Problem.Banned, "banned from this project")
        : Entry.Accept();
// the entry-validator interface
export const validateEntry = before(Rooms.entry, { room: 'battle' },
  (entry: EntryRequest) =>
    entry.player.isBanned
      ? Entry.reject(Problem.banned, 'banned from this project')
      : Entry.accept());
@before(rooms.entry, room="battle")                   # the entry-validator interface
def validate_entry(entry: EntryRequest) -> Verdict:
    if entry.player.is_banned:
        return entry.reject(Problem.BANNED, "banned from this project")
    return entry.accept()
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 — a Unreal assina os eventos que ele gera. 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 — a Unity assina os eventos que ele gera.

No cliente:

Buscar salas CTF por filtro, entrar com dados de loadout e reagir a quem chega
var rooms = await playserv.Rooms.Browse("mode == 'ctf' && players < capacity");
var room  = await playserv.Rooms.Join(rooms.First(), with: new { loadout = "scout" });
room.OnMemberJoined(m => Hud.Add(m));
const rooms = await playserv.rooms.browse("mode == 'ctf' && players < capacity");
const room = await playserv.rooms.join(rooms[0], { with: { loadout: 'scout' } });
room.onMemberJoined((m) => hud.add(m));
rooms = await playserv.rooms.browse("mode == 'ctf' && players < capacity")
room = await playserv.rooms.join(rooms[0], with_data={"loadout": "scout"})
room.on_member_joined(lambda m: hud.add(m))
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->Rooms->Of<FBattle>()->Select()
    .Where(PSFields::Room::Mode == TEXT("ctf"))
    .Then(TPSOnResult<TPSPage<FPSRoomInfo>>::CreateWeakLambda(this, [this](const TPSResult<TPSPage<FPSRoomInfo>>& Found)
    {
        if (!Found.HasValue()) { return; }
        // join the first match; the join data rides along
        Client->Rooms->Join(Found.Value().Rows[0], FPSJoinData{{ TEXT("loadout"), TEXT("scout") }},
            TPSOnResult<FPSRoom*>::CreateWeakLambda(this, [this](const TPSResult<FPSRoom*>& JoinResult)
            {
                if (!JoinResult.HasValue()) { return; }
                TPSSubscription Roster = JoinResult.Value()->Subscribe->Presence(
                    [this](const FPSPresence& Presence) { Hud->Add(Presence); });
            }));
    }));
// 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
var rooms = await playserv.Rooms.Browse("mode == 'ctf' && players < capacity");
var room  = await playserv.Rooms.Join(rooms.First(), with: new { loadout = "scout" });
room.OnMemberJoined(m => Hud.Add(m));

O modelo

Uma sala é um grupo com regras, e não uma entidade. A participação vem dos grupos; o estado de sistema da sala fica no nível da plataforma, enquanto o estado do jogo vive em entidades com escopo nela. E nenhum código seu roda dentro de uma sala, em nenhum dos modos de autoridade.

O que um tipo de sala declara.

DeclaraO que é
capacidadeem vagas; a vaga é a unidade de capacidade, separada da participação: pode ser reservada antes da entrada e mantida durante a inatividade. A reserva tem tempo limitado, com um prazo declarado depois do qual a vaga é liberada sem que ninguém entre
visibilidadelistada · por nome ou código · oculta
modo de criaçãoum de três, e o modo na primeira entrada é obrigado a declarar um hook de inicialização
dois timeouts independenteso timeout de inatividade — quanto tempo um participante pode ficar em silêncio; e o TTL de sala vazia — quanto tempo sobrevive uma sala sem ninguém dentro
janela de retornodentro dela, um retorno restaura a mesma participação e a mesma vaga, em vez de criar um novo participante
modo de autoridadesimulação da plataforma ou autoridade externa, e não há padrão
confiança em um resultado reportadopara autoridade externa: aceitar · verificar com um hook · não aceitar. Também sem padrão
comportamento quando o host caiesperar o fim do período de tolerância · fechar a sala · admitir um substituto
instância de mapa e estratos do mundoopcionalmente, qual instância a sala ocupa e quais estratos dentro dela
entidades com escopo de salaquais entidades do estúdio têm o escopo da sala — expresso por um predicado, e não por um mecanismo novo

As duas máquinas de estados.

DeEstados
uma salacreated → open → closed → torn down, em que torn down é terminal e closed significa que a sala não aceita novas entradas, e não que deixou de existir
uma participaçãoactive ⇄ inactive → departed, com departed terminal para aquela participação

O que vale para toda sala.

SempreO que é
perder a conexão e sairsão eventos diferentes, e o desfecho da janela é observável: “voltou” e “a janela expirou” são distinguíveis, então o cliente nunca fica adivinhando o que aconteceu
um espectadornão é um participante degenerado: está presente, não ocupa vaga e fica fora da lista de participantes (roster) tratada como “os jogadores”
sem papéis dentro da salao dono da sala é um ator que tem um direito (Access & Roles), e não uma posição na lista de membros
presençatem histórico; a lista de participantes, não: fica registrado quem entrou, caiu, voltou e saiu, e as mudanças da lista não formam um segundo histórico
a interfaceé a de uma sala específica: você se dirige a esta sala, não só ao tipo dela
três eixos, não doisa API que abrange todas as salas (buscar, registrar, listar); a API por sala que qualquer membro chama (entrar, sair); e uma interface de administração por instância — expulsar, trancar, alterar a configuração, fechar, descartar esta sala —, liberada para quem tem o papel de administrador ou de host daquela instância, e não para quem é membro
one room type — one set of declarationsexternal authorityour simulationdedicated server · host keymaster client · room-ownerbackend · backend-service a match on a fleeta player's machinea mini-game
Quem executa o tick: um tipo de sala, com seus hosts agrupados por modo de autoridade

Os dois modos de autoridade.

ModoQuem executa o tick
simulação da plataformaa implementação de sala da plataforma e seus módulos
autoridade externaum processo que roda o SDK da plataforma, dentro da sala, com um papel autoritativo: o servidor de jogo do estúdio ou o cliente de um jogador, como cliente mestre

O que o modo decide, e o que não decide.

O que é
a fronteiraé traçada pelo papel, e não por quem é dono do processo. Um servidor dedicado é o mesmo cliente, sem a renderização; o que o separa da máquina de um jogador é a confiança, e não a construção. É também por isso que o peer-to-peer dispensa um terceiro modo: ele é uma sala em modo de autoridade externa cuja autoridade é um host cliente
o que é idênticoregras de entrada, presença, reconexão e todas as declarações, nos três casos. Muda apenas qual processo detém a autoridade e quanto dela esse processo recebe
a sala não se mudanenhum código seu roda dentro de uma sala em nenhum dos modos, e o estado de sessão e o estado persistente ficam na plataforma nos dois. O cliente mestre é um membro com papel autoritativo: o tick é calculado nele, mas a sala não mora nele

Hospedar uma sala.

Registrar salas a partir do modelo publicado: uma chave de idempotência para cada uma, várias por processo
var room   = await playserv.Rooms.Register("battle", key: "caves-eu-1");
var second = await playserv.Rooms.Register("battle", key: "caves-eu-2"); // several per process

room.OnMemberJoined(m => Seat(m));
await room.SetConfig(c => c.Set("mapRotation", "night"));                // live config, no restart

await room.Dispose();
const room   = await playserv.rooms.register('battle', { key: 'caves-eu-1' });
const second = await playserv.rooms.register('battle', { key: 'caves-eu-2' }); // several per process

room.onMemberJoined((m) => seat(m));
await room.setConfig((c) => c.set('mapRotation', 'night'));

await room.dispose();
room   = await playserv.rooms.register("battle", key="caves-eu-1")
second = await playserv.rooms.register("battle", key="caves-eu-2")   # several per process

room.on_member_joined(lambda m: seat(m))
await room.set_config(lambda c: c.set("mapRotation", "night"))

await room.dispose()
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->Rooms->Of<FBattle>()->Create(FPSIdempotencyKey(TEXT("caves-eu-1")),
    TPSOnResult<FPSRoom*>::CreateWeakLambda(this, [this](const TPSResult<FPSRoom*>& Result)
    {
        if (!Result.HasValue()) { return; }
        OnRoomUp(Result.Value());
    }));
Client->Rooms->Of<FBattle>()->Create(FPSIdempotencyKey(TEXT("caves-eu-2")), OnSecondRoom);

// in OnRoomUp(FPSRoom* Room):
TPSSubscription Roster = Room->Subscribe->Presence([this](const FPSPresence& Presence) { Seat(Presence); });
Room->Config->Modify({ .MapRotation = TEXT("night") });

Room->Delete();   // demolish — the declared end of the room's existence
// 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 — como build de cliente mestre — um cliente que registra a sala tem, em runtime, a mesma interface de host
var room   = await playserv.Rooms.Register("battle", key: "caves-eu-1");
var second = await playserv.Rooms.Register("battle", key: "caves-eu-2"); // several per process

room.OnMemberJoined(m => Seat(m));
await room.SetConfig(c => c.Set("mapRotation", "night"));                // live config, no restart

await room.Dispose();
SempreO que é
o handleé o mesmo objeto que a aba do cliente usa: não existe bootstrap de host nem handle exclusivo de servidor. Ele atende a essas chamadas porque, em runtime, o papel do ator as inclui — um servidor dedicado ou um cliente mestre rodando com uma chave de host (Access & Roles)
o registrorecebe uma chave de idempotência, porque sem ela um timeout no registro não teria como ser recuperado: repita a chamada com a mesma chave e você recebe de volta a mesma sala, e não uma segunda cujo endereço ninguém conhece

Entrada, presença e reconexão.

seat reserved by Matchmakingseat freed, with an eventyour entry hookrefused — a code and a reasonactive member — room state, then live trafficinactive — the seat is helddeparted — a leave, with an event 90 s, no joinjoin with datarejectedacceptedconnection lostback in 45 s45 s passed
A vida de uma vaga: reservada, verificada na porta, mantida durante uma queda (90 s e 45 s são os valores do modelo battle)
O que é
entradaé um pedido validado: quem entra envia dados ao entrar, e o hook de entrada aceita ou recusa com um código e um motivo. É a partir desses dados que o membro se inicializa — em battle, o Tank do membro faz spawn com o loadout enviado na entrada
vagas e reservaso módulo Matchmaking reserva uma vaga pelo prazo de reserva do modelo — 90 segundos em battle — e depois o cliente entra diretamente. As reservas contam para a capacidade, e a expiração libera a vaga com um evento, e não em silêncio
queda não é saídaum membro desconectado mantém a vaga durante o período de tolerância (45s em battle) e reconecta na mesma participação; quando o período expira, a queda vira saída, e o evento informa qual dos dois aconteceu. Quem volta um segundo atrasado ouve que a sala está viva e a participação não — uma resposta diferente de “essa sala não existe”
entrada tardia é estado, não históricoquem entra recebe o estado atual da sala e, depois, o tráfego ao vivo

Eventos não são reenviados depois de uma reconexão

Os eventos enviados enquanto o jogador estava fora não são reenviados, nem para quem entra atrasado, nem para um membro que volta. O que precisa sobreviver a esse intervalo tem de ser estado: a mina que um jogador plantou é uma entidade com escopo de sala, e não uma mensagem MinePlaced que alguém precisa pegar a tempo.

Erros

  • Uma sala que não existe ou que um predicado oculta e uma sala desmontada respondem not found: uma recusa nunca revela uma sala que você não pode ver.
  • Capacidade esgotada é um conflito, e vagas reservadas contam como ocupadas; vale tentar de novo quando uma vaga liberar. Fechada para entradas também é um conflito, e vale tentar de novo se ela reabrir.
  • Uma entrada recusada por uma regra é um conflito; recusada por um hook, ela traz o código e o motivo do próprio hook, para que “uma regra do jogo disse não” nunca chegue parecendo uma falha de transporte.
  • A janela de retorno (rejoin window) expirou é um conflito: entre como novo participante, com uma nova vaga.
  • Uma reserva vencida é um conflito: faça uma nova.
  • Uma instância de mapa indisponível ou inexistente é uma recusa de validação.
  • Uma transferência recusada pela sala de destino é um conflito, e o que fazer depende do motivo.
  • Criar salas acima do limite responde com rate limit ou com conflito, conforme o limite atingido.

Limites

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

  • Capacidade de uma sala: a entrada é recusada como conflito, e as vagas reservadas contam como ocupadas.
  • Salas por projeto: a criação é recusada como conflito.
  • Salas por ator: a criação é recusada, e salas já criadas nunca são desmontadas para abrir espaço.
  • Taxa de criação de salas: rate limit com prazo.
  • Timeout de inatividade de um participante: saída forçada com um evento e um motivo declarado.
  • TTL de sala vazia: desmontagem com um evento; pode ser desligado no tipo de zona persistente.
  • Prazo de uma reserva de vaga: liberação com um evento.
  • Salas em uma instância de mapa: a criação em uma instância ocupada é recusada, a menos que o tipo tenha declarado ocupação compartilhada.
  • Tamanho do payload de um evento de sala: a publicação é recusada antes do envio, nunca truncada.