Цю сторінку ще не перекладено — показано англійську версію.
Matchmaking
Як привести гравця в потрібну кімнату. Тікет описує гравця й відбирає решту. Матчмейкер знаходить розміщення й резервує місце, а далі ігровий тра фік іде просто в кімнату.
Матчмейкер стоїть на шляху лише раз — щоб вирішити, куди вам потрапити. У самому матчі він не бере участі: результат його роботи — розміщення і резерв місця (seat) на обмежений час, а від моменту входу ігровий трафік іде просто в кімнату. Тож навантажена черга ніколи не перетворюється на навантажену гру.
Коли це потрібно
- Гравців (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, поки не вирішено (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.// 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, поки не вирішено (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[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, поки не вирішено (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з подією. - Щаб лів драбини: оголошення із забагатьма щаблями відхиляється ще на етапі оголошення.
- Розмір групи в тікеті: тікет відхиляється як помилка валідації.
- Оголошених властивостей на тип черги: відхиляється на етапі оголошення.
- Частота створення тікетів: відмова через обмеження частоти із зазначенням, коли можна повторити.
- Зберігання історії підбору: після оголошеного терміну запис більше не можна прочитати.