Nakama JavaScript客户端指南 #

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

Sagi-shi gameplay screen
Sagi-shi gameplay

前提条件 #

开始之前,确保您已经:

完整版API文档 #

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

安装 #

以下各项均有此客户端:

如使用的是NPM或Yarn,只需将依赖项添加到package.json文件:

1
2
yarn add "@heroiclabs/nakama-js"
yarn install

安装客户端后,将其导入项目:

1
import {Client} from "@heroiclabs/nakama-js"

在主JavaScript函数中,创建客户端对象

更新 #

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

异步编程 #

JavaScript SDK中可用的Nakama API的许多方法都是异步和非阻塞的。

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

1
await client.authenticateDevice("<deviceId>");

了解async函数await运算符

处理异常 #

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

Sagi-shi中的API调用被一个try块和catch语句包围,以得当处理错误:

1
2
3
4
5
6
try {
    await client.authenticateDevice("<deviceId>");
}
catch (err) {
    console.log("Error authenticating device: %o:%o", err.statusCode, err.message);
}

新手入门 #

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

Nakama客户端 #

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

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

1
var client = new nakamajs.Client("defaultkey", "127.0.0.1", 7350);

配置请求超时时长 #

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

1
client.timeout = 10000;

Nakama套接字 #

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

从客户端创建套接字:

1
2
3
4
const socket = client.createSocket();

var appearOnline = true;
await socket.connect(session, appearOnline);

身份验证 #

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
22
23
24
// This import is only required with React Native
var deviceInfo = require('react-native-device-info');

var deviceId = null;
// If the user's device ID is already stored, grab that - alternatively get the System's unique device identifier.
try {
  const value = await AsyncStorage.getItem('@MyApp:deviceKey');
  if (value !== null){
    deviceId = value
  } else {
    deviceId = deviceInfo.getUniqueID();
    // Save the user's device ID so it can be retrieved during a later play session for re-authenticating.
    AsyncStorage.setItem('@MyApp:deviceKey', deviceId).catch(function(error) {
      console.log("An error occurred: %o", error);
    });
  }
} catch (error) {
  console.log("An error occurred: %o", error);
}

// Authenticate with the Nakama server using Device Authentication.
var create = true;
const session = await client.authenticateDevice(deviceId, create, "mycustomusername");
console.info("Successfully authenticated:", session);

Facebook身份验证 #

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

要使用Nakama Facebook身份验证,为Unity(外部)安装Facebook SDK

1
2
3
4
5
6
7
8
9
const oauthToken = "<token>";
const importFriends = true;
try {
    const session = await client.authenticateFacebook(oauthToken, true, "mycustomusername", importFriends);
    console.log("Successfully authenticated:", session);
}
catch(err) {
    console.log("Error authenticating with Facebook: %o", err.message);
}

自定义身份验证 #

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

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

链接身份验证 #

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

链接设备ID身份验证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Acquiring the unique device ID has been shortened for brevity, see previous example.
var deviceId = "<uniqueDeviceId>";

// Link Device Authentication to existing player account.
try {
    await client.linkDevice(session, deviceId);
    console.log("Successfully linked Device ID authentication to existing player account");
}
catch(err) {
    console.log("Error linking Device ID: %o", err.message);
}

链接Facebook身份验证

1
2
3
4
5
6
7
8
9
const oauthToken = "<token>";
const import = true;
try {
    const session = await client.linkFacebook(session, oauthToken, true, import);
    console.log("Successfully linked Facebook authentication to existing player account");
}
catch(err) {
    console.log("Error authenticating with Facebook: %o", err.message);
}

会话变量 #

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

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

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

1
2
3
4
5
6
7
8
const vars = {
  deviceId = localStorage.getItem("deviceId"),
  deviceOs = localStorage.getItem("deviceOs"),
  inviteUserId = "<someUserId>",
  // ...
}

const session = await client.authenticateDevice(deviceId, null, true, vars);

要访问客户端的会话变量,请使用session对象上的vars属性:

1
var deviceOs = session.vars["deviceOs"];

会话生命周期 #

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

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

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

保存令牌以便日后使用:

1
2
var authToken = session.token;
var refreshToken = session.refresh_token;

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

