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

Эта страница ещё не переведена — показана английская версия.

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 с событием.
  • Ступеней лестницы: объявление со слишком большим числом ступеней отклоняется при объявлении.
  • Размер группы в тикете: тикет отклоняется как ошибка валидации.
  • Объявленных свойств на тип очереди: отклоняется при объявлении.
  • Частота создания тикетов: отказ по ограничению частоты с указанием, когда можно повторить.
  • Хранение истории подбора: после объявленного срока запись больше нельзя прочитать.