Esta página ainda não foi traduzida: mostrando a versão em inglês.
Torneio diário
Um torneio diário com período de inscrição (entry window), salas preparadas de antemão e entrega de prêmios, montado só com declarações e hooks em módulos que já têm página própria: rankings, matchmaking, salas, comércio e mensagens.
Declare o ranking com período de inscrição e limite de tentativas
Um torneio é uma declaração comum de ranking com restrições de participação: um período de inscrição, um limite de inscritos e um número de tentativas por ciclo. A pontuação não muda em nada: a chave de ordenação, a agregação e o reinício continuam como em qualquer ranking (leaderboard).
daily-tournament — pontuação em ordem decrescente, reinício diário, período de inscrição de 2 horas, 64 inscritos, três tentativas[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)Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.
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;
}Publique a declaração, e o painel mostra uma chave de torneio vazia, com o período já agendado. Com joinRequired: true, os inscritos passam a ser uma participação (membership), e não qualquer um que jogue — por isso o envio (submit) de quem não se inscreveu é recusado. O resto da lista de eixos e o que cada restrição faz no seu limite estão em Leaderboards.
O período abre: uma party entra, as salas são preparadas
Quando o período de inscrição abre, os jogadores entram na fila como em qualquer partida: criam uma party ou entram em uma, e depois fazem uma chamada Find. O matchmaker coloca a party numa chave do torneio, e o Rooms prepara a partida: é o mesmo caminho de alocação e vaga (seat) de qualquer partida, restrito à fila do torneio.
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)Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.
// 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);Os limites do passo 1 pertencem ao ranking, não ao matchmaker: a fila só aloca as parties, e é o ranking que barra um inscrito acima do limite ou um jogador sem tentativas. O 65º inscrito de 64 é recusado com um conflito, sem que ninguém seja removido, e um quarto envio no mesmo ciclo recebe “attempts exhausted” — também um conflito, que some com o reinício diário, e não pedindo uma permissão.
As pontuações são enviadas pelo hook de encerramento da sala
Uma sala (room) não informa sozinha o vencedor a um ranking: essa ligação é um hook, com o mesmo contrato do Extensibility de todo o resto. O hook on dispose da sala (Rooms) é a última coisa que roda com o estado final da partida em mãos, e é dele que sai o envio.
[After] no encerramento da sala envia a pontuação final da chave[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)Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.
Um hook é uma cloud function: ele roda na plataforma, não na engine. Escreva em C#, TypeScript ou Python — seu código Unreal assina o evento rank changed que resulta dele. Funções escritas na engine estão planejadas: grafos Blueprint e C++ (traduzido para um formato de script validado pela plataforma). Os dois serão analisados e publicados como qualquer declaração, e a execução continua na plataforma.
Um hook é uma cloud function: ele roda na plataforma, não na engine. Escreva em C#, TypeScript ou Python — seu código Unity assina o evento rank changed que resulta dele.
Winner e FinalScore são campos que o próprio modelo de sala (room template) deste jogo declara no seu estado — a plataforma não acrescenta nada ao snapshot (o estado do modelo é declarado em Rooms). O hook de encerramento (Rooms.Disposed) entrega o snapshot final, então a partida nunca é recalculada. O hook anterior ao envio de Leaderboards continua rodando primeiro: a pontuação de uma chave segue o mesmo contrato de qualquer outro envio — corrigir, limitar ou rejeitar.
O ciclo fecha: prêmios concedidos, jogador notificado
O reinício diário do passo 1 fecha o ciclo como qualquer reinício de ranking e dispara CycleClosed com o rótulo do ciclo fechado. O hook lê a tabela por esse rótulo e, assim, pega a tabela que acabou de fechar, e não a vazia que acabou de abrir.
Um hook faz o resto: concede o prêmio como direito de uso (entitlement) pelo Catalog & Commerce e envia o resultado pelo Messaging, então não há um job separado de pagamento para rodar. Uma notificação vai para um único ator, por isso o top 8 é um loop de oito notificações, cada uma levando o próprio argumento rank para o template.
[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})Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.
Um hook é uma cloud function: ele roda na plataforma, não na engine. Escreva em C#, TypeScript ou Python — o Unreal assina os eventos de fechamento de ciclo e de notificação. Funções escritas na engine estão planejadas: grafos Blueprint e C++ (traduzido para um formato de script validado pela plataforma). Os dois serão analisados e publicados como qualquer declaração, e a execução continua na plataforma.
Um hook é uma cloud function: ele roda na plataforma, não na engine. Escreva em C#, TypeScript ou Python — o Unity assina os eventos de fechamento de ciclo e de notificação.
Os números do passo 1 são valores iniciais
A equipe de live-ops ajusta o período, o limite de inscritos e o número de tentativas no painel, e o próximo deploy não sobrescreve esses ajustes: uma nova publicação não mexe nos valores seed, então uma mudança posterior neles no código não chega ao ranking já publicado (Esquema como código).
Para transformar este torneio em semanal basta uma edição: Reset.Daily() vira Reset.Weekly(DayOfWeek.Monday), e os passos 2 a 4 ficam como estão.
Próximos passos
- Leaderboards — os eixos do torneio (período de inscrição, máximo de inscritos, tentativas).
- Matchmaking → Rooms — parties, alocação e preparação das salas.
- Extensibility → Catalog & Commerce → Messaging — a cadeia de hooks que entrega os prêmios.
- Conceitos principais — o vocabulário que toda página de módulo pressupõe.