1
session = session.restore(authToken, refreshToken);

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Check whether a session has expired or is close to expiry.
if (session.isexpired || session.isexpired(Date.now + 1) {
    try {
        // Attempt to refresh the existing session.
        session = await client.sessionRefresh(session);
    } catch (error) {
        // Couldn't refresh the session so reauthenticate.
        session = await client.authenticateDevice(deviceId);
        var refreshToken = session.refresh_token;
    }

    var authToken = session.token;
}

自动刷新会话 #

JavaScript客户端库具有一个自动刷新即将到期的会话的功能。

默认启用这个功能,但可以在首次创建 Nakama客户端时使用以下参数进行配置:

  • autoRefreshSession - 指示该功能是否启用的布尔值,默认为true
  • expiredTimespanMs - 会话到期前自动刷新的时间,默认设置为300000(5分钟)

结束会话 #

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

1
await client.sessionLogout(session);

用户账户 #

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

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

Sagi-shi player profile screen
Player profile

获取用户账户 #

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

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

1
2
3
4
5
const account = await client.getAccount(session);
const user = account.user;
var username = user.username;
var avatarUrl = user.avatarUrl;
var userId = user.id;

更新用户账户 #

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

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

1
2
3
4
5
6
7
var newUsername = "NotTheImp0ster";
var newDisplayName = "Innocent Dave";
var newAvatarUrl = "https://example.com/imposter.png";
var newLangTag = "en";
var newLocation = "Edinburgh";
var newTimezone = "BST";
await client.updateAccount(session, newUsername, newDisplayName, newAvatarUrl, newLangTag, newLocation, newTimezone);

获取用户 #

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

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

1
var users = await client.getUsers(session, ["<AnotherUserId>"]);

存储元数据 #

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

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

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

读取元数据 #

获取更新的账户对象,分析JSON元数据:

1
2
3
4
5
6
7
8
9
// Get the updated account object.
var account = await client.getAccount(session);

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

console.log("Title: %o", metadata.title);
console.log("Hat: %o", metadata.hat);
console.log("Skin: %o", metadata.skin);

钱包 #

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

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

访问钱包 #

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

1
2
3
4
5
6
7
var account = await client.getAccount(session);
var wallet = JSON.parse(account.wallet);
var keys = wallet.keys;

keys.forEach(function(currency) {
    console.log("%o: %o", currency, wallet[currency].toString())
});

更新钱包 #

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

验证应用内的购买行为 #

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

示例请见应用程序内购买行为验证文档。

存储引擎 #

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

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

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

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

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

Sagi-shi player items screen
Player items

读取存储对象 #

使用集合名称、键和用户id创建新的存储对象。然后读取存储对象并对JSON数据进行语法分析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var readObjectId = new storageObjectId {
    collection = "Unlocks",
    key = "Hats",
    userId = session.user.id
};

var result = await client.readStorageObjects(session, readObjectId);

if (result.objects.any())
{
    var storageObject = result.objects.first();
    var unlockedHats = JSON.parse(storageObject.value);
    console.log("Unlocked hats: %o", string.join(",", unlockedHats.Hats));
}

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

写入存储对象 #

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var favoriteHats = new {
    hats = ["cowboy", "alien"]
};

var writeObject = new WriteStorageObject {
    collection = "favorites",
    ley = "Hats",
    value = JSON.stringify(favoriteHats),
    permissionRead = 1, // Only the server and owner can read
    permissionWrite = 1 // The server and owner can write
};

await client.writeStorageObjects(session, writeObject);

您也可以将多个对象传递到WriteStorageObjectsAsync方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var writeObjects = {
    new WriteStorageObject {
        //...
    },
    new WriteStorageObject
    {
        // ...
    }
};

await client.writeStorageObjects(session, writeObjects);

条件性写入 #

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Assuming we already have a storage object (storageObject)
var writeObject = new WriteStorageObject {
    collection = storageObject.collection,
    key = storageObject.key,
    value = "<NewJSONValue>",
    permissionWrite = 0,
    permissionRead = 1,
    version = storageObject.version
};

try {
    await client.writeStorageObjects(session, writeObjects);
}
catch (error) {
    console.log(error.message);
}

列出存储对象 #

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var limit = 3;
var cursor = null;
var unlocksObjectList = await client.listStorageObjects(session, "Unlocks", limit, cursor);

unlocksObjectList.objects.forEach(function(unlockStorageObject) {
    switch(unlockStorageObject.key) {
        case "Titles":
            var unlockedTitles = JSON.parse<TitlesStorageObject>(unlockStorageObject.value);
            // Display the unlocked titles
            break;
        case "Hats":
            var unlockedHats = JSON.parse<HatsStorageObject>(unlockStorageObject.value);
            // Display the unlocked hats
            break;
        case "Skins":
            var unlockedSkins = JSON.parse<SkinsStorageObject>(unlockStorageObject.value);
            // Display the unlocked skins
            break;
    }
});

分页结果 #

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

例如:

  • 如果游标的值为5,您将从第五个对象开始获取结果。
  • 如果游标的值为null,您将从第一个对象开始获取结果。
1
objectList = await client.listStorageObjects(session, "<CollectionName>", limit, objectList.cursor);

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

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

远程程序调用 #

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

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

创建服务器逻辑 #

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

客户端RPC #

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

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

1
2
3
4
5
6
7
8
try {
    var payload = { "item": "cowboy"};
    var response = await client.rpc(session, "EquipHat", payload);
    console.log("New hat equipped successfully", response);
}
catch (error) {
    console.log("Error: %o", error.message);
}

套接字RPC #

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

1
var response = await socket.rpc("<rpcId>", "<payloadString>");

好友 #

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

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

Sagi-shi Friends screen
Friends screen

添加好友 #

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

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

1
2
3
4
5
6
7
// Add friends by Username.
var usernames = ["AlwaysTheImposter21", "SneakyBoi"];
await client.addFriends(session, usernames);

// Add friends by User ID.
var ids = ["<SomeUserId>", "<AnotherUserId>"];
await client.addFriends(session, ids);

好友关系的状态 #

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

状态
0共同好友
1已发出的等待接受的好友请求
2已收到的等待接受的好友请求
4被封禁

列出好友 #

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

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

1
2
3
4
5
6
7
var limit = 20; // Limit is capped at 1000
var friendshipState = 0;
var result = await client.listFriends(session, friendshipState, limit, cursor: null);

result.forEach((friend) => {
    console.log("ID: %o", friend.user.id);
});

接受好友请求 #

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

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

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

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

1
2
3
4
5
6
var limit = 1000;
var result = await client.listFriends(session, 2, limit, cursor: null);

result.forEach((friend) => {
    await client.addFriends(session, friend.user.id);
});

删除好友 #

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

1
2
3
4
5
6
7
// Delete friends by User ID.
var ids = ["<SomeUserId>", "<AnotherUserId>"];
await client.deleteFriends(session, ids});

