토너먼트 #

토너먼트는 짧은 기간 동안 서로 상을 받기 위해 경쟁하는 대회입니다.

이 문서에서는 Nakama 서버용 토너먼트 디자인에 대해 간단하게 설명합니다. 플레이어가 토너먼트, 참가할 수 있는 토너먼트, 점수 제출 시기를 찾는 방법 및 토너먼트 종료 시 보상이 분배되는 방법에 대한 규칙을 다룹니다.

규칙 #

선택 사항인 재설정 일정과 기간이 있는 토너먼트가 생성됩니다. 이 값을 사용하면 토너먼트가 다음 기간을 위해 재설정되기 전까지 토너먼트를 재생할 수 있는 시간을 편리하게 제어할 수 있습니다. 예를 들어 매일 정오에 시작하여 한 시간 동안만 진행되는 토너먼트를 만들 수 있습니다. 이 규칙은 CRON 표현식(“0 12 * * *")과 3600초의 기간으로 표현됩니다.

토너먼트에는 허용되는 상대방 수를 제한하고 (즉, 선착순) 선택적인 참가 요구 사항을 적용할 수 있습니다. 예를 들어 최초 10,000명의 상대방만 가입이 허용되는 경우 모든 상대방은 가입해야 점수를 제출할 수 있습니다.

토너먼트는 프로그래밍 방식으로 생성되어 나중에 시작하거나 생성 직후에 시작되며 모두 특별한 구성의 순위표로 표현됩니다.

토너먼트는 사용자가 아닌 상대도 플레이할 수 있습니다. 예를 들어 길드 ID로 점수를 제출하는 길드 토너먼트를 구현할 수 있습니다.

토너먼트 나열 #

서버에서 생성된 토너먼트를 찾습니다. 토너먼트는 카테고리와 시작 및 종료 시간을 통해 필터링할 수 있습니다.

시작 및 종료 시간 매개변수를 생략하면 진행 중인 토너먼트와 미래 토너먼트가 반환됩니다.

종료 시간 매개변수를 0(으)로 설정하면 결과에 종료 시간이 설정되지 않은 토너먼트만 포함됩니다.

종료 시간을 > 0 Unix 타임스탬프로 설정하면 상한선 역할을 하며 그 이전에 종료되는 토너먼트만 반환합니다(종료 시간이 없는 토너먼트 제외).

시작 시간을> 0 Unix 타임스탬프로 설정하면 이보다 늦은 시간에 시작하는 토너먼트가 반환됩니다.

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/tournament?category_start=<category_start>&category_end=<category_end>&start_time=<start_time>&end_time=<end_time>&limit=<limit>&cursor=<cursor>" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
6
7
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);
Client
1
2
3
4
5
6
7
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.ListTournamentsAsync(session, categoryStart, categoryEnd, startTime, endTime, limit, cursor);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
auto successCallback = [](NTournamentListPtr list)
{
    std::cout << "Tournament count " << list->tournaments.size() << std::endl;

    for (auto& tournament : list->tournaments)
    {
        std::cout << "Tournament ID " << tournament.id << ", title " << tournament.title << std::endl);
    }
};

uint32_t categoryStart = 1;
uint32_t categoryEnd = 2;
uint32_t startTime = 1538147711;
uint32_t endTime = null; // all tournaments from the start time
int32_t limit = 100; // number to list per page

client->listTournaments(session, categoryStart, categoryEnd, startTime, endTime, limit, opt::nullopt, successCallback);
Client
1
2
3
4
5
6
7
int categoryStart = 1;
int categoryEnd = 2;
int startTime = 1538147711;
int endTime = null; // all tournaments from the start time
int limit = 100; // number to list per page
String cursor = null;
TournamentList tournaments = client.listTournaments(session, categoryStart, categoryEnd, startTime, endTime, limit, cursor).get();
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var category_start = 1
var category_end = 2
var start_time = 1538147711
var end_time = null # all tournaments from the start time
var limit = 100 # number to list per page
var cursor = null
var result : NakamaAPI.ApiTournamentList = yield(client.list_tournaments_async(session, category_start, category_end, start_time, end_time, limit, cursor), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
print("Tournaments: %s" % [result])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
GET /v2/tournament
  ?category_start=<category_start>
  &category_end=<category_end>
  &start_time=<start_time>
  &end_time=<end_time>
  &limit=<limit>
  &cursor=<cursor>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Code snippet for this language Defold has not been found. Please choose another language to show equivalent examples.

토너먼트 가입 #

소유자가 점수를 제출하려면 토너먼트 가입이 필요할 수 있습니다. 이 작업은 멱등성이고 소유자가 이미 토너먼트에 가입한 경우에도 항상 성공합니다.

Client
1
2
curl -X POST "http://127.0.0.1:7350/v2/tournament/<tournament_id>/join" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
var id = "someid";
var success = await client.joinTournament(session, id);
Client
1
2
var id = "someid";
var success = await client.JoinTournamentAsync(session, id);
Client
1
2
3
4
5
6
7
auto successCallback = []()
{
    std::cout << "Successfully joined tournament" << std::cout;
};

string id = "someid";
client->joinTournament(session, id, successCallback);
Client
1
2
String id = "someid";
client.joinTournament(session, id).get();
Client
1
2
3
4
5
6
var id = "someid"
var success : NakamaAsyncResult = yield(client.join_tournament_async(session, id), "completed")
if success.is_exception():
    print("An error occurred: %s" % success)
    return
print("Joined tournament")
Client
1
2
3
4
5
POST /v2/tournament/<tournament_id>/join
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Code snippet for this language Defold has not been found. Please choose another language to show equivalent examples.

토너먼트 기록 나열 #

토너먼트 기록의 혼합 목록과 특정 소유자에게 속한 기록 배치를 가져옵니다. 이를 통해 상위 100명의 플레이어와 현재 사용자와 친구 사이의 점수를 보여주는 순위표 보기를 만들 수 있습니다.

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/tournament/<tournament_id>?owner_ids=<owner_ids>&limit=<limit>&cursor=<cursor>" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
6
var id = "someid";
var ownerIds = ["some", "friends", "user ids"];
var result = await client.listTournamentRecords(session, id, owenrIds);
result.records.forEach(function(record) {
  console.log("Record username %o and score %o", record.username, record.score);
});
Client
1
2
3
4
var id = "someid";
var limit = 100;
var cursor = null;
var result = await client.ListTournamentRecordsAsync(session, id, new []{ session.UserId }, limit, cursor);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
auto successCallback = [](NTournamentRecordListPtr list)
{
    for (auto& record : list->records)
    {
        std::cout << "Record username " << record.username << " and score " << record.score << std::endl;
    }
};

string id = "someid";
client->listTournamentRecords(session, id, opt::nullopt, opt::nullopt, {}, successCallback);
Client
1
2
String id = "someid";
LeaderboardRecordList records = client.listLeaderboardRecords(session, id, session.getUserId()).get();
Client
1
2
3
4
5
6
7
8
var id = "someid"
var limit = 100
var cursor = null
var result : NakamaAPI.ApiTournamentRecordList = yield(client.list_tournament_records_async(session, id, [session.user_id], limit, cursor), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
print("Records: %s" % [result])
Client
1
2
3
4
5
GET /v2/tournament/<tournament_id>?owner_ids=<owner_ids>&limit=<limit>&cursor=<cursor>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Code snippet for this language Defold has not been found. Please choose another language to show equivalent examples.

소유자 중심의 토너먼트 기록 나열 #

소유자 중심의 토너먼트 기록 목록을 가져옵니다.

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/tournament/<tournament_id>/owner/<owner_id>?limit=<limit>" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
var id = "someid";
var ownerId = "some user ID";
var limit = 100;
var result = await client.listTournamentRecordsAroundOwner(session, id, ownerId, limit);
Client
1
2
3
4
var id = "someid";
var ownerId = session.UserId;
var limit = 100;
var result = await client.ListTournamentRecordsAroundOwnerAsync(session, id, ownerId, limit);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
auto successCallback = [](NTournamentRecordListPtr list)
{
    for (auto& record : list->records)
    {
        std::cout << "Record username " << record.username << " and score " << record.score << std::endl;
    }
};

string id = "someid";
string ownerId = session->getUserId();
int32_t limit = 100;
client->listTournamentRecordsAroundOwner(session, id, ownerId, limit, successCallback);
Client
1
2
3
4
5
String id = "someid";
String ownerId = session.getUserId();
int expiry = -1;
int limit = 100;
TournamentRecordList records = client.listTournamentRecordsAroundOwner(session, id, ownerId, expiry, limit).get();
Client
1
2
3
4
5
6
7
8
var id = "someid"
var owner_id = "some user ID"
var limit = 100
var result : NakamaAPI.ApiTournamentRecordList = yield(client.list_tournament_records_around_owner_async(session, id, owner_id, limit), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
print("Records: %s" % [result])
Client
1
2
3
4
5
GET /v2/tournament/<tournament_id>/owner/<owner_id>?limit=<limit>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Code snippet for this language Defold has not been found. Please choose another language to show equivalent examples.

토너먼트 기록 쓰기 #

토너먼트 순위표에 점수 및 선택적 하위 점수를 제출합니다. 토너먼트가 가입 필요로 구성된 경우 소유자가 이미 토너먼트에 가입되어 있지 않으면 실패합니다.

Client
1
2
3
curl -X PUT "http://127.0.0.1:7350/v2/tournament/<tournament_id>" \
  -H 'Authorization: Bearer <session token>' \
  -d '{"score": 100, "subscore": 10, "metadata": "{"weather_conditions": "sunny", "track_name" : "Silverstone" }"}'
Client
1
2
3
4
5
6
7
8
var id = "someid";
var score = 100;
var subscore = 10;
var metadata = {
  "weather_conditions": "sunny",
  "track_name": "Silverstone"
}
var newrecord = client.writeTournamentRecord(session, id, score, subscore, metadata);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var id = "someid";
var score = 100L;
var subscore = 10L;
// using Nakama.TinyJson;
var metadata = new Dictionary<string, string>()
{
    { "weather_conditions", "sunny" },
    { "track_name", "Silverstone" }
}.ToJson();
var newRecord = await client.WriteTournamentRecordAsync(session, id, score, subscore, metadata);
Console.WriteLine(newRecord);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
auto successCallback = [this](const NLeaderboardRecord& record)
{
    std::cout << "written tournament record" << std::endl;
};

string id = "someid";
int64_t score = 100;
int64_t subscore = 10;
string metadata = "{"weather_conditions": "sunny", "track_name" : "Silverstone" }";
client->writeTournamentRecord(session, id, score, subscore, metadata, successCallback);
Client
1
2
3
4
5
string id = "someid";
int score = 10;
int subscore = 20;
final String metadata = "{"tarmac": "wet"}";
LeaderboardRecord record = client.writeTournamentRecord(session, id, score, subscore, metadata).get();
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var id = "someid"
var score = 100
var subscore = 10
var metadata = JSON.print({
    "weather_conditions": "sunny",
    "track_name": "Silverstone"
})
var new_record : NakamaAPI.ApiLeaderboardRecord = yield(client.write_tournament_record_async(session, id, score, subscore, metadata), "completed")
if new_record.is_exception():
    print("An error occurred: %s" % new_record)
    return
print("Record: %s" % [new_record])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
PUT /v2/tournament/<tournament_id>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>

{
  "score": 100,
  "subscore": 10,
  "metadata": "{"weather_conditions": "sunny", "track_name" : "Silverstone" }"
}
Code snippet for this language Defold has not been found. Please choose another language to show equivalent examples.

권한 부여 기능 #

런타임 기능은 서버 프레임워크를 통해 액세스할 수 있으며 사용자 지정 로직을 사용하여 토너먼트의 다양한 측면에 추가 규칙을 적용할 수 있습니다. 예를 들어 토너먼트에 가입하려는 상대방은 특정 수준보다 높아야 할 수 있습니다.

토너먼트 생성 #

모든 구성 옵션으로 토너먼트를 만드십시오.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
local id = "4ec4f126-3f9d-11e7-84ef-b7c182b36521"
local sort = "desc"     -- one of: "desc", "asc"
local operator = "best" -- one of: "best", "set", "incr"
local reset = "0 12 * * *" -- noon UTC each day
local metadata = {
  weather_conditions = "rain"
}
title = "Daily Dash"
description = "Dash past your opponents for high scores and big rewards!"
category = 1
start_time = nk.time() / 1000 -- starts now in seconds
end_time = 0                  -- never end, repeat the tournament each day forever
duration = 3600               -- in seconds
max_size = 10000              -- first 10,000 players who join
max_num_score = 3             -- each player can have 3 attempts to score
join_required = true          -- must join to compete
nk.tournament_create(id, sort, operator, duration, reset, metadata, title, description, category, start_time, end_time, max_size, max_num_score, join_required)
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// import "github.com/gofrs/uuid"
id := uuid.Must(uuid.NewV4())
sortOrder := "desc"  // one of: "desc", "asc"
operator := "best"   // one of: "best", "set", "incr"
resetSchedule := "0 12 * * *" // noon UTC each day
metadata := map[string]interface{}{}
title := "Daily Dash"
description := "Dash past your opponents for high scores and big rewards!"
category := 1
startTime := int(time.Now().UTC().Unix()) // start now
endTime := 0                         // never end, repeat the tournament each day forever
duration := 3600                     // in seconds
maxSize := 10000                     // first 10,000 players who join
maxNumScore := 3                     // each player can have 3 attempts to score
joinRequired := true                 // must join to compete
err := nk.TournamentCreate(ctx, id.String(), sortOrder, operator, resetSchedule, metadata, title, description, category, startTime, endTime, duration, maxSize, maxNumScore, joinRequired)
if err != nil {
      logger.Printf("unable to create tournament: %q", err.Error())
      return "", runtime.NewError("failed to create tournament", 3)
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let id = '4ec4f126-3f9d-11e7-84ef-b7c182b36521';
let sortOrder = nkruntime.SortOrder.DESCENDING;
let operator = nkruntime.Operator.BEST;
let duration = 3600;     // In seconds.
let resetSchedule = '0 12 * * *'; // Noon UTC each day.
let metadata = {
  weatherConditions: 'rain',
};
let title = 'Daily Dash';
let description = "Dash past your opponents for high scores and big rewards!";
let category = 1;
let startTime = 0;       // Start now.
let endTime = 0;         // Never end, repeat the tournament each day forever.

let maxSize = 10000;     // First 10,000 players who join.
let maxNumScore = 3;     // Each player can have 3 attempts to score.
let joinRequired = true; // Must join to compete.

try {
  nk.tournamentCreate(id, sortOrder, operator, duration, resetSchedule, metadata, title, description, category, startTime, endTime, maxSize, maxNumScore, joinRequired);
} catch (error) {
  // Handle error
}
재설정 일정으로 토너먼트를 생성하지 않으면 종료 시간을 제공해야 합니다.

토너먼트 삭제 #

ID로 토너먼트를 삭제합니다.

Server
1
2
local id = "4ec4f126-3f9d-11e7-84ef-b7c182b36521"
nk.tournament_delete(id)
Server
1
2
3
4
5
err := nk.TournamentDelete(ctx, id)
if err != nil {
    logger.Printf("unable to delete tournament: %q", err.Error())
    return "", runtime.NewError("failed to delete tournament", 3)
}
Server
1
2
3
4
5
6
let id = '4ec4f126-3f9d-11e7-84ef-b7c182b36521';
try {
  nk.tournamentDelete(id);
} catch (error) {
  // Handle error
}

득점 시도 추가 #

소유자의 토너먼트 기록에 추가 득점 시도를 추가합니다. 이것은 이 특정 소유자에 대해 토너먼트에서 허용되는 최대 득점 시도 수를 재정의합니다.

Server
1
2
3
4
local id = "someid"
local owner = "someuserid"
local attempts = 10
nk.tournament_add_attempt(id, owner, attempts)
Server
1
2
3
4
5
6
7
8
id := "someid"
userID := "someuserid"
attempts := 10
err := nk.TournamentAddAttempt(ctx, id, userID, attempts)
if err != nil {
    logger.Printf("unable to update user %v record attempts: %q", userID, err.Error())
    return "", runtime.NewError("failed to add tournament attempts", 3)
}
Server
1
2
3
4
5
6
7
8
let id = '4ec4f126-3f9d-11e7-84ef-b7c182b36521';
let owner = 'leaderboard-record-owner';
let count = -10;
try {
  nk.tournamentAddAttempt(id, owner, count);
} catch (error) {
  // Handle error
}

보상 분배 #

토너먼트의 활성 기간이 끝나면 서버에 등록된 기능이 호출되어 만료된 기록을 전달하여 보상을 계산하고 소유자에게 분배하는 데 사용할 수 있습니다.

Go에서 보상 분배 기능을 등록하려면 initializer을(를) 사용합니다.

Server
1
2
3
4
5
local nk = require("nakama")
local function distribute_rewards(_context, tournament, session_end, expiry)
  -- ...
end
nk.register_tournament_end(distribute_rewards)
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import (
    "context"
    "database/sql"
    "log"

    "github.com/heroiclabs/nakama/api"
    "github.com/heroiclabs/nakama/runtime"
)

func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
    if err := initializer.RegisterTournamentEnd(distributeRewards); err != nil {
        return err
    }
}

func distributeRewards(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, tournament *api.Tournament, end int64, reset int64) error {
    // ...
    return nil
}
Server
1
2
3
4
5
6
let distributeTournamentRewards: nkruntime.TournamentEndFunction = function(ctx: Context, logger: Logger, nk: Nakama, tournament: Tournament, end: number, reset: number) {
  // ...
}

// Inside InitModule function
initializer.registerTournamentEnd(tournamentEndFn);

상위 10명의 플레이어에게 지속적으로 알림을 보내 본인들이 승리했음을 알리고 가상 지갑에 코인을 추가하는 간단한 보상 분배 기능은 다음과 같습니다:

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local nk = require("nakama")
local function distribute_rewards(_context, tournament, session_end, expiry)
  local notifications = {}
  local wallet_updates = {}
  local records, owner_records, nc, pc = nk.leaderboard_records_list(tournament.id, nil, 10, nil, expiry)
  for i = 1, #records do
    notifications[i] = {
      code = 1,
      content = { coins = 100 },
      persistent = true,
      subject = "Winner",
      user_id = records[i].owner_id
    }
    wallet_updates[i] = {
      user_id = records[i].owner_id,
      changeset = { coins = 100 },
      metadata = {}
    }
  end

  nk.wallets_update(wallet_updates, true)
  nk.notifications_send(notifications)
end
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func distributeRewards(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, tournament *api.Tournament, end int64, reset int64) error {
    wallets := []*runtime.WalletUpdate{}
    notifications := []*runtime.NotificationSend{}
    content := map[string]interface{}{}
    changeset := map[string]int64{"coins": 100}
    records, _, _, _, err := nk.LeaderboardRecordsList(ctx, tournament.Id, []string{}, 10, "", reset)
    for _, record := range records {
        wallets = append(wallets, &runtime.WalletUpdate{record.OwnerId, changeset, content})
        notifications = append(notifications, &runtime.NotificationSend{record.OwnerId, "Leaderboard winner", content, 1, "", true})
    }
    _, err = nk.WalletsUpdate(ctx, wallets, false)
    if err != nil {
        logger.Error("failed to update winner wallets: %v", err)
        return err
    }
    err = nk.NotificationsSend(ctx, notifications)
    if err != nil {
        logger.Error("failed to send winner notifications: %v", err)
        return err
    }
    return nil
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
let distributeTournamentRewards: nkruntime.TournamentEndFunction = function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, tournament: nkruntime.Tournament, end: number, reset: number) {
  let notifications: nkruntime.NotificationRequest[] = [];
  let walletUpdates: nkruntime.WalletUpdate[] = []
  let results = nk.leaderboardRecordsList(tournament.id, [], 10, '', reset);
  results.records?.forEach(function (r) {
    notifications.push({
      code: 1,
      content: { coins: 100 },
      persistent: true,
      subject: "Winner",
      userId: r.ownerId,
    });

    walletUpdates.push({
      userId: r.ownerId,
      changeset: { coins: 100 },
    });
  });

  nk.walletsUpdate(walletUpdates, true)
  nk.notificationsSend(notifications)
}

고급 #

토너먼트를 사용하여 리그 시스템을 만들 수 있습니다. 리그와 토너먼트의 주요 차이점은 리그는 일반적으로 계절적이며 상대방이 진행할 수 있는 래더 또는 계층이 통합되어 있다는 것입니다.

리그는 동일한 재설정 일정과 기간을 공유하는 토너먼트 모음으로 구성될 수 있습니다. 보상 분배 기능을 사용하여 각 재설정 일정 사이에 한 토너먼트와 다음 토너먼트 사이에서 상대방을 진행할 수 있습니다.

이에 대한 예는 티어 리그 가이드를 참조하세요.

토너먼트 메타데이터 #

각 토너먼트 및 토너먼트 기록에는 토너먼트 자체에 대한 추가 데이터, 제출 중인 점수 및 점수 소유자에 대한 추가 데이터가 선택적으로 포함될 수 있습니다. 추가 필드는 JSON으로 인코딩되어 메타데이터로 제출해야 합니다.

메타데이터의 용례는 날씨와 같은 운전 게임의 레이스 조건에 대한 정보로서 사용자가 레코드를 나열할 때 추가 UI 힌트를 제공할 수 있습니다.

1
2
3
4
5
{
  "surface": "wet",
  "timeOfDay": "night",
  "car": "Porsche 918 Spyder"
}