Цю сторінку ще не перекладено — показано англійську версію.
Щоденний турнір
Щоденний турнір із вікном реєстрації, розподілом учасників по кімнатах і виплатою призів. Його зібрано з оголошень (declaration) і хуків на модулях, які вже мають свої сторінки: таблиці лідерів, підбір гравців, кімнати, комерція і повідомлення.
Оголосіть таблицю з вікном реєстрації та лімітом спроб
Турнір — це звичайне оголошення таблиці лідерів (leaderboard), до якого додано обмеження на участь. Це вікно реєстрації (entry window), ліміт учасників і кількість спроб за цикл. Підрахунок очок не змінюється: ключ сортування, агрегація і скидання такі самі, як у будь-якої таблиці.
daily-tournament — очки за спаданням, щоденне скидання, вікно реєстрації на 2 години, 64 учасники, три спроби[Leaderboard("daily-tournament")]
public static class DailyTournament
{
public static Owner Owner = Owner.Player;
public static Aggregation Agg = Aggregation.Best;
public static Reset Reset = Reset.Daily(); // 00:00 UTC
public static Submit Submit = Submit.ServerOnly;
public static Tournament Rules = Tournament.Define(
entryWindow: TimeSpan.FromHours(2), maxEntrants: 64,
attemptsPerCycle: 3, joinRequired: true);
[Rank(1, Sort.Descending)] public static int Score;
}@Leaderboard('daily-tournament')
export class DailyTournament {
static owner = Owner.Player;
static agg = Aggregation.Best;
static reset = Reset.daily(); // 00:00 UTC
static submit = Submit.ServerOnly;
static rules = Tournament.define({ entryWindow: hours(2), maxEntrants: 64,
attemptsPerCycle: 3, joinRequired: true });
@rank(1, Sort.Descending) static score: number;
}@leaderboard("daily-tournament")
class DailyTournament:
owner = Owner.PLAYER
agg = Aggregation.BEST
reset = Reset.daily() # 00:00 UTC
submit = Submit.SERVER_ONLY
rules = Tournament.define(entry_window=hours(2), max_entrants=64,
attempts_per_cycle=3, join_required=True)
score: int = rank(1, Sort.DESCENDING)Як оголошувати через атрибути в Go, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.
USTRUCT(PSLeaderboard = (Name = "daily-tournament", Owner = "Player", Aggregation = "Best",
Reset = "Daily", Submit = "ServerOnly"),
PSTournament = (EntryWindow = "2h", MaxEntrants = 64,
AttemptsPerCycle = 3, JoinRequired = "true"))
struct FDailyTournament
{
GENERATED_BODY()
UPROPERTY(PSRank = (Order = 1, Sort = "Descending")) int32 Score;
};
// declarations compile into the same pushed model — playserv push from the UE project or CI[Leaderboard("daily-tournament")]
public static class DailyTournament
{
public static Owner Owner = Owner.Player;
public static Aggregation Agg = Aggregation.Best;
public static Reset Reset = Reset.Daily(); // 00:00 UTC
public static Submit Submit = Submit.ServerOnly;
public static Tournament Rules = Tournament.Define(
entryWindow: TimeSpan.FromHours(2), maxEntrants: 64,
attemptsPerCycle: 3, joinRequired: true);
[Rank(1, Sort.Descending)] public static int Score;
}Після завантаження панель покаже порожню турнірну сітку із запланованим вікном. З joinRequired: true учасники — це ті, хто записався на турнір, а не всі, хто грає: участь (membership) потребує запису, тож результат від гравця без запису платформа відхилить. Решту осей і те, як кожне обмеження поводиться на своїй межі, описано на сторінці Leaderboards.
Вікно відкрилося: паті записується, учасників розподіляють по кімнатах
Коли вікно реєстрації відкривається, гравці (player) стають у чергу так само, як на будь-який матч: створюють паті або приєднуються до неї, а потім роблять один виклик Find. Матчмейкер додає паті до турнірної сітки, а модуль Rooms готує матч. Шлях той самий, що й для будь-якого матчу, — розміщення і місце (seat), лише в межах турнірної черги.
var party = await playserv.Matchmaking.Party.Create();
await party.Invite(friendId);
var seat = await playserv.Matchmaking.Find("daily-tournament");
var room = await playserv.Rooms.Join(seat);const party = await playserv.matchmaking.party.create();
await party.invite(friendId);
const seat = await playserv.matchmaking.find('daily-tournament');
const room = await playserv.rooms.join(seat);party = await playserv.matchmaking.party.create()
await party.invite(friend_id)
seat = await playserv.matchmaking.find("daily-tournament")
room = await playserv.rooms.join(seat)Як оголошувати через атрибути в Go, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.
// a party first; the ticket then carries the party
Client->Matchmaking->Parties->Create(FPSIdempotencyKey(PartyId),
TPSOnResult<FPSParty*>::CreateWeakLambda(this, [this](const TPSResult<FPSParty*>& PartyResult)
{
if (!PartyResult.HasValue()) { return; }
FPSParty* Party = PartyResult.Value();
Party->Invitations->Create(FriendId);
Client->Matchmaking->Of<FDailyTournament>()->Tickets->Create(FPSTicketClaim{ .Party = Party },
TPSOnResult<FPSTicket*>::CreateWeakLambda(this, [this](const TPSResult<FPSTicket*>& TicketResult)
{
if (!TicketResult.HasValue()) { return; }
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; }
EnterTournament(JoinResult.Value());
}));
});
}));
}));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.var party = await playserv.Matchmaking.Party.Create();
await party.Invite(friendId);
var seat = await playserv.Matchmaking.Find("daily-tournament");
var room = await playserv.Rooms.Join(seat);Ліміти з кроку 1 належать таблиці, а не матчмейкеру. Черга лише розміщує паті, а учасника понад ліміт чи гравця, який вичерпав спроби, зупиняє саме таблиця. 65-й учасник за ліміту в 64 отримує відмову з конфліктом, і нікого заради нього не витісняють. На четверте надсилання за цикл таблиця відповідає «attempts exhausted» — це теж конфлікт. Знімає його щоденне скидання, а не запит дозволу (permission).
Результат надсилає хук закриття кімнати
Кімната (room) сама не повідомляє таблиці лідерів переможця. Цей зв’язок створює хук, і правила для нього ті самі, що й скрізь (Extensibility). Хук кімнати on dispose (Rooms) виконується останнім, поки підсумковий стан матчу ще під рукою, — звідти він і надсилає результат.
[After] на закриття кімнати надсилає підсумковий рахунок у турнірну сітку[After(Rooms.Disposed, room: "daily-tournament")]
public static Task SubmitScore(RoomDisposed e) =>
PlayServ.Leaderboards.Submit("daily-tournament", e.State.Winner, score: e.State.FinalScore);export const submitScore = after(Rooms.disposed, { room: 'daily-tournament' }, (e: RoomDisposed) =>
PlayServ.leaderboards.submit('daily-tournament', e.state.winner, { score: e.state.finalScore }));@after(rooms.disposed, room="daily-tournament")
async def submit_score(e: RoomDisposed):
await playserv.leaderboards.submit("daily-tournament", e.state.winner, score=e.state.final_score)Як оголошувати через атрибути в Go, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.
Хук — це хмарна функція: він працює на платформі, а не в рушії. Напишіть його на C#, TypeScript або Python, а ваш код на Unreal підпишеться на подію rank changed, яку він породжує. У планах — функції, написані в самому рушії: графи Blueprint і C++ (його транслюватимуть у скрипт, який перевіряє платформа). Їх аналізуватимуть і завантажуватимуть, як будь-яке оголошення, а виконувати їх і далі буде платформа.
Хук — це хмарна функція: він працює на платформі, а не в рушії. Напишіть його на C#, TypeScript або Python, а ваш код на Unity підпишеться на подію rank changed, яку він породжує.
Winner і FinalScore — поля, які шаблон кімнати (room template) цієї гри оголошує у своєму стані. Сама платформа нічого до снапшота не додає, а як оголосити стан шаблону, описано на сторінці Rooms. Хук закриття (Rooms.Disposed) отримує підсумковий снапшот, тож матч ніхто не перераховує. Хук перед надсиланням із Leaderboards і далі спрацьовує першим. До турнірного результату застосовують ті самі правила, що й до будь-якого іншого надсилання: його або виправляють і обрізають до ліміту, або відхиляють.
Цикл закрито: нагороди видано, гравців сповіщено
Щоденне скидання з кроку 1 закриває цикл так само, як будь-яке скидання таблиці лідерів, і породжує подію CycleClosed з міткою закритого циклу. Хук читає таблицю за цією міткою, тож отримує ту, що щойно закрилася, а не порожню, що щойно відкрилася.
Решту робить один хук. Він видає приз як право володіння (entitlement) через модуль Catalog & Commerce і надсилає результат через Messaging, тож окремого завдання на виплату запускати не треба. Сповіщення адресують одному акторові, тому топ-8 обходять у циклі з восьми кроків, і кожне сповіщення передає в шаблон свій аргумент rank.
[After(Leaderboards.CycleClosed, board: "daily-tournament")]
public static async Task RewardAndNotify(CycleClosed closed)
{
var final = await PlayServ.Leaderboards.Top("daily-tournament", 8, cycle: closed.Cycle);
foreach (var row in final)
{
await PlayServ.Commerce.Grant(row.PlayerId, entitlement: "trophy.daily", origin: Grant.Reward);
await PlayServ.Messaging.Notify(row.PlayerId, Template.Named("daily-tournament-won"),
args: new { rank = row.Rank });
}
}export const rewardAndNotify = after(Leaderboards.cycleClosed, { board: 'daily-tournament' },
async (closed: CycleClosed) => {
const final = await PlayServ.leaderboards.top('daily-tournament', 8, { cycle: closed.cycle });
for (const row of final) {
await PlayServ.commerce.grant(row.playerId, { entitlement: 'trophy.daily', origin: Grant.Reward });
await PlayServ.messaging.notify(row.playerId, Template.named('daily-tournament-won'),
{ args: { rank: row.rank } });
}
});@after(leaderboards.cycle_closed, board="daily-tournament")
async def reward_and_notify(closed: CycleClosed):
final = await playserv.leaderboards.top("daily-tournament", 8, cycle=closed.cycle)
for row in final:
await playserv.commerce.grant(row.player_id, entitlement="trophy.daily", origin=Grant.REWARD)
await playserv.messaging.notify(row.player_id, Template.named("daily-tournament-won"),
args={"rank": row.rank})Як оголошувати через атрибути в Go, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.
Хук — це хмарна функція: він працює на платформі, а не в рушії. Напишіть його на C#, TypeScript або Python, а Unreal підпишеться на події закриття циклу і сповіщень. У планах — функції, написані в самому рушії: графи Blueprint і C++ (його транслюватимуть у скрипт, який перевіряє платформа). Їх аналізуватимуть і завантажуватимуть, як будь-яке оголошення, а виконувати їх і далі буде платформа.
Хук — це хмарна функція: він працює на платформі, а не в рушії. Напишіть його на C#, TypeScript або Python, а Unity підпишеться на події закриття циклу і сповіщень.
Числа з кроку 1 — лише початкові значення
Команда live-ops налаштовує в панелі вікно, ліміт учасників і кількість спроб, і наступний деплой її правок не перезапише. Повторне завантаження не чіпає значень seed, тож якщо пізніше змінити їх у коді, до розгорнутої таблиці це не дійде (Схема в коді).
Щоб турнір став щотижневим, досить однієї правки: замінити Reset.Daily() на Reset.Weekly(DayOfWeek.Monday). Кроки 2–4 лишаються без змін.
Що далі
- Leaderboards — осі турніру (вікно реєстрації, ліміт учасників, спроби).
- Matchmaking → Rooms — паті, розміщення і розподіл по кімнатах.
- Extensibility → Catalog & Commerce → Messaging — ланцюжок хуків, що виплачує призи.
- Основні поняття — словник, на який спирається кожна сторінка модуля.