// Delete friends by Username.
var usernames = ["AlwaysTheImposter21", "SneakyBoi"];
await client.deleteFriends(session, null, usernames});

屏蔽用户 #

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

1
2
3
4
5
6
7
// Block friends by User ID.
var ids = ["<SomeUserId>", "<AnotherUserId>"];
await client.blockFriends(session, ids);

// Block friends by Username.
var usernames = ["AlwaysTheImposter21", "SneakyBoi"];
await client.blockFriends(session, usernames);

进一步了解关于屏蔽好友和相关的好友关系状态

被屏蔽的好友也可以作为好友列出,但好友关系状态相应变为(3)。

状态与显示 #

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
// Subscribe to the Status event.
socket.onstatuspresence = (e) => {
    e.joins.forEach(function(presence){
        console.log("%o is online with status: %o", presence.username, presence.status);
    })
    e.leaves.forEach(function(presence){
        console.log("%o went offline", presence.username);
    })
};

// Follow mutual friends and get the initial Status of any that are currently online.
var friendsResult = await client.listFriends(session, 0);
var friendIds = [];
friendsResult.friends.forEach(function(friend) {
    friendIds.push(friend.user.id);
});
var result = await socket.followUsers(friendIds);

result.presences.forEach(function(presence){
    console.log("%o is online with status: %o", presence.username, presence.status);
});

取消关注用户 #

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

1
await socket.unfollowUsers(["<UserId>"]);

更新玩家状态 #

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

1
await socket.updateStatus("Viewing the Main Menu");

群组 #

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

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

