Esta página aún no está traducida: se muestra la versión en inglés.
Rooms
Una sala es una sesión de juego; a la plataforma no le importa qué la aloja. Una sola abstracción cubre un servidor dedicado por partida, un gran mapa compartido dividido en capas lógicas, una sala alojada en un cliente maestro y un minijuego alojado en el backend. Lo que pasa dentro de la sala es cosa nuestra; tú la manejas desde afuera.
Cuándo usarlo
- Tu juego tiene sesiones (partidas, lobbies, mazmorras, carreras) y algo tiene que encargarse de su ciclo de vida, de la participación (membership) y de las reconexiones.
- Alojas el juego en servidores dedicados, en el cliente maestro de un jugador (player) o en el propio backend, y necesitas que los jugadores lleguen ahí.
- Un mismo mapa compartido tiene que ejecutar muchas sesiones lógicas: capas, delimitadas por Visibility.
- Los jugadores entran a mitad de la sesión y tienen que ver el estado actual: primero el estado de la sala (room) al llegar y después el tráfico en vivo.
- Perder la conexión no debe costar el lugar (seat). El período de gracia (grace window) de la plantilla (45s en
battle) retoma la misma participación. - No lo necesitas si una funcionalidad es solo solicitud y respuesta sobre registros. Con el módulo Data & Subscriptions ya alcanza.
Quién hace qué
| Actor | Qué hace en esta página |
|---|---|
room-owner | registra salas desde cualquier parte del proceso; en una instancia de sala concreta usa la interfaz de administración por instancia: modifica la configuración en vivo, expulsa, bloquea, difunde mensajes y libera la sala |
entry-validator | acepta o rechaza las solicitudes de ingreso con un código y un motivo |
room-visitor | explora salas, entra con datos, se reconecta dentro del período de gracia y sale |
spectator | entra sin competir; recibe las difusiones y el tráfico en vivo |
match-organizer | reserva lugares que cuentan para la capacidad; una reserva vence en el plazo de la plantilla (90s en battle) |
En resumen
battle: capacidad, tick, tipo de host, un mapa con nombre y las dos ventanas de un lugar[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 longTodaví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(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
}Dondequiera que se escriba, la plantilla de sala (room template) subida tiene versiones y se puede editar en el panel, así que live-ops reajusta un tipo de sala sin volver a desplegar el motor. Alojar salas creadas a partir de ella es la misma interfaz, a la que llega otro rol, y tanto un servidor dedicado de Unreal como un cliente maestro la tienen completa: registrar, alojar varias por proceso, modificar la configuración en vivo, expulsar, publicar y liberar.
entry-validator: los jugadores baneados se rechazan en la puerta, con un código y un 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()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.
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; Unreal se suscribe a los eventos que resultan. 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; Unity se suscribe a los eventos que resultan.
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))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->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));El modelo
Una sala es un grupo con reglas, y no es una entidad. La participación viene de los grupos; su estado de sistema pertenece a la plataforma, mientras que el estado del juego vive en entidades con el alcance de la sala. Y ningún código del consumidor se ejecuta dentro de una sala, en ninguno de los dos modos de autoridad.
Qué declara un tipo de sala.
| Declara | Qué es |
|---|---|
capacidad | en lugares; un lugar es la unidad de capacidad separada de la participación: puede reservarse antes de un ingreso y conservarse durante la inactividad. Una reserva tiene tiempo limitado, con un plazo declarado tras el cual el lugar se libera si nadie entró |
visibilidad | aparece en la lista · por nombre o código · oculta |
modo de creación | uno de tres; el modo al primer ingreso está obligado a declarar un hook de inicialización |
dos tiempos de espera independientes | el tiempo de espera por inactividad: cuánto tiempo puede estar en silencio un participante; y el TTL de sala vacía: cuánto sobrevive una sala sin nadie adentro |
ventana de reingreso | dentro de ella, un regreso restaura la misma participación y el mismo lugar, en vez de crear un participante nuevo |
modo de autoridad | simulación de la plataforma o autoridad externa, y no hay valor predeterminado |
confianza en un resultado reportado | para una autoridad externa: aceptarlo · verificarlo con un hook · no aceptarlo. Tampoco aquí hay valor predeterminado |
comportamiento cuando el host se cae | esperar a que venza el período de gracia · cerrar la sala · admitir un reemplazo |
instancia de mapa y estratos del mundo | opcionalmente, qué instancia ocupa y qué estratos dentro de ella |
entidades con alcance de sala | cuáles de las entidades del estudio tienen el alcance de la sala; se expresa con un predicado, no con un mecanismo nuevo |
Las dos máquinas de estados.
| De | Estados |
|---|---|
| una sala | created → open → closed → torn down, donde torn down es terminal y closed significa que no admite nuevos ingresos, no que desapareció |
| una participación | active ⇄ inactive → departed, donde departed es terminal para esa participación |
Qué se cumple en toda sala.
| Siempre | Qué es |
|---|---|
perder la conexión y salir | son eventos distintos, y el resultado de la ventana es observable: “regresó” y “la ventana venció” se distinguen, así que un cliente nunca tiene que adivinar qué pasó |
un espectador | no es un participante degradado: está presente, no ocupa ningún lugar y no forma parte de la lista de participantes (roster) a la que se habla como “los jugadores” |
no hay roles dentro de la sala | el dueño de la sala es un actor que tiene un derecho (Access & Roles), no un rango en la lista de miembros |
presencia | tiene historial, y la lista de participantes no: se conserva quién entró, se desconectó, regresó y se fue; los cambios de la lista no son un segundo historial |
la interfaz | es la de una sala concreta: te diriges a esta sala, no solo a su tipo |
tres ejes, no dos | la API que abarca todas las salas (explorar, registrar, listar); la API por sala que llama cualquier miembro (entrar, salir); y una interfaz de administración por instancia — expulsar, bloquear, modificar la configuración, cerrar y liberar esta sala —, reservada a quien tenga el rol de administrador o de host de esa instancia concreta, no a sus miembros |
Los dos modos de autoridad.
| Modo | Quién ejecuta el tick |
|---|---|
| simulación de la plataforma | nuestra implementación de la sala y sus módulos |
| autoridad externa | un proceso que ejecuta nuestro SDK y está en la sala con un rol autoritativo: el servidor de juego del estudio o el cliente de un jugador como cliente maestro |
Qué decide el modo y qué no.
| Qué es | |
|---|---|
la línea | se traza por rol, no según de quién es el proceso. Un servidor dedicado es el mismo cliente sin el renderizado; lo que lo separa de la máquina de un jugador es la confianza, no cómo está hecho. Por eso el peer-to-peer no necesita un tercer modo: es una sala en modo de autoridad externa cuya autoridad es un host cliente |
qué es idéntico | las reglas de ingreso, la presencia, la reconexión y todas las declaraciones, en los tres casos. Lo único que cambia es qué proceso tiene la autoridad y cuánta se le otorga |
la sala no se muda | en ningún modo se ejecuta código del consumidor dentro de una sala, y el estado de la sesión y el persistente se quedan con nosotros en ambos. Un cliente maestro es un miembro con un rol autoritativo: ahí se calcula el tick, pero la sala no vive ahí |
Alojar una 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()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->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();| Siempre | Qué es |
|---|---|
el handle | es el mismo objeto que usa la pestaña del cliente: no hay arranque de host ni un handle exclusivo del servidor. Responde a estas llamadas porque, en runtime, el rol del actor las incluye: un servidor dedicado o un cliente maestro que se ejecuta con una clave de host (Access & Roles) |
el registro | recibe una clave de idempotencia porque, sin ella, si el registro agota el tiempo de espera no hay forma de recuperarse: repite la llamada con la misma clave y recibirás la misma sala, no una segunda cuya dirección nadie conoce |
Ingreso, presencia y reconexión.
| Qué es | |
|---|---|
ingreso | es una solicitud validada: quien entra aporta datos al unirse y el hook de ingreso acepta o rechaza con un código y un motivo. Con esos datos se inicializa el miembro: en battle, el Tank del miembro aparece con el equipamiento que trajo al entrar |
lugares y reservas | el módulo Matchmaking reclama un lugar durante el plazo de reserva de la plantilla (90 segundos en battle) y después el cliente entra directamente. Las reservas cuentan para la capacidad, y al vencer liberan el lugar con un evento, no en silencio |
una desconexión no es una salida | un miembro desconectado conserva su lugar durante el período de gracia (45s en battle) y se reconecta a la misma participación; al vencer, la desconexión pasa a ser una salida, y el evento indica cuál de las dos fue. A quien vuelve un segundo tarde se le dice que la sala sigue viva y la participación no, una respuesta distinta de “esa sala no existe” |
el ingreso tardío es estado, no un registro de eventos | quien entra recibe el estado actual de la sala y después el tráfico en vivo |
Los eventos no se reproducen después de una reconexión
Los eventos enviados mientras un jugador no estaba no se reproducen, ni para quien entra tarde ni para un miembro que regresa. Todo lo que tenga que sobrevivir a esa pausa debe ser estado: la mina que colocó un jugador es una entidad con alcance de sala, no un mensaje MinePlaced que alguien tiene que atrapar a tiempo.
Errores
- Una sala que no existe o que un predicado oculta, y una sala desmantelada, responden not found: un rechazo nunca revela una sala que no puedes ver.
- La capacidad agotada es un conflicto, y los lugares reservados cuentan como ocupados; vale la pena reintentar cuando se libere un lugar. Cerrada a nuevos ingresos también es un conflicto; vale la pena reintentar si se vuelve a abrir.
- Un ingreso rechazado por una regla es un conflicto; uno rechazado por un hook trae el código y el motivo del propio hook, así que “una regla del juego dijo que no” nunca llega disfrazado de falla de transporte.
- La ventana de reingreso venció es un conflicto: entra como participante nuevo, con un lugar nuevo.
- Una reserva vencida es un conflicto: toma una nueva.
- Una instancia de mapa que no está disponible o no existe es un rechazo de validación.
- Un traslado que rechazó la sala de destino es un conflicto, y qué hacer depende de su motivo.
- Crear salas por encima del límite responde con un límite de frecuencia o con un conflicto, según qué límite se alcanzó.
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.
- La capacidad de una sala: el ingreso se rechaza como conflicto, y los lugares reservados cuentan como ocupados.
- Salas por proyecto: la creación se rechaza como conflicto.
- Salas por actor: la creación se rechaza, y las salas ya creadas nunca se desmantelan para hacer lugar.
- La frecuencia de creación de salas: un límite de frecuencia con un plazo.
- El tiempo de espera por inactividad de un participante: una salida forzada con un evento y un motivo declarado.
- El TTL de sala vacía: desmantelamiento con un evento; se puede desactivar en el tipo de zona persistente.
- El plazo de una reserva de lugar: liberación con un evento.
- Salas en una instancia de mapa: la creación en una instancia ocupada se rechaza, salvo que el tipo haya declarado ocupación compartida.
- El tamaño del payload de un evento de sala: la publicación se rechaza antes de enviar, nunca se trunca.