Nakama C++客户端指南 #

本客户端库指南将介绍如何开发名为Sagi-shi(“Imposter”的日语名称)游戏的Nakama特定部分(无完整的游戏逻辑或UI),此游戏受Among Us(外部)启发,从而展示如何在**C++**中使用Nakama的核心功能。

Sagi-shi gameplay screen
Sagi-shi gameplay

前提条件 #

开始之前,确保您已:

完整版API文档 #

请在API文档中查看完整版API文档。

安装 #

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

请在GitHub上查看Nakama C++ SDK README,获取关于设置您的C++项目和安装Nakama C++ SDK的指南。

更新 #

Nakama C++客户端的新版本和相应的改进记录在 更改日志中。

Tick #

tick方法在您的线程中抽取请求队列并执行回调。在您的线程中,您必须定期调用它(建议每隔50ms调用一次)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
while(!done)
{
    client->tick();
    
    if (rtClient)
    {
        rtClient->tick();
    }
    
    this_thread::sleep_for(chrono::milliseconds(50));
}

否则,默认客户端和实时客户端不会执行任何操作,您将无法从服务器获得响应。

异步编程 #

许多Nakama API采用异步、非阻塞操作方式。

Sagi-shi可以调用这些方法而不去阻止调用线程,从而提高游戏响应能力和效率。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
bool done = false;

auto loginFailedCallback = [&done](const NError& error)
{
    cout << "Failed to login" << endl;
    cout << error.message << endl;
    done = true;
};

auto loginSucceededCallback = [&done, &rtClient](NSessionPtr session)
{
    cout << "Login successful" << endl;
    cout << session->getAuthToken() << endl;

    rtClient->connect(session, true);
};

string deviceId = "e872f976-34c1-4c41-88fe-fd6aef118782";

client->authenticateDevice(
        deviceId,
        opt::nullopt,
        opt::nullopt,
        {},
        loginSucceededCallback,
        loginFailedCallback);

处理异常 #

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

Sagi-shi中的API调用可以得当处理错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto errorCallback = [](const NError& error)
{
    cout << "An error occurred: " << error.message << endl;

    if (error.code == ErrorCode::ConnectionError)
    {
        cout << "The server is currently unavailable. Check internet connection." << endl;
    }
};

client->getAccount(session, successCallback, errorCallback);

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

本指南全程使用nlohmann/json库对JSON进行序列化和反序列化处理。

新手入门 #

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

Nakama客户端 #

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

要为Sagi-shi创建一个客户端,请将以下连接详情输入您的服务器:

1
2
3
4
5
NClientParameters params;
params.serverKey = "defaultkey";
params.host = "127.0.0.1";
params.port = DEFAULT_PORT;
auto client = createDefaultClient(params);

Nakama套接字 #

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

通过客户端创建实时客户端:

1
2
3
4
5
NRtClientPtr rtClient;
rtClient = client->createRtClient();

bool createStatus = true;
rtClient->connect(session, createStatus);

身份验证 #

Nakama支持许多身份验证方法,也支持在服务器上创建自定义身份验证

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

Sagi-shi login screen
Login screen and Authentication options

设备身份验证 #

Nakama设备身份验证使用物理设备的唯一标识符轻松验证用户,也可以为没有账户的设备创建账户。

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Typically you would get the system's unique device identifier here.
string deviceId = "e872f976-34c1-4c41-88fe-fd6aef118782";

auto loginFailedCallback = [&done](const NError& error)
{
    cout << "An error occurred: " << error.message << endl;
};

auto loginSucceededCallback = [&done, &rtClient](NSessionPtr session)
{
    cout << "Successfully authenticated: " << session->getAuthToken() << endl;
};

// Authenticate with the Nakama server using Device Authentication.
client->authenticateDevice(
        deviceId,
        opt::nullopt,
        opt::nullopt,
        {},
        loginSucceededCallback,
        loginFailedCallback);

Facebook身份验证 #

Nakama Facebook身份验证操作简便,您可以有选择性地导入玩家的Facebook好友,并将这些好友添加到玩家的Nakama好友列表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Authenticate with the Nakama server using Facebook Authentication.
string accessToken = "<Token>";
bool importFriends = true;

client->authenticateFacebook(
        accessToken,
        "mycustomusername",
        true,
        importFriends,
        {},
        loginSucceededCallback,
        loginFailedCallback);

自定义身份验证 #

Nakama支持自定义身份验证方法,以便与其他身份服务相集成。

示例请见Itch.io自定义身份验证配方。

链接身份验证 #

Nakama允许玩家在进行身份验证后通过链接身份验证 方法前往玩家账户。

链接设备ID身份验证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto linkSuccessCallback = []()
{
    cout << "Successfully linked Device ID authentication to existing player account" << endl;    
};

auto linkErrorCallback = [](const NError& error)
{
    cout << "Error linking Device ID: " << error.message << endl;
};

// Link Device Authentication to existing player account.
client->linkDevice(
        session,
        deviceId,
        linkSuccessCallback,
        linkErrorCallback
        );

链接Facebook身份验证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto linkSuccessCallback = []()
{
    cout << "Successfully linked Facebook authentication to existing player account" << endl;    
};

auto linkErrorCallback = [](const NError& error)
{
    cout << "Error linking Facebook: " << error.message << endl;
};

client->linkFacebook(
        session,
        accessToken,
        importFriends,
        linkSuccessCallback,
        linkErrorCallback
        );

会话变量 #

在身份验证时可以存储Nakama 会话变量,并且只要会话处于活动状态,就可以在客户端和服务器上使用Nakama会话变量。

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
NStringMap vars = {
    { "DeviceOS", "<OperatingSystem>" },
    { "DeviceModel", "<DeviceModel>" },
    { "GameVersion", "<GameVersion>" },
    { "InviterUserId", "<SomeUserId>" },
};

/// ...

client->authenticateDevice(
        deviceId,
        opt::nullopt,
        opt::nullopt,
        vars,
        loginSucceededCallback,
        loginFailedCallback);

