Saltar al contenido principal
Ejemplos

Esta página aún no está traducida: se muestra la versión en inglés.

Torneo diario

Un torneo diario con ventana de inscripción (entry window), salas armadas con los participantes y pago de premios, construido con declaraciones y hooks sobre módulos que ya tienen su propia página: tablas de clasificación, emparejamiento, salas, comercio y mensajería.

Declara la tabla con ventana de inscripción y límite de intentos

Un torneo es una declaración común de tabla de clasificación (leaderboard) más restricciones de participación: una ventana de inscripción, un tope de participantes y un número de intentos por ciclo. La puntuación no cambia: la clave de orden, la agregación y el reinicio quedan igual que en cualquier tabla.

daily-tournament — puntaje en orden descendente, reinicio diario, ventana de inscripción de 2 horas, 64 participantes, tres intentos
[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)
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

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 — los mismos atributos de C#; un cliente de Unity lee la tabla del torneo en el paso 3 y no puede enviarle puntos
[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;
}

Súbela y el panel muestra la tabla del torneo vacía, con su ventana ya programada. Con joinRequired: true, la participación (membership) exige inscribirse: los participantes son quienes se inscribieron, no todos los que juegan, así que se rechaza el envío de alguien que no está inscrito. En Leaderboards encontrarás el resto de la lista de ejes y qué hace cada restricción al llegar a su límite.

Se abre la ventana: una party entra y se arman las salas

Cuando se abre la ventana de inscripción, los jugadores (player) entran en la cola igual que para cualquier partida: crean una party o se unen a una, y luego hacen una sola llamada a Find. El matchmaker coloca la party en la tabla del torneo y el módulo Rooms arma la partida: es el mismo camino de ubicación y lugar (seat) que sigue toda partida, solo que limitado a la cola del torneo.

Una party encuentra la cola del torneo y entra en la sala que se armó para ella
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)
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

// 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 — funciona tal cual en Unity con los tipos generados
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);

Los topes del paso 1 pertenecen a la tabla, no al matchmaker: la cola solo coloca parties, y es la tabla la que frena a un participante que excede el tope o a un jugador que agotó sus intentos. El participante 65 de 64 se rechaza como conflicto sin desalojar a nadie, y un cuarto envío en el mismo ciclo recibe “attempts exhausted”: también es un conflicto, y lo resuelve el reinicio diario, no la solicitud de un permiso.

Los puntajes se envían desde el hook de cierre de la sala

Las salas (room) no le reportan por sí solas un ganador a una tabla de clasificación; esa conexión es un hook, con el mismo contrato de Extensibility que en todas partes. El hook on dispose de la sala (Rooms) es lo último que se ejecuta con el estado final de la partida a mano, y el puntaje se envía desde ahí.

Un hook [After] sobre el cierre de la sala envía el puntaje final a la tabla del torneo
[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)
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; tu código de Unreal se suscribe al evento rank changed que resulta. Están previstas funciones escritas desde el motor: grafos de Blueprint y C++ (traducido a un script que valida la plataforma). Ambos se analizan y se suben como cualquier declaración; la ejecución sigue en la plataforma.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; tu código de Unity se suscribe al evento rank changed que resulta.

Winner y FinalScore son campos (field) propios de este juego: los declara en su estado la plantilla de sala (room template), y la plataforma no agrega nada al snapshot (el estado de la plantilla se declara en Rooms). El hook de cierre (Rooms.Disposed) entrega el snapshot final, así que la partida nunca se vuelve a calcular. El hook previo al envío de Leaderboards se sigue ejecutando primero: el puntaje de un torneo se somete al mismo contrato que cualquier otro envío — se corrige, se recorta al tope o se rechaza.

Se cierra el ciclo: se otorgan las recompensas y se notifica al jugador

El reinicio diario del paso 1 cierra el ciclo igual que cualquier reinicio de una tabla de clasificación, y dispara CycleClosed con la etiqueta del ciclo cerrado. El hook lee la tabla por esa etiqueta, así que obtiene la que acaba de cerrarse y no la vacía que acaba de abrirse.

Un solo hook hace el resto: otorga el premio como derecho de uso (entitlement) mediante Catalog & Commerce y envía el resultado a través de Messaging, así que no hay ninguna tarea de pago aparte que ejecutar. Cada notificación va dirigida a un solo actor, por lo que los 8 primeros se recorren en un bucle de ocho vueltas, y cada una lleva su propio argumento rank a la plantilla.

El hook de cierre de ciclo otorga un derecho de uso y notifica a cada uno de los 8 primeros
[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})
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; Unreal se suscribe a los eventos de cierre de ciclo y de notificación. Están previstas funciones escritas desde el motor: grafos de Blueprint y C++ (traducido a un script que valida la plataforma). Ambos se analizan y se suben como cualquier declaración; la ejecución sigue en la plataforma.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; Unity se suscribe a los eventos de cierre de ciclo y de notificación.

Los números del paso 1 son valores iniciales

El equipo de live-ops reajusta en el panel la ventana, el tope de participantes y el número de intentos, y el siguiente deploy no sobrescribe sus cambios: al volver a subir el modelo, los valores seed quedan intactos, así que si más tarde los cambias en el código, ese cambio no llega a la tabla desplegada (Esquema como código).

Convertirlo en un torneo semanal es un solo cambio — Reset.Daily() pasa a ser Reset.Weekly(DayOfWeek.Monday), y los pasos 2 a 4 quedan igual.

Siguientes pasos