# Godot

**URL:** https://heroiclabs.com/docs/zh/nakama/client-libraries/godot/
**Summary:** 官方Godot客户端实时处理与服务器的所有通信。它实现了服务器中的所有功能。本指南将展示如何开发一项游戏的Nakama特定部分，从而展示如何在Godot中使用Nakama的核心功能。

---


# Nakama Godot客户端指南

本客户端库指南将展示如何开发名为Sagi-shi（“Imposter”的日语名称）游戏的Nakama特定部分（无完整的游戏逻辑或UI），此游戏受[Among Us（外部）](https://www.innersloth.com/games/among-us/)启发，从而展示如何在**Godot**中使用Nakama的核心功能。

<figure>
  <img src="/docs/nakama/client-libraries/images/gameplay.png" alt="Sagi-shi gameplay screen">
  <figcaption>Sagi-shi gameplay</figcaption>
</figure>

## 前提条件

开始之前，确保您已经：

* 安装[Nakama服务器](../../getting-started/install/docker/)
* [下载并安装Godot](https://godotengine.org/download)
* [安装Nakama Godot SDK](#installation)

### 安装

可通过以下方式下载客户端：

* [Godot资产库](https://godotengine.org/asset-library/asset)
* [Heroic Labs GitHub发布页面](https://github.com/heroiclabs/nakama-godot/releases/latest)

下载客户端压缩包后，将其内容提取到您的Godot项目文件夹。

通过`Project -> Project Settings -> Autoload`菜单添加`Nakama.gd` 单例模式（见 `addons/com.heroiclabs.nakama/`）。

创建[客户端对象](#nakama-client)与服务器交互。

#### 更新

Nakama Godot [更改日志](https://github.com/heroiclabs/nakama-godot/blob/master/CHANGELOG.md)中。


### 异步编程

许多Nakama API采用异步、非阻塞操作方式，在Godot SDK中可以作为异步方法使用。

Sagi-shi使用`await`运算符调用异步方法，不去阻止调用线程，从而提高游戏响应能力和效率。

```gdscript
yield(client.authenticate_device_async("<device_id>"), "completed")
```

阅读[官方Godot文档](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#awaiting-for-signals-or-coroutines)中关于共常式和`await`的更多内容。


### 处理异常

网络编程需要额外的保护措施，防止出现连接和有效负载方面的问题。

Godot不支持异常处理，所以我们可以在发出异步请求时使用以下`is_exception()`方法：

```gdscript
var invalid_session = NakamaSession.new() # An empty session, which will cause an error
var invalid_account = yield(client.get_account_async(invalid_session), "completed")
print(invalid_account) # This will print the exception
if invalid_account.is_exception():
    print("We got an exception.")
```


### 对数据进行序列化和反序列化处理

通过网络发送和接收数据时，需要对数据进行适当的序列化和反序列化处理。两种最常见的方式是使用JSON和二进制数据。

两种示例都将展示如何对以下`Dictionary`对象进行序列化和反序列化处理，但也可以用于任何`Variant`对象。

```gdscript
var data = {
  "Key": "Value",
  "AnotherKey": "AnotherValue"
}
```


#### JSON

Godot本地支持使用全局`JSON`对象对JSON进行序列化和反序列化处理。

```gdscript
// Serialize
var serialized = JSON.print(data)

// Deserialize
var deserialized = JSON.parse(serialized).result
```


#### 二进制

Godot可以使用全局访问`var2bytes`和`bytes2var`函数对`byte[]`阵列进行序列化和反序列化处理。

```gdscript
// Serialize
var serialized = var2bytes(data)

// Deserialize
var deserialized = bytes2var(serialized)
```


## 新手入门

新手入门需要使用Nakama客户端和套接字对象开始创建Sagi-shi和您自己的游戏。


### Nakama客户端

Nakama客户端与Nakama服务器连接，是访问Nakama功能的入口。建议为每个游戏的每个服务器创建一个客户端。

要为Sagi-shi创建一个客户端，请将以下详细连接信息输入您的服务器：

```gdscript
extends Node

var client : NakamaClient

func _ready():
    client = Nakama.create_client("defaultkey", "127.0.0.1", 7350, "http")
```

### 配置请求超时时长

客户端对Nakama发出的每个请求必须在一定时间内完成，超过这个时间段会被认为超时。您可以通过在客户端设置`timeout`值来配置该时间段的长度（以秒为单位）：

```gdscript
client.timeout = 10
```


### Nakama套接字

Nakama套接字用于玩法和实时延迟敏感功能，如聊天、派对、比赛和RPC。

从客户端创建套接字：

```gdscript
# Make this a node variable or it will disconnect when the function that creates it returns

onready var socket = Nakama.create_socket_from(client)

func _ready():
    var connected : NakamaAsyncResult = yield(socket.connect_async(session), "completed")
    if connected.is_exception():
        print("An error occurred: %s" % connected)
        return
    print("Socket connected.")
```


## 身份验证

Nakama支持多种[身份验证方法](../../concepts/authentication/)，还支持在服务器上创建[自定义身份验证](../../concepts/authentication/#custom)。

Sagi-shi将通过链接到同一个用户账户的设备和Facebook进行身份验证，以便玩家可以使用不同的设备玩游戏。

<figure>
  <img src="/docs/nakama/client-libraries/images/login.png" alt="Sagi-shi login screen">
  <figcaption>Login screen and Authentication options</figcaption>
</figure>


### 设备身份验证

Nakama[设备身份验证](../../concepts/authentication/#device)使用物理设备的唯一标识符轻松验证用户，也可以为没有账户的设备创建账户。

仅使用设备身份验证时，您无需使用登录UI，因为游戏启动时会自动验证玩家身份。

身份验证是从Nakama客户端实例访问Nakama功能的示例。

```gdscript
# Get the System's unique device identifier
var device_id = OS.get_unique_id()

# Authenticate with the Nakama server using Device Authentication
var session : NakamaSession = yield(client.authenticate_device_async(device_id), "completed")
if session.is_exception():
    print("An error occurred: %s" % session)
    return
print("Successfully authenticated: %s" % session)
```


### Facebook身份验证

Nakama [Facebook身份验证](../../concepts/authentication/#facebook)操作简便，您可以有选择性地导入玩家的Facebook好友，并将这些好友添加到玩家的Nakama好友列表。

```gdscript
var oauth_token = "<token>"
var import_friends = true
var session : NakamaSession = yield(client.authenticate_facebook_async(oauth_token, import_friends), "completed")
if session.is_exception():
    print("An error occurred: %s" % session)
    return
print("Successfully authenticated: %s" % session)
```

### 自定义身份验证

Nakama支持[自定义身份验证](../../concepts/authentication/#custom)方法，以便与其他身份服务相集成。

示例请见[Itch.io自定义身份验证](../../client-libraries/snippets/custom-authentication/)片段。

### 链接身份验证

Nakama允许玩家在进行身份验证后通过[链接身份验证](../../concepts/authentication/#link-or-unlink) 方法前往玩家账户。


**链接设备ID身份验证**

```gdscript
var device_id = "<unique_device_id>"

# Link Device Authentication to existing player account.
var linked : NakamaAsyncResult = yield(client.link_custom_async(session, device_id), "completed")
if linked.is_exception():
    print("An error occurred: %s" % linked)
    return
print("Id '%s' linked for user '%s'" % [device_id, session.user_id])
```

**链接Facebook身份验证**

```gdscript
var oauth_token = "<token>"
var import_friends = true
var session : NakamaSession = yield(client.link_facebook_async(session, oauth_token, import_friends), "completed")
if session.is_exception():
    print("An error occurred: %s" % linked)
    return
print("Facebook authentication linked for user '%s'" % [session.user_id])
```

### 会话变量

在身份验证时可以存储Nakama [会话变量](../../concepts/session/#session-variables)，只要会话处于活动状态，即可在客户端和服务器上使用该变量。

Sagi-shi使用会话变量执行分析、推荐和奖励计划等。

在进行身份验证时将会话变量作为参数传递即可存储会话变量：

```gdscript
var vars = {
    "device_os" : OS.get_name,
    "device_model" : OS.get_model_name,
    "invite_user_id" : "<some_user_id>,
    # ...
}

var session : NakamaSession = yield(client.authenticate_device_async("<device_id>", null, true, vars), "completed")
```

要访问客户端的会话变量，请使用`Session`对象上的`Vars`属性：

```gdscript
var device_os = session.vars["device_os"];
```

### 会话生命周期

Nakama [会话](../../concepts/session/)在您的服务器[配置](../../getting-started/configuration/#session)中设定的时间之后过期。使不活动会话过期是良好的安全做法。

Nakama提供了多种恢复会话的方法，例如Sagi-shi玩家重新启动游戏，或在游戏过程中刷新令牌保持会话处于活动状态。

使用身份验证并刷新会话对象上的令牌可以恢复或刷新会话。

恢复会话而不必重新进行身份验证：

```gdscript
var auth_token = "restored from save location"
var session = NakamaClient.restore_session(auth_token)
```

检查会话是否已过期或即将过期，刷新会话使其保持活动状态：

```gdscript
# Check whether a session has expired or is close to expiry
if session.expired:
    # Attempt to refresh the existing session.
    session = yield(client.session_refresh_async(session), "completed)
    if session.is_exception():
        # Couldn't refresh the session so reauthenticate.
        session = yield(client.authenticate_device_async(device_id), "completed")
        # Save the new refresh token
        <save_file>.set_value("refresh_token", session.refresh_token)
    }

    # Save the new auth token
    <save_file>.set_value("auth_token", session.auth_token)
}
```


### 结束会话

退出登录并结束当前会话：

```gdscript
yield(client.session_logout_async(session), "completed")
```


## 用户账户

Nakama [用户账户](../../concepts/user-accounts/)存储Nakama定义的用户信息和自定义的开发者元数据。

Sagi-shi允许玩家编辑账户，并存储游戏进度和游戏内商品等内容的元数据。

<figure>
  <img src="/docs/nakama/client-libraries/images/profile.png" alt="Sagi-shi player profile screen">
  <figcaption>Player profile</figcaption>
</figure>


### 获取用户账户

通过经身份验证的会话可以访问Nakama的许多功能，比如[获取用户帐户](../../concepts/user-accounts/#fetch-account)。

获取Sagi-shi玩家的完整用户账户，包括基本[用户信息](../../concepts/user-accounts/#fetch-account) 和用户ID：

```gdscript
var account = yield(client.get_account_async(session), "completed")
var username = account.user.username
var avatar_url = account.user.avatar_url
var user_id = account.user.id
```

### 更新用户账户

Nakama为更新服务器存储的资源（如用户账户）提供了简单的方法。

Sagi-shi玩家需要能够更新其公开资料：

```gdscript
var new_username = "NotTheImp0ster"
var new_display_name = "Innocent Dave"
var new_avatar_url = "https://example.com/imposter.png"
var new_lang_tag = "en"
var new_location = "Edinburgh"
var new_timezone = "BST"
yield(client.update_account_async(session, new_username, new_display_name, new_avatar_url, new_lang_tag, new_location, new_timezone), "completed")
```


### 获取用户

除了获取玩家当前经身份验证的用户账户外，Nakama还可以方便地从其他玩家的ID或用户名中获取其他玩家公开资料列表。

结合使用Nakama的其他功能时，Sagi-shi可以通过这种方法显示用户资料：

```gdscript
var ids = ["userid1", "userid2"]
var users : NakamaAPI.ApiUsers = yield(client.get_users_async(session, ids), "completed")
```


### 存储元数据

Nakama[用户元数据](../../concepts/user-accounts/#user-metadata)允许开发人员使用公开的用户字段扩展用户账户。

仅可在服务器上更新用户元数据。示例请见[更新用户元数据](../../client-libraries/snippets/user-metadata)配方。

Sagi-shi将使用元数据存储玩家装备的游戏内商品：


### 读取元数据

定义描述元数据的类，并对JSON元数据进行语法解析：

```gdscript
class_name metadata

export(String) var title
export(String) var hat
export(String) var skin

# Get the updated account object
var account : NakamaAPI.ApiAccount = yield(client.get_account_async(session), "completed")

# Parse the account user metadata.
var metadata = JSON.parse(account.user.metadata)

Print("Title: %s", metadata.title)
Print("Hat: %s", metadata.hat)
Print("Skin: %s", metadata.skin)
```


### 钱包

Nakama[用户钱包](../../concepts/user-accounts/#virtual-wallet)可以将多种数字货币存储为字符串/整数的键/值对。

Sagi-shi玩家可以使用游戏内的虚拟货币解锁或购买头衔、皮肤和帽子。


#### 访问钱包

对用户账户中的JSON钱包数据进行语法解析：

```gdscript
var account : NakamaAPI.ApiAccount = yield(client.get_account_async(session), "completed")
var wallet = JSON.parse(account.wallet)
for currency in wallet
    Print("%s, %s" % [currency, wallet[currency].string(int from)])
```


#### 更新钱包

仅可在服务器上更新钱包。示例请见[用户账户虚拟钱包](../../concepts/user-accounts/#virtual-wallet)文档。


#### 验证应用内的购买行为

Sagi-shi玩家可以通过应用程序内的购买行为购买游戏内的虚拟货币，这些购买行为需要经过服务器授权并且通过合法性验证。

示例请见[应用程序内购买行为验证](../../concepts/iap-validation/)文档。


## 存储引擎

Nakama[存储引擎](../../concepts/storage/)是为您的游戏而设计的，基于文件的分布式、可扩展存储解决方案。

通过存储引擎，您可以更好地控制数据在集合中的[访问方式](../../concepts/storage/permissions/#object-permissions)和[结构](../../concepts/storage/collections/)。

这些集合会被命名，并将JSON数据存储在唯一的键和用户ID下。

默认玩家拥有创建、读取、更新和删除自己的存储对象的全部权限。

Sagi-shi玩家可以解锁或购买存储引擎中存储的许多商品。

<figure>
  <img src="/docs/nakama/client-libraries/images/player-items.png" alt="Sagi-shi player items screen">
  <figcaption>Player items</figcaption>
</figure>


### 读取存储对象

读取存储对象并对JSON数据进行语法解析：

```gdscript
var read_object_id = NakamaStorageObjectId.new("unlocks", "hats", session.user_id)

var result : NakamaAPI.ApiStorageObjects = yield(client.read_storage_objects_async(session, read_object_id), "completed")

print("Unlocked hats: ")
for o in result.objects:
    print("%s" % o)
```

要读取其他玩家的公开存储对象，应该使用其`user_id`。
玩家仅可读取自己拥有的存储对象或公开的存储对象（`PermissionRead`值为`2`）。


### 写入存储对象

Nakama允许开发人员从客户端和服务器写入存储引擎。

在决定写入逻辑的存放位置时，要考虑恶意用户会对您的游戏和财物产生何种不利影响，例如仅允许经过授权后写入数据（即游戏解锁或进度）。

Sagi-shi允许玩家收藏商品，以便在UI界面轻松查看这些商品，并通过客户端安全地写入这些数据。

将存储对象写入存储引擎：

```gdscript
var favorite_hats = ["cowboy", "alien"]
var can_read = 1 # Only the server and owner can read
var can_write = 1 # The server and owner can write

var acks : NakamaAPI.ApiStorageObjectAcks = yield(client.write_storage_objects_async(session, [
    NakamaWriteStorageObject.new("hats", "favorite_hats", can_read, can_write)]), "completed")
```

您也可以将多个对象传递到`write_storage_objects_async`方法：

```gdscript
var acks : NakamaAPI.ApiStorageObjectAcks = yield(client.write_storage_objects_async(session, [
    NakamaWriteStorageObject.new(...),
    NakamaWriteStorageObject.new(...)
]), "completed")
```

### 条件性写入

存储引擎[条件性写入](../../concepts/storage/collections/#conditional-writes)确保仅当对象在您访问后未改变时才会发生写入操作。

这样可以保护您的数据不被覆盖，例如，Sagi-shi服务器可能在玩家上次访问对象后更新对象。

要执行条件性写入，要用最新的对象版本向写入存储对象添加版本：

```gdscript
# Assuming we already have a storage object
var favorite_hats = ["cowboy", "alien"]
var can_read = 1 # Only the server and owner can read
var can_write = 1 # The server and owner can write
var version = <version>

var acks : NakamaAPI.ApiStorageObjectAcks = yield(client.write_storage_objects_async(session, [
    NakamaWriteStorageObject.new("hats", "favorite_hats", can_read, can_write, version)]), "completed")
if acks.is_exception():
    print("An error occurred: %s" % acks)
    return
```

### 列出存储对象

您可以在一个集合中列出玩家可以查看的所有存储对象，而不是通过单独的键发出多次读取请求。

Sagi-shi列出玩家未解锁或已购买的所有头衔、帽子和皮肤：

```gdscript
var limit = 3
var unlocks_object_list : NakamaAPI.ApiStorageObjectList = yield(client.list_storage_objects_async(session, "titles", "hats", "skins", session.user_id, limit), "completed")
if unlocks_object_list.is_exception():
    print("An error occurred: %s" % unlocks_object_list)
    return
print("Unlocked objects: ")
for o in unlocks_object_list.objects:
    print("%s" % o)
```


### 分页结果

Nakama列出结果的方法会退回游标，将其传递给Nakama的后续调用，指示在集合中检索对象的起始点。

例如：
- 如果游标的值为5，您将从第五个对象开始获取结果。
- 如果游标的值为`null`，您将从第一个对象开始获取结果。

```gdscript
object_list : NakamaAPI.ApiStorageObjectList = yield(client.list_storage_objects_async(session, "<object>", limit, object_list.cursor), "completed")
```


### 保护服务器上的存储操作

可以在服务器上保护Nakama存储引擎操作，从而保护不应被玩家修改的数据（即 游戏解锁或进度）。参见[经授权写入存储引擎](../../client-libraries/snippets/authoritative-write/)配方。


## 远程过程调用

Nakama[服务器](../../server-framework/)允许开发人员写入自定义逻辑，并将其作为[RPC](../../server-framework/introduction/#functionality)向客户端公开。

Sagi-shi包含各种需要在服务器上保护的逻辑，比如在装备设备之前检查玩家是否拥有设备。


### 创建服务器逻辑

关于创建远程过程检查玩家在装备设备之前是否拥有设备的示例，请参见[经授权处理玩家设备](../../client-libraries/snippets/authoritative-read/)配方。


### 客户端RPC

可以从客户端调用Nakama远程过程，并获取可选的JSON负载。

Sagi-shi客户端允许RPC安全地装备帽子：

```gdscript
var payload = {"hat": "cowboy"}
var rpc_id = equip_hat
var response : NakamaAPI.ApiRpc = yield(client.rpc_async(session, rpc_id, JSON.print(payload)), "completed")
if response.is_exception():
    print("An error occurred: %s" % response)
    return
```


### 套接字RPC

当您需要与Nakama实时功能交互时，也可以通过套接字调用Nakama远程过程。这些实时功能需要实时套接字（和相应的会话标识符）。可以在携带相同标识符的套接字上进行RPC。

```gdscript
var response : NakamaAPI.ApiRpc = yield(socket.rpc_async("<rpc_id>", "<payload>"), "completed")
```


## 好友

Nakama[好友](../../concepts/friends/)提供完整的社交图谱系统来管理玩家之间的好友关系。

Sagi-shi允许玩家添加好友，管理好友关系，一起玩游戏。

<figure>
  <img src="/docs/nakama/client-libraries/images/friends.png" alt="Sagi-shi Friends screen">
  <figcaption>Friends screen</figcaption>
</figure>


### 添加好友

在Nakama中添加好友不会立即添加共同好友。它会向每个用户发送好友请求，需要用户接受请求。

Sagi-shi允许玩家按用户名或用户id添加好友：

```gdscript
var ids = ["some_user_id", "another_user_id]
var usernames = ["AlwaysTheImposter21", "SneakyBoi"]

# Add friends by username
var result : NakamaAsyncResult = yield(client.add_friends_async(session, usernames), "completed")

# Add friends by user id
var result : NakamaAsyncResult = yield(client.add_friends_async(session, ids), "completed")
```


### 好友关系的状态

在Nakama中，好友关系可以有以下几种[状态](../../concepts/friends/#friend-state)：

| 值 | 状态 |
| ----- | ----- |
| 0 | 共同好友 |
| 1 | 发出等待接受的好友请求 |
| 2 | 收到等待接受的好友请求 |
| 3 | 被用户屏蔽 |


### 列出好友

Nakama允许开发人员按[好友关系的状态](../../concepts/friends/#friend-state)列出玩家的好友。

Sagi-shi列出最近20位共同好友：

```gdscript
var limit = 20 # Limit is capped at 1000
var friendship_state = 0
var list : NakamaAPI.ApiFriendList = yield(client.list_friends_async(session, limit, friendship_state), "completed")
if list.is_exception():
    print("An error occurred: %s" % list)
    return
for f in list.friends:
    var friend = f as NakamaAPI.ApiFriend
    print("Friends %s [friend.user.id])
```


### 接受好友请求

在Nakama中接受好友请求时，玩家会添加[双向好友关系](../../concepts/friends/best-practices/#modeling-relationships)。

Nakama会将两个玩家的好友状态从待接受改为共同好友。

在完整的游戏中，您可以允许玩家单独接受某些请求。

Sagi-shi仅获取并接受收到的所有好友请求：

```gdscript
var limit = 1000
var result : NakamaAsyncResult = yield(client.list_friends_async(session, 2, limit, cursor: null)
for f in result.friends:
    yield(client.add_friend_async(session, f.user.id), "completed")
```


### 删除好友

Sagi-shi玩家可以按用户名或用户id删除好友：

```gdscript
var ids = ["some_user_id", "another_user_id]
var usernames = ["AlwaysTheImposter21", "SneakyBoi"]

# Delete friends by username
var result : NakamaAsyncResult = yield(client.delete_friends_async(session, usernames), "completed")

# Delete friends by user id
var result : NakamaAsyncResult = yield(client.delete_friends_async(session, ids), "completed")
```


### 屏蔽用户

Sagi-shi玩家可以按用户名或用户id屏蔽用户：

```gdscript
var ids = ["some_user_id", "another_user_id]
var usernames = ["AlwaysTheImposter21", "SneakyBoi"]

# Block friends by username
var result : NakamaAsyncResult = yield(client.block_friends_async(session, usernames), "completed")

# Block friends by user id
var result : NakamaAsyncResult = yield(client.block_friends_async(session, ids), "completed")
```

进一步了解关于[屏蔽好友](../../concepts/friends/#block-a-friend)和相关的[好友关系状态](../../concepts/friends/best-practices/#relationship-state)。

被屏蔽的好友也可以作为[好友列出](#listing-friends)，但好友关系状态相应变为（`3`）。


## 状态与显示

Nakama[状态](../../concepts/status/)是一种实时状态与显示的服务，允许用户设置显示的在线状态，更新状态消息并关注其他用户的更新。

玩家可以关注其他人但不与之成为好友。

Sagi-shi使用状态消息和显示的在线状态，当好友在线时通知玩家并分享比赛。

<figure>
  <img src="/docs/nakama/client-libraries/images/status.png" alt="Sagi-shi status update screen">
  <figcaption>Updating player status</figcaption>
</figure>


### 关注用户

Nakama实时API允许开发人员订阅套接字上的事件并实时接收这些事件，如状态显示变更。

[关注用户](../../concepts/status/#follow-users)的方法也会返回至当前的在线用户（即显示的在线状态）及其状态。

Sagi-shi会关注玩家的好友，并当好友在线时通知玩家：

```gdscript
func _ready():
    # Setup the socket and subscribe to the status event
    socket.connect("received_status_presence", self, "_on_status_presence")

func _on_status_presence(p_presence : NakamaRTAPI.StatusPresenceEvent):
    print(p_presence)
    for j in p_presence.joins:
        print("%s is online with status: %s" % [j.user_id, j.status])
    for j in p_presence.leaves:
        print("%s went offline" % [j.user_id])

# Follow mutual friends and get the initial Status of any that are currently online
var friends_result = yield(client.list_friends_async(session, 0), "completed")
var friend_ids = []
for friend in friends_result:
	var f = friend as NakamaAPI.ApiFriend
	if not f or not f.user.online:
		continue
	friend_ids.append(f.user)
var result : NakamaAsyncResult = yield(socket.follow_users_async(friend_ids)

for p in result.presences:
    print("%s is online with status: %s" % [presence.user_id, presence.status])
```


### 取消关注用户

Sagi-shi玩家可以取消关注其他人：

```gdscript
yield(socket.unfollow_users_async("<user_id>"), "completed")
```


### 更新玩家状态

Sagi-shi玩家可以更改状态并向关注自己的人发布状态：

```gdscript
yield(socket.update_status_async("Viewing the Main Menu"), "completed")
```


## 群组

Nakama[群组](../../concepts/groups/)是指一个公开/私密的群组或家族体系，拥有用户成员资格和权限、元数据和群组聊天功能。

Sagi-shi允许玩家创建和加入群组，可以社交或竞赛。

<figure>
  <img src="/docs/nakama/client-libraries/images/groups-list.png" alt="Sagi-shi groups screen">
  <figcaption>Groups list screen</figcaption>
</figure>


### 创建群组

群组可以为公开或私密“开放”。每个人都可以加入公开的群组，但如想加入私密群组，必须要请求加入，得到超级管理员/管理员接受才可。

Sagi-shi玩家可以围绕共同的兴趣创建群组：

```gdscript
var name = "Imposters R Us"
var description = "A group for people who love playing the imposter."
var open = true # public group
var max_size = 100

var group : NakamaAPI.ApiGroup = yield(client.create_group_async(session, name, description, open, max_size), "completed")
```

### 更新群组的可见性

Nakama允许群组的超级管理员或管理员成员从客户端更新某些属性，如开放可见性：

```gdscript
var open = false
yield(client.update_group_async(session, "<group_id>", name: null, open), "completed")
```


### 更新群组规模

其他属性只能在服务器上更改，例如群组成员的最大数量。

示例请见[更新群组规模](../../concepts/groups/#updating-group-size)配方和[群组服务器功能参考](../../server-framework/typescript-runtime/function-reference/#groups)，进一步了解在服务器上更新群组的信息。

<figure>
  <img src="/docs/nakama/client-libraries/images/group-edit.png" alt="Sagi-shi group edit screen">
  <figcaption>Sagi-shi group edit</figcaption>
</figure>


### 列出和过滤群组

群组可以像Nakama的其他资源一样列出，也可以使用通配符群组名称[过滤](../../concepts/groups/#list-and-filter-groups)。

Sagi-shi玩家通过列出和过滤群组搜索加入现有的群组：


```gdscript
var limit = 20
var result : NakamaAPI.ApiGroupList = yield(client.list_groups_async(session, "imposter%", limit), "completed")

for g in result.groups:
    var group = g as NakamaAPI.ApiGroup
    print("Group: name &s, open %s", [group.name, group.open])

$ Get the next page of results
var next_results : NakamaAPI.ApiGroupList = yield(client.list_groups_async(session, name: "imposter%", limit, result.cursor)
```


### 删除群组

Nakama允许群组的超级管理员删除群组。

开发人员可以完全禁用此功能，请在[Guarding API指南](../../guides/server-framework/guarding-apis/)中查看如何保护Nakama各种API的示例。

Sagi-shi玩家可以删除自己在其中作为超级管理员的群组：

```gdscript
yield(client.delete_group_async(session, "<group_id>"), "completed")
```


### 群组元数据

与用户账户一样，群组可以拥有公开元数据。

Sagi-shi使用群组元数据存储群组的兴趣、玩家的活跃时间和使用的语言。

仅可在服务器上更新群组元数据。示例请见[更新群组元数据](../../client-libraries/snippets/group-metadata)配方。

Sagi-shi客户端使用群组元数据负载进行RPC：

```gdscript
var payload = {
    group_id = "<group_id>",
    interests = ["Deception", "Sabotage", "Cute Furry Bunnies"],
    active_times = ["9am-2pm Weekdays", "9am-10pm Weekends"],
    languages = ["English", "German"],
}

var result : NakamaAsyncResult = yield(client.rpc_async(session, "update_group_metadata", JSON.stringify(payload))
if result.is_exception():
    print("An error occurred: %s" % result)
    return
print("Successfully updated group metadata")
```


### 群组成员资格状态

在Nakama中，成员资格可以有以下几种[状态](../../concepts/groups/#groups-and-clans)：

| 代码 | 用途 |
| ---- | ------- | - |
|    0 | 超级管理员 | 任何群组都必须拥有至少一位超级管理员。超级管理员拥有管理员的所有权限，另外还可以删除群组和升级管理员成员。 |
|    1 | 管理员 | 可以有一个或多个管理员。管理员可以更新群组，也可以接受、踢出、升级、降级、封禁或添加成员。 |
|    2 | 成员 | 群组常规成员。他们无法接受新用户的加入请求。 |
|    3 | 加入请求 | 新用户发送的新的加入请求。这不会被计入群组成员的最大数量。 |


### 加入群组

如果用户加入公开群组，可立即成为群组成员，但如果尝试加入私密群组，必须等待群组管理员接受请求。

Sagi-shi玩家可以加入群组：

```gdscript
yield(client.join_group_async(session, "<group_id>"), "completed")
```


### 列出用户的群组

Sagi-shi玩家可以列出自己所在的群组：

```gdscript
var user_id = "<user id>"
var result : NakamaAPI.ApiUserGroupList = yield(client.list_user_groups_async(session, user_id), "completed")

for ug in result.user_groups:
    var g = ug.group as NakamaAPI.ApiGroup
    print("Group %s role %s", g.id, ug.state)
```


### 列出成员

Sagi-shi玩家可以列出群组的成员：

```gdscript
var group_id = "<group id>"
var member_list : NakamaAPI.ApiGroupUserList = yield(client.list_group_users_async(session, group_id), "completed")

for ug in member_list.group_users:
    var u = ug.user as NakamaAPI.ApiUser
    print("User %s role %s" % [u.id, ug.state])
```


### 接受加入请求

私密群组管理员或超级管理员可以通过将用户重新添加到群组来接受加入请求。

Sagi-shi首先列出处于请求加入状态的用户，然后遍历并将这些用户添加到群组：

```gdscript
var result : NakamaAPI.ApiGroupUserList = yield(client.list_group_users_async(session, "<group_id>", 3), "completed")

for gu in result.group_users:
    var u = gu.user as NakamaAPI.ApiUser
    yield(client.add_group_users_async(session, "<group_id>", u), "completed"))
```


### 升级成员

Nakama群组成员可以升级为管理员或超级管理员角色，帮助管理规模不断扩大的群组，或在成员离开时接任。

管理员可以将其他成员升级为管理员，超级管理员可以将其他成员升级为超级管理员。

成员将被提升一级。例如：

- 成员可以升级为管理员
- 管理员可以升级为超级管理员

```gdscript
yield(client.promote_group_users_async(session, "<group_id>", "<user_id>")
```


### 降级成员

Sagi-shi群组管理员和超级管理员可以降级成员：

```gdscript
yield(client.demote_group_users_async(session, "<group_id>", "<user_id>")
```


### 踢出成员

Sagi-shi群组管理员和超级管理员可以移除群组成员：

```gdscript
yield(client.kick_group_users_async(session, "<group_id>", "<user_id>")
```


### 封禁成员

当降级用户或踢出用户不够严重时，Sagi-shi群组管理员和超级管理员可以封禁成员：

```gdscript
yield(client.ban_group_users_async(session, "<group_id>", "<user_id>")
```

### 退出群组

Sagi-shi玩家可以退出群组：

```gdscript
yield(client.leave_group_async(session, "<group_id>")
```


## 聊天

Nakama聊天是针对群组、私密/直接消息以及动态聊天室的实时聊天系统。

Sagi-shi在比赛期间使用动态聊天，玩家可以相互误导，讨论谁是内鬼，群组聊天和私密/直接消息。

<figure>
  <img src="/docs/nakama/client-libraries/images/chat.png" alt="Sagi-shi chat screen">
  <figcaption>Sagi-shi Chat</figcaption>
</figure>


### 加入动态聊天室

Sagi-shi比赛设有非永久聊天室，供玩家交流：

```gdscript
var roomname = "<match_id>"
var persistence = false
var hidden = false
var type = NakamaSocket.ChannelType.Room
var channel : NakamaRTAPI.Channel = yield(socket.join_chat_async(roomname, type, persistence, hidden), "completed")

print("Connected to dynamic room channel: '%s'" % [channel.id])
```


### 加入群组聊天

Sagi-shi群组成员可以在持久的群组聊天频道中跨越游戏会话进行交流：

```gdscript
var group_id = "<group_id>"
var persistence = true
var hidden = false
var type = NakamaSocket.ChannelType.Group
var channel : NakamaRTAPI.Channel = yield(socket.join_chat_async(group_id, type, persistence, hidden), "completed")

print("Connected to group channel: '%s'" % [channel.id])
```


### 加入直接聊天

Sagi-shi玩家也可以在比赛中或比赛后一对一私下交流，可以查看消息历史记录：

```gdscript
var user_id = "<user_id>"
var persistence = true
var hidden = false
var type = NakamaSocket.ChannelType.DirectMessage
var channel : NakamaRTAPI.Channel = yield(socket.join_chat_async(user_id, type, persistence, hidden), "completed")

print("Connected to direct message channel: '%s'" % [channel.id])
```


### 发送消息

在每种聊天频道中，消息的发送都是一样的。消息包含聊天文本和表情，以JSON序列化数据的形式发送：

```gdscript
var channel_id = "<channel_id>"

var message_content = { "message": "I think Red is the imposter!" }

var message_ack : NakamaRTAPI.ChannelMessageAck = yield(socket.write_chat_message_async(channel_id, message_content), "completed")

var emote_content = {
    "emote": "point",
    "emoteTarget": "<red_player_user_id>",
    }

var emote_ack : NakamaRTAPI.ChannelMessageAck = yield(socket.write_chat_message_async(channel_id, emote_content), "completed")
```


### 列出消息历史

消息列表需要一个参数，指示接收的消息是从最早到最新（向前）还是从最新到最早排列。

Sagi-shi玩家可以列出群组的消息历史：

```gdscript
var limit = 100
var forward = true
var group_id = "<group_id>"
var result : NakamaAPI.ApiChannelMessageList = yield(client.list_channel_messages_async(session, group_id, limit, forward), "completed")

for m in result.messages:
    var message : NakamaAPI.ApiChannelMessage = m as NakamaAPI.ApiChannelMessage
    print(message.user_id, message.content)
```

聊天还有可缓存游标，获取最新消息。阅读[列表通知](../../concepts/notifications/#list-notifications)文档中关于可缓存游标的更多信息。


### 更新消息

Nakama还支持更新消息。是否使用此功能取决于您，但在类似Sagi-shi的游戏中，它可以增加额外的欺骗元素。

例如玩家发送以下消息：

```gdscript
var channel_id = "<channel_id>"
var message_content = {"message": "I think Red is the imposter!" }

var message_ack : NakamaRTAPI.ChannelMessageAck = yield(socket.write_chat_message_async(channel_id, message_content), "completed")
```

然后他们迅速编辑消息以迷惑他人：

```gdscript
var new_message_content = { "message": "I think BLUE is the imposter!" }

var message_update_ack : NakamaRTAPI.ChannelMessageAck = yield(socket.update_chat_message_async(channel_id, new_message_content), "completed")
```


## 比赛

Nakama支持[服务器授权](../../concepts/multiplayer/authoritative/)和[服务器中继](../../concepts/multiplayer/relayed/)的多人比赛。

在服务器授权比赛中，服务器控制玩法循环，并且必须使所有的客户端与游戏的当前状态保持同步。

在服务器中继比赛中，客户端处于控制地位，服务器仅将信息中继到其他连接的客户端。

在竞争性游戏中，如Sagi-shi，服务器授权比赛可防止客户端以未经授权的方式与您的游戏交互。

在本指南中，为方便起见，采用了服务器中继模式。


### 创建比赛

Sagi-shi玩家可以自行创建比赛并邀请在线好友加入：

```gdscript
var match : NakamaRTAPI.Match = yield(socket.create_match_async(), "completed")
var friends_list = yield(client.list_friends_async(session, 0, 100)
var online_friends = []
for friend in friends_list:
    var f = friend as NakamaAPI.ApiFriend
	if not f or not f.user.online:
		continue
    online_friends.append(f.user)

for f in online_friends:
    var content = {
        "message": "Hey %s, join me for a match!",
        match_id = match.id,
    }
    var channel = yield(socket.join_chat_async(f.id, NakamaSocket.ChannelType.DirectMessage), "completed")
    var message_ack = yield(socket.write_chat_message_async(channel.id, content), "completed")
```

**使用比赛名称创建比赛**

Sagi-shi玩家还可以使用特定的比赛名称创建比赛，以便通过将比赛名称告知好友来邀请好友加入。需要注意的是，使用比赛名称创建比赛时（任意名称，而不是与经授权的比赛处理程序绑定的名称），比赛将始终为中继比赛而非授权比赛。

```gdscript
var match_name = "NoImpostersAllowed"
var match : NakamaRTAPI.Match = yield(socket.create_match_async(match_name), "completed")
```

### 加入比赛

如果知道id，Sagi-shi玩家可以尝试加入已有比赛：

```gdscript
var match_id = "<matchid>"
var match = yield(socket.join_match_async(match_id), "completed")
```

或者设置实时配对监听器，将自己加入到配对：

```gdscript
func _on_matchmaker_matched(p_matched : NakamaRTAPI.MatchmakerMatched):
  var match : NakamaRTAPI.Match = yield(socket.join_match_async(p_matched), "completed")

var min_players = 2
var max_players = 10
var query = ""

var matchmaking_ticket : NakamaRTAPI.MatchmakerTicket = yield(
  socket.add_matchmaker_async(query, min_players, max_players),
  "completed"
)
```


**按玩家状态加入比赛**

Sagi-shi玩家可以在加入新比赛时更新状态：

```gdscript
var status = {
    "status": "Playing a match",
    "matchid": "<match_id>",
    }

yield(socket.update_status_async(status), "completed")
```

关注玩家的用户可以接收实时状态事件，并尝试加入比赛：

```gdscript
func _on_status_presence(p_presence : NakamaRTAPI.StatusPresenceEvent):
    # Join the first match found in a friend's status
    for j in p_presence.joins:
        var status = JSON.parse(p_presence.status)
        if matchid in status:
            yield(socket.join_match_async(status["matchid"]), "completed")
```

### 列出比赛

[比赛列表](../../concepts/multiplayer/match-listing/)需要一些标准来过滤比赛，包括玩家人数、匹配标签和可以进行更复杂的[搜索查询](../../concepts/multiplayer/query-syntax/)的选项。

在大厅状态时可以开始Sagi-shi比赛。比赛会存在于服务器，但只有当加入的玩家人数足够时才会开始比赛。

然后Sagi-shi可以列出正在等待更多玩家的比赛：

```gdscript
var min_players = 2
var max_players = 10
var limit = 10
var authoritative = true
var label = ""
var query = ""
var result : NakamaRTApi.Match = yield(client.list_matches_async(session, min_players, max_players, limit, authoritative, label, query)

for m in result.matches:
    print("%s: %s/10 players", match.match_id, match.size)
```

找到标签为`"an_exact_match_label"`的比赛：

```gdscript
var label = "an_exact_match_label"
```

**高级：**

为了使用更复杂的结构化查询，匹配标签必须为JSON格式。

要查找预期玩家技能级别为`>100`且可选游戏模式为`"sabotage"`的比赛：

```gdscript
var query = "+label.skill:>100 label.mode:sabotage"
```


### 生成玩家

比赛对象有一个当前在线用户列表，称为显示在线的用户。

Sagi-shi使用比赛显示的在线状态在客户端上生成玩家：

```gdscript
var match = yield(socket.join_match_async(match_id), "completed")

var players = {}

for p in match.presences:
    // Spawn a player for this presence and store it in a dictionary by session id.
    var go = <player_node>.new()
    players.add(presence.session_id, go)
```

Sagi-shi使用接收到的比赛显示的在线状态事件，使生成的玩家在离开和加入比赛时保持最新状态：

```gdscript
func _on_match_presence(p_presence : NakamaRTApi.MatchPresenceEvent):
    # For each player that has joined in this event...
    for p in p_presence.joins:
        # Spawn a player for this presence and store it in a dictionary by session id.
        var go = <player_node>.new()
        players.add(p_presence.session_id, go)
    # For each player that has left in this event...
    for p in p_presence.leaves:
        # Remove the player from the game if they've been spawned
        if presence.session_id in players:
            <player_node>.remove_and_skip()
            players.remove(presence.session_id)
```

### 发送比赛状态

Nakama拥有实时网络，可以在玩家移动和与游戏世界互动时[发送](../../concepts/multiplayer/relayed/#send-data-messages)和[接收](../../concepts/multiplayer/relayed/#receive-data-messages)比赛状态。

比赛过程中，Sagi-shi的每个客户端都会将比赛状态发送到服务器，从而中继给其他客户端。

匹配状态包含一个操作代码，使接收者了解所接收的数据，以便对数据进行反序列化处理并更新其游戏视图。

Sagi-shi使用的操作代码示例：
- 1：玩家位置
- 2：玩家调用投票


**发送玩家位置**

定义一个类别来代表Sagi-shi玩家的位置状态。

```gdscript
class_name position_state

var X
var Y
var Z
```

在玩家的转换中创建一个实例，设置操作代码并发送JSON编码的状态：

```gdscript
var state = {
    X = transform.x,
    Y = transform.y,
    Z = transform.z,
}

var op_code = 1

yield(socket.send_match_state_async(match.id, op_code, JSON.print(state), "completed")
```


**作为静态类操作代码**

Sagi-shi有许多联网游戏动作。操作代码使用静态常量类将更容易遵循和维护：

```gdscript
class_name op_codes

const position = 1
const vote = 2

yield(socket.send_match_state_async(match.id, op_codes.position, JSON.print(state), "completed")
```


### 接收比赛状态

Sagi-shi玩家可以通过订阅比赛状态接收事件，通过其他连接的客户端接收比赛数据：

```gdscript
func _on_match_state(p_state : NakamaRTAPI.MatchData):
    match match_state.op_code:
        op_code.position:
        # Get the updated position data
        var position_state = JSON.parse(match_state.state)
        # Update the game object associated with that player
        var user = match_state.user_presence.session_id
        if user in players:
            # Here we would normally do something like smoothly interpolate to the new position, but for this example let's just set the position directly.
            players[user].transform.Vector3 = vec(position_state.x, position_state.y, position_state.z)
        _:
            print("Unsupported op code.")

```


## 配对程序

开发人员可以使用比赛列表或Nakama[配对程序](../../concepts/multiplayer/matchmaker/)为玩家寻找匹配，使玩家能够加入实时匹配池，并在匹配到其他符合指定标准的玩家时收到通知。

配对可以帮助玩家找到一起玩游戏的伙伴，但不会创建比赛。这种解耦的设计允许您将配对用于寻找游戏匹配之外的其他目的。例如，如果您想社交，可以使用配对匹配别人进行聊天。


### 添加配对程序

匹配标准可以很简单，即找到2名玩家，也可以更复杂，即找到2-10名对特定游戏模式感兴趣的拥有最低技能水平的玩家。

Sagi-shi允许玩家加入匹配池，使服务器将这些玩家与其他玩家进行匹配：

```gdscript
var min_players = 2
var max_players = 10
var query = "+skill:>100 mode:sabotage"
var string_properties = { "mode": "sabotage" }
var numeric_properties = { "skill": 125 }
var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = yield(
  socket.add_matchmaker_async(query, min_players, max_players, string_properties, numeric_properties)
```

按照规定的标准成功匹配后，玩家可以加入比赛：

```gdscript
func _on_matchmaker_matched(p_matched : NakamaRTAPI.MatchmakerMatched):
  var joined_match : NakamaRTAPI.Match = yield(socket.join_match_async(p_matched), "completed")
```

## 派对

Nakama[派对](../../concepts/parties/)是一个实时系统，允许玩家组成短暂的派对，这些派对在所有玩家断开连接后不会持续存在。

Sagi-shi允许好友组成派对并一起配对。


### 创建派对

创建派对的玩家是派对的领导者。派对玩家的数量有上限，派对既可以为开放式，即自动接受玩家，也可以为封闭式，即等待派对领导者接受玩家发来的加入请求。

Sagi-shi使用封闭式派对，最多可以有四名玩家：

```gdscript
var open = false
var max_players = 4
var party = yield(socket.create_party_async(open, max_players), "completed")
```

Sagi-shi通过私密/直接消息将派对id分享给好友：

```gdscript
var friends_list : NakamaAPI.ApiFriendList = yield(client.list_friends_async(session, limit, friendship_state), "completed")
var online_friends = []
for friend in friends_list:
    var f = friend as NakamaAPI.ApiFriend
    if not f or not f.user.online:
        continue
    online_friends.append(f.user)

for f in online_friends:
    var content = {
        "message": "Hey %s, wanna join the party?",
        party_id = party.id,
    }
    var channel = yield(socket.join_chat_async(f.id, NakamaSocket.ChannelType.DirectMessage), "completed")
    var message_ack = yield(socket.write_chat_message_async(channel.id, content), "completed")
```


### 加入派对

Safi-shi玩家可以通过查看聊天消息中的派对id来加入派对。首先，必须连接
套接字的`received_channel_message`信号。

```gdscript
socket.connect("received_channel_message", self, "_on_received_channel_message")
```

然后，当接收到这个信号时，可以查看消息内容并加入派对。

```gdscript
func _on_received_channel_message(message):
    var data = JSON.parse(message.content)
    if data.result.party_id:
        var join = yield(socket.join_party_async(data.result.party_id), "completed")
        if join.is_exception():
            print("error joining party)
```


### 升级成员

Sagi-shi派对成员可以升级为派对领导者：

```gdscript
var new_leader = "<user_id>"
var party_id = "<party_id>"
var leader: NakamaAsyncResult = yield(socket.received_party_leader(party_id, new_leader), "completed)
```

### 退出派对

Sagi-shi玩家可以退出派对：

```gdscript
var party_id = "<party_id>"
var party: NakamaAsyncResult = yield(socket.leave_party_async(party_id), "completed")
```


### 派对配对

加入派对的一个主要好处是，所有玩家都可以同时加入匹配池。

Sagi-shi玩家可以收听配对程序匹配事件，并在找到匹配人时加入比赛：

```gdscript
func _on_matchmaker_matched(p_matched : NakamaRTAPI.MatchmakerMatched):
  var joined_match : NakamaRTAPI.Match = yield(socket.join_match_async(p_matched), "completed")
```

派对领导者将开始为派对进行匹配：

```gdscript
var party_id = "<party_id>"
var min_players = 2
var max_players = 10
var query = ""
var matchmaker_ticket = yield(socket.add_matchmaker_party_async(party_id, query, min_players, max_Players)
```


## 排行榜

Nakama[排行榜](../../concepts/leaderboards/)为您的游戏引入竞争因素，提高了玩家的参与度和保留率。

Sagi-shi有一个内鬼获胜的周排行榜，玩家每次获胜都会增加得分，同样，也有船员获胜的周排行榜。

<figure>
  <img src="/docs/nakama/client-libraries/images/leaderboard.png" alt="Sagi-shi leaderboard screen">
  <figcaption>Sagi-shi Leaderboard</figcaption>
</figure>


### 创建排行榜

必须在服务器上创建排行榜，请在[排行榜](../../concepts/leaderboards/#create-a-leaderboard)文档中查看有关创建排行榜的详细信息。


### 提交分数

玩家提交分数时，Nakama将会把提交的分数值加到玩家的现有分数中。

除了分数值外，Nakama还有一个子分数，当分数值相同时可使用子分数进行排序。

Sagi-shi玩家可以向排行榜提交带有情境元数据的分数，例如取得分数的地图：

```gdscript
var score = 1
var subscore = 0
var metadata = { "map": "space_station" }
var record : NakamaAPI.ApiLeaderboardRecord = yield(client.write_leaderboard_record_async(session, "weekly_imposter_wins", score, subscore, JSON.print(metadata), "completed")
```

### 列出最高记录

Sagi-shi玩家可以列出排行榜的最高记录：

```gdscript
var limit = 20
var leaderboard_name = "weekly_imposter_wins"
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_async(session, leaderboard_name, owner_ids: null, expiry: null, limit, cursor: null), "completed")

for r in result.records:
    print("%s:%s", record.owner_id, record.score)
```


**列出用户周围的记录**

Nakama允许开发人员列出玩家周围的排行榜记录。

Sagi-shi向玩家简要介绍其与周围玩家的对抗情况：

```gdscript
var limit = 20
var leaderboard_name = "weekly_imposter_wins"
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_async(session, leaderboard_name, session.user_id, expiry: null, limit), "completed")

for r in result.records:
    print("%s:%s", record.owner_id, record.score)
```


**列出一系列用户的记录**

Sagi-shi玩家可以通过向所有者id参数提供他们的用户id来获取好友分数：

```gdscript
var friends_list : NakamaAPI.ApiFriendList = yield(client.list_friends_async(session, 0, 100, cursor: null), "completed")
var user_ids = []
for friend in friends_list.friends:
    var f = friend as NakamaAPI.ApiFriend
    user_ids.append(f.user.id)

var record_list : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_around_owner_async(session, "weekly_imposter_wins", user_ids, expiry: null, 100, cursor: null), "completed")

for record in record_list.records:
    print("%s scored %s", record.username, record.score)
```

同样也可以通过向所有者id参数提供他们的用户id来获取群组成员的分数：

```gdscript
var group_id = "<groupid>"
var group_user_list : NakamaAPI.ApiGroupUserList = yield(client.list_group_users_async(session, group_id, 100, cursor: null), "completed")
var user_ids = []
for gu in group_user_list.group_users:
    var u = gu as NakamaAPI.ApiUser
    user_ids.append(u.user.id)

var record_list : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_around_owner_async(session, "weekly_imposter_wins", user_ids, expiry: null, 100, cursor: null), "completed")
```


### 删除记录

Sagi-shi玩家可以删除自己的排行榜记录：

```gdscript
yield(client.delete_leaderboard_record_async(session, "<leaderboard_id>"), "completed")
```


## 锦标赛

Nakama[锦标赛](../../concepts/tournaments/)是玩家争夺奖品的短暂比赛。

Sagi-shi玩家可以查看、过滤和加入正在进行的锦标赛。

<figure>
  <img src="/docs/nakama/client-libraries/images/tournaments.png" alt="Sagi-shi tournaments screen">
  <figcaption>Sagi-shi Tournaments</figcaption>
</figure>


### 创建锦标赛

必须在服务器上创建锦标赛，请在[锦标赛](../../concepts/tournaments/#create-tournament)文档中查看有关创建锦标赛的详细信息。

Sagi-shi每周都会举行锦标赛，玩家需要投票给最准确的内鬼。本周结束时，排名靠前的玩家将获得游戏货币奖励。


### 加入锦标赛

默认Nakama玩家不必加入锦标赛也可提交分数，但Sagi-shi强制要求加入锦标赛后方可提交分数：

```gdscript
yield(client.join_tournament_async(session, "<id>"), "completed")
```

### 列出锦标赛

Sagi-shi玩家可以根据各种标准列出和筛选锦标赛：

```gdscript
var category_start = 1
var category_end = 2
int start_time = null
int end_time = null
var limit = 100
var result : NakamaAPI.ApiTournamentRecordList = yield(client.list_tournament_records_async(session, category_start, category_end, start_time, end_time, limit, cursor: null), "completed")

for t in result.tournaments:
    print("%s:%s", tournament.id, tournament.title)
```

出于性能方面的考虑，通过范围而不是单个数字来过滤类别。您可以利用这一点构建您的类别（例如，所有的PvE锦标赛属于1XX范围，所有的PvP锦标赛属于2XX范围）。


### 列出记录

Sagi-shi玩家可以列出锦标赛记录：

```gdscript
var limit = 20
var tournament_name = "weekly_top_detective"
var result : NakamaAPI.ApiTournamentRecordList = yield(client.list_tournament_records_async(session, tournament_name, owner_ids: null, expiry: null, limit, cursor: null), "completed")

for r in result.records:
    print("%s:%s", record.owner_id, record.score)
```


**列出用户周围的记录**

与排行榜类似，Sagi-shi玩家可以获取周围其他玩家的记录：

```gdscript
var limit = 20
var tournament_name = "weekly_top_detective"
var result : NakamaAPI.ApiTournamentRecordList = yield(client.list_tournament_records_async(session, tournament_name, session.user_id, expiry: null, limit), "completed")

for r in result.records:
    print("%s:%s", record.owner_id, record.score)
```


### 提交分数

Sagi-shi玩家可以向锦标赛提交分数、子分数和元数据：

```gdscript
var score = 1
var subscore = 0
var metadata = JSON.print({
    "map": "space_station" })
var new_record : NakamaAPI.ApiLeaderboardRecord = yield(client.write_tournament_record_async(session, "weekly_top_detective", score, subscore, metadata), "completed")
```


## 通知

游戏服务器可利用Nakama[通知](../../concepts/notifications/)向玩家广播实时消息。

通知可以是持续性（在玩家查看之前一直保留）或暂时性的（仅在玩家当前在线的情况下接收）。

Sagi-shi使用通知将获奖情况告知锦标赛获奖者。

<figure>
  <img src="/docs/nakama/client-libraries/images/notifications.png" alt="Sagi-shi notification screen">
  <figcaption>Sagi-shi notifications</figcaption>
</figure>


### 接收通知

通知必须从服务器发出。

Nakama使用代码区分通知。`0` 和以下代码是为Nakama内部构件保留的[系统代码](../../concepts/notifications/#notification-codes)。

Sagi-shi玩家可以订阅收到的通知事件。Sagi-shi使用代码`100`表示赢得锦标赛：

```gdscript
cont reward_code = 100

func _on_notification(p_notification : NakamaAPI.ApiNotification):
    match notification.code:
        reward_code:
            print("Congratulations, you won the tournament!\n%s\n%s", notification.subject, notification.content)
        _:
            print("Other notification: %s:%s\n%s", notification.code, notification.subject, notification.content)
```


### 列出通知

Sagi-shi玩家可以列出离线时收到的通知：

```gdscript
var limit = 100
var result : NakamaAPI.ApiNotificationList = yield(client.list_notifications_async(session, limit), "completed")

for n in result.notifications:
    print("Notification: %s:{%s\n%s", notification.code, notification.subject, notification.content)
```


**分页及可缓存游标**

与其他列出方法一样，可以使用游标或可缓存游标将通知结果分页。

```gdscript
var result : NakamaAPI.ApiNotificationList = yield(client.list_notifications_async(session, 1), "completed")
var cacheable_cursor = result.cacheable_cursor
```

玩家下次登录时，可使用可缓存游标列出未读通知。

```gdscript
var next_results = yield(client.list_notifications_async(session, limit, cacheable_cursor)
```


### 删除通知

Sagi-shi玩家可以在阅读通知后将其删除：

```gdscript
var notification_ids = ["<notification-id>"]
var delete : NakamaAsyncResult = yield(client.delete_notifications_async(session, notification_ids), "completed")
```
