用户账户 #

在服务器中,用户以个人身份的形式存在。每个用户都经过注册,设有个人资料,可供其他玩家查找和添加好友或加入群组聊天

用户可以拥有记录,将公开信息共享给其他用户,并通过各种社交供应商进行身份验证。

系统所有者身份使用带有空UUID (00000000-0000-0000-0000-000000000000).
的用户账户表示

获取账户 #

用户有会话时,您可以检索用户账户。个人资料包含各种信息,其中包括各种有“链接”关系的社交供应商。

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/account" \
  -H 'authorization: Bearer <session token>'
Client
1
2
3
4
const account = await client.getAccount(session);
const user = account.user;
console.info("User id '%o' and username '%o'.", user.id, user.username);
console.info("User's wallet:", account.wallet);
Client
1
2
3
4
var account = await client.GetAccountAsync(session);
var user = account.User;
System.Console.WriteLine("User id '{0}' username '{1}'", user.Id, user.Username);
System.Console.WriteLine("User wallet: '{0}'", account.Wallet);
Client
1
2
3
4
5
6
auto successCallback = [](const NAccount& account)
{
    std::cout << "User's wallet: " << account.wallet.c_str() << std::endl;
};

client->getAccount(session, successCallback);
Client
1
2
3
4
Account account = client.getAccount(session);
User user = account.getUser();
System.out.format("User id %s username %s", user.getId(), user.getUsername());
System.out.format("User wallet %s", account.getWallet());
Client
1
2
3
4
5
6
7
8
9
var account : NakamaAPI.ApiAccount = yield(client.get_account_async(session), "completed")

if account.is_exception():
    print("An error occurred: %s" % account)
    return

var user = account.user
print("User id '%s' and username '%s'." % [user.id, user.username])
print("User's wallet: %s." % account.wallet)
Client
1
2
3
4
5
GET /v2/account
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
6
7
8
9
local result = client.get_account()

if result.error then
  print(result.message)
  return
end

local user = result.user
print(("User id '%s' and username '%s'."):format(user.id, user.username))

钱包、设备ID和自定义ID是私密信息,但部分资料对其他用户是可见的。

Public FieldDescription
user.id用户的唯一标识符。
user.username用户的唯一昵称。
user.display_name显示的用户名称(默认为空)。
user.avatar_url包含用户个人资料图片的网址(默认为空)。
user.lang用户的首选语言设置(默认为“英文”)。
user.location用户的位置(默认为空)。
user.timezone用户的时区(默认为空)。
user.metadata用户可添加自定义信息的插槽 - 仅可从客户端读取。
user.edge_count用户的好友数量。
user.facebook_id用户关联的 Facebook 标识符。
user.google_id用户关联的 Google 标识符。
user.gamecenter_id用户关联的 GameCenter 标识符。
user.steam_id用户关联的 Steam 标识符。
user.create_time创建用户时的时间戳。
user.update_time用户上次更新时的时间戳。
user.online显示用户当前是否在线的布尔值。
Private FieldDescription
email用户关联的电子邮箱。
devices用户关联的设备 ID 列表。
custom_id用户关联的自定义标识符。
wallet用户钱包 - 仅可从客户端读取。
verify_time用户验证时间戳(目前仅用于 Facebook)。

用户元数据 #

您可以在user.metadata中为用户存储其他字段,这可以帮助您与其他用户共享您想要公开的数据。我们建议使用用户元数据来存储显示给其他用户的常见字段。例如,必要时可以使用用户元数据让用户显示个人详情,或者其角色名称、等级和游戏统计信息。

您可以将所有其他信息存储为拥有公开阅读权限,以便于其他用户查找。

每位用户的元数据大小限制在16KB。仅可以通过脚本运行设置,类似于wallet

以下示例展示了使用用户元数据存储VIP状态,然后将VIP状态用作参加锦标赛前的钩子,这个钩子仅允许VIP成员加入锦标赛。

Server
 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
// Assuming a user metadata structure as follows
const metadata = {
  vip: true
};

// Add a before hook to only allow VIP users to join the vip_only tournament
let BeforeJoinTournament: nkruntime.BeforeHookFunction<JoinTournamentRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: nkruntime.JoinTournamentRequest): nkruntime.JoinTournamentRequest | void {
  const account = nk.accountGetId(ctx.userId)

  // Only do the following checks if the tournament id is `vip_only`
  if (data.tournamentId != "vip_only") {
    return data;
  }

  // Only continue with the Join Tournament if the actioning user is a vip
  if (account.user.metadata["vip"]) {
    return data;
  }

  logger.warn("you must be a vip to join this tournament")
  return null;
};

// Register inside InitModule
initializer.registerBeforeJoinTournament(BeforeJoinTournament);
Server
 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
// Assuming a user metadata structure as follows
type UserMetadata struct {
	Vip bool `json:"vip"`
}

metadata := &UserMetadata {
  Vip: true,
}

// Add a before hook to only allow VIP users to join the vip_only tournament
if  err := initializer.RegisterBeforeJoinTournament(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.JoinTournamentRequest) (*api.JoinTournamentRequest, error) {
  userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
  if !ok {
    logger.Error("invalid user")
    return nil, runtime.NewError("invalid user", 13)
  }

  // Get the user's metadata
  account, err := nk.AccountGetId(ctx, userId)
  if err != nil {
    logger.Error("error getting user account")
    return nil, runtime.NewError("error getting user account", 13)
  }

  // Only do the following checks if the tournament id is `vip_only`
  if in.TournamentId != "vip_only" {
    return in, nil
  }

  // Only continue with the Join Tournament if the actioning user is a vip
  var metadata UserMetadata
  if err := json.Unmarshal([]byte(account.User.GetMetadata()), &metadata); err != nil {
    logger.Error("error deserializing metadata")
    return nil, runtime.NewError("error deserializing metadata", 13)
  }

  if metadata.Vip {
    return in, nil
  }

  return nil, runtime.NewError("you must be a vip user to join this tournament", 7)
}); err != nil {
  logger.Error("unable to register before join tournament hook: %v", err)
  return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- Assuming a user metadata structure as follows
local metadata = {
    ["vip"] = true
}

-- Add a before hook to only allow VIP users to join the vip_only tournament
local function before_join_tournament(context, payload)
    local account = nk.account_get_id(context.user_id)

    -- Only do the following checks if the tournament id is `vip_only`
    if payload.tournament_id ~= "vip_only" then
        return payload
    end

    -- Only continue with the Join Tournament request if the actioning user is a vip
    if account.user.metadata["vip"] then
        return payload
    end

    nk.logger_error("you must be a vip user to join this tournament")
    return nil
end

nk.register_req_before(before_join_tournament, "JoinTournament")

虚拟钱包 #

Nakama有虚拟钱包和交易分类账的概念。Nakama允许开发人员创建、更新和列出对用户钱包作出的变更。这个操作有事务担保,并且仅可通过脚本运行来实现。

通过服务器端代码,可以更新用户的钱包。

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
local nk = require("nakama")
local user_id = "8f4d52c7-bf28-4fcf-8af2-1d4fcf685592"

local changeset = {
  coins = 10, -- Add 10 coins to the user's wallet.
  gems = -5   -- Remove 5 gems from the user's wallet.
}

local metadata = {
  game_result = "won"
}

local updated, previous = nk.wallet_update(user_id, changeset, metadata, true)
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
userID := "8f4d52c7-bf28-4fcf-8af2-1d4fcf685592"
changeset := map[string]interface{}{
    "coins": 10, // Add 10 coins to the user's wallet.
    "gems":  -5, // Remove 5 gems from the user's wallet.
}
metadata := map[string]interface{}{
    "game_result": "won",
}
updated, previous, err := nk.WalletUpdate(ctx, userID, changeset, metadata, true)
if err != nil {
    logger.WithField("err", err).Error("Wallet update error.")
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
let user_id = '8f4d52c7-bf28-4fcf-8af2-1d4fcf685592';

let changeset = {
  coins: 10, // Add 10 coins to the user's wallet.
  gems: -5,   // Remove 5 gems from the user's wallet.
};

let metadata = {
  gameResult: 'won'
};

let result: nkruntime.WalletUpdateResult;

try {
    result = nk.walletUpdate(user_id, changeset, metadata, true);
} catch (error) {
    // Handle error
}

钱包属于用户隐私,对其他用户不可见。您可以通过获取账户操作获取用户的钱包信息。

在线指示器 #

Nakama可通过两种方式汇报用户在线指示器:

  1. 获取用户信息。这将为您提供用户在线指示器的快速快照视图,但无法可靠地检测用户显示的在线状态。
  2. 发布和订阅用户显示的在线状态更新。用户的在线状态发生变化时,将会向您更新(同时还有自定义消息)。

获取用户 #

您可以通过用户的ID或手柄获取一个或多个用户。这有助于向其他用户显示公开的个人资料。

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/user?ids=userid1&ids=userid2&usernames=username1&usernames=username2&facebook_ids=facebookid1" \
  -H 'authorization: Bearer <session token>'
Client
1
2
3
4
5
const users = await client.getUsers(session, ["user_id1"], ["username1"], ["facebookid1"]);

users.foreach(user => {
  console.info("User id '%o' and username '%o'.", user.id, user.username);
});
Client
1
2
3
4
5
6
7
8
9
var ids = new[] {"userid1", "userid2"};
var usernames = new[] {"username1", "username2"};
var facebookIds = new[] {"facebookid1"};
var result = await client.GetUsersAsync(session, ids, usernames, facebookIds);

foreach (var u in result.Users)
{
    System.Console.WriteLine("User id '{0}' username '{1}'", u.Id, u.Username);
}
Client
1
2
3
4
5
6
7
8
auto successCallback = [](const NUsers& users)
{
    for (auto& user : users.users)
    {
        std::cout << "User id '" << user.id << "' username " << user.username << std::endl;
    }
};
client->getUsers(session, { "user_id1" }, { "username1" }, { "facebookid1" }, successCallback);
Client
1
2
3
4
5
6
7
8
List<String> ids = Arrays.asList("userid1", "userid2");
List<String> usernames = Arrays.asList("username1", "username1");
String[] facebookIds = new String[] {"facebookid1"};
Users users = client.getUsers(session, ids, usernames, facebookIds).get();

for (User user : users.getUsersList()) {
  System.out.format("User id %s username %s", user.getId(), user.getUsername());
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var ids = ["userid1", "userid2"]
var usernames = ["username1", "username2"]
var facebook_ids = ["facebookid1"]
var result : NakamaAPI.ApiUsers = yield(client.get_users_async(session, ids, usernames, facebook_ids), "completed")

if result.is_exception():
    print("An error occurred: %s" % result)
    return

for u in result.users:
    print("User id '%s' username '%s'" % [u.id, u.username])
Client
1
2
3
4
5
GET /v2/user?ids=userid1&ids=userid2&usernames=username1&usernames=username2&facebook_ids=facebookid1
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
local ids = { "userid1", "userid2" }
local usernames = { "username1", "username2" }
local facebook_ids = { "facebookid1" }
local result = client.get_users(ids, usernames, facebook_ids)

if result.error then
  print(result.message)
  return
end

local users = result.users

for _,user in ipairs(users) do
  print(("User id '%s' and username '%s'."):format(user.id, user.username))
end

您也可以在服务器端代码中获取一个或多个用户。

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
local nk = require("nakama")

local user_ids = {
  "3ea5608a-43c3-11e7-90f9-7b9397165f34",
  "447524be-43c3-11e7-af09-3f7172f05936"
}

local users = nk.users_get_id(user_ids)

for _, u in ipairs(users) do
  local message = ("username: %q, displayname: %q"):format(u.username, u.display_name)
  nk.logger_info(message)
end
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if users, err := nk.UsersGetId(ctx, []string{
    "3ea5608a-43c3-11e7-90f9-7b9397165f34",
    "447524be-43c3-11e7-af09-3f7172f05936",
}); err != nil {
    // Handle error.
} else {
    for _, u := range users {
      logger.Info("username: %s, displayname: %s", u.Username, u.DisplayName)
    }
}

更新账户 #

用户注册时,大部分个人资料采用默认值设置。用户可以更新自己的个人资料以更改字段,但无法修改其他用户的个人资料。

Client
1
2
3
4
5
6
7
curl -X PUT "http://127.0.0.1:7350/v2/account" \
  -H 'authorization: Bearer <session token>' \
  --data '{
    "display_name": "My new name",
    "avatar_url": "http://graph.facebook.com/avatar_url",
    "location": "San Francisco"
  }'
Client
1
2
3
4
5
await client.updateAccount(session, {
  display_name: "My new name",
  avatar_url: "http://graph.facebook.com/avatar_url",
  location: "San Francisco"
});
Client
1
2
3
4
const string displayName = "My new name";
const string avatarUrl = "http://graph.facebook.com/avatar_url";
const string location = "San Francisco";
await client.UpdateAccountAsync(session, null, displayName, avatarUrl, null, location);
Client
1
client->updateAccount(session, opt::nullopt, "My new name", "http://graph.facebook.com/avatar_url", opt::nullopt, "San Francisco");
Client
1
2
3
4
String displayName = "My new name";
String avatarUrl = "http://graph.facebook.com/avatar_url";
String location = "San Francisco";
client.updateAccount(session, null, displayName, avatarUrl, null, location);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var display_name = "My new name";
var avatar_url = "http://graph.facebook.com/avatar_url";
var location = "San Francisco";
var update : NakamaAsyncResult = yield(client.update_account_async(session, null, display_name, avatar_url, null, location), "completed")

if update.is_exception():
    print("An error occurred: %s" % update)
    return

print("Account updated")
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
PUT /v2/account HTTP/1.1
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "display_name": "My new name",
  "avatar_url": "http://graph.facebook.com/avatar_url",
  "location": "San Francisco"
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
local display_name = "Björn"
local avatar_url = "http://graph.facebook.com/avatar_url"
local lang_tag = ""
local timezone = ""
local username = ""
local location = "Stockholm"

local result = client.update_account(client, avatar_url, display_name, lang_tag, location, timezone, username)

if result.error then
  print(result.message)
  return
end

print("Account updated")

通过服务器端代码,可以更新任意用户的个人资料。

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
local nk = require("nakama")

local user_id = "4ec4f126-3f9d-11e7-84ef-b7c182b36521" -- some user's id.
local metadata = {}
local username = "my-new-username"
local display_name = "My new Name"
local timezone = nil
local location = "San Francisco"
local lang_tag = nil
local avatar_url = "http://graph.facebook.com/avatar_url"

local status, err = pcall(nk.account_update_id, user_id, metadata, username, display_name, timezone, location, lang_tag, avatar_url)

if (not status) then
  nk.logger_info(("Account update error: %q"):format(err))
end
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
userID := "4ec4f126-3f9d-11e7-84ef-b7c182b36521" // some user's id.
username := "my-new-username" // must be unique
metadata := make(map[string]interface{})
displayName := "My new name"
timezone := ""
location := "San Francisco"
langTag := ""
avatarUrl := "http://graph.facebook.com/avatar_url"

if err := nk.AccountUpdateId(ctx, userID, username, metadata, displayName, timezone, location, langTag, avatarUrl); err != nil {
    // Handle error.
    logger.Error("Account update error: %s", err.Error())
}