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ê
| Ator | O que faz nesta página |
|---|---|
room-owner | registra 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-validator | aceita ou recusa pedidos de entrada com um código e um motivo |
room-visitor | navega pelas salas, entra com dados, reconecta dentro do período de tolerância, sai |
spectator | entra sem disputar; recebe as difusões e o tráfego ao vivo |
match-organizer | reserva vagas que contam para a capacidade; a reserva expira no prazo do modelo (90s em battle) |
Resumo
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 longAinda 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[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.
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()Ainda 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 — 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.
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:
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))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.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.
| Declara | O que é |
|---|---|
capacidade | em 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 |
visibilidade | listada · por nome ou código · oculta |
modo de criação | um de três, e o modo na primeira entrada é obrigado a declarar um hook de inicialização |
dois timeouts independentes | o 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 retorno | dentro dela, um retorno restaura a mesma participação e a mesma vaga, em vez de criar um novo participante |
modo de autoridade | simulação da plataforma ou autoridade externa, e não há padrão |
confiança em um resultado reportado | para autoridade externa: aceitar · verificar com um hook · não aceitar. Também sem padrão |
comportamento quando o host cai | esperar o fim do período de tolerância · fechar a sala · admitir um substituto |
instância de mapa e estratos do mundo | opcionalmente, qual instância a sala ocupa e quais estratos dentro dela |
entidades com escopo de sala | quais 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.
| De | Estados |
|---|---|
| uma sala | created → 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ção | active ⇄ inactive → departed, com departed terminal para aquela participação |
O que vale para toda sala.
| Sempre | O que é |
|---|---|
perder a conexão e sair | sã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 espectador | nã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 sala | o dono da sala é um ator que tem um direito (Access & Roles), e não uma posição na lista de membros |
presença | tem 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 dois | a 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 |
Os dois modos de autoridade.
| Modo | Quem executa o tick |
|---|---|
| simulação da plataforma | a implementação de sala da plataforma e seus módulos |
| autoridade externa | um 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êntico | regras 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 muda | nenhum 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.
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()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.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();| Sempre | O 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 registro | recebe 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.
| 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 reservas | o 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ída | um 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órico | quem 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.