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

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

Matchmaking

Як привести гравця в потрібну кімнату. Тікет описує гравця й відбирає решту. Матчмейкер знаходить розміщення й резервує місце, а далі ігровий трафік іде просто в кімнату.

Матчмейкер стоїть на шляху лише раз — щоб вирішити, куди вам потрапити. У самому матчі він не бере участі: результат його роботи — розміщення і резерв місця (seat) на обмежений час, а від моменту входу ігровий трафік іде просто в кімнату. Тож навантажена черга ніколи не перетворюється на навантажену гру.

a ticket — a player, or a party entering wholeyour pre-enqueue hook — stamps trusted criteriathe queue — the requirement widens rung by rungexpired — an outcome, not an errormatched — atomicallyRoomPlacement — a room and reserved seatsRosterSet — the roster only, no roomthe room lifetime passeda simultaneous gamean asynchronous gamethe player joins; the matchmaker leaves the path
Шлях тікета: дані проставляють на сервері, вимоги послаблюють щабель за щаблем, склад збирають атомарно, а потім тікет передають кімнаті, і матчмейкер іде

Коли це потрібно

  • Гравців (player) треба розподіляти по кімнатах за оголошеними критеріями — режимом, регіоном, рангом, — а не через саморобний список лобі.
  • Критерії підбору гравців (matchmaking) мають братися з даних платформи, а не зі слів клієнта: проставте ранг у хуку перед постановкою в чергу.
  • Черга має з часом розширюватися на боці сервера, поки клієнт тримає один тікет і нічого не опитує.
  • Паті має потрапити в матч разом: група (group) входить уся або не входить зовсім.
  • У вас власний зовнішній матчмейкер, і вам треба лише довести його рішення до розміщення й резерву місця.
  • Не потрібно, якщо гравці самі вибирають сесію. З цим упораються список кімнат і Join.

Хто що робить

АкторЩо робить на цій сторінці
playerстворює й скасовує власний тікет, входить у складі паті
match-organizerоголошує черги матчмейкера й послаблення вимог; читає результати розміщення
backend-serviceпроставляє довірені критерії перед постановкою в чергу; виконує рішення зовнішнього матчмейкера

Коротко

Пошук матчу: один виклик Find повертає зарезервоване місце, у яке можна ввійти
// client — one call for the common case
var seat = await playserv.Matchmaking.Find("ranked-duo");
var room = await playserv.Rooms.Join(seat);
// client — one call for the common case
const seat = await playserv.matchmaking.find('ranked-duo');
const room = await playserv.rooms.join(seat);
# client — one call for the common case
seat = await playserv.matchmaking.find("ranked-duo")
room = await playserv.rooms.join(seat)
Незабаром — Go

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

// client — one call for the common case
Client->Matchmaking->Of<FRankedDuo>()->Tickets->Create(FPSTicketClaim{ .Mode = TEXT("duo") },
    TPSOnResult<FPSTicket*>::CreateWeakLambda(this, [this](const TPSResult<FPSTicket*>& TicketResult)
    {
        if (!TicketResult.HasValue()) { return; }
        // the seat arrives as the ticket's outcome
        TPSSubscription Placement = TicketResult.Value()->Subscribe([this](const FPSSeat& Seat)
        {
            Client->Rooms->Join(Seat,
                TPSOnResult<FPSRoom*>::CreateWeakLambda(this, [this](const TPSResult<FPSRoom*>& JoinResult)
                {
                    if (!JoinResult.HasValue()) { return; }
                    EnterMatch(JoinResult.Value());
                }));
        });
    }));
// 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 без змін, зі згенерованими типами
// client — one call for the common case
var seat = await playserv.Matchmaking.Find("ranked-duo");
var room = await playserv.Rooms.Join(seat);
Оголошення черги ranked-duo: взаємні фільтри й драбина послаблення з двох щаблів
[Matchmaker("ranked-duo")]
public static class RankedDuo
{
    public static Size Size = Size.Exactly(4, multiple: 2);
    public static string Filter = "mode == 'duo' && region == self.region";
    public static Relax[] Relax =
    {
        Relax.After(15.Seconds(), "abs(rank - self.rank) < 300"),
        Relax.After(45.Seconds(), "abs(rank - self.rank) < 800"),
    };
}
@Matchmaker('ranked-duo')
export class RankedDuo {
  static size = Size.exactly(4, { multiple: 2 });
  static filter = "mode == 'duo' && region == self.region";
  static relax = [
    Relax.after(seconds(15), 'abs(rank - self.rank) < 300'),
    Relax.after(seconds(45), 'abs(rank - self.rank) < 800'),
  ];
}
@matchmaker("ranked-duo")
class RankedDuo:
    size = Size.exactly(4, multiple=2)
    filter = "mode == 'duo' && region == self.region"
    relax = [
        Relax.after(seconds(15), "abs(rank - self.rank) < 300"),
        Relax.after(seconds(45), "abs(rank - self.rank) < 800"),
    ]
Незабаром — Go

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

USTRUCT(PSMatchmaker = (Name = "ranked-duo", Size = "Exactly:4", Multiple = 2,
                       Filter = "mode == 'duo' && region == self.region"))
struct FRankedDuo
{
    GENERATED_BODY()

    UPROPERTY(PSRelax = (After = "15s", Filter = "abs(rank - self.rank) < 300")) FPSRelax First;
    UPROPERTY(PSRelax = (After = "45s", Filter = "abs(rank - self.rank) < 800")) FPSRelax Second;
};
// 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 тут немає
[Matchmaker("ranked-duo")]
public static class RankedDuo
{
    public static Size Size = Size.Exactly(4, multiple: 2);
    public static string Filter = "mode == 'duo' && region == self.region";
    public static Relax[] Relax =
    {
        Relax.After(15.Seconds(), "abs(rank - self.rank) < 300"),
        Relax.After(45.Seconds(), "abs(rank - self.rank) < 800"),
    };
}

Критерії, у яких клієнтові не можна довіряти, проставляє хук перед постановкою в чергу:

Хук перед постановкою в чергу бере ранг із даних платформи, а не зі слів клієнта
[Before(Matchmaking.Enqueue)]                     // the server has the last word
public static async Task<Ticket> StampRank(Ticket t)
{
    var rows = await PlayServ.Leaderboards.ForOwners("ranked", new[] { t.Player });
    t.Properties["rank"] = rows[0].Rank;          // the row carries its rank in the full table
    return t;
}
// the server has the last word
export const stampRank = before(Matchmaking.enqueue, async (t: Ticket) => {
  const rows = await PlayServ.leaderboards.forOwners('ranked', [t.player]);
  t.properties.rank = rows[0].rank;               // the row carries its rank in the full table
  return t;
});
@before(matchmaking.enqueue)                      # the server has the last word
async def stamp_rank(t: Ticket) -> Ticket:
    rows = await playserv.leaderboards.for_owners("ranked", [t.player])
    t.properties["rank"] = rows[0].rank           # the row carries its rank in the full table
    return t
Незабаром — Go

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

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

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

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

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

Хук читає рядки за списком власників із модуля Leaderboards — так само, як для когорти друзів, — і кожен повернений рядок містить ранг свого власника в усій таблиці. Хук може запросити чужий рядок, бо предикат таблиці лідерів (leaderboard) дозволяє це хмарній функції. Сесія гравця з тим самим запитом отримає лише власний рядок.

Модель

Що несе тікет.

ЧастинаЩо цеХто їй вірить
самоописоголошені властивості учасника — рейтинг, режим, мова, вибрана мапаніхто без перевірки: це лише заява того, хто викликає
вимогапредикат, якому має відповідати рештаплатформа, бо саме вона його застосовує

Учасник тікета — актор або група. Група входить цілою, і це й є паті. Її тікет неподільний: група потрапляє до складу (roster) або вся, або ніяк.

Що оголошує тип черги.

Що оголошуєЩо це
властивостіз назвою і типом. Властивість, не оголошену тут, платформа не ігнорує в тікеті, а відхиляє як помилку валідації
розмір складумінімум, максимум і крок сумісності — кратність, за якої склад допустимий. Тож команди «по п’ять» — це саме п’ять, а не будь-яке число від двох до десяти
драбина вимогупорядкований набір предикатів із вікнами: кожен щабель — ширша вимога і час, після якого підбір переходить далі. Послаблення вимог — це оголошення, а не довільна логіка в обробнику
мова предикатівта сама, що й скрізь, і в її словнику є власні властивості тікета — можна виразити «рейтинг у межах ±100 від мого».
взаємністьчи допустимий склад, у якому A приймає B, а B не приймає A. Значення за замовчуванням немає
час життя тікетаколи він спливає, тікет переходить у expired з подією (event)
результатRoomPlacement — посилання на кімнату (room) плюс резерви в ній, для синхронної гри; або RosterSet — лише склад, без кімнати й без резервів, для асинхронної гри, де суперник не в мережі

Стани тікета. created → queued → matched · cancelled · expired; останні три — кінцеві.

ЗавждиЩо це
один живий тікет на учасника в черзідругий тікет — це конфлікт, а не друга заявка: прочитайте наявний
причина, чому склалася паравидима: вона потрапляє в подію підбору й в історію. У вбудованих алгоритмах це щабель драбини. Реалізація, що їх перевизначає, може не мати щаблів, і тоді причина — непрозоре значення, яке вона оголошує. Але причина є завжди
спливання термінуце результат, а не помилка: «склад не зібрався за оголошений час» — нормальне завершення, яке приходить як результат тікета
результатприходить через підписку (subscription), а не опитуванням: клієнт тримає один тікет і більше нічого не питає
matchedатомарний: для RoomPlacement або склад зібрано і кожен учасник має резерв, або тікети лишаються в черзі. Для RosterSet атомарний результат — лише склад

Втрата зв’язку скасовує тікет

Тікет — це заявка на гру просто зараз, тож коли гравець від’єднується, платформа його скасовує. Якби матчмейкер підібрав відсутнього гравця, склад став би гіршим для всіх інших.

Помилки

  • Другий тікет у тій самій черзі — це конфлікт. Не повторюйте запит, а прочитайте наявний тікет.
  • Неоголошена властивість або вимога, що на неї посилається, — помилка валідації, а не мовчазне ігнорування, яке згодом обернулося б повідомленням «суперників не знайдено».
  • Черга призупинена — відповідь unavailable, а не forbidden: права того, хто викликає, на місці, а ситуація тимчасова, тож правильно повторювати запит зі зростаючою затримкою.
  • Не вдається створити кімнату для результату — теж unavailable: повторюйте зі зростаючою затримкою.
  • Не вдалося зарезервувати місце — конфлікт, який варто повторити: тікет лишається в черзі.
  • Тікет не знайдено або він чужий, а також актор, якого предикат не пускає в чергу, — в обох випадках відповідь not found, тож відмова не видає ні тікета, ні черги.

Обмеження

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

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