# 排行榜

**URL:** https://heroiclabs.com/docs/zh/nakama/tutorials/unity/pirate-panic/leaderboards/
**Summary:** 学习如何在Pirate Panic教程游戏中添加排行榜。

---


# 排行榜

我们设置了比赛、好友、部落和存储数据的方法。我们来将其整合到一起，显示顶尖玩家的[排行榜](../../../../concepts/leaderboards/)，促进竞争。

![排行榜]({{< fingerprint_image "/images/pages/nakama/tutorials/unity/pirate-panic/leaderboard.png" >}})

## 设置排行榜

对于想要创建的每个排行榜，我们运行 `leaderboardCreate`。此函数有多个参数：

- ID/名称
- 排行榜是否为权威的布尔值（如为是，则仅有服务器可以更新排行榜）
- 排序（`SortOrder.ASCENDING` 或 `SortOrder.DESCENDING`）
- 对分数求和的运算符
    - `Operator.BEST`：始终取提交的最好分数
    - `Operator.SET`：始终取提交的最新分数
    - `Operator.INCR`：将提交的分数添加到总分
-  重置时间表，其为 [CRON 表达式](https://en.wikipedia.org/wiki/Cron)或从不重置的排行榜的 `null`
-  要存储到排行榜中的元数据

对于 Pirate Panic，我们设置一个全局排行榜，显示所有玩家所有时候的最高分数：

**main.ts**

```typescript
const id = "global_leaderboard";
const authoritative = false;
const metadata = {};
const scoreOperator = nkruntime.Operator.BEST;
const sortOrder = nkruntime.SortOrder.DESCENDING;
const resetSchedule = null;
nk.leaderboardCreate(id, authoritative, sortOrder, scoreOperator, resetSchedule, metadata);
```

选择排行榜可以接受新的记录。

## 向排行榜添加

每场比赛结束后，我们将把玩家分数记录到排行榜上。每当比赛结束时，客户端都会向服务器发送 RPC 请求以处理游戏结束行为：

**match.ts**

```typescript
interface MatchEndRequest { // Create a structure to align the payload to
    matchId : string;
    placement: MatchEndPlacement;
    time : number;
    towersDestroyed : number;
}


const rpcHandleMatchEnd: nkruntime.RpcFunction = function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string {
    ... // Add gems to wallet, calculate score, etc. etc.
    let request : MatchEndRequest = JSON.parse(payload);
    let score = calculateScore(request.placement == MatchEndPlacement.Winner, request.towersDestroyed, request.time);
    nk.leaderboardRecordWrite(globalLeaderboard, ctx.userId, ctx.username, score);
    ...
}
```

如何计算分数取决于您和游戏机制。您可以考虑谁赢了，花了多长时间才赢了，或者他们像我们在这个例子中一样摧毁了多少座塔。所有这些信息都从客户端传递到有效载荷中。

一旦计算出分数，我们将其作为一个数字与用户 ID 和用户名一起传递到 `leaderboardRecordWrite`，然后排行榜引擎将进行后续处理。

此外，还可以使用 [WriteLeaderboardRecordAsync](../../../../concepts/leaderboards/#submit-a-score) 在 Unity 端达到这个目的。

## 列出排行榜记录

现在已为处理新的排行榜记录设置好服务器，让我们为玩家提供一种查看服务器上存储的内容的方式。这在客户端使用 `ListLeaderboardRecordsAsync` 完成：

**LeaderboardsMenuUI.cs**

```csharp
[SerializeField] private int _recordsPerPage = 100;
...
public async void ShowGlobalLeaderboards(string cursor = null)
{
    // Fetch all records from the leaderboard "global"
    IApiLeaderboardRecordList records = await _connection.Client.ListLeaderboardRecordsAsync(_connection.Session, "global", ownerIds: null, expiry: null, _recordsPerPage, cursor);

    SetLeaderboardsCursor(records, ShowGlobalLeaderboards);
    ...
}
```

对于上述示例，`SetLeaderboardsCursor` 会形如：

**LeaderboardsMenuUI.cs**

```csharp
private void SetLeaderboardsCursor(IApiLeaderboardRecordList records, Action<string> caller)
{
    if (records.PrevCursor != null) {
        _prevPageButton.interactable = true;
        _prevPageButton.onClick.RemoveAllListeners();
        _prevPageButton.onClick.AddListener(() => caller(records.PrevCursor));
    } else {
        _prevPageButton.interactable = false;
    }
    if (records.NextCursor != null) {
        _nextPageButton.interactable = true;
        _nextPageButton.onClick.RemoveAllListeners();
        _nextPageButton.onClick.AddListener(() => caller(records.NextCursor));
    } else {
        _nextPageButton.interactable = false;
    }
}
```

`ListLeaderboardRecordsAsync` 返回的 `records` 列表带有两个函数，`PrevCursor` 和 `NextCursor`，每个都抓取上一页或下一页记录（如果存在这一页）。

通过将这些功能绑定到按钮，我们可以让玩家轻松浏览记录。`PrevCursor` 和 `NextCursor` 都将遵守在原始 `ListLeaderboardRecordsAsync` 调用中设置的每页记录数，并将更新 `records` 对象，以保留下一页记录。

为实际显示这些记录，可以在 `records` 中的每条记录上使用 `foreach` 循环：

**LeaderboardsMenuUI.cs**

```csharp
foreach (IApiLeaderboardRecord record in records)
{
    string username = record.Username;
    if (localId == record.OwnerId)
    {
        username += " (You)";
    }
    ...
}
```

每个 `record` 保留一个 `Username`、`OwnerId`、`Score`，以及存储在可选数据库中的任何其他自定义字段。

## 过滤排行榜

我们还可以过滤出全局排行榜，看看我们的好友或部落的表现如何。

在前一部分，我们以 `null` 退出 `ownerIds` ，因为我们想要获取所有条目。现在，我们可以使用此参数仅获取具有特定所有者的条目。

例如，我们可以使用 `ListFriendsAsync` 获取所有好友：

```csharp
var friends = await _connection.Client.ListFriendsAsync(_connection.Session);
```

然后，我们可以提取他们的所有用户 ID 并将其存储到列表中：

```csharp
List<string> ids = friends.Friends.Select(x => x.User.Id).ToList();
```

最后，我们将 `ids` 列表传入过滤器，在 `records` 中将仅显示此列表中的用户：

```csharp
IApiLeaderboardRecordList records = await _connection.Client.ListLeaderboardRecordsAsync(_connection.Session, "global", ids, null, 1, cursor);
```

在这里，我们从同一排行榜 (`global`) 获取记录，并将 `ids` 作为 `ownerIds`，并将每页记录数设置为 `1`。

可以执行一个非常类似的过程，对排行榜进行过滤，以仅显示您的部落的成员的分数：

```csharp
var users = await _connection.Client.ListGroupUsersAsync(_connection.Session, _userClan.Group.Id, null, 1, null);
IEnumerable<string> ids = users.GroupUsers.Select(x => x.User.Id);

IApiLeaderboardRecordList list = await _connection.Client.ListLeaderboardRecordsAsync(_connection.Session, "global", ids, null, 1, cursor);
```

## 下一主题

[通知](../notifications/)