Sagi-shi groups screen
Groups list screen

创建群组 #

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

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

1
2
3
4
5
6
7
8
9
const groupName = "Imposters R Us";
const description = "A group for people who love playing the imposter.";

const group = await client.createGroup(session {
    name: groupName,
    description: description,
    open: true, // public group
    maxSize = 100
});

更新群组的可见性 #

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

1
2
3
4
const groupId = "<groupId>";
await client.updateGroup(session, groupId, {
    open: false
});

更新群组规模 #

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

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

Sagi-shi group edit screen
Sagi-shi group edit

列出和过滤群组 #

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

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

1
2
3
4
5
6
7
8
9
var limit = 20;
var result = await client.ListGroupsAsync(session, "imposter%", limit);

result.groups.forEach(function(group){
    console.log("%o group is %o", group.name, group.open);
});

// Get the next page of results.
var nextResults = await client.listGroups(session, name: "imposter%", limit, result.cursor);

删除群组 #

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

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

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

1
2
const groupId = "<groupId>";
await client.deleteGroup(session, groupId);

群组元数据 #

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const payload = new {
    groupId = "<GroupId>",
    interests = ["Deception", "Sabotage", "Cute Furry Bunnies"],
    activeTimes = ["9am-2pm Weekdays", "9am-10pm Weekends"],
    languages = ["English", "German"]
};

try {
    var result = await client.rpc(session, "UpdateGroupMetadata", JSON.stringify(payload));
    console.log("Successfully updated group metadata");
}
catch (error) {
    console.log("Error: %o", error.message);
}

群组成员资格状态 #

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

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

加入群组 #

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

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

1
2
const group_id = "<group id>";
await client.joinGroup(session, group_id);

列出用户的群组 #

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

1
2
3
4
5
const userId = "<user id>";
const groups = await client.listUserGroups(session, userId);
groups.user_groups.forEach(function(userGroup){
  console.log("Group: name '%o' State: '%o'.", userGroup.group.name, userGroup.state);
});

列出成员 #

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

1
2
3
4
5
const groupId = "<group id>";
const groups = await client.listUserGroups(session, groupId);
groups.group_users.forEach(function(groupUser){
  console.log("User: ID '%o' State: '%o'.", groupUser.user.id, groupUser.state);
});

接受加入请求 #

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

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

1
2
3
4
5
const groupId = "<group id>";
const result = await client.listGroupUsers(session, groupId);
groups.group_users.forEach(function(groupUser){
    await client.addGroupUsers(session, groupId, [groupUser.user.id]);
});

升级成员 #

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

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

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

  • 成员可以升级为管理员
  • 管理员可以升级为超级管理员
1
2
3
const groupId = "<group id>";
const userId = "<user id>";
await client.promoteGroupUsers(session, groupId, [userId]);

降级成员 #

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

1
2
3
const groupId = "<group id>";
const userId = "<user id>";
await client.demoteGroupUsers(session, groupId, [userId]);

踢出成员 #

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

1
2
3
const groupId = "<group id>";
const userId = "<user id>";
await client.kickGroupUsers(session, groupId, [userId]);

封禁成员 #

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

1
2
3
const groupId = "<group id>";
const userId = "<user id>";
await client.banGroupUsers(session, groupId, [userId]);

退出群组 #

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

1
2
const groupId = "<group id>";
await client.leaveGroup(session, groupId);

聊天 #

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

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

Sagi-shi chat screen
Sagi-shi Chat

加入动态聊天室 #

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

1
2
3
4
5
6
7
const roomName = "<match id>";
const persistence = false;
const hidden = false;
// 1 = Room, 2 = Direct Message, 3 = Group
const channel = await socket.joinChat(roomName, 1, persistence, hidden);

console.log("Connected to dynamic room channel: %o", channel.id);

加入群组聊天 #

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

1
2
3
4
5
6
7
const groupId = "<group id>";
const persistence = true;
const hidden = false;
// 1 = Room, 2 = Direct Message, 3 = Group
const channel = await socket.joinChat(3, groupId, persistence, hidden);

console.log("Connected to group channel: %o", channel.id);

加入直接聊天 #

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

