버킷으로 구성된 순위표 #

진행하기 전에 순위표 문서를 확인하여 관련 개념과 기능을 파악하십시오.

게임의 플레이어 기반이 성장함에 따라 온라인 게임에서 사회적 상호 작용의 근본적인 문제가 나타납니다. 글로벌 순위표와 일반적으로 순위표는 특정 기술이나 점수 수준에서 거의 움직이지 않는 정적인 상태가 됩니다. 이로 인해 새로운 플레이어의 참여가 없거나 부족하여 게임이 계속되지 않습니다. 이에 대한 한 가지 해결책은 버킷으로 구성된 순위표를 만드는 것입니다.

버킷으로 구성된 순위표에서 플레이어는 다른 모든 플레이어와 게임을 하지 않고 대신 다른 플레이어(대체로 25~50명)의 제한된 보기를 봅니다. 이들은 순위표가 만료되거나 다른 시작 시간으로 이월될 때까지 이 그룹과 경쟁합니다. 이러한 소규모 플레이어 그룹은 버킷으로 구성된 순위표 이름이 유래된 “플레이어 버킷” 또는 코호트로 알려져 있습니다.

버킷으로 구성된 순위표는 Rovio의 Angry Birds 프랜차이즈에 있는 여러 타이틀을 포함하여 많은 인기 게임에서 볼 수 있습니다. Nakama의 저장소 엔진, 순위표토너먼트 기능을 사용하면 서버 런타임 코드, 특히 RPC 함수를 사용하여 게임에서 버킷으로 구성된 순위표를 구현할 수 있습니다.

Nakama 순위표 API를 사용하면 이미 해당 플레이어만 포함된 순위표의 “버킷 보기"를 생성하는 데 사용되는 필터가 되는 일련의 사용자 ID(또는 사용자 이름)를 전달할 수 있습니다.

이 경우 특정 플레이어에 대해 설정된 사용자 ID를 구성하는 방법과 개인화되거나 다른 게임 기준을 따르는지 여부가 결정되어야 합니다. 이것은 구현 성공의 핵심이며 게임의 특정 메커니즘에 따라 다릅니다. 비슷한 순위의 플레이어, VIP 플레이어, 사용자의 친구, 같은 지역의 플레이어로 구성된 “보기"가 필요할 수 있습니다.

여기의 예에서는 특정 플레이어에게 표시되는 버킷으로 사용자의 “임의” 선택을 사용하겠습니다. 먼저 코드의 주요 부분을 살펴보고 마지막에 참조용으로 전체 파일을 제공하겠습니다.

버킷으로 구성된 순위표 생성 #

여기에서 매주 이월되는 새 버킷으로 구성된 순위표 Bucketed Weekly #1을(를) 만듭니다.

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
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
  // Set up the bucketed leaderboards tournament
  id := "bucketed_weekly"
  authoritative := true
  sortOrder := "desc"
  operator := "incr"
  resetSchedule := "0 0 * * 0"
  metadata := map[string]interface{}{}
  title := "Bucketed Weekly #1"
  description := ""
  category := 1
  startTime := 0
  endTime := 0
  duration := 604800
  maxSize := 100000000
  maxNumScore := 100000000
  joinRequired := false

  if err := nk.TournamentCreate(ctx, id, authoritative, sortOrder, operator, resetSchedule, metadata, title, description, category, startTime, endTime, duration, maxSize, maxNumScore, joinRequired); err != nil {
    return err
  }
  if err := initializer.RegisterRpc("get_bucket_records", rpcGetBucketRecordsFn([]string{id}, 2000)); err != nil {
    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
let InitModule: nkruntime.InitModule = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  // Set up the bucketed leaderboards tournament
  const id = 'bucketed_weekly';
  const authoritative = true;
  const sortOrder = nkruntime.SortOrder.DESCENDING;
  const operator = nkruntime.Operator.INCREMENTAL;
  const duration = 604800;
  const resetSchedule = '0 0 * * 0';
  const metadata = {};
  const title = 'Bucketed Weekly #1';
  const description = '';
  const category = 1;
  const startTime = 0;
  const endTime = 0;
  const maxSize = 100000000;
  const maxNumScore = 10000000;
  const joinRequired = false;

  nk.tournamentCreate(id, authoritative, sortOrder, operator, duration, resetSchedule, metadata, title, description, category, startTime, endTime, maxSize, maxNumScore, joinRequired);
  initializer.registerRpc('get_bucket_records', RpcGetBucketRecords([id], 2000));
}
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

사용자 버킷 생성 #

각 플레이어는 자신을 위해 생성되며 고유한 개별 버킷(자신이 상대하는 상대방 세트)을 갖습니다. 먼저 버킷 저장소 객체를 정의합니다:

Server
1
2
3
4
5
// Define the bucketed leaderboard storage object
type userBucketStorageObject struct {
  ResetTimeUnix uint32   `json:"resetTimeUnix"`
  UserIDs       []string `json:"userIds"`
}
Server
1
2
3
4
5
// Define the bucketed leaderboard storage object
interface UserBucketStorageObject {
  resetTimeUnix: number,
  userIds: string[]
}
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

그런 다음 플레이어의 ID를 가져오고 기존 버킷이 있는지 확인하는 RPC 함수를 정의합니다. 있으면 가져오고 없으면 새 버킷을 만듭니다. 마지막으로 실제 순위표를 가져옵니다.

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
// Get a user's bucket (records) and generate a new bucket if needed
func RpcGetBucketRecordsFn(ids []string, bucketSize int) func(context.Context, runtime.Logger, *sql.DB, runtime.NakamaModule, string) (string, error) {
  return func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
    if len(payload) > 0 {
      return "", ErrNoInputAllowed
    }

    userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
    if !ok {
      return "", ErrNoUserIdFound
    }

    collection := "buckets"
    key := "bucket"

    objects, err := nk.StorageRead(ctx, []*runtime.StorageRead{
      {
        Collection: collection,
        Key:        key,
        UserID:     userID,
      },
    })
    if err != nil {
      logger.Error("nk.StorageRead error: %v", err)
      return "", ErrInternalError
    }

    // Fetch any existing bucket or create one if none exist
    userBucket := &userBucketStorageObject{ResetTimeUnix: 0, UserIDs: []string{}}
    if len(objects) > 0 {
      if err := json.Unmarshal([]byte(objects[0].GetValue()), userBucket); err != nil {
        logger.Error("json.Unmarshal error: %v", err)
        return "", ErrUnmarshal
      }
    }

    // Fetch the leaderboard
    leaderboards, err := nk.LeaderboardsGetId(ctx, ids)
    if err != nil {
      logger.Error("nk.LeaderboardsGetId error: %v", err)
      return "", ErrInternalError
    }
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
const RpcGetBucketRecordsFn = function (ids: string[], bucketSize: number) {
  return function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) {
    if (!payload) {
      throw new Error('no payload input allowed');
    }

    const collection = 'buckets';
    const key = 'bucket';

    const objects = nk.storageRead([
      {
        collection,
        key,
        userId: ctx.userId
      }
    ]);

    // Fetch any existing bucket or create one if none exist
    let userBucket: UserBucketStorageObject = { resetTimeUnix: 0, userIds: [] };

    if (objects.length > 0) {
      userBucket = objects[0].value as UserBucketStorageObject;
    }

    // Fetch the tournament leaderboard
    const leaderboards = nk.tournamentsGetId(ids);
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

버킷 상태 확인 #

계속 진행하기 전에 순위표가 재설정되었는지 또는 상대방이 없는지 확인합니다. 어느 경우든 새로운 상대방 세트를 생성해야 합니다.

Server
1
2
3
4
5
6
// Leaderboard has reset or no current bucket exists for user
if userBucket.ResetTimeUnix != leaderboards[0].GetPrevReset() || len(userBucket.UserIDs) < 1 {
  logger.Debug("rpcGetBucketRecordsFn new bucket for %q", userID)

  // Code below goes here...
}
Server
1
2
3
4
5
6
// Leaderboard has reset or no current bucket exists for user
if (userBucket.resetTimeUnix != leaderboards[0].endActive || userBucket.userIds.length < 1) {
  logger.debug(`RpcGetBucketRecordsFn new bucket for ${ctx.userId}`);

  // Code below goes here
}
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

상대방 세트 생성 #

임의 상대방 목록을 생성하기 위해 Nakama 3.5.0에서 사용할 수 있는 UsersGetRandom 함수를 사용합니다.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
userBucket.UserIDs = nil
logger.Debug("rpcGetBucketRecordsFn new bucket for %q", userID)

users, err := nk.UsersGetRandom(ctx, bucketSize)
if err != nil {
  logger.Error("Error getting random users.")
  return "", ErrInternalError
}

for _, user := range users {
  userBucket.UserIDs = append(userBucket.UserIDs, user.Id)
}
Server
1
2
3
4
const users = nk.usersGetRandom(bucketSize);
users.forEach(function (user: nkruntime.User) {
  userBucket.userIds.push(user.userId);
});
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

궁극적으로 구체적으로 정의된 상대방 목록(예: 수준 10-20 플레이어만 해당)으로 끝내고 싶다면 데이터베이스 쿼리에서 원하는 버킷 크기의 요소로 (오버)스캔한 다음 애플리케이션 계층에서 관련 기준(플레이어 메타데이터)에 따라 필터링하는 것이 좋습니다.

새 버킷 쓰기 #

새 상대방 목록을 생성한 후 이 새 사용자 버킷을 작성하고 먼저 버킷 재설정 시간과 순위표 종료 시간이 일치하도록 설정합니다.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Set the Reset and Bucket end times to be in sync
userBucket.ResetTimeUnix = leaderboards[0].GetNextReset()

value, err := json.Marshal(userBucket)
if err != nil {
  return "", ErrMarshal
}

// Store generated bucket for the user
if _, err := nk.StorageWrite(ctx, []*runtime.StorageWrite{
  {
    Collection:      collection,
    Key:             key,
    PermissionRead:  0,
    PermissionWrite: 0,
    UserID:          userID,
    Value:           string(value),
  },
}); err != nil {
  logger.Error("nk.StorageWrite error: %v", err)
  return "", ErrInternalError
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Set the Reset and Bucket end times to be in sync
userBucket.resetTimeUnix = leaderboards[0].endActive;

// Store generated bucket for the user
nk.storageWrite([{
  collection,
  key,
  userId: ctx.userId,
  value: userBucket,
  permissionRead: 0,
  permissionWrite: 0
}]);
Code snippet for this language Lua 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
// Add self to the list of leaderboard records to fetch
userBucket.UserIDs = append(userBucket.UserIDs, userID)

_, records, _, _, err := nk.LeaderboardRecordsList(ctx, ids[0], userBucket.UserIDs, bucketSize, "", 0)
if err != nil {
  logger.Error("nk.LeaderboardRecordsList error: %v", err)
  return "", ErrInternalError
}

result := &api.LeaderboardRecordList{Records: records}
encoded, err := json.Marshal(result)
if err != nil {
  return "", ErrMarshal
}

logger.Debug("rpcGetBucketRecordsFn resp: %s", encoded)
return string(encoded), nil
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Add self to the list of leaderboard records to fetch
userBucket.userIds.push(ctx.userId);

// Get the leaderboard records
const records = nk.tournamentRecordsList(ids[0], userBucket.userIds, bucketSize);

const result = JSON.stringify(records);
logger.debug(`RpcGetBucketRecordsFn resp: ${result}`);

return JSON.stringify(records);
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

Nakama 콘솔을 사용하여 순위표 및 해당 설정을 검증할 수 있습니다:

버킷으로 구성된 순위표
버킷으로 구성된 순위표

예제 파일 #

전체 예제 파일 bucketed_leaderboards.go, bucketed_leaderboards.ts을 다운로드합니다.