Перейти до основного вмісту
Жива гра

Цю сторінку ще не перекладено — показано англійську версію.

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

Як оголошувати через атрибути в 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
Unity C# is the same C# API here — той самий клас шаблону компілюється в Unity на базовій версії 2021.3 — синтаксису C# 12 тут немає
[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

Як оголошувати через атрибути в Go, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.

Виконується поза рушієм

Хук — це хмарна функція: він виконується на платформі, а не в рушії. Напишіть його на C#, TypeScript або Python, а Unreal підпишеться на події, які він породжує. У планах — функції, написані просто в рушії: графи Blueprint і C++, який транслюватиметься у скрипт, що його перевіряє платформа. Їх аналізуватимуть і завантажуватимуть так само, як будь-яке оголошення, а виконуватимуться вони, як і раніше, на платформі.

Виконується поза рушієм

Хук — це хмарна функція: він виконується на платформі, а не в рушії. Напишіть його на C#, TypeScript або Python, а Unity підпишеться на події, які він породжує.

Клієнт:

Знайти кімнати CTF за фільтром, увійти з даними спорядження і реагувати на нових гравців
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

Як оголошувати через атрибути в 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.
Unity C# is the same C# API here — працює в 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));

Модель

Кімната — це група з правилами, а не сутність. Участь у кімнаті походить із груп. Системний стан кімнати живе на рівні платформи, а стан гри — у сутностях, прив’язаних до цієї кімнати. І всередині кімнати не виконується жоден код споживача — в жодному з режимів авторитетності.

Що оголошує тип кімнати.

Що оголошуєЩо це
вмісткістьвимірюється в місцях. Місце — одиниця вмісткості, відокремлена від участі: його можна зарезервувати до входу й утримувати, поки учасник неактивний. Резерв обмежений у часі: він має оголошений крайній термін, після якого місце звільняється, якщо ніхто так і не ввійшов
видимістьу загальному списку · за назвою чи кодом · прихована
режим створенняодин із трьох; режим під час першого входу зобов’язаний оголосити хук ініціалізації
два незалежні тайм-аутитайм-аут бездіяльності — скільки учасник може мовчати; і TTL порожньої кімнати — скільки живе кімната, у якій нікого немає
вікно поверненняякщо гравець повернувся в межах цього вікна, він отримує ту саму участь і те саме місце, а не стає новим учасником
режим авторитетностісимуляція платформи або зовнішнє авторитетне джерело (external authority), і значення за замовчуванням немає
довіра до надісланого результатудля зовнішнього авторитетного джерела: прийняти · перевірити хуком · не приймати. Значення за замовчуванням тут теж немає
поведінка, коли хост відпадаєперечекати пільговий період · закрити кімнату · допустити заміну
екземпляр мапи і яруси світуза бажанням — який екземпляр мапи займає кімната і які яруси всередині нього
сутності кімнатиякі сутності (entity) студії прив’язані до кімнати. Це задає предикат, а не окремий механізм

Дві машини станів.

Об’єктСтани
кімнатаcreated → open → closed → torn down. torn down — кінцевий стан, а closed означає, що нових входів немає, а не що кімнати більше не існує
участьactive ⇄ inactive → departed; для цієї участі departed — кінцевий стан

Що справджується для будь-якої кімнати.

ЗавждиЩо це
втрата зв’язку і вихід— різні події, і результат вікна видно: «повернувся» і «вікно спливло» можна розрізнити, тож клієнтові не доводиться вгадувати, що сталося
глядач— не урізаний учасник: він присутній, але не займає місця і не входить до складу, до якого звертаються як до «гравців»
ролей усередині кімнати немаєвласник кімнати (room owner) — це актор, що має право (Access & Roles), а не звання в списку учасників
присутністьу присутності (presence) є історія, а в складу — немає. Платформа зберігає, хто ввійшов, відпав, повернувся і пішов, а зміни складу (roster) другою історією не стають
інтерфейсналежить конкретній кімнаті: ви звертаєтеся до цієї кімнати, а не лише до її типу
три осі, а не двіAPI для всіх кімнат (перегляд, реєстрація, список); API окремої кімнати, який викликає будь-який учасник (вхід, вихід); і адмінський інтерфейс екземпляра — вигнати, закрити вхід, змінити конфіг, закрити, знищити цю кімнату. Доступ до нього дає не участь, а роль адміністратора чи хоста саме цього екземпляра
one room type — one set of declarationsexternal authorityour simulationdedicated server · host keymaster client · room-ownerbackend · backend-service a match on a fleeta player's machinea mini-game
Хто виконує тік: один тип кімнати, хости згруповано за режимом авторитетності

Два режими авторитетності.

РежимХто виконує тік
симуляція платформинаша реалізація кімнати та її модулі
зовнішнє авторитетне джерелопроцес із нашим 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

Як оголошувати через атрибути в 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.
Unity C# is the same C# API here — як збірка майстер-клієнта: клієнт, що зареєстрував кімнату, під час роботи отримує той самий інтерфейс хоста
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)
реєстраціяприймає ключ ідемпотентності, бо інакше після тайм-ауту її вже не відновити. Повторіть виклик із тим самим ключем — і отримаєте ту саму кімнату, а не другу, адреси якої ніхто не знає

