远程配置 #

远程配置是一种通过存储在远程服务器上的应用程序内参数自定义应用或游戏行为的方式。这可以用于实现 功能标志 或调整改变应用程序或游戏外观或行为的设置。

开发人员可以使用远程配置来避免冗长的审查过程、修改游戏或应用程序然后等待用户更新等繁琐环节。这使它特殊适用于移动项目。

管理应用程序内参数 #

需要将发送到应用程序或游戏的配置设置存储到服务器。存储信息的最佳方式取决于数据更改的频率。

对于大部分静态数据,将其作为数据结构嵌入服务器端代码是最高效的方式,对于更加动态的数据,最好使用只读存储记录

通过这两种方法,您可以在完成注册/登录或连接用户会话之前访问远程配置。您配置的应用程序内参数可以在应用程序启动的最早点进行初始化。

静态参数 #

最简单的方法是使用服务器端代码将应用程序内参数表示为静态变量。在服务器启动后更改参数需要更新Lua代码并重新启动服务器。

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
-- The code could be stored in a module named `"rc.lua"` and placed in the runtime path for the server.

local nk = require("nakama")

-- In-app parameters stored in a static variable.
local parameters = {
  reachable_levels = 10,
  max_player_level = 90,
  min_version = 12
}

local function remote_configuration(_context, _payload)
  return nk.json_encode({ rc = parameters })
end

nk.register_rpc(remote_configuration, "rc")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var parameters = map[string]interface{}{
  "reachable_levels": 10,
  "max_player_level": 90,
  "min_version":      12,
}

func RemoteConfig(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
  responseBytes, err := json.Marshal(map[string]interface{}{"rc": parameters})
  if err != nil {
    return "", err
  }

  return string(responseBytes), nil
}

// Register as RPC function, this call should be in InitModule.
if err := initializer.RegisterRpc("rc", RemoteConfig); err != nil {
  logger.Error("Unable to register: %v", err)
  return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const parameters = {
  reachableLevels: 10,
  maxPlayerLevel: 90,
  minVersion: 12
};

const RemoteConfigRpc : nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string | void {
  return JSON.stringify(parameters);
}

// Register as RPC function, this should be in InitModule.
initializer.registerRpc('rc', RemoteConfigRpc);

动态参数 #

对于可以通过Analytics或LiveOps仪表板更改的应用程序内参数,将配置设置作为只读记录存储在存储引擎中更为灵活。

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
--Same as above we'll use server-side code with a module named `"rc.lua"` and placed in the runtime path for the server.

local nk = require("nakama")

local parameters = {
  reachable_levels = 10,
  max_player_level = 90,
  min_version = 12
}

local object = {
  collection = "configuration",
  key = "rc",
  value = parameters,
  permission_read = 1,
  permission_write = 0,
  version = "*" -- Only write object if it does not already exist.
}
pcall(nk.storage_write, { object }) -- Write object, ignore errors.

local function remote_configuration(_context, _payload)
  local rc = {
    collection = object.collection,
    key = object.key
  }
  local objects = nk.storage_read({ rc })
  return objects[1].value
end

nk.register_rpc(remote_configuration, "rc")
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const (
  configCollection = "configuration"
  configKey = "rc"
)

func SaveConfig(ctx context.Context, nk runtime.NakamaModule, logger runtime.NakamaModule) error {
  parameters := map[string]interface{}{
    "reachable_levels": 10,
    "max_player_level": 90,
    "min_version":      12,
  }

  b, err := json.Marshal(parameters)
  if err != nil {
    return err
  }

  objects := []\*runtime.StorageWrite{
    &runtime.StorageWrite{
      Collection:      configCollection,
      Key:             configKey,
      Value:           string(b),
      Version:         "\*", // Only write if object does not exist already.
      PermissionRead:  1,
      PermissionWrite: 0,
    },
  }

  if _, err := nk.StorageWrite(ctx, objects); err != nil {
    return err
  }
  return nil
}

func RemoteConfig(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
  objectIds := []*runtime.StorageRead{
    &runtime.StorageRead{
      Collection: configCollection,
      Key:        configKey,
    },
  }

  records, err := nk.StorageRead(ctx, objectIds)
  if err != nil {
    return "", err
  }
  if len(records) == 0 {
    return "", errors.New("No config found.")
  }
  return records[0].Value, nil
}

// Ensure the configuration object is stored, this call should be in InitModule.
SaveConfig(ctx, nk, logger)
// Register as RPC function, this call should be in InitModule.
if err := initializer.RegisterRpc("rc", RemoteConfig); err != nil {
  logger.Error("Unable to register: %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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const configCollection = 'configuration';
const configKey = 'rc';

const SaveConfig = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama) {
  let parameters = {
    reachableLevels: 10,
    maxPlayerLevel: 90,
    minVersion: 12
  };

  const storageWrite : nkruntime.StorageWriteRequest = {
    collection: configCollection,
    key: configKey,
    value: parameters,
    userId: '00000000-0000-0000-0000-000000000000',
    version: '*', // Only write if object doesn't exist already,
    permissionRead: 1,
    permissionWrite: 0
  };

  nk.storageWrite([ storageWrite ]);
}

const RemoteConfigRpc : nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string | void {
  const readRequest : nkruntime.StorageReadRequest = {
    collection: configCollection,
    key: configKey,
    userId: '00000000-0000-0000-0000-000000000000'
  };
  
  const records = nk.storageRead([readRequest]);
  if (records.length == 0) {
    throw new Error('No config found')
  }
  
  return JSON.stringify(records[0].value);
}

// Ensure the configuration object is stored, this call should be in InitModule.
SaveConfig(ctx, logger, nk)

// Register as RPC function, this call should be in InitModule.
initializer.registerRpc('rc', RemoteConfigRpc);

获取应用程序内参数 #

无论使用哪种方法存储应用程序内参数,您都可以通过HTTP请求获取配置。

.Net/Unity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Remember to change the host, port, and auth values for how you've setup your server
var host = "127.0.0.1";
var port = 7350;
var path = "rc";
var auth = "defaultkey";

var format = "http://{0}:{1}/v2/rpc/{2}?http_key={3}";
var url = string.Format(format, Host, Port, Path, Auth);
var headers = new Dictionary<string, string>();
headers.Add("Content-Type", "application/json");
headers.Add("Accept", "application/json");

WWW www = new WWW(url, null, headers);
yield return www;
if (!string.IsNullOrEmpty(www.error)) {
    Debug.LogErrorFormat("Error occurred: {0}", www.error);
} else {
    var response = Encoding.UTF8.GetString(www.bytes);
    Debug.Log(response);
    // output
    // {"rc":{"max_player_level":90,"min_version":12,"reachable_levels":10}}
}

Curl:

1
2
3
4
5
curl -X POST "http://127.0.0.1:7350/v2/rpc/rc?http_key=defaultkey" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json'
# output
# {"rc":{"max_player_level":90,"min_version":12,"reachable_levels":10}}