Cette page n’est pas encore traduite : la version anglaise est affichée.
Rooms
Une salle est une session de jeu, et peu importe à la plateforme ce qui l’héberge. Une seule abstraction couvre un serveur dédié par match, une grande carte partagée découpée en couches logiques, une salle hébergée par un client maître et un mini-jeu hébergé par le backend. Le fonctionnement interne d’une salle est notre affaire ; vous la pilotez de l’extérieur.
Quand l’utiliser
- Votre jeu a des sessions (matchs, lobbies, donjons, courses), et quelque chose doit prendre en charge leur cycle de vie, l’appartenance (membership) et les reconnexions.
- Vous hébergez sur des serveurs dédiés, sur le client maître d’un joueur ou sur le backend lui-même, et il faut y acheminer les joueurs.
- Une seule carte (map) partagée doit faire tourner de nombreuses sessions logiques : ce sont des couches, délimitées par Visibility.
- Des joueurs arrivent en cours de session et doivent voir l’état actuel : l’état de la salle (room) à leur arrivée, puis le trafic en direct.
- Une déconnexion ne doit pas faire perdre sa place (seat) : pendant le délai de grâce du modèle (45 s dans
battle), le joueur retrouve la même appartenance. - Inutile quand une fonctionnalité se résume à des requêtes et des réponses sur des enregistrements : Data & Subscriptions suffit.
Qui fait quoi
| Acteur | Ce qu’il fait sur cette page |
|---|---|
room-owner | enregistre des salles depuis n’importe quel point du processus ; sur une instance de salle, dispose de l’interface d’administration propre à cette instance : modifie la configuration en direct, expulse, verrouille, diffuse, détruit la salle |
entry-validator | accepte ou refuse les demandes d’entrée, avec un code et un motif |
room-visitor | parcourt les salles, en rejoint une en fournissant des données, se reconnecte pendant le délai de grâce (grace window), repart |
spectator | rejoint la salle sans prendre part à la partie ; reçoit les diffusions et le trafic en direct |
match-organizer | réserve des places qui comptent dans la capacité ; une réservation expire au terme fixé par le modèle de salle (room template), soit 90 s dans battle |
En bref
battle : capacité, tick, type d’hôte, une carte nommée et les deux délais qui protègent une place[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 longLa déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.
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
}Où qu’il ait été écrit, le modèle publié est versionné et modifiable dans le panneau : l’équipe live-ops réajuste un type de salle sans redéployer le moteur. Héberger les salles qui en sont issues passe par la même interface, atteinte avec un autre rôle, et un serveur dédié Unreal comme un client maître en disposent entièrement : enregistrer, héberger plusieurs salles par processus, modifier la configuration en direct, expulser, publier, détruire.
entry-validator : les joueurs bannis sont refusés à l’entrée, avec un code et un motif[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()La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.
Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python — Unreal s’abonne aux événements qu’il produit. Des fonctions écrites dans le moteur sont prévues : graphes Blueprint et C++ (traduit vers une cible de script validée par la plateforme). Les unes comme les autres sont analysées et publiées comme toute déclaration ; l’exécution reste sur la plateforme.
Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python — Unity s’abonne aux événements qu’il produit.
Côté client :
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))La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.
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));Le modèle
Une salle est un groupe doté de règles, et ce n’est pas une entité. L’appartenance vient des groupes ; l’état système de la salle relève de la plateforme, tandis que l’état du jeu vit dans des entités rattachées à la salle. Et aucun code du studio ne s’exécute dans une salle, quel que soit le mode d’autorité.
Ce que déclare un type de salle.
| Déclare | Ce que c’est |
|---|---|
capacité | en places, la place étant l’unité de capacité distincte de l’appartenance : on peut la réserver avant l’arrivée et la conserver pendant une inactivité. Une réservation est limitée dans le temps, avec une échéance déclarée au-delà de laquelle la place est libérée si personne n’est arrivé |
visibilité | listée · accessible par nom ou par code · cachée |
mode de création | l’un des trois, et le mode à la première arrivée est tenu de déclarer un hook d’initialisation |
deux délais indépendants | le délai d’inactivité, qui fixe combien de temps un participant peut rester silencieux, et le TTL d’une salle vide, qui fixe combien de temps survit une salle où il n’y a personne |
fenêtre de reconnexion | tant qu’elle court (rejoin window), un retour restitue la même appartenance et la même place, au lieu de créer un nouveau participant |
mode d'autorité | simulation de la plateforme ou autorité externe, et il n’y a pas de valeur par défaut |
confiance dans un résultat rapporté | pour une autorité externe : l’accepter · le vérifier par un hook · ne pas l’accepter. Là non plus, pas de valeur par défaut |
comportement à la perte de l'hôte | attendre la fin du délai de grâce · fermer la salle · admettre un remplaçant |
instance de carte et strates du monde | facultatif : l’instance qu’elle occupe et les strates qu’elle y utilise |
entités rattachées à la salle | les entités du studio qui ont la portée de la salle, exprimées par un prédicat et non par un nouveau mécanisme |
Les deux machines à états.
| Pour | États |
|---|---|
| une salle | created → open → closed → torn down, où torn down est terminal et où closed signifie « plus de nouvelles arrivées », et non « disparue » |
| une appartenance | active ⇄ inactive → departed, departed étant terminal pour cette appartenance |
Ce qui vaut pour toute salle.
| Toujours | Ce que c’est |
|---|---|
perdre la connexion et partir | sont deux événements différents, et l’issue de la fenêtre est observable : « revenu » et « la fenêtre a expiré » se distinguent, si bien qu’un client n’a jamais à deviner ce qui s’est passé |
un spectateur | n’est pas un participant au rabais : il est présent, n’occupe aucune place et ne fait pas partie de la composition (roster) qu’on désigne comme « les joueurs » |
pas de rôles dans la salle | le propriétaire de la salle (room owner) est un acteur qui détient un droit (Access & Roles), pas un rang dans la liste des membres |
la présence | a un historique, la composition non : on conserve qui est arrivé, a décroché, est revenu et est parti ; les changements de composition ne forment pas un second historique |
l'interface | est celle d’une salle précise : vous vous adressez à cette salle, pas seulement à son type |
trois axes, pas deux | l’API qui couvre toutes les salles (parcourir, enregistrer, lister) ; l’API propre à une salle, qu’appelle n’importe quel membre (rejoindre, quitter) ; et une interface d’administration par instance — expulser, verrouiller, modifier la configuration, fermer, détruire cette salle —, réservée à qui détient le rôle d’administrateur ou d’hôte de cette instance précise, et non aux membres |
Les deux modes d’autorité.
| Mode | Qui exécute le tick |
|---|---|
| simulation de la plateforme | notre implémentation de la salle et ses modules |
| autorité externe | un processus qui exécute notre SDK et siège dans la salle avec un rôle faisant autorité : le serveur de jeu du studio, ou le client d’un joueur, en tant que client maître |
Ce que le mode décide, et ce qu’il ne décide pas.
| Ce que c’est | |
|---|---|
la frontière | passe par le rôle, et non par le propriétaire du processus. Un serveur dédié est le même client, sans le rendu ; ce qui le distingue de la machine d’un joueur, c’est la confiance, pas la construction. C’est aussi pourquoi le pair-à-pair n’a pas besoin d’un troisième mode : c’est une salle en mode autorité externe dont l’autorité est un client hôte |
ce qui ne change pas | les règles d’entrée, la présence, la reconnexion et chaque déclaration, dans les trois cas. Seuls diffèrent le processus qui détient l’autorité et la part d’autorité qui lui est accordée |
la salle ne déménage pas | dans aucun des deux modes le code du studio ne s’exécute dans une salle, et l’état de session comme l’état persistant restent chez nous. Un client maître est un membre doté d’un rôle faisant autorité : le tick y est calculé, mais la salle n’y vit pas |
Héberger une salle.
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()La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.
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();| Toujours | Ce que c’est |
|---|---|
le handle | est le même objet que dans l’onglet client : pas d’amorçage propre à l’hôte, pas de handle réservé au serveur. Il répond à ces appels parce que le rôle de l’acteur les inclut au runtime — un serveur dédié ou un client maître qui tourne sous une clé hôte (Access & Roles) |
l'enregistrement | prend une clé d’idempotence, faute de quoi un timeout serait irrattrapable : répétez l’appel avec la même clé et vous récupérez la même salle, et non une seconde dont personne ne connaît l’adresse |
Entrée, présence et reconnexion.
| Ce que c’est | |
|---|---|
l'entrée | est une demande validée : celui qui arrive fournit des données, et le hook d’entrée accepte ou refuse avec un code et un motif. C’est à partir de ces données que le membre s’initialise — dans battle, le Tank du membre apparaît avec l’équipement apporté à l’arrivée |
places et réservations | le module Matchmaking réserve une place pour la durée fixée par le modèle (90 secondes dans battle), puis le client rejoint la salle directement. Les réservations comptent dans la capacité, et leur expiration libère la place avec un événement, pas en silence |
une déconnexion n'est pas un départ | un membre déconnecté garde sa place pendant le délai de grâce (45 s dans battle) et retrouve la même appartenance en se reconnectant ; à l’expiration, la déconnexion devient un départ, et l’événement indique de quel cas il s’agit. Revenir une seconde trop tard, c’est apprendre que la salle existe toujours mais plus l’appartenance — une autre réponse que « cette salle n’existe pas » |
une arrivée tardive reçoit l'état, pas un journal | celui qui arrive reçoit l’état actuel de la salle, puis le trafic en direct |
Les événements ne sont pas rejoués après une reconnexion
Les événements envoyés pendant l’absence d’un joueur ne sont pas rejoués, ni pour celui qui arrive en retard ni pour un membre qui revient. Tout ce qui doit survivre à cette coupure doit être de l’état : la mine posée par un joueur est une entité rattachée à la salle, pas un message MinePlaced que quelqu’un doit attraper au vol.
Erreurs
- Une salle qui n’existe pas ou qu’un prédicat masque, ainsi qu’une salle démantelée, répondent not found : un refus ne révèle jamais une salle que vous n’avez pas le droit de voir.
- La capacité épuisée est un conflit, et les places réservées comptent comme occupées ; réessayez dès qu’une place se libère. Une salle fermée aux arrivées est aussi un conflit, à réessayer si elle rouvre.
- Une arrivée refusée par une règle est un conflit ; refusée par un hook, elle porte le code et le motif propres au hook, si bien qu'« une règle du jeu a dit non » ne ressemble jamais à une panne de transport.
- Une fenêtre de reconnexion expirée est un conflit : rejoignez la salle comme nouveau participant, avec une nouvelle place.
- Une réservation échue est un conflit : prenez-en une nouvelle.
- Une instance de carte indisponible ou inexistante donne lieu à un refus de validation.
- Un transfert refusé par la salle cible est un conflit ; la marche à suivre dépend du motif.
- Une création au-delà de la limite de salles répond par une limite de débit ou par un conflit, selon la limite atteinte.
Limites
Les chiffres viendront avec le chapitre sur les limites de la plateforme ; ce qui se passe à chaque plafond, lui, est déjà fixé.
- La capacité d’une salle : l’arrivée est refusée comme conflit, les places réservées comptant comme occupées.
- Salles par projet : la création est refusée comme conflit.
- Salles par acteur : la création est refusée, et les salles existantes ne sont jamais démantelées pour faire de la place.
- Le rythme de création de salles : une limite de débit, avec une échéance.
- Le délai d’inactivité d’un participant : un départ forcé, avec un événement et un motif déclaré.
- Le TTL d’une salle vide : démantèlement avec un événement ; on peut le désactiver pour le type de zone persistante.
- L’échéance d’une réservation de place : libération avec un événement.
- Salles sur une même instance de carte : la création sur une instance occupée est refusée, sauf si le type a déclaré une occupation partagée.
- La taille de la charge utile d’un événement de salle : la publication est refusée avant l’envoi, jamais tronquée.