Цю сторінку ще не перекладено — показано англійську версію.
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, і майстер-клієнт отримують його повністю: реєструють кім нати, тримають кілька кімнат в одному процесі, змінюють живий конфіг, виганяють гравців, публікують і знищують кімнати.
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 секунд, — а потім клієнт входить у кімнату напряму. Резерви враховуються у вмісткості, а коли резерв спливає, місце звільняється з подією (event), а не мовчки |
обрив — не вихід | учасник, що від’єднався, зберігає місце на пільговий період (у battle це 45 с) і перепідключається в ту саму участь. Коли період спливає, обрив стає виходом, і подія повідомляє, що саме сталося. Хто повернувся на секунду пізніше, дізнається, що кімната жива, а його участі вже немає, — і це інша відповідь, ніж «такої кімнати немає» |
пізній вхід — це стан, а не журнал | той, хто ввійшов, отримує поточний стан кімнати, а потім живий трафік |
Після перепідключення події не повторюються
Події, які надійшли, поки гравця не було, повторно не приходять — ні тому, хто запізнився, ні тому, хто повернувся. Усе, що має пережити розрив, повинно бути станом: міна, яку поставив гравець, — це сутність кімнати, а не повідомлення MinePlaced, яке хтось має встигнути перехопити.
Помилки
- Кімната, якої немає або яку приховує предикат, а також знищена кімната відповідають not found: відмова ніколи не видає кімнату, якої вам не можна бачити.
- Вмісткість вичерпано — це конфлікт, і зарезервовані місця вважаються зайнятими. Повторити варто, коли місце звільниться. Вхід закрито — теж конфлікт; повторити варто, якщо вхід відкриють.
- Вхід, який відхилило правило, — це конфлікт. Вхід, який відхилив хук, приходить із кодом і причиною від самого хука, тож «правила гри не пустили» ніколи не схоже на збій транспорту.
- Вікно повернення (rejoin window) спливло — це конфлікт: увійдіть як новий учасник, на нове місце.
- Резерв сплив — це конфлікт: візьміть новий.
- Недоступний або неіснуючий екземпляр мапи — відмова під час валідації.
- Перехід, який відхилила цільова кімната, — це конфлікт, а що робити далі, залежить від причини.
- Створення понад ліміт кімнат відповідає обмеженням частоти або конфліктом — залежно від того, який ліміт спрацював.
Обмеження
Конкретні числа з’являться в розділі про обмеження платформи, а що відбувається на кожній стелі, визначено вже зараз.
- Вмісткість кімнати: вхід відхиляється як конфлікт, зарезервовані місця вважаються зайнятими.
- Кімнат на проєкт: створення відхиляється як конфлікт.
- Кімнат на актора: створення відхиляється, а вже створені кімнати платформа ніколи не знищує, щоб звільнити місце.
- Частота створення кімнат: обмеження частоти із зазначенням, коли можна повторити.
- Тайм-аут бездіяльності учасника: примусовий вихід з подією й оголошеною причиною.
- TTL порожньої кімнати: знищення з подією; для типу постійної зони його можна вимкнути.
- Крайній термін резерву місця: звільнення з подією.
- Кімнат на одному екземплярі мапи: створення на зайнятому екземплярі відхиляється, якщо тип не оголосив спільного використання.
- Розмір корисного навантаження події кімнати: публікацію відхиляють до надсилання, а не обрізають.