원격 구성 #

원격 구성은 원격 서버에 저장된 인앱 매개변수를 사용하여 앱 또는 게임의 동작을 사용자 지정하는 방법입니다. 이것은 다음 구현에 사용할 수 있습니다 기능 플래그 또는 앱 또는 게임의 모양이나 동작을 변경하는 설정을 조정합니다.

개발자는 원격 구성을 사용하여 긴 검토 프로세스나 게임 또는 앱을 수정한 다음 사용자의 업데이트를 대기하는 번거로움을 없앨 수 있습니다. 이는 모바일 프로젝트에서 특히 유용합니다.

인앱 매개변수 관리 #

앱이나 게임으로 전송된 구성 설정은 서버에 저장해야 합니다. 정보를 저장하는 가장 좋은 방법은 데이터의 변경 빈도에 따라 다릅니다.

대부분 정적 데이터의 경우 서버 측 코드에 데이터 구조로 포함하는 것이 가장 효율적이며 더 동적인 데이터의 경우 읽기 전용 저장소 레코드를 사용하는 것이 좋습니다.

이 두 가지 접근 방식을 모두 사용하는 경우 등록/로그인을 완료하거나 사용자 세션으로 연결하기 전에 원격 구성에 액세스할 수 있습니다. 구성하는 인앱 매개변수는 애플리케이션 시작 초기에 초기화할 수 있습니다.

정적 매개변수 #

가장 간단한 접근 방식의 경우 서버 측 코드를 사용하여 인앱 매개변수를 정적 변수로 나타냅니다. 서버가 시작된 후 매개변수를 변경하려면 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);

Dynamic parameters #

For in-app parameters which may be changed via Analytics or with a LiveOps dashboard it’s more flexible to store the configuration settings in the storage engine as a read-only record.

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}}