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

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

Щоденний турнір

Щоденний турнір із вікном реєстрації, розподілом учасників по кімнатах і виплатою призів. Його зібрано з оголошень (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

Як оголошувати через атрибути в 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
Unity C# is the same C# API here — ті самі атрибути C#; клієнт на Unity читає турнірну сітку на кроці 3, але надсилати в неї не може
[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

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

Як оголошувати через атрибути в 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.

Хук закриття циклу видає право володіння і сповіщає кожного з топ-8
[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

Як оголошувати через атрибути в 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 лишаються без змін.

Що далі