Friend and Group Leaderboards

Nakama Leaderboards give players a competitive activity to engage around. As a game grows, massive leaderboards can feel static and more challenging for players to climb the ranks. You can address this with:

Using the leaderboard list API you can pass in a list of user IDs to create a custom leaderboard view.

The following code samples show you how to get leaderboard records for a group’s members or user’s friends.

Creating the leaderboard

Create a leaderboard on the server that resets every Monday at 00:00.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
  // Create a weekly leaderboard
  id := "weekly_leaderboard"
  authoritative := false
  sortOrder := "desc"
  operator := "incr"
  resetSchedule := "0 0 * * 1"
  metadata := make(map[string]interface{})

  if err := nk.LeaderboardCreate(ctx, id, authoritative, sortOrder, operator, resetSchedule, metadata); err != nil {
    logger.Error("error creating leaderboard")
    return err
  }
}

Getting a custom view of the leaderboard

Create a payload struct for the RPC:

1
2
3
4
5
6
type leaderboardRecord struct {
  Username string `json:"username"`
  UserId   string `json:"userId"`
  Score    int    `json:"score"`
  Rank     int    `json:"rank"`
}

Create a helper function that will take an array of user IDs and return an array of those records along with the relative Rank value based on user scores.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func getLeaderboardForUsers(leaderboardId string, userIds []string, ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule) ([]leaderboardRecord, error) {
  // Get all leaderboard records for user Ids
  _, records, _, _, err := nk.LeaderboardRecordsList(ctx, leaderboardId, userIds, 0, "", 0)
  if err != nil {
    return nil, err
  }

  // Create result slice and add a rank value
  results := []leaderboardRecord{}
  for i, record := range records {
    r := leaderboardRecord{
      Username: record.Username.Value,
      UserId:   record.OwnerId,
      Score:    int(record.Score),
      Rank:     len(records) - i,
    }
    results = append(results, r)
  }

  return results, nil
}

Getting the group leaderboard view

Now that you have a general function to get a leaderboard for a list of users, create a function to get a leaderboard for a group.

Create a payload struct for the remote procedure:

1
2
3
4
type groupLeaderboardRecordsPayload struct {
  GroupId       string `json:"groupId"`
  LeaderboardId string `json:"leaderboardId"`
}

Create a function that will get a slice of members from a group and then get the leaderboard for them:

 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
func getGroupLeaderboardRecords(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
  // Unmarshal the payload
  data := &groupLeaderboardRecordsPayload{}
  if err := json.Unmarshal([]byte(payload), data); err != nil {
    logger.Error("error unmarshaling payload")
    return "", err
  }

  // Get leaderboard
  leaderboards, err := nk.LeaderboardsGetId(ctx, []string{data.LeaderboardId})
  if err != nil {
    logger.Error("error getting leaderboards")
    return "", err
  }

  if len(leaderboards) == 0 {
    errorMessage := fmt.Sprintf("error finding leaderboard: %s", data.LeaderboardId)
    logger.Error(errorMessage)
    return "", errors.New(errorMessage)
  }

  // Get group members
  members, _, err := nk.GroupUsersList(ctx, data.GroupId, 100, nil, "")
  if err != nil {
    logger.Error("error getting group members")
    return "", err
  }

  // Get a slice of memberIds
  memberIds := []string{}
  for _, member := range members {
    memberIds = append(memberIds, member.User.Id)
  }

  // Get all leaderboard records for users
  results, err := getLeaderboardForUsers(leaderboards[0].Id, memberIds, ctx, logger, nk)
  if err != nil {
    logger.Error("error getting leaderboard records")
    return "", err
  }

  // Return the leaderboard records to the user
  bytes, err := json.Marshal(results)
  if err != nil {
    logger.Error("error marshaling result")
    return "", err
  }

  return string(bytes), nil
}

Register the function and expose it as a remote procedure that can be called from the client:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
  // ...

  // Register RPC to retrieve a custom view over a leaderboard based on group members
  if err := initializer.RegisterRpc("getGroupLeaderboardRecords", getGroupLeaderboardRecords); err != nil {
    logger.Error(`error registering "getGroupLeaderboardRecords" rpc`)
    return err
  }

  return nil
}

Getting the friend leaderboard view

Similarly, create a function to get a leaderboard for a user’s friends.

Create a payload struct for the remote procedure:

1
2
3
type friendsLeaderboardRecordsPayload struct {
  LeaderboardId string `json:"leaderboardId"`
}

Create a function that will get a slice of a user’s friends and then get the leaderboard for them:

 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
func getFriendLeaderboardRecords(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
  // Unmarshal the payload
  data := &friendsLeaderboardRecordsPayload{}
  if err := json.Unmarshal([]byte(payload), data); err != nil {
    logger.Error("error unmarshaling payload")
    return "", err
  }

  // Get leaderboard
  leaderboards, err := nk.LeaderboardsGetId(ctx, []string{data.LeaderboardId})
  if err != nil {
    logger.Error("error getting leaderboards")
    return "", err
  }

  if len(leaderboards) == 0 {
    errorMessage := fmt.Sprintf("error finding leaderboard: %s", data.LeaderboardId)
    logger.Error(errorMessage)
    return "", errors.New(errorMessage)
  }

  // Get user id from context
  userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
  if !ok {
    errorMessage := fmt.Sprintf("error getting user id from context")
    logger.Error(errorMessage)
    return "", errors.New(errorMessage)
  }

  // Get friends (where state is 0 - mutual friends)
  state := 0
  friends, _, err := nk.FriendsList(ctx, userId, 100, &state, "")
  if err != nil {
    logger.Error("error getting friends")
    return "", err
  }

  // Get a slice of memberIds
  friendIds := []string{}
  for _, member := range friends {
    friendIds = append(friendIds, member.User.Id)
  }

  // Get all leaderboard records for users
  results, err := getLeaderboardForUsers(leaderboards[0].Id, friendIds, ctx, logger, nk)
  if err != nil {
    logger.Error("error getting leaderboard records")
    return "", err
  }

  // Return the leaderboard records to the user
  bytes, err := json.Marshal(results)
  if err != nil {
    logger.Error("error marshaling result")
    return "", err
  }

  return string(bytes), nil
}

Register the function and expose it as a remote procedure that can be called from the client:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
  // ...

  // Register RPC to retrieve a custom view over a leaderboard based on friends
  if err := initializer.RegisterRpc("getFriendLeaderboardRecords", getFriendLeaderboardRecords); err != nil {
    logger.Error(`error registering "getFriendLeaderboardRecords" rpc`)
    return err
  }

  return nil
}