Эта страница ещё не переведена — показана английская версия.
Rooms
Комната — это игровая сессия, и платформе всё равно, где она хостится. Одна абстракция покрывает и выделенный сервер на каждый матч, и одну большую общую карту, разбитую на логические слои, и комнату на мастер-клиенте, и мини-игру, которую хостит бэкенд. Внутренности комнаты — наша забота, а вы управляете комнатой снаружи.
Когда это нужно
- В игре есть сессии — матчи, лобби, подземелья, гонки, — и кто-то должен отвечать за их жизненный цикл, участие игроков и переподключения.
- Вы хостите игру на выделенных серверах, на мастер-клиенте игрока или прямо на бэкенде, и игроков (player) нужно направлять туда.
- На одной общей карте (map) должно идти много логических сессий — слоёв, которые разграничивает модуль Visibility.
- Игроки входят посреди сессии и должны увидеть текущее состояние: сначала состояние комнаты (room) на момент входа, затем живой трафик.
- Обрыв связи не должен стоить игроку места (seat): в течение льготного периода (grace window) из шаблона — в
battleэто 45 с — восстанавливается то же самое участие (membership). - Не нужно, если фича сводится к запросам и ответам над записями. С этим справится обычный модуль Data & Subscriptions.
Кто что делает
| Актор | Что делает на этой странице |
|---|---|
room-owner | регистрирует комнаты для всего процесса; на отдельном экземпляре комнаты получает её админский интерфейс: правит живой конфиг, выгоняет игроков, закрывает вход, рассылает сообщения, уничтожает комнату |
entry-validator | принимает или отклоняет запросы на вход, указывая код и причину |
room-visitor | просматривает список комнат, входит с данными, переподключается в пределах льготного периода, выходит |
spectator | входит, не участвуя в борьбе; получает рассылки и живой трафик |
match-organizer | резервирует места, которые учитываются при подсчёте вместимости; резерв истекает через срок из шаблона (в battle это 90 с) |
Коротко
battle: вместимость, тик, тип хоста, именованная карта и два окна для места[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Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.
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
}Где бы ни был написан шаблон комнаты (room template), после загрузки он версионируется и редактируется в панели, поэтому live-ops перенастраивает тип комнаты без передеплоя движка. Хостинг комнат по этому шаблону — тот же интерфейс, только доступ ный другой роли (role). И выделенный сервер Unreal, и мастер-клиент получают его целиком: регистрируют комнаты, держат несколько комнат в одном процессе, правят живой конфиг, выгоняют игроков, публикуют события (event) и уничтожают комнаты.
entry-validator: забаненных игроков не пускают на входе, указывая код и причину[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()Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.
Хук — это облачная функция: он выполняется на платформе, а не в движке. Напишите его на C#, TypeScript или Python, а Unreal подпишется на собы тия, которые он порождает. В планах — функции, написанные прямо в движке: графы Blueprint и C++, который будет транслироваться в скрипт, проверяемый платформой. Их будут анализировать и загружать так же, как любые объявления, а выполняться они по-прежнему будут на платформе.
Хук — это облачная функция: он выполняется на платформе, а не в движке. Напишите его на C#, TypeScript или Python, а Unity подпишется на события, которые он порождает.
Клиент:
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))Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.
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));Модель
Комната — это группа с правилами, а не сущность. Участие в комнате берётся из групп. Системное состояние комнаты живёт на уровне платформы, а состояние игры — в сущностях с областью видимости этой комнаты. И внутри комнаты не выполняется никакой код потребителя — ни в одном из режимов авторитетности.
Что объявляет тип комнаты.
| Что объявляется | Что это |
|---|---|
вместимость | измеряется в местах. Место — единица вместимости, отделённая от участия: его можно зарезервировать до входа и удерживать, пока участник неактивен. Резерв ограничен по времени: у него объявлен крайний срок, после которого место освобождается, если никто так и не вошёл |
видимость | в общем списке · по имени или коду · скрытая |
режим создания | один из трёх; в режиме при первом входе обязательно объявить хук инициализации |
два независимых тайм-аута | тайм-аут бездействия — сколько участник может молчать; и TTL пустой комнаты — сколько живёт комната, в которой никого нет |
окно возврата | если игрок вернулся в пределах этого окна, ему восстанавливается то же участие и то же место, а не создаётся новый участник |
режим авторитетности | симуляция платформы или внешний авторитетный источник (external authority), и значения по умолчанию нет |
доверие к присланному итогу | для внешнего авторитетного источника: принять · проверить хуком · не принимать. Значения по умолчанию здесь тоже нет |
поведение при падении хоста | переждать льготный период · закрыть комнату · пустить замену |
экземпляр карты и ярусы мира | при желании — какой экземпляр карты занимает комната и какие ярусы внутри него |
сущности комнаты | какие сущности (entity) студии получают область видимости комнаты. Это задаётся предикатом, а не отдельным механизмом |
Две машины состояний.
| Объект | Состояния |
|---|---|
| комната | created → open → closed → torn down. torn down — конечное состояние, а closed значит, что новых входов нет, а не что комнаты больше не существует |
| участие | active ⇄ inactive → departed; для этого участия departed — конечное состояние |
Что верно для любой комнаты.
| Всегда | Что это |
|---|---|
потеря связи и выход | — разные события, и чем закончилось окно, видно: «вернулся» и «окно истекло» различимы, так что клиенту не приходится гадать, что произошло |
зритель | — не урезанный участник: он присутствует, но не занимает места и не входит в состав, к которому обращаются как к «игрокам» |
ролей внутри комнаты нет | владелец комнаты (room owner) — это актор, у которого есть право (Access & Roles), а не звание в списке участников |
присутствие | у присутствия (presence) есть история, а у состава (roster) — нет: сохраняется, кто вошёл, отвалился, вернулся и ушёл, а изменения состава второй историей не становятся |
интерфейс | принадлежит конкретной комнате: вы обращаетесь к этой комнате, а не только к её типу |
три оси, а не две | API для всех комнат сразу (просмотр, регистрация, список); API отдельной комнаты, который вызывает любой участник (вход, выход); и админский интерфейс экземпляра — выгнать, закрыть вход, поправить конфиг, закрыть, уничтожить эту комнату. Доступ к нему даёт не участие, а роль администратора или хоста именно этого экземпляра |
Два режима авторитетности.
| Режим | Кто выполняет тик |
|---|---|
| симуляция платформы | наша реализация комнаты и её модули |
| внешний авторитетный источник | процесс с нашим SDK, который находится в комнате с авторитетной ролью: игровой сервер студии или клиент игрока в роли мастер-клиента |
Что режим решает, а что нет.
| Что это | |
|---|---|
граница | проходит по роли, а не по тому, чей это процесс. Выделенный сервер — тот же клиент, только без рендеринга. От машины игрока его отличает доверие, а не устройство. Поэтому для peer-to-peer третий режим не нужен: это комната в режиме внешнего авторитетного источника, где этим источником служит хост на клиенте |
что одинаково | правила входа, присутствие, переподключение и все объявления — во всех трёх случаях. Различается только то, какой процесс авторитетен и сколько прав ему выдано |
комната никуда не переезжает | ни в одном режиме внутри комнаты не выполняется код потребителя, а состояние сессии и постоянное состояние в обоих режимах остаются у нас. Мастер-клиент — это участник с авторитетной ролью: на нём считается тик, но комната на нём не живёт |
Хостинг комнаты.
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()Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.
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();| Всегда | Что это |
|---|---|
дескриптор | — тот же объект, что и во вкладке клиента: отдельной инициализации хоста и серверного дескриптора нет. Он отвечает на эти вызовы, потому что во время работы они входят в роль актора — выделенного сервера или мастер-клиента, запущенного с ключом хоста (host key) (Access & Roles) |
регистрация | принимает ключ идемпотентности, иначе после тайм-аута её уже не восстановить. Повторите вызов с тем же ключом — и получите ту же комнату, а не вторую, адреса которой никто не знает |
Вход, присутствие и переподключение.
| Что это | |
|---|---|
вход | — это проверяемый запрос. Входящий передаёт данные, а хук входа принимает или отклоняет запрос с кодом и причиной. Из этих данных участник себя и инициализирует: в battle танк участника появляется с тем снаряжением, которое тот передал при входе |
места и резервы | модуль Matchmaking занимает место на срок резерва из шаблона — в battle это 90 секунд, — после чего клиент входит в комнату напрямую. Резервы учитываются при подсчёте вместимости, а когда резерв истекает, место освобождается с событием, а не молча |
обрыв — не выход | отключившийся участник сохраняет место на льготный период (в battle это 45 с) и переподключается в то же участие. Когда период истекает, обр ыв превращается в выход, и событие сообщает, чем именно всё закончилось. Тот, кто вернулся на секунду позже, узнает, что комната жива, а его участия уже нет, — и это другой ответ, чем «такой комнаты нет» |
поздний вход — это состояние, а не журнал | вошедший получает текущее состояние комнаты, а затем живой трафик |
После переподключения события не воспроизводятся
События, отправленные, пока игрока не было, заново не приходят — ни опоздавшему, ни вернувшемуся участнику. Всё, что должно пережить разрыв, должно быть состоянием: мина, которую поставил игрок, — это сущность комнаты, а не сообщение MinePlaced, которое кто-то должен успеть поймать.
Ошибки
- Несуществующая комната, комната, скрытая предикатом, и уничтоженная комната отвечают not found: по отказу нельзя узнать о комнате, которую вам не положено видеть.
- Нет свободной вместимости — это конфликт, и зарезервированные места считаются занятыми. Повторить стоит, когда место освоб одится. Вход закрыт — тоже конфликт; повторить стоит, если вход откроют.
- Вход, отклонённый правилом, — это конфликт. Вход, отклонённый хуком, приходит с кодом и причиной из самого хука, поэтому «правила игры не пустили» никогда не выглядит как сбой транспорта.
- Окно возврата истекло — это конфликт: войдите как новый участник, на новое место.
- Резерв истёк — это конфликт: возьмите новый.
- Недоступный или несуществующий экземпляр карты — отказ при валидации.
- Переход, который отклонила целевая комната, — это конфликт, и что делать дальше, зависит от причины.
- Создание комнаты сверх ограничения отвечает ограничением частоты или конфликтом — в зависимости от того, какое ограничение сработало.
Ограничения
Конкретные числа появятся в главе об ограничениях платформы, а что происходит при достижении каждого потолка, определено уже сейчас.
- Вместимость комнаты: вход отклоняется как конфликт, зарезервированные места считаются занятыми.
- Комнат на проект: создание отклоняется как конфликт.
- Комнат на актора: создание отклоняется, а уже созданные комнаты ради свободного места никогда не уничтожаются.
- Частота создания комнат: ограничение частоты с указанием, когда можно повторить.
- Тайм-аут бездействия участника: принудительный выход с событием и объявленной причиной.
- TTL пустой комнаты: уничтожение с событием; для типа постоянной зоны его можно отключить.
- Крайний срок резерва места: освобождение с событием.
- Комнат на одном экземпляре карты: создание на занятом экземпляре отклоняется, если тип не объявил совместное использование.
- Размер полезной нагрузки события комнаты: публикация отклоняется до отправки, а не обрезается.