1
2
3
4
5
6
7
const userId = "<user id>";
const persistence = true;
const hidden = false;
// 1 = Room, 2 = Direct Message, 3 = Group
const channel = await socket.joinChat(2, userId, persistence, hidden);

console.log("Connected to direct message channel: %o", channel.id);

发送消息 #

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

1
2
3
4
5
6
7
8
9
var channelId = "<channel id>";
var data = { "message": "I think Red is the imposter!" };
const messageAck = await socket.writeChatMessage(channelId, data);

var emoteData = {
    "emote": "point",
    "emoteTarget": "<redPlayerUserId>"
}
const emoteMessageAck = await socket.writeChatMessage(channelId, emoteData);

列出消息历史 #

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

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

1
2
3
4
5
6
7
8
const groupId = "<group id>";
const limit = 100;
const forward = true;

const result = await client.listChannelMessages(session, groupId, limit, forward, cursor: null);
result.messages.forEach((message) => {
  console.log("%o: %o", message.username, message.data);
});

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

1
2
const cursor = result.cacheable_cursor;
const nextResults = await client.listChannelMessages(session, groupId, limit, forward, cursor);

更新消息 #

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

例如玩家发送以下消息:

1
2
3
var channelId = "<ChannelId>";
var messageData = {"message": "I think Red is the imposter!" };
const messageSendAck = await socket.writeChatMessage(channelId, messageData);

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

1
2
var newMessageData = {"message": "I think BLUE is the imposter!" };
const messageUpdateAck = await socket.updateChatMessage(channelId, messageSendAck.message.id, newMessageData));

比赛 #

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

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

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

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

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

创建比赛 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var match = await socket.createMatch();
var friendsList = await client.listFriends(session);
var onlineFriends = [];
friendsList.friends.forEach((friend){
    if (friend.user.online){
        onlineFriends.push(friend.user);
    }
});

onlineFriends.friend.forEach(function(friend){
    var messageData = {"message": "Hey %o, join me for a match!", friends.username},
    var matchId = match.id,
    const channel = await socket.joinChat(2, friend.id),
    const messageAck = await socket.writeChatMessage(channel, messageData)
});

加入比赛 #

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

1
2
var matchId = "<MatchId>";
var match = await socket.joinMatch(matchId);

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

1
2
3
4
5
6
7
8
9
socket.onmatchmakermatched = async (matchmakerMatched) => {
    var match = await socket.joinMatch(matchmakerMatched);
};

var minPlayers = 2;
var maxPlayers = 10;
var query = "";

var matchmakingTicket = await socket.addMatchmaker(query, minPlayers, maxPlayers);

按玩家状态加入比赛

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

1
2
3
4
5
6
var status = {
    "Status": "Playing a match",
    "MatchId": "<MatchId>"
};

await socket.updateStatus(JSON.stringify(status));

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

1
2
3
4
5
6
7
8
9
socket.onstatuspresence = async (e) => {
    // Join the first match found in a friend's status
    e.joins.forEach(function(presence){
        var status = JSON.parse(presence.status),
        if (status.hasOwnProperty("MatchId")) {
            await socket.joinMatch(status["MatchId"]);
            break;
        }
    });

列出比赛 #

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var minPlayers = 2;
var maxPlayers = 10;
var limit = 10;
var authoritative = true;
var label = "";
var query = "";
const result = await client.listMatches(session, minPlayers, maxPlayers, limit, authoritative, label, query);

result.matches.forEach(function(match){
    console.log("%o: %o/10 players", match.id, match.size);
});

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

1
var label = "AnExactMatchLabel";

高级:

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

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

1
var query = "+label.skill:>100 label.mode:sabotage";

生成玩家 #

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

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

1
2
3
4
5
6
7
8
var match = await socket.joinMatch(matchId);

var players = {};

match.presences.forEach(function(presence){
    var go = spawnPlayer(); // Instantiate player object
    players.push(presence.session.id, go);
});

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
socket.onmatchpresence = (matchPresenceEvent) => {
    // For each player that has joined in this event...
    matchPresenceEvent.joins.forEach(function(presence){
        // Spawn a player for this presence and store it in a dictionary by session id.
        var go = // Instantiate player object;
        players.push(presence.session.id, go);
    })

    // For each player that has left in this event...
    matchPresenceEvent.leaves.forEach(function(presence){
        // Remove the player from the game if they've been spawned
        if (players.hasOwnProperty("SessionId"){
            const index = players.session.id;
            if (index > -1) {
                players.splice(index, 1);
            }
        })
    })
};

发送比赛状态 #

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

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

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

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

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

发送玩家位置

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

1
2
3
4
5
class PositionState {
    static X;
    static Y;
    static Z;
}

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

1
2
3
4
5
6
7
8
9
var state = new PositionState {
    x = transform.position.x,
    y = transform.position.y,
    z = transform.position.z
};

var opCode = 1;

await socket.sendMatchState(match.Id, opCode, JSON.stringify(state));

作为静态类操作代码

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

1
2
3
4
5
6
class OpCodes {
    static position = 1;
    static vote = 2;
}

await socket.sendMatchState(match.Id, OpCodes.position, JSON.stringify(state));

接收比赛状态 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
socket.onmatchdata = (matchState) => {
    switch (matchState.opCode) {
        case opCodes.position:
            // Get the updated position data
            var stateJson = matchState.state;
            var positionState = JSON.parse(stateJson);

            // Update the GameObject associated with that player
            if (players.hasOwnProperty(matchState.user_presence.session.id)) {
                // 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[matchState.user_presence.session.id].transform.position = new Vector3(positionState.s, positionState.y, positionState.z);
            }
            break;
        default:
            console.log("Unsupported op code");
            break;
    }
};

配对程序 #

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

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

添加配对程序 #

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

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

1
2
3
4
5
6
var minPlayers = 2;
var maxPlayers = 10;
var query = "+skill:>100 mode:sabotage";
var stringProperties = { "mode": "sabotage"};
var numericProperties = { "skill": 125};
var matchmakerTicket = await socket.addMatchmaker(query, minPlayers, maxPlayers, stringProperties, numericProperties);

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

1
2
3
4
socket.onmatchmakermatched = (matched) => {
  const matchId = null;
  socket.joinMatch(matchId, matched.token);
};

派对 #

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

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

创建派对 #

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

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

1
2
3
var open = false;
var maxPlayers = 4;
const party = await socket.createParty(open, maxPlayers);

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var friendsList = await client.listFriends(session);
var onlineFriends = [];
friendsList.friends.forEach((friend){
    if (friend.user.online){
        onlineFriends.push(friend.user);
    }
});

onlineFriends.friend.forEach(function(friend){
    var messageData = {"message": "Hey %o, wanna join the party?", friends.username};
    var partyId = party.id;
    const channel = await socket.joinChat(2, friend.id);
    const messageAck = await socket.writeChatMessage(channel, messageData);
});

加入派对 #

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

1
2
3
4
5
6
socket.onchannelmessage = async (m) => {
    var content = JSON.parse(m.content);
    if (content.hasOwnProperty("partyId")) {
        await socket.joinParty(content["partyId"]);
    }
};

升级成员 #

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

1
2
var newLeader = "<user id>";
await socket.promotePartyMember(party.Id, newLeader);

退出派对 #

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

1
await socket.leaveParty(party.Id);

派对配对 #

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

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

1
2
3
socket.onmatchmakermatched = async (matchmakerMatched) => {
    await socket.joinMatch(matchmakerMatched.match.id);
};

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

1
2
3
4
5
var partyId = "<party id>";
var minPlayers = 2;
var maxPlayers = 10;
var query = "";
var matchmakerTicket = await socket.addMatchmakerParty(partyId, query, minPlayers, maxPlayers);

排行榜 #

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

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

Sagi-shi leaderboard screen
Sagi-shi Leaderboard

创建排行榜 #

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

提交分数 #

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

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

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

1
2
3
4
var score = 1;
var subscore = 0;
var metadata = { "map": "space_station"};
await client.writeLeaderboardRecord(session, "weekly_imposter_wins", score, subscore, JSON.stringify(metadata));

列出最高记录 #

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

1
2
3
4
5
6
7
var limit = 20;
var leaderboardName = "weekly_imposter_wins";
const result = await client.listLeaderboardRecords(session, leaderboardName, ownerIds: null, expiry: null, limit, cursor: null);

result.records.forEach(fuction(record){
    console.log("%o:%o", record.owner.id, record.score);
});

列出用户周围的记录

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

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

1
2
3
4
5
6
7
8
var userId = session.user.id;
var limit = 20;
var leaderboardName = "weekly_imposter_wins";
var result = await client.listLeaderboardRecordsAroundOwner(session, leaderboardName, userId, expiry: null, limit);

result.records.forEach(fuction(record){
    console.log("%o:%o", record.owner.id, record.score);
});

列出一系列用户的记录

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var friendsList = await client.ListFriendsAsync(session);
var userIds = [];
friendsList.friends.forEach(function(friend){
    userIds.push(friend.user.id);
});
var recordList = await client.listLeaderboardRecords(session, "weekly_imposter_wins", userIds, expiry: null, 100, cursor: null);

recordList.records.forEach(fuction(record){
    console.log("%o:%o", record.username, record.score);
});

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var groupId = "<group id>";
var groupUserList = await client.listGroupUsers(session, groupId);
var userIds = [];
groupUserList.forEach(function(group_user){
    if (group_user.state < 3){
        userIds.push(group_user.id);
    }
});

var recordList = await client.listLeaderboardRecords(session, "weekly_imposter_wins", userIds, expiry: null, 100, cursor: null);
recordList.records.forEach(fuction(record){
    console.log("%o:%o", record.username, record.score);
});

删除记录 #

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

1
2
var leaderboardId = "<leaderboard id>";
await client.deleteLeaderboardRecord(session, leaderboardId);

锦标赛 #

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

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

Sagi-shi tournaments screen
Sagi-shi Tournaments

创建锦标赛 #

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

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

加入锦标赛 #

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

1
2
var id = "<tournament id>";
await await client.joinTournament(session, id);

列出锦标赛 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var categoryStart = 1;
var categoryEnd = 2;
var startTime = 1538147711;
var endTime = null; // all tournaments from the start time
var limit = 100; // number to list per page
var cursor = null;
var result = await client.listTournaments(session, categoryStart, categoryEnd, startTime, endTime, limit, cursor);

result.tournaments.forEach(function(tournament) {
    console.log("%o:%o", tournament.id, tournament.title);
});

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

列出记录 #

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

1
2
3
4
5
6
var tournamentName = "weekly_top_detective";
var limit = 20;
var result = await client.listTournamentRecords(session, tournamentName, limit);
result.records.forEach(function(record) {
  console.log("%o:%o", record.owner.id, record.score);
});

列出用户周围的记录

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

1
2
3
4
5
6
7
var userId = "<user id>";
var limit = 20;
var tournamentName = "weekly_top_detective";
var result = await client.listTournamentRecordsAroundOwner(session, tournamentName, userId, limit);
result.records.forEach(function(record) {
  console.log("%o:%o", record.owner.id, record.score);
});

提交分数 #

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

1
2
3
4
5
var tournamentName = "weekly_top_detective";
var score = 1;
var subscore = 0;
var metadata = { "map": "space_station"};
await client.writeTournamentRecord(session, tournamentName, score, subscore, metadata);

通知 #

游戏服务器可利用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
socket.onnotification = (notification) => {
    const rewardCode = 100;
    switch (notification.code) {
        case rewardCode:
            console.log("Congratulations, you won the tournament!\n%o\n%o", notification.subject, notification.content);
            break;
        default:
            console.log("Other notification: %o:%o\n%o", notification.code, notification.subject, notification.content);
            break;
    }
};

列出通知 #

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

1
2
3
4
5
const result = await client.listNotifications(session, 10);
result.notifications.forEach(notification => {
  console.info("Notification code %o and subject %o.", notification.code, notification.subject);
});
console.info("Fetch more results with cursor:", result.cacheable_cursor);
1
2
3
4
5
6
var limit = 100;
var cacheableCursor = null;
var result = await client.listNotifications(session, limit, cacheableCursor);
result.notification.forEach(function(notification) {
    console.log("Notification: %o:%o\n%o", notification.code, notification.subject, notification.content)
});

分页及可缓存游标

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

1
const cacheableCursor = result.cacheable_cursor;

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

1
var nextResults = await client.listNotifications(session, limit, cacheableCursor);

删除通知 #

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

1
2
var notificationId = "<notification id>";
await client.deleteNotifications(session, [notificationId]);