Вхід, присутність і перепідключення.

seat reserved by Matchmakingseat freed, with an eventyour entry hookrefused — a code and a reasonactive member — room state, then live trafficinactive — the seat is helddeparted — a leave, with an event 90 s, no joinjoin with datarejectedacceptedconnection lostback in 45 s45 s passed
Життя місця: резерв, перевірка на вході, утримання під час обриву зв’язку (90 с і 45 с — значення з шаблону battle)
Що це
вхід— це запит, який перевіряють. Той, хто входить, передає дані, а хук входу приймає або відхиляє запит із кодом і причиною. З цих даних учасник себе й ініціалізує: у battle танк учасника з’являється з тим спорядженням, яке гравець передав під час входу
місця й резервимодуль Matchmaking займає місце на термін резерву з шаблону — у battle це 90 секунд, — а потім клієнт входить у кімнату напряму. Резерви враховуються у вмісткості, а коли резерв спливає, місце звільняється з подією (event), а не мовчки
обрив — не вихідучасник, що від’єднався, зберігає місце на пільговий період (у battle це 45 с) і перепідключається в ту саму участь. Коли період спливає, обрив стає виходом, і подія повідомляє, що саме сталося. Хто повернувся на секунду пізніше, дізнається, що кімната жива, а його участі вже немає, — і це інша відповідь, ніж «такої кімнати немає»
пізній вхід — це стан, а не журналтой, хто ввійшов, отримує поточний стан кімнати, а потім живий трафік

Після перепідключення події не повторюються

Події, які надійшли, поки гравця не було, повторно не приходять — ні тому, хто запізнився, ні тому, хто повернувся. Усе, що має пережити розрив, повинно бути станом: міна, яку поставив гравець, — це сутність кімнати, а не повідомлення MinePlaced, яке хтось має встигнути перехопити.

Помилки

  • Кімната, якої немає або яку приховує предикат, а також знищена кімната відповідають not found: відмова ніколи не видає кімнату, якої вам не можна бачити.
  • Вмісткість вичерпано — це конфлікт, і зарезервовані місця вважаються зайнятими. Повторити варто, коли місце звільниться. Вхід закрито — теж конфлікт; повторити варто, якщо вхід відкриють.
  • Вхід, який відхилило правило, — це конфлікт. Вхід, який відхилив хук, приходить із кодом і причиною від самого хука, тож «правила гри не пустили» ніколи не схоже на збій транспорту.
  • Вікно повернення (rejoin window) спливло — це конфлікт: увійдіть як новий учасник, на нове місце.
  • Резерв сплив — це конфлікт: візьміть новий.
  • Недоступний або неіснуючий екземпляр мапи — відмова під час валідації.
  • Перехід, який відхилила цільова кімната, — це конфлікт, а що робити далі, залежить від причини.
  • Створення понад ліміт кімнат відповідає обмеженням частоти або конфліктом — залежно від того, який ліміт спрацював.

Обмеження

Конкретні числа з’являться в розділі про обмеження платформи, а що відбувається на кожній стелі, визначено вже зараз.

  • Вмісткість кімнати: вхід відхиляється як конфлікт, зарезервовані місця вважаються зайнятими.
  • Кімнат на проєкт: створення відхиляється як конфлікт.
  • Кімнат на актора: створення відхиляється, а вже створені кімнати платформа ніколи не знищує, щоб звільнити місце.
  • Частота створення кімнат: обмеження частоти із зазначенням, коли можна повторити.
  • Тайм-аут бездіяльності учасника: примусовий вихід з подією й оголошеною причиною.
  • TTL порожньої кімнати: знищення з подією; для типу постійної зони його можна вимкнути.
  • Крайній термін резерву місця: звільнення з подією.
  • Кімнат на одному екземплярі мапи: створення на зайнятому екземплярі відхиляється, якщо тип не оголосив спільного використання.
  • Розмір корисного навантаження події кімнати: публікацію відхиляють до надсилання, а не обрізають.