访问会话变量:

1
string deviceOs = session->getVariable("DeviceOS");

会话生命周期 #

Nakama 会话在您的服务器配置中设定的时间之后过期。使不活动会话过期是良好的安全做法。

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

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

保存令牌以便日后使用:

1
2
string authToken = session->getAuthToken();
string refreshToken = session->getRefreshToken();

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

1
session = restoreSession(authToken, refreshToken);

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Check whether a session has expired or is close to expiry.
if (session->isExpired() || session->isExpired(time(0) + 24\*60\*60))
{
    auto refreshSuccessCallback = [](NSessionPtr session)
    {
        cout << "Session successfully refreshed" << endl;
    };
    
    auto refreshErrorCallback = [](const NError& error)
    {
        // Couldn't refresh the session so reauthenticate.
        // client->authenticateDevice(...)
    };
    
    // Refresh the existing session
    client->authenticateRefresh(session, refreshSuccessCallback, refreshErrorCallback);
}

用户账户 #

Nakama 用户账户存储Nakama定义的用户信息和自定义的开发者元数据。

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

Sagi-shi player profile screen
Player profile

获取用户账户 #

通过经身份验证的会话可以访问Nakama的许多功能,例如获取用户帐户

获取Sagi-shi玩家的完整用户账户,包括基本用户信息 和用户ID:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
auto successCallback = [](const NAccount& account)
{
    string username = account.user.username;
    string avatarUrl = account.user.avatarUrl;
    string userId = account.user.id;
};

auto errorCallback = [](const NError& error)
{
    cout << "Failed to get user account: " << error.message << endl;
};

client->getAccount(session, successCallback, errorCallback);

更新用户账户 #

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
auto successCallback = []()
{
    cout << "Account successfully updated" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error updating account: " << error.message << endl;
};

client->updateAccount(
        session,
        newUsername,
        newDisplayName, 
        newAvatarYrl,
        newLangTag,
        newLocation,
        newTimezone,
        successCallback,
        errorCallback);

获取用户 #

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

Sagi-shi在结合Nakama的其他功能时,通过这种方法显示用户的资料:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
auto successCallback = [](const NUsers& users)
{
    cout << "Successfully retrieved " << users.users.size() << " users." << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error retrieving users: " << error.message << endl;
};

vector<string> userIds = { "<AnotherUserId" };
client->getUsers(session, userIds, {}, {}, successCallback, errorCallback);

存储元数据 #

Nakama用户元数据允许开发人员使用公开的用户字段扩展用户账户。

仅可在服务器上更新用户元数据。示例请见更新用户元数据配方。

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

读取元数据 #

定义描述元数据的分类,并对JSON元数据进行语法解析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
auto successCallback = [](const NAccount& account)
{
    struct metadata {
        string title;
        string hat;
        string skin;
    };

    // Parse the account user metadata (using nlohmann/json library).
    json j = json::parse(account.user.metadata);
    metadata m {
        j["title"].get<string>(),
        j["hat"].get<string>(),
        j["skin"].get<string>()
    };

    cout << "Title: " << m.title << endl;
    cout << "Hat: " << m.hat << endl;
    cout << "Skin: " << m.title << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error retrieving metadata: " << error.message << endl;
};

client->getAccount(session, successCallback, errorCallback);

钱包 #

Nakama用户钱包可以将多种数字货币存储为字符串/整数的键/值对。

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

访问钱包 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [](const NAccount& account)
{
    // Parse the account user wallet (using nlohmann/json library).
    json j = json::parse(account.wallet);

    for (auto item = j.begin(); item != j.end(); item++)
    {
        cout << item.key() << ": " << item.value() << endl;
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error retrieving wallet: " << error.message << endl;
};

client->getAccount(session, successCallback, errorCallback);

更新钱包 #

仅可在服务器上更新钱包。示例请见用户账户虚拟钱包文档。

存储引擎 #

Nakama存储引擎是为您的游戏而设的分布式、可扩展的基于文件的存储解决方案。

通过存储引擎,您可以更好地控制数据在集合中的访问方式结构

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

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

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

Sagi-shi player items screen
Player items

读取存储对象 #

定义描述存储对象的类别,并使用集合名称、键和用户id创建新的存储对象id。最后,读取存储对象并对JSON数据进行语法解析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
struct hatsStorageObject
{
    vector<string> hats;
};

auto successCallback = [](const NStorageObjects& storageObjects)
{
    if (storageObjects.size() > 0)
    {
        NStorageObject storageObject = storageObjects[0];
        json j = json::parse(storageObject.value);

        hatsStorageObject h {
            j["hats"].get<vector<string>>()
        };

        cout << "Unlocked hats: " << endl;
        for (string hat : h.hats)
        {
            cout << hat << endl;
        }
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error reading storage object: " << error.message << endl;
};

vector<NReadStorageObjectId> objectIds = {
        {
            collection: "Unlocks",
            key: "Hats",
            userId: session->getUserId()
        }
};

client->readStorageObjects(session, objectIds, successCallback, errorCallback);

要读取其他玩家的公开存储对象,应该使用他们的UserId。注意,玩家仅可读取自己拥有的存储对象或公开的存储对象(PermissionRead值为2)。

写入存储对象 #

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

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

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

使用集合名称、键和JSON编码的数据创建写入存储对象。最后,将存储对象写入存储引擎:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
auto successCallback = [](const NStorageObjectAcks& storageObjectAcks)
{
    cout << "Success writing storage object" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error writing storage object: " << error.message << endl;
};

hatsStorageObject favoriteHats {
    { "cowboy", "alien" }
};

json j;
j["hats"] = favoriteHats.hats;

NStorageObjectWrite writeObject {
    collection: "favorites",
    key: "hats",
    value: j.dump(),
    permissionRead: NStoragePermissionRead::OWNER_READ, // Only the server and owner can read
    permissionWrite: NStoragePermissionWrite::OWNER_WRITE // The server and owner can write
};

client->writeStorageObjects(session, { writeObject }, successCallback, errorCallback);

您也可以传递多个写入对象:

1
client->writeStorageObjects(session, { writeObject1, writeObject2, writeObject3 }, successCallback, errorCallback);

条件写入 #

存储引擎条件写入确保仅当对象在您访问后未改变时才会发生写入操作。

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

要执行有条件写入,应该使用最新的对象版本向写入存储对象添加版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Assuming we already have a storage object (storageObject).
NStorageObjectWrite writeObject {
    collection: "favorites",
    key: "hats",
    value: "<NewJsonValue>",
    permissionRead: NStoragePermissionRead::OWNER_READ, // Only the server and owner can read
    permissionWrite: NStoragePermissionWrite::OWNER_WRITE, // The server and owner can write
    version: storageObject.version
};

client->writeStorageObjects(session, { writeObject }, successCallback, errorCallback);

列出存储对象 #

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
auto successCallback = [](NStorageObjectListPtr storageObjectList)
{
    for (NStorageObject storageObject : storageObjectList->objects)
    {
        json j = json::parse(storageObject.value);

        if (storageObject.key == "Titles")
        {
            titlesStorageObject o {
                j["titles"].get<vector<string>>()
            };
            // Display the unlocked titles
        }
        else if (storageObject.key == "Hats")
        {
            hatsStorageObject o {
                    j["hats"].get<vector<string>>()
            };
            // Display the unlocked hats
        }
        else if (storageObject.key == "Skins")
        {
            skinsStorageObject o {
                    j["skins"].get<vector<string>>()
            };
            // Display the unlocked skins
        }
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error reading storage objects: " << error.message << endl;
};

int limit = 3;
string cursor = "";
client->listUsersStorageObjects(session, "Unlocks", session->getUserId(), limit, cursor, successCallback, errorCallback);

分页结果 #

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

例如:

  • 如果游标的值为5,您将从第五个对象开始获取结果。
  • 如果游标的值为null,您将从第一个对象开始获取结果。
1
client->listUsersStorageObjects(session, "Unlocks", session->getUserId(), limit, storageObjectList->cursor, successCallback, errorCallback);

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

可以在服务器上保护Nakama存储引擎操作,从而保护不应被玩家修改的数据(即 游戏解锁或进度)。参见经授权写入存储引擎配方。

远程程序调用 #

Nakama服务器允许开发人员写入自定义逻辑,并将其作为RPC向客户端公开。

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

创建服务器逻辑 #

关于创建远程过程检查玩家在装备设备之前是否拥有设备的示例,参见经授权处理玩家设备配方。

客户端RPC #

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
auto successCallback = [](const NRpc& rpc)
{
    cout << "New hat equipped successfully" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error calling RPC " << error.message << endl;
};

json j;
j["item"] = "cowboy";
string payload = j.dump();
client->rpc(session, "EquipHat", payload, successCallback, errorCallback);

套接字RPC #

需要与Nakama实时功能交互时,还可以通过套接字调用Nakama远程过程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = [](const NRpc& rpc)
{
    cout << "Successfully called RPC" << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error calling RPC " << error.message << endl;
};

rtClient->rpc("<RpcId>", "<PayloadString>", successCallback, errorCallback);

好友 #

Nakama好友提供完整的社交图谱系统来管理玩家之间的好友关系。

Sagi-shi允许玩家添加好友,管理好友关系,组队玩游戏。

Sagi-shi Friends screen
Friends screen

添加好友 #

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
auto successCallback = []()
{
    cout << "Successfully added friends" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error adding friends " << error.message << endl;
};

client->addFriends(session, {}, { "AlwaysTheImposter21", "SneakyBoi" }, successCallback, errorCallback);
client->addFriends(session, { "<SomeUserId>", "<AnotherUserId>" }, {}, successCallback, errorCallback);

好友关系的状态 #

在Nakama中,好友关系分为以下几种状态:

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

列出好友 #

Nakama允许开发人员按好友关系的状态列出玩家的好友。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [](NFriendListPtr friendList)
{
    for(NFriend f : friendList->friends)
    {
        cout << "ID: " << f.user.id << endl;
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error retrieving friends " << error.message << endl;
};

int limit = 20; // Limit is capped at 1000
NFriend::State friendshipState = NFriend::State::FRIEND;
string cursor = "";
client->listFriends(session, limit, friendshipState, cursor, successCallback, errorCallback);

接受好友请求 #

在Nakama中接受好友请求时,玩家会添加双向好友关系

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [client, session](NFriendListPtr friendList)
{
    for(NFriend f : friendList->friends)
    {
        client->addFriends(session, { f.user.id }, {}, nullptr, nullptr);
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error retrieving friends " << error.message << endl;
};

int limit = 1000;
NFriend::State friendshipState = NFriend::State::INVITE_RECEIVED;
string cursor = "";
client->listFriends(session, limit, friendshipState, cursor, successCallback, errorCallback);

删除好友 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
auto successCallback = []()
{
    cout << "Successfully deleted friends" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error deleting friends " << error.message << endl;
};

// Delete friends by User ID.
client->deleteFriends(session, { "<SomeUserId>", "<AnotherUserId>" }, {}, successCallback, errorCallback);

// Delete friends by Username.
client->deleteFriends(session, {}, { "<SomeUsername>", "<AnotherUsername>" }, successCallback, errorCallback);

屏蔽用户 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
auto successCallback = []()
{
    cout << "Successfully blocked friends" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error blocking friends " << error.message << endl;
};

// Block friends by User ID.
client->blockFriends(session, { "<SomeUserId>", "<AnotherUserId>" }, {}, successCallback, errorCallback);

// Block friends by Username.
client->blockFriends(session, {}, { "<SomeUsername>", "<AnotherUsername>" }, successCallback, errorCallback);

状态与显示 #

Nakama状态是一种实时状态与显示的服务,允许用户设置显示的在线状态,更新状态消息并关注其他用户的更新。

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

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

Sagi-shi status update screen
Updating player status

关注用户 #

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

关注用户的方法也会返回至当前的在线用户(即显示的在线状态)及其状态。

Sagi-shi关注玩家的好友,并当好友在线时通知玩家:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Subscribe to the Status event.
NRtDefaultClientListener listener;
rtClient->setListener(&listener);

listener.setStatusPresenceCallback([](const NStatusPresenceEvent& statusPresenceEvent) {
    for (NUserPresence presence : statusPresenceEvent.joins)
    {
        cout << presence.username << " is online with status: " << presence.status << endl;
    }

    for (NUserPresence presence : statusPresenceEvent.leaves)
    {
        cout << presence.username << "went offline" << endl;
    }
});

// Follow mutual friends and get the initial Status of any that are currently online.
auto successCallback = [&rtClient](NFriendListPtr friendList)
{
    auto followSuccessCallback = [](const NStatus& status)
    {
        for (NUserPresence presence : status.presences)
        {
            cout << presence.username << " is online with status: " << presence.status << endl;
        }
    };
    
    auto followErrorCallback = [](const NRtError& error)
    {
        cout << "Error following friends: " << error.message << endl;
    };
    
    for (NFriend f : friendList->friends)
    {
        rtClient->followUsers({ f.user.id }, followSuccessCallback, followErrorCallback);
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error listing friends: " << error.message << endl;
};

// Follow mutual friends and get the initial Status of any that are currently online.
client->listFriends(session, 1000, NFriend::State::FRIEND, "", successCallback, errorCallback);

取消关注用户 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = []()
{
    cout << "Successfully unfollowed users" << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error unfollowing users: " << error.message << endl;
};

rtClient->unfollowUsers({ "<UserId>" }, successCallback, errorCallback);

更新玩家状态 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = []()
{
    cout << "Successfully updated status" << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error updating status: " << error.message << endl;
};

rtClient->updateStatus("Viewing the Main Menu", successCallback, errorCallback);

群组 #

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

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

Sagi-shi groups screen
Groups list screen

创建群组 #

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
auto successCallback = [](const NGroup& group)
{
    cout << "Successfully created group: " << group.id << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error creating group: " << error.message << endl;
};

string name = "Imposters R Us";
string description = "A group for people who love playing the imposter.";
string avatarUrl = "";
string langTag = "";
bool open = true; // public group
int maxSize = 100;

client->createGroup(session, name, description, avatarUrl, langTag, open, maxSize, successCallback, errorCallback);

更新群组的可见性 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
auto successCallback = []()
{
    cout << "Successfully updated group" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error creating group: " << error.message << endl;
};

auto name = opt::nullopt;
auto description = opt::nullopt;
auto avatarUrl = opt::nullopt;
auto langTag = opt::nullopt;
bool open = false;
client->updateGroup(session, "<GroupId>", name, description, avatarUrl, langTag, open, successCallback, errorCallback);

更新群组规模 #

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

参见更新群组规模配方示例和群组服务器功能参考,以进一步了解在服务器上更新群组的信息。

Sagi-shi group edit screen
Sagi-shi group edit

列出和过滤群组 #

群组可以像Nakama的其他资源一样列出,也可以使用通配符群组名称进行过滤

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
auto successCallback = [&session](NGroupListPtr groupList)
{
    for (NGroup group : groupList->groups)
    {
        cout << group.name << ": " << (group.open ? "Public" : "Private") << endl;
    }
    
    // Get the next page of results using groupList->cursor.
};

auto errorCallback = [](const NError& error)
{
    cout << "Error listing groups: " << error.message << endl;
};

int limit = 20;
string cursor = "";
client->listGroups(session, "imposter%", limit, cursor, successCallback, errorCallback);

删除群组 #

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

开发人员可以完全禁用此功能,请在Guarding API指南中查看如何保护Nakama各种API的示例。

Sagi-shi玩家可以删除自己担任超级管理员的群组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = []()
{
    cout << "Successfully deleted group" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error deleting group: " << error.message << endl;
};

client->deleteGroup(session, "<GroupId>", successCallback, errorCallback);

群组元数据 #

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

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

仅可在服务器上更新群组元数据。示例请见更新群组元数据配方。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [](const NRpc& rpc)
{
    cout << "Successfully updated group metadata" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error updating group metadata: " << error.message << endl;
};

json j;
j["groupId"] = "<GroupId>";
j["interests"] = { "Deception", "Sabotage", "Cute Furry Bunnies" };
j["activeTimes"] = { "9am-2pm Weekdays", "9am-10pm Weekends" };
j["languages"] = { "English", "German" };

client->rpc(session, "UpdateGroupMetadata", j.dump(), successCallback, errorCallback);

群组成员资格状态 #

在Nakama中,成员资格可以有以下几种状态:

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

加入群组 #

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = []()
{
    cout << "Successfully joined group" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error joining group: " << error.message << endl;
};

client->joinGroup(session, "<GroupId>", successCallback, errorCallback);

列出用户的群组 #

Sagi-shi玩家可以列出其所在的群组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [](NUserGroupListPtr userGroupList)
{
    for (NUserGroup userGroup : userGroupList->userGroups)
    {
        cout << userGroup.group.name << ": " << static_cast<int>(userGroup.state) << endl;
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error listing user groups: " << error.message << endl;
};

auto limit = opt::nullopt;
auto state = opt::nullopt;
string cursor = "";
client->listUserGroups(session, limit, state, cursor, successCallback, errorCallback);

列出成员 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [](NGroupUserListPtr groupUserList)
{
    for (NGroupUser groupUser : groupUserList->groupUsers)
    {
        cout << groupUser.user.id << ": " << static_cast<int>(groupUser.state) << endl;
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error listing group users: " << error.message << endl;
};

auto limit = opt::nullopt;
auto state = opt::nullopt;
string cursor = "";
client->listGroupUsers(session, "", limit, state, cursor, successCallback, errorCallback);

接受加入请求 #

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [&client](NGroupUserListPtr groupUserList)
{
    for (NGroupUser groupUser : groupUserList->groupUsers)
    {
        client->addGroupUsers(session, "<GroupId>", { groupUser.user.id }, nullptr, nullptr);
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error listing group users: " << error.message << endl;
};

auto limit = opt::nullopt;
NUserGroupState state = NUserGroupState::JOIN_REQUEST;
string cursor = "";
client->listGroupUsers(session, "", limit, state, cursor, successCallback, errorCallback);

升级成员 #

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

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

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

  • 成员可以升级为管理员
  • 管理员可以升级为超级管理员
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = []()
{
    cout << "Successfully promoted group users" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error promoting group users: " << error.message << endl;
};

client->promoteGroupUsers(session, "<GroupId>", { "<UserId>" }, successCallback, errorCallback);

降级成员 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = []()
{
    cout << "Successfully demoted group users" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error demoting group users: " << error.message << endl;
};

client->demoteGroupUsers(session, "<GroupId>", { "<UserId>" }, successCallback, errorCallback);

踢出成员 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = []()
{
    cout << "Successfully kicked group users" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error kicking group users: " << error.message << endl;
};

client->kickGroupUsers(session, "<GroupId>", { "<UserId>" }, successCallback, errorCallback);

退出群组 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = []()
{
    cout << "Successfully left group" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error leaving group: " << error.message << endl;
};

client->leaveGroup(session, "<GroupId>", successCallback, errorCallback);

聊天 #

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

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

Sagi-shi chat screen
Sagi-shi Chat

加入动态聊天室 #

Sagi-shi比赛设有非持久性聊天室,供玩家交流:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
auto successCallback = [](NChannelPtr channel)
{
    cout << "Connected to dynamic room channel: " << channel->id << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error connecting to channel: " << error.message << endl;
};

string roomName = "<MatchId>";
bool persistence = false;
bool hidden = false;
rtClient->joinChat(roomName, NChannelType::ROOM, persistence, hidden, successCallback, errorCallback);

加入群组聊天 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
auto successCallback = [](NChannelPtr channel)
{
    cout << "Connected to group channel: " << channel->id << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error connecting to channel: " << error.message << endl;
};

string groupId = "<GroupId>";
bool persistence = false;
bool hidden = false;
rtClient->joinChat(groupId, NChannelType::GROUP, persistence, hidden, successCallback, errorCallback);

加入直接聊天 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
auto successCallback = [](NChannelPtr channel)
{
    cout << "Connected to direct message channel: " << channel->id << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error connecting to channel: " << error.message << endl;
};

string userId = "<UserId>";
bool persistence = true;
bool hidden = false;
rtClient->joinChat(userId, NChannelType::DIRECT_MESSAGE, persistence, hidden, successCallback, errorCallback);

发送消息 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
auto successCallback = [](const NChannelMessageAck& messageAck)
{
    cout << "Successfully sent message: " << messageAck.messageId << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error sending message: " << error.message << endl;
};

string channelId = "<ChannelId>";

json j;
j["message"] = "I think Red is the imposter!";

rtClient->writeChatMessage(channelId, j.dump(), successCallback, errorCallback);

json j2;
j2["emote"] = "point";
j2["emoteTarget"] = "<RedPlayerUserId>";

rtClient->writeChatMessage(channelId, j2.dump(), successCallback, errorCallback);

列出消息历史 #

消息列表需要一个参数来表示接收消息的顺序为从最早到最新(向前)或从最新到最早。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
auto successCallback = [](NChannelMessageListPtr messageList)
{
    for (NChannelMessage m : messageList->messages)
    {
        cout << m.username << ": " << m.content << endl;
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error retrieving messages: " << error.message << endl;
};

int limit = 100;
bool forward = true;
string groupId = "<GroupId>";
string cursor = "";
client->listChannelMessages(session, groupId, limit, cursor, forward, successCallback, errorCallback);

聊天还带有用于获取最新消息的可缓存游标。阅读列表通知文档中关于可缓存游标的更多信息。

1
2
string cursor = messageList->nextCursor;
client->listChannelMessages(session, groupId, limit, cursor, forward, successCallback, errorCallback);

更新消息 #

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

例如玩家发送以下消息:

1
2
3
4
5
string channelId = "<ChannelId>";
json j;
j["message"] = "I think Red is the imposter!";

rtClient->writeChatMessage(channelId, j.dump(), successCallback, errorCallback);

然后改玩家迅速编辑消息来迷惑他人:

1
2
3
4
5
6
auto successCallback = [&rtClient](const NChannelMessageAck& messageAck)
{
    json j;
    j["message"] = "I think BLUE is the imposter!";
    rtClient->updateChatMessage(messageAck.channelId, messageAck.messageId, j.dump(), nullptr, nullptr);
};

比赛 #

Nakama支持服务器授权服务器中继的多人比赛。

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

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

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

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

创建比赛 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
auto errorRtCallback = [](const NRtError& error)
{
    cout << "Error: " << error.message << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

auto listFriendsSuccessCallback = [&rtClient, &errorRtCallback, &match](NFriendListPtr friendList)
{
    for (NFriend f : friendList->friends)
    {
        if (!f.user.online)
        {
            continue;
        }
        
        auto joinChannelSuccessCallback = [&rtClient, &f, &match](NChannelPtr channel)
        {
            json j;
            j["message"] = "Hey " + f.user.username + ", join me for a match!";
            j["matchId"] = match.matchId;
            
            rtClient->writeChatMessage(channel->id, j.dump(), nullptr, nullptr);
        };
        
        bool persistence = false;
        bool hidden = false;
        rtClient->joinChat(f.user.id, NChannelType::DIRECT_MESSAGE, persistence, hidden, joinChannelSuccessCallback, errorRtCallback);
    }
};

int limit = 100;
NFriend::State state = NFriend::State::FRIEND;
string cursor = "";
client->listFriends(session, limit, state, cursor, listFriendsSuccessCallback, errorCallback);

加入比赛 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
auto successCallback = [](const NMatch& match)
{
    cout << "Successfully joined match: " << match.matchId << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error: " << error.message << endl;
};

string matchId = "<MatchId>";
NStringMap metadata = {
        { "Region", "EU"}
};

rtClient->joinMatch(matchId, metadata, successCallback, errorCallback);

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
listener.setMatchmakerMatchedCallback([&rtClient](NMatchmakerMatchedPtr matchmakerMatched) {
    auto successCallback = [](const NMatch match)
    {
        cout << "Successfully joined match: " << match.matchId << endl;
    };
    
    auto errorCallback = [](const NRtError& error)
    {
        cout << "Error: " << error.message << endl;
    };
    
    rtClient->joinMatch(matchmakerMatched->matchId, {}, successCallback, errorCallback);
});

auto successCallback = [](const NMatchmakerTicket& matchmakerTicket)
{
    cout << "Successfully joined matchmaker: " << matchmakerTicket.ticket << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error: " << error.message << endl;
};

int minPlayers = 2;
int maxPlayers = 10;
string query = "";
NStringMap stringProperties = {};
NStringDoubleMap numericProperties = {};
auto countMultiple = opt::nullopt;
rtClient->addMatchmaker(minPlayers, maxPlayers, query, stringProperties, numericProperties, countMultiple, successCallback, errorCallback);

按玩家状态加入比赛

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

1
2
3
4
5
json j;
j["status"] = "Playing a match";
j["matchId"] = "<MatchID>";

rtClient->updateStatus(j.dump(), successCallback, errorCallback);

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
listener.setStatusPresenceCallback([&rtClient](const NStatusPresenceEvent& statusPresence) {
    for (NUserPresence presence : statusPresence.joins)
    {
        auto successCallback = [](const NMatch& match)
        {
            cout << "Successfully joined match: " << match.matchId << endl;
        };
        
        auto errorCallback = [](const NRtError& error)
        {
            cout << "Error: " << error.message << endl;
        };
        
        json j = json::parse(presence.status);
        if (j.contains("matchId"))
        {
            NStringMap metadata = {};
            rtClient->joinMatch(j["matchId"], metadata, successCallback, errorCallback);
        }
    }
});

列出比赛 #

比赛列表需要一些标准来过滤比赛,包括玩家人数、匹配标签和可以进行更复杂的搜索查询的选项。

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

之后,Sagi-shi可以列出正在等待更多玩家的比赛:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
auto successCallback = [](NMatchListPtr matchList)
{
    for (NMatch m : matchList->matches)
    {
        cout << m.matchId << ": " << m.size << "/10 players" << endl;
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

int minPlayers = 2;
int maxPlayers = 10;
int limit = 10;
bool authoritative = true;
string label = "";
client->listMatches(session, minPlayers, maxPlayers, limit, label, authoritative, successCallback, errorCallback);

找到标签为"AnExactMatchLabel"的比赛:

1
string label = "AnExactMatchLabel";

生成玩家 #

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

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

1
2
3
4
5
6
7
8
9
// Assuming a GameObject type
//class GameObject { };

map<string, GameObject*> players = {};
for (NUserPresence userPresence : match.presences)
{
    GameObject* gameObject = spawnPlayer(); // Instantiate player object
    players.insert({ userPresence.sessionId, gameObject });
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
listener.setMatchPresenceCallback([&players](const NMatchPresenceEvent matchPresence) {
    // For each player that has joined in this event...
    for (NUserPresence presence : matchPresence.joins)
    {
        // Spawn a player for this presence and store it in the dictionary by session id.
        GameObject* gameObject = spawnPlayer();
        players.insert({ presence.sessionId, gameObject });
    }

    // For each player that has left in this event...
    for (NUserPresence presence : matchPresence.leaves)
    {
        // Remove the player from the game if they've been spawned
        if (players.count(presence.sessionId) > 0)
        {
            players.erase(presence.sessionId);
        }
    }
});

发送比赛状态 #

Nakama拥有实时网络,可以在玩家移动和与游戏世界互动时发送接收比赛状态。

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

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

Sagi-shi使用的操作代码示例:

  • 1:玩家位置
  • 2:玩家调用投票

发送玩家位置

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

1
2
3
4
5
struct positionState {
    float x;
    float y;
    float z;
};

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

1
2
3
4
5
6
7
8
9
// Assuming a position variable
json j;
j["x"] = position.x;
j["y"] = position.y;
j["z"] = position.z;

int opCode = 1;

rtClient->sendMatchData(match.matchId, opCode, j.dump());

作为静态类操作代码

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class OpCodes
{
public:
    static const int POSITION = 1;
    static const int VOTE = 2;
};

// ...

rtClient->sendMatchData(match.matchId, OpCodes::POSITION, j.dump());

接收比赛状态 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
listener.setMatchDataCallback([&players](const NMatchData& matchData) {
    switch (matchData.opCode)
    {
        case OpCodes::POSITION:
        {
            // Get the updated position data
            json j = json::parse(matchData.data);

            positionState position {
                    j["x"].get<float>(),
                    j["y"].get<float>(),
                    j["z"].get<float>()
            };

            // Update the GameObject associated with that player.
            if (players.count(matchData.presence.sessionId) > 0)
            {
                // 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[matchData.presence.sessionId].position = new Vector3(position.x, position.y, position.z);
            }
        }
        default:
            cout << "Unsupported opcode";
            break;
    }
});

配对程序 #

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

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

添加配对程序 #

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

Sagi-shi允许玩家加入匹配池,让服务器将这些玩家与其他玩家匹配:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [](const NMatchmakerTicket& matchmakerTicket)
{
    cout << "Received a matchmaker ticket: " << matchmakerTicket.ticket << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error: " << error.message << endl;
};

int minPlayers = 2;
int maxPlayers = 10;
string query = "+skill:>100 mode:sabotage";
NStringMap stringProperties = { { "mode", "sabotage" }};
NStringDoubleMap numericProperties = { { "skill", 125 }};
auto countMultiple = opt::nullopt;
rtClient->addMatchmaker(minPlayers, maxPlayers, query, stringProperties, numericProperties, countMultiple, successCallback, errorCallback);

派对 #

Nakama派对是一个实时系统,允许玩家组成短暂的派对,这些派对在所有玩家断开连接后不会持续存在。

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

创建派对 #

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

Sagi-shi使用封闭式派对,最多可以有4名玩家:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
auto successCallback = [](const NParty& party)
{
    cout << "Successfully created party: " << party.id << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error: " << error.message << endl;
};

bool open = false;
int maxPlayers = 4;
rtClient->createParty(open, maxPlayers, successCallback, errorCallback);

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
auto successCallback = [&rtClient](NFriendListPtr friendList)
{
    for (NFriend f : friendList->friends)
    {
        if (!f.user.online)
        {
            continue;
        }

        auto joinChatSuccessCallback = [&rtClient, &f, &party](NChannelPtr channel)
        {
            json j;
            j["message"] = "Hey" + f.user.username + ", wanna join the party?!";
            j["partyId"] = party.id;

            rtClient->writeChatMessage(channel->id, j.dump(), nullptr, nullptr);
        };

        auto joinChatErrorCallback = [](const NRtError& error)
        {
            cout << "Error: " << error.message << endl;
        };

        auto persistence = true;
        auto hidden = false;
        rtClient->joinChat(f.user.id, NChannelType::DIRECT_MESSAGE, persistence, hidden, joinChatSuccessCallback, joinChatErrorCallback);
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

int limit = 100;
NFriend::State state = NFriend::State::FRIEND;
string cursor = "";
client->listFriends(session, limit, state, cursor, successCallback, errorCallback);

加入派对 #

Safi-shi玩家可以通过查看聊天消息中的派对id来加入派对:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
listener.setChannelMessageCallback([&rtClient](const NChannelMessage& channelMessage) {
    auto successCallback = []()
    {
        cout << "Successfully joined party" << endl;
    };

    auto errorCallback = [](const NRtError& error)
    {
        cout << "Error: " << error.message << endl;
    };

    json j = json::parse(channelMessage.content);
    if (j.contains("partyId"))
    {
        rtClient->joinParty(j["partyId"], successCallback, errorCallback);
    }
});

升级成员 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = []()
{
    cout << "Successfully promoted party member" << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error: " << error.message << endl;
};

for (NUserPresence presence : party.presences)
{
    if (presence.sessionId != party.leader.sessionId)
    {
        rtClient->promotePartyMember(party.id, presence, successCallback, errorCallback);
    }
}

退出派对 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = []()
{
    cout << "Successfully left party" << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error: " << error.message << endl;
};

rtClient->leaveParty(party.id, successCallback, errorCallback);

派对配对 #

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
listener.setMatchmakerMatchedCallback([&rtClient](NMatchmakerMatchedPtr matchmakerMatched) {
    auto successCallback = [](const NMatch& match)
    {
        cout << "Successfully joined match: " << match.matchId << endl;
    };

    auto errorCallback = [](const NRtError& error)
    {
        cout << "Error: " << error.message << endl;
    };
    
    rtClient->joinMatch(matchmakerMatched->matchId, {}, successCallback, errorCallback);
});

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [](const NPartyMatchmakerTicket& partyMatchmakerTicket)
{
    cout << "Successfully joined matchmaker as party: " << partyMatchmakerTicket.ticket << endl;
};

auto errorCallback = [](const NRtError& error)
{
    cout << "Error: " << error.message << endl;
};

int minPlayers = 2;
int maxPlayers = 10;
string query = "";
NStringMap stringProperties = {};
NStringDoubleMap numericProperties = {};
auto countMultiple = opt::nullopt;
rtClient->addMatchmakerParty(party.id, query, minPlayers, maxPlayers, stringProperties, numericProperties, countMultiple, successCallback, errorCallback);

排行榜 #

Nakama排行榜为您的游戏引入竞争因素,提高了玩家的参与度和保留率。

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

Sagi-shi leaderboard screen
Sagi-shi Leaderboard

创建排行榜 #

必须在服务器上创建排行榜,请在排行榜文档中查看有关创建排行榜的详细信息。

提交分数 #

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
auto successCallback = [](const NLeaderboardRecord& leaderboardRecord)
{
    cout << "Successfully submitted leaderboard record" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

int score = 1;
int subscore = 0;
json j;
j["map"] = "space_station";
client->writeLeaderboardRecord(session, "weekly_imposter_wins", score, subscore, j.dump(), successCallback, errorCallback);

列出最高记录 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
auto successCallback = [](NLeaderboardRecordListPtr leaderboardRecordList)
{
    for (NLeaderboardRecord record : leaderboardRecordList->records)
    {
        cout << record.ownerId << ":" << record.score << endl;
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

int limit = 20;
string leaderboardName = "weekly_imposter_wins";
vector<string> ownerIds = {};
string cursor = "";
client->listLeaderboardRecords(session, leaderboardName, ownerIds, limit, cursor, successCallback, errorCallback);

列出用户周围的记录

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [](NLeaderboardRecordListPtr leaderboardRecordList)
{
    for (NLeaderboardRecord record : leaderboardRecordList->records)
    {
        cout << record.ownerId << ":" << record.score << endl;
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

int limit = 20;
string leaderboardName = "weekly_imposter_wins";
string cursor = "";
client->listLeaderboardRecordsAroundOwner(session, leaderboardName, session->getUserId(), limit, successCallback, errorCallback);

列出一系列用户的记录

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
auto successCallback = [&client, &session](NFriendListPtr friendList)
{
    vector<string> friendIds = {};

    for (NFriend f : friendList->friends)
    {
        friendIds.push_back(f.user.id);
    }

    auto successCallback = [](NLeaderboardRecordListPtr leaderboardRecordList)
    {
        for (NLeaderboardRecord record : leaderboardRecordList->records)
        {
            cout << record.username << " scored " << record.score << endl;
        }
    };

    auto errorCallback = [](const NError& error)
    {
        cout << "Error: " << error.message << endl;
    };

    int limit = 20;
    string leaderboardName = "weekly_imposter_wins";
    string cursor = "";
    client->listLeaderboardRecords(session, leaderboardName, friendIds, limit, cursor, successCallback, errorCallback);
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

int limit = 100;
NFriend::State state = NFriend::State::FRIEND;
string cursor = "";
client->listFriends(session, limit, state, cursor, successCallback, errorCallback);

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
auto successCallback = [&client, &session](NGroupUserListPtr groupUserList)
{
    vector<string> memberIds = {};

    for (NGroupUser g : groupUserList->groupUsers)
    {
        memberIds.push_back(g.user.id);
    }

    auto successCallback = [](NLeaderboardRecordListPtr leaderboardRecordList)
    {
        for (NLeaderboardRecord record : leaderboardRecordList->records)
        {
            cout << record.username << " scored " << record.score << endl;
        }
    };

    auto errorCallback = [](const NError& error)
    {
        cout << "Error: " << error.message << endl;
    };

    int limit = 20;
    string leaderboardName = "weekly_imposter_wins";
    string cursor = "";
    client->listLeaderboardRecords(session, leaderboardName, memberIds, limit, cursor, successCallback, errorCallback);
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

int limit = 100;
auto state = opt::nullopt;
string cursor = "";
client->listGroupUsers(session, "<GroupId>", limit, state, cursor, successCallback, errorCallback);

删除记录 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = []()
{
    cout << "Successfully deleted leaderboard record" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

client->deleteLeaderboardRecord(session, "<LeaderboardId>", successCallback, errorCallback);

锦标赛 #

Nakama锦标赛是玩家争夺奖励的短暂比赛。

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

Sagi-shi tournaments screen
Sagi-shi Tournaments

创建锦标赛 #

必须在服务器上创建锦标赛,请在锦标赛文档中查看有关创建锦标赛的详细信息。

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

加入锦标赛 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = []()
{
    cout << "Successfully joined tournament" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

client->joinTournament(session, "<TournamentId>", successCallback, errorCallback);

列出锦标赛 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
auto successCallback = [](NTournamentListPtr tournamentList)
{
    for (NTournament t : tournamentList->tournaments)
    {
        cout << t.id << ":" << t.title << endl;
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

int categoryStart = 1;
int categoryEnd = 2;
auto startTime = opt::nullopt;
auto endTime = opt::nullopt;
int limit = 100;
string cursor = "";
client->listTournaments(session, categoryStart, categoryEnd, startTime, endTime, limit, cursor, successCallback, errorCallback);

为了获得较好的过滤效果,应该通过范围而非单个数字来过滤类别。您可以利用这一点构建您的类别(例如,所有的PVE锦标赛属于1XX范围,所有的PVP锦标赛属于2XX范围)。

列出记录 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
auto successCallback = [](NTournamentRecordListPtr tournamentRecordList)
{
    for (NLeaderboardRecord record : tournamentRecordList->records)
    {
        cout << record.ownerId << ":" << record.score << endl;
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

int limit = 20;
string tournamentName = "weekly_top_detective";
string cursor = "";
vector<string> ownerIds = {};
client->listTournamentRecords(session, tournamentName, limit, cursor, ownerIds, successCallback, errorCallback);

列出用户周围的记录

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [](NTournamentRecordListPtr tournamentRecordList)
{
    for (NLeaderboardRecord record : tournamentRecordList->records)
    {
        cout << record.ownerId << ":" << record.score << endl;
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

int limit = 20;
string tournamentName = "weekly_top_detective";
string cursor = "";
client->listTournamentRecordsAroundOwner(session, tournamentName, session->getUserId(), limit, successCallback, errorCallback);

提交分数 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
auto successCallback = [](const NLeaderboardRecord& leaderboardRecord)
{
    cout << "Successfully submit tournament score" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

string tournamentName = "weekly_top_detective";
int score = 1;
int subscore = 0;
json j;
j["map"] = "space_station";
client->writeTournamentRecord(session, tournamentName, score, subscore, j.dump(), successCallback, errorCallback);

通知 #

游戏服务器可利用Nakama通知向玩家广播实时消息。

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

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

Sagi-shi notification screen
Sagi-shi notifications

接收通知 #

必须通过服务器发送通知。

Nakama使用代码区分通知。0 和以下代码是为Nakama内部构件保留的系统代码

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
listener.setNotificationsCallback([](const NNotificationList& notificationList) {
    const int rewardCode = 100;
    
    for (NNotification n : notificationList.notifications)
    {
        switch (n.code)
        {
            case rewardCode:
                cout << "Congratulations, you won the tournament!" << endl << n.subject << endl << n.content << endl;
                break;
            default:
                cout << "Other notification:" << endl << n.subject << endl << n.content << endl;
                break;
        }
    }
});

列出通知 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
auto successCallback = [](NNotificationListPtr notificationList)
{
    for (NNotification n : notificationList->notifications)
    {
        cout << "Notification:" << endl << n.subject << endl << n.content << endl;
    }
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

int limit = 100;
string cursor = "";
client->listNotifications(session, limit, cursor, successCallback, errorCallback);

分页及可缓存游标

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

1
string cacheableCursor = notificationList->cacheableCursor;

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

1
client->listNotifications(session, limit, cacheableCursor, successCallback, errorCallback);

删除通知 #

Sagi-shi玩家可以在阅读后删除通知:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
auto successCallback = []()
{
    cout << "Successfully deleted notifications" << endl;
};

auto errorCallback = [](const NError& error)
{
    cout << "Error: " << error.message << endl;
};

vector<string> notificationIds = { "<NotificationId>" };
client->deleteNotifications(session, notificationIds, successCallback, errorCallback);