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.

Server
 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
  }
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let InitModule: nkruntime.InitModule = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  const id = 'weekly_leaderboard';
  const authoritative = false;
  const sortOrder = nkruntime.SortOrder.DESCENDING;
  const operator = nkruntime.Operator.INCREMENTAL;
  const resetSchedule = '0 0 * * 1';
  const metadata = {};

  nk.leaderboardCreate(id, authoritative, sortOrder, operator, resetSchedule, metadata);
}
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

Getting a custom view of the leaderboard #

Define the payload structure for the RPC:

Server
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"`
}
Server
1
2
3
4
5
6
interface LeaderboardRecord {
  username: string,
  userId: string,
  score: number,
  rank: number
}
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

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.

Server
 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
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const getLeaderboardForUsers = function (leaderboardId: string, userIds: string[], logger: nkruntime.Logger, nk: nkruntime.Nakama): LeaderboardRecord[] {
  const recordsList = nk.leaderboardRecordsList(leaderboardId, userIds);
  const results: LeaderboardRecord[] = [];

  recordsList.records.forEach(function (leaderboardRecord, i) {
    results.push({
      username: leaderboardRecord.username,
      userId: leaderboardRecord.ownerId,
      score: leaderboardRecord.score,
      rank: recordsList.records.length - i
    });
  });

  return results;
}
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

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.

Define a payload structure for the RPC:

Server
1
2
3
4
type groupLeaderboardRecordsPayload struct {
  GroupId       string `json:"groupId"`
  LeaderboardId string `json:"leaderboardId"`
}
Server
1
2
3
4
interface groupLeaderboardRecordsPayload {
  groupId: string,
  leaderboardId: string
}
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

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

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
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
}
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
const getGroupLeaderboardRecords: nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string | void {
  // Parse the payload
  const data = <groupLeaderboardRecordsPayload> JSON.parse(payload);
  
  if (!data) {
    throw new Error('Error parsing payload');
  }
  
  // Get leaderboard
  const leaderboards = nk.leaderboardsGetId([data.leaderboardId]);
  
  if (leaderboards.length == 0) {
    throw new Error(`Error finding leaderboard: ${data.leaderboardId}`);
  }
  
  // Get group members
  const members = nk.groupUsersList(data.groupId, 100);
  
  // Get a slice of memberIds
  const memberIds: string[] = [];
  members.groupUsers.forEach(function (groupUser) {
    memberIds.push(groupUser.user.userId);
  });
  
  // Get all leaderboard records for users
  const results = getLeaderboardForUsers(leaderboards[0].id, memberIds, logger, nk);
  
  // Return the leaderboard records to the user
  return JSON.stringify(results);
};
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

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

Server
 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
}
Server
1
2
3
4
5
6
let InitModule: nkruntime.InitModule = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  // ... 

  // Register RPC to retrieve a custom view over a leaderboard based on group members
  initializer.registerRpc('getGroupLeaderboardRecords', getGroupLeaderboardRecords);
}
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

Getting the friend leaderboard view #

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

Define a payload structure for the RPC:

Server
1
2
3
type friendsLeaderboardRecordsPayload struct {
  LeaderboardId string `json:"leaderboardId"`
}
Server
1
2
3
interface friendsLeaderboardReocrdsPayload {
  leaderboardId: string
}
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

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

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
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
}
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
const getFriendLeaderboardRecords: nkruntime.RpcFunction = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string | void {
  // Parse the payload
  const data = <friendsLeaderboardReocrdsPayload> JSON.parse(payload);

  if (!data) {
    throw new Error('Error parsing payload');
  }

  // Get leaderboard
  const leaderboards = nk.leaderboardsGetId([data.leaderboardId]);

  if (leaderboards.length == 0) {
    throw new Error(`Error finding leaderboard: ${data.leaderboardId}`);
  }

  // Get friends (where state is 0 - mutual friends)
  const friendsList = nk.friendsList(ctx.userId, 100, 0);

  // Get a slice of friendIds
  const friendIds: string[] = [];
  friendsList.friends.forEach(function (friend) {
    friendIds.push(friend.user.userId);
  });

  // Get all leaderboard records for users
  const results = getLeaderboardForUsers(leaderboards[0].id, friendIds, logger, nk);

  // Return the leaderboard records to the user
  return JSON.stringify(results);
};
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.

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

Server
 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
}
Server
1
2
3
4
5
6
let InitModule: nkruntime.InitModule = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  // ...

  // Register RPC to retrieve a custom view over a leaderboard based on friends
  initializer.registerRpc('getFriendLeaderboardRecords', getFriendLeaderboardRecords);
}
Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.