跳到主要内容
示例

本页尚未翻译,当前显示英文版本。

Tanks 中的排行榜

Tanks 是 快速入门 里的示例竞技场,目前还没有排行榜(leaderboard)。本课分三步加上一个每周击杀(kill)榜:声明榜单,在击杀 hook 中提交成绩,在客户端读取榜单。每一步都链接到负责相应功能的模块页面。

声明榜单

榜单就是一份声明(declaration):按哪个字段排名、重复提交如何合并、何时重置、谁可以提交。Aggregation.Increment 把每次提交累加到当前总数上,所以一次击杀记一分。Submit.ServerOnly 是默认值,它禁止客户端提交,因此第 2 步是写入榜单的唯一途径。

tanks-weekly-kills:按击杀数降序,累加计分,每周一重置,仅服务器可提交
[Leaderboard("tanks-weekly-kills")]
public static class WeeklyKills
{
    public static Owner Owner = Owner.Player;
    public static Aggregation Agg = Aggregation.Increment;
    public static Reset Reset = Reset.Weekly(DayOfWeek.Monday);
    public static Submit Submit = Submit.ServerOnly;

    [Rank(1, Sort.Descending)] public static int Kills;
}
@Leaderboard('tanks-weekly-kills')
export class WeeklyKills {
  static owner = Owner.Player;
  static agg = Aggregation.Increment;
  static reset = Reset.weekly(DayOfWeek.Monday);
  static submit = Submit.ServerOnly;

  @rank(1, Sort.Descending) static kills: number;
}
@leaderboard("tanks-weekly-kills")
class WeeklyKills:
    owner = Owner.PLAYER
    agg = Aggregation.INCREMENT
    reset = Reset.weekly(DayOfWeek.MONDAY)
    submit = Submit.SERVER_ONLY

    kills: int = rank(1, Sort.DESCENDING)
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。

USTRUCT(PSLeaderboard = (Name = "tanks-weekly-kills", Owner = "Player", Aggregation = "Increment",
                        Reset = "Weekly:Monday", Submit = "ServerOnly"))
struct FWeeklyKills
{
    GENERATED_BODY()

    UPROPERTY(PSRank = (Order = 1, Sort = "Descending")) int32 Kills;
};
// 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("tanks-weekly-kills")]
public static class WeeklyKills
{
    public static Owner Owner = Owner.Player;
    public static Aggregation Agg = Aggregation.Increment;
    public static Reset Reset = Reset.Weekly(DayOfWeek.Monday);
    public static Submit Submit = Submit.ServerOnly;

    [Rank(1, Sort.Descending)] public static int Kills;
}

用 playserv push 推送后,面板里就会出现这个空榜单,周一的周期也已排好。你没有设置的维度保持默认值。完整的维度列表(所有者、排序键、显示字段、锦标赛规则)见 Leaderboards。

时间均为 UTC

每周重置在周一 00:00 UTC 执行,不按玩家所在时区。

从击杀 hook 提交

Tanks 已经靠坦克上声明的生命值阈值结束一条命:生命值归零时触发 death 状态转换,平台随后调用 hook。hook 是云函数(cloud function),输入输出都有类型,所以在里面提交一次击杀只需一行代码。

生命值耗尽时的 [After] hook 为击杀者提交一次击杀
[After(Stats.Depleted, stat: "hp")]
public static Task SubmitKill(StatEvent e) =>
    PlayServ.Leaderboards.Submit("tanks-weekly-kills", e.By.PlayerId,
        kills: 1, idempotencyKey: e.Id);
export const submitKill = after(Stats.depleted, { stat: 'hp' }, (e: StatEvent) =>
  PlayServ.leaderboards.submit('tanks-weekly-kills', e.by.playerId,
    { kills: 1, idempotencyKey: e.id }));
@after(stats.depleted, stat="hp")
async def submit_kill(e: StatEvent):
    await playserv.leaderboards.submit("tanks-weekly-kills", e.by.player_id,
        kills=1, idempotency_key=e.id)
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。

在引擎之外运行

hook 是云函数,在平台上执行,不在引擎里执行。用 C#、TypeScript 或 Python 编写,Unreal 代码订阅由此产生的 rank changed 事件。在引擎中编写函数的方式已在计划中:Blueprint 图,以及 C++(转换为经平台校验的脚本目标)。两者都像其他声明一样经过分析并推送,执行仍在平台上。

在引擎之外运行

hook 是云函数,在平台上执行,不在引擎里执行。用 C#、TypeScript 或 Python 编写,Unity 代码订阅由此产生的 rank changed 事件。

e.By 是伤害中携带的攻击者,无需另外记录谁打了谁。e.Id 是事件自身的 id,Increment 榜单正需要把它作为幂等键(idempotency key)传入:重复投递的击杀事件只计一次,不会计两次。hook 挂载点、顺序保证和否决约定见 Extensibility;触发它的阈值是坦克上的 属性预设(stat),它写入的成绩行则是可以查询的普通 数据。

在客户端读取榜单

整个界面只需两次读取:榜首,以及本地玩家附近的区间,即上下各五行加上你自己。两者都返回带击杀数和显示名称的排名条目,可以直接绑定到列表。比赛进行时,订阅(subscription)让面板保持最新,它只推送本地玩家的名次。

前 20 名、我上下各五行,外加实时名次订阅
var top    = await playserv.Leaderboards.Top("tanks-weekly-kills", 20);
var around = await playserv.Leaderboards.AroundMe("tanks-weekly-kills", 5);

playserv.Leaderboards.OnRankChanged("tanks-weekly-kills", r => UpdateHud(r));
const top    = await playserv.leaderboards.top('tanks-weekly-kills', 20);
const around = await playserv.leaderboards.aroundMe('tanks-weekly-kills', 5);

playserv.leaderboards.onRankChanged('tanks-weekly-kills', (r) => updateHud(r));
top    = await playserv.leaderboards.top("tanks-weekly-kills", 20)
around = await playserv.leaderboards.around_me("tanks-weekly-kills", 5)

playserv.leaderboards.on_rank_changed("tanks-weekly-kills", lambda r: update_hud(r))
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。

Client->Leaderboards->Of<FWeeklyKills>()->Get(
    TPSOnResult<FPSBoard*>::CreateWeakLambda(this, [this](const TPSResult<FPSBoard*>& Result)
    {
        if (!Result.HasValue()) { return; }
        OnBoard(Result.Value());
    }));

// in OnBoard(FPSBoard* Board):
Board->Entries->Select().Page(20).Then(
    TPSOnResult<TPSPage<FPSLeaderboardEntry>>::CreateWeakLambda(this, [this](const TPSResult<TPSPage<FPSLeaderboardEntry>>& Top)
    {
        if (!Top.HasValue()) { return; }
        Hud->ShowTop(Top.Value().Rows);
    }));

Board->Entries->SelectAround(MyPlayerId, /*Radius*/ 5,
    TPSOnResult<TArray<FPSLeaderboardEntry>>::CreateWeakLambda(this, [this](const TPSResult<TArray<FPSLeaderboardEntry>>& Around)
    {
        if (!Around.HasValue()) { return; }
        Hud->ShowWindow(Around.Value());
    }));

TPSSubscription MyRank = Board->Subscribe->Mine(
    [this](const FPSLeaderboardEntry& Mine) { UpdateHud(Mine); });
// 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 top    = await playserv.Leaderboards.Top("tanks-weekly-kills", 20);
var around = await playserv.Leaderboards.AroundMe("tanks-weekly-kills", 5);

playserv.Leaderboards.OnRankChanged("tanks-weekly-kills", r => UpdateHud(r));

周一的重置只关闭周期,不删除它,所以上周的榜单仍可按标签读取:同样调用 Top,加上 cycle: 参数即可。顺理成章的第四步是在周期关闭时触发的奖励 hook,详见 Leaderboards。

后续阅读

  • Leaderboards:维度、周期、锦标赛,以及给可疑成绩封顶的提交前 hook。
  • Extensibility:按顺序列出所有 hook 挂载点及否决约定。
  • Entity Presets:第 2 步中触发击杀的属性阈值。
  • Tanks 中的医疗箱:另一个 Tanks 示例,两份声明加一个 hook。
  • 快速入门:本课所扩展的 Tanks 竞技场。
  • 核心概念:每个模块页面默认你已掌握的术语。