本页尚未翻译,当前显示英文版本。
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 中基于特性的声明方式尚未确定(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[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 中基于特性的声明方式尚未确定(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)让面板保持最新,它只推送本地玩家的名次。
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 中基于特性的声明方式尚未确定(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.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 竞技场。
- 核心概念:每个模块页面默认你已掌握的术语。