Leaderboards are a great way to add a social and competitive element to any game. They’re a fun way to drive competition among your players. The server supports an unlimited number of individual leaderboards with each one as a scoreboard which tracks separate records.
The server has no special requirement on what the score value should represent from your game. A leaderboard is created with a sort order on values. If you’re using lap time or currency in records you’ll want to order the results in ASC or DESC mode, as preferred. At creation time you must also specify the operator which controls how scores are submitted to the leaderboard: “best”, “set”, “incr”, or “decr”.
You can use a leaderboard to track any score you like. Some good examples are: highest points, longest survival time, fastest lap time, quickest level completion, and anything else which can be competed over!
Leaderboards are dynamic in the server because they don’t need to be preconfigured like would be needed if you’ve used Google Play Games or Apple Game Center in the past. A leaderboard can be created via server-side code.
Each leaderboard is a collection of records where each record is a ranked score with metadata. A leaderboard is uniquely identified by an ID.
Leaderboard records are sorted based on their configured sort order: DESC (default) or ASC. The sort order is decided when a leaderboard is created and cannot be changed later.
Configure how leaderboard record scores are set using the following operator values:
set - The value will be set to the submitted score
best - The value will only be updated if the score being submitted is higher than the existing score
incr - The value will be incremented by the score being submitted to it
decr - The value will be decreased by the score being submitted to it
The operator cannot be changed after the leaderboard is created. All leaderboard configuration is immutable once created. You should delete the leaderboard and create a new one if you need to change the sort order or operator.
You can implement arcade-style leaderboards - where a single user posts multiple entries - by setting the entry’s id to a combination of the user’s ID and a current Unix time (e.g., <user-id>:<unix-time>).
You can assign each leaderboard an optional reset schedule. Records contained in the leaderboard will expire based on this schedule and users will be able to submit new scores for each reset cycle. At the expiry of each reset period the server triggers callbacks with the current leaderboard state. Read about about it Leaderboards Best Practices and see the Tiered Leagues guide for an example use case.
Reset schedules are defined in CRON format when the leaderboard is created. If a leaderboard has no reset schedule set its records will never expire.
Each leaderboard contains a list of records ordered by their scores.
All records belong to an owner. This is usually a user but other objects like a group ID or some other custom ID can be used. Each owner will only have one record per leaderboard. If a leaderboard expires each owner will be able to submit a new score which rolls over.
The score in each record can be updated as the owner progresses. Scores can be updated as often as wanted and can increase or decrease depending on the combination of leaderboard sort order and operator.
Each record can optionally include additional data about the score or the owner when submitted. The extra fields must be JSON encoded and submitted as the metadata. A good use case for metadata is info about race conditions in a driving game, such as weather, which can give extra UI hints when users list scores.
varleaderboardId="level1";varsubmission={score:100};varrecord=awaitclient.writeLeaderboardRecord(session,leaderboardId,submission);console.log("New record username %o and score %o",record.username,record.score);
Client
1
2
3
4
conststringleaderboardId="level1";constlongscore=100L;varr=awaitclient.WriteLeaderboardRecordAsync(session,leaderboardId,score);System.Console.WriteLine("New record for '{0}' score '{1}'",r.Username,r.Score);
Client
1
2
3
4
5
6
7
8
9
autosuccessCallback=[](constNLeaderboardRecord&record){std::cout<<"New record with score "<<record.score<<std::endl;};stringleaderboardId="level1";int64_tscore=100;client->writeLeaderboardRecord(session,leaderboardId,score,opt::nullopt,opt::nullopt,successCallback);
Client
1
2
3
4
finalStringleaderboard="level1";longscore=100L;LeaderboardRecordr=client.writeLeaderboardRecord(session,leaderboard,score);System.out.format("New record for %s score %s",r.getUsername(),r.getScore());
Client
1
2
3
4
5
6
7
varleaderboard_id="level1"varscore=100varrecord:NakamaAPI.ApiLeaderboardRecord=yield(client.write_leaderboard_record_async(session,leaderboard_id,score),"completed")ifrecord.is_exception():print("An error occurred: %s"%record)returnprint("New record username %s and score %s"%[record.username,record.score])
A leaderboard can be created via server-side code at startup or within a registered function. The ID given to the leaderboard is used to submit scores to it.
A user can submit a score to a leaderboard and update it at any time. When a score is submitted the leaderboard’s pre-configured sort order and operator determine what effect the operation will have.
The “set” operator will ensure the leaderboard record always keeps the latest value, even if it is worse than the previous one.
Submitting to a leaderboard with the “best” operator ensures the record tracks the best value it has seen for that record. For a descending leaderboard this means the highest value, for an ascending one the lowest. If there is no previous value for the record, this behaves like “set”.
With the “incr” operator the new value is added to any existing score for that record. If there is no previous value for the record, this behaves like “set”.
varleaderboardId="level1";varsubmission={score:100};varrecord=awaitclient.writeLeaderboardRecord(session,leaderboardId,submission);console.log("New record username %o and score %o",record.username,record.score);
Client
1
2
3
4
conststringleaderboard="level1";constlongscore=100L;varr=awaitclient.WriteLeaderboardRecordAsync(session,leaderboard,score);System.Console.WriteLine("New record for '{0}' score '{1}'",r.Username,r.Score);
Client
1
2
3
4
5
6
7
8
9
autosuccessCallback=[](constNLeaderboardRecord&record){std::cout<<"New record with score "<<record.score<<std::endl;};stringleaderboardId="level1";int64_tscore=100;client->writeLeaderboardRecord(session,leaderboardId,score,opt::nullopt,opt::nullopt,successCallback);
Client
1
2
3
4
finalStringleaderboard="level1";longscore=100L;LeaderboardRecordr=client.writeLeaderboardRecord(session,leaderboard,score);System.out.format("New record for %s score %d",r.getUsername(),r.getScore());
Client
1
2
3
4
5
6
7
varleaderboard_id="level1"varscore=100varrecord:NakamaAPI.ApiLeaderboardRecord=yield(client.write_leaderboard_record_async(session,leaderboard_id,score),"completed")ifrecord.is_exception():print("An error occurred: %s"%record)returnprint("New record username %s and score %s"%[record.username,record.score])
The standard way to list records is ordered by score based on the sort order in the leaderboard.
Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>"\
-H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
varleaderboardId="level1";varresult=awaitclient.listLeaderboardRecords(session,leaderboardId);result.records.forEach(function(record){console.log("Record username %o and score %o",record.username,record.score);});
Client
1
2
3
4
5
6
conststringleaderboardId="level1";varresult=awaitclient.ListLeaderboardRecordsAsync(session,leaderboardId);foreach(varrinresult.Records){System.Console.WriteLine("Record for '{0}' score '{1}'",r.Username,r.Score);}
Client
1
2
3
4
5
6
7
8
9
10
11
autosuccessCallback=[](NLeaderboardRecordListPtrrecordsList){for(auto&record:recordsList->records){std::cout<<"Record username "<<record.username<<" and score "<<record.score<<std::endl;}};stringleaderboardId="level1";client->listLeaderboardRecords(session,leaderboardId,{},opt::nullopt,opt::nullopt,successCallback);
Client
1
2
3
4
5
finalStringleaderboard="level1";LeaderboardRecordListrecords=client.listLeaderboardRecords(session,leaderboard);for(LeaderboardRecordrecord:records.getRecordsList()){System.out.format("Record for %s score %d",record.getUsername(),record.getScore());}
Client
1
2
3
4
5
6
7
8
varleaderboard_id="level1"varresult:NakamaAPI.ApiLeaderboardRecordList=yield(client.list_leaderboard_records_async(session,leaderboard_id),"completed")ifresult.is_exception():print("An error occurred: %s"%result)returnforrinresult.records:varrecord:NakamaAPI.ApiLeaderboardRecord=rprint("Record username %s and score %s"%[record.username,record.score])
You can fetch the next set of results with a cursor.
Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>?cursor=<next_cursor>"\
-H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
varleaderboardId="level1";varresult=awaitclient.listLeaderboardRecords(session,leaderboardId);result.records.forEach(function(record){console.log("Record username %o and score %o",record.username,record.score);});// If there are more results get next page.
if(result.next_cursor){result=awaitclient.listLeaderboardRecords(session,leaderboardId,null,null,result.next_cursor);result.records.forEach(function(record){console.log("Record username %o and score %o",record.username,record.score);});}
Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
conststringleaderboardId="level1";varresult=awaitclient.ListLeaderboardRecordsAsync(session,leaderboardId);foreach(varrinresult.Records){System.Console.WriteLine("Record for '{0}' score '{1}'",r.Username,r.Score);}// If there are more results get next page.if(result.NextCursor!=null){varc=result.NextCursor;result=awaitclient.ListLeaderboardRecordsAsync(session,leaderboardId,null,100,c);foreach(varrinresult.Records){System.Console.WriteLine("Record for '{0}' score '{1}'",r.Username,r.Score);}}
autosuccessCallback=[this](NLeaderboardRecordListPtrrecordsList){for(auto&record:recordsList->records){std::cout<<"Record username "<<record.username<<" and score "<<record.score<<std::endl;}if(!recordsList->nextCursor.empty()){autosuccessCallback=[this](NLeaderboardRecordListPtrrecordsList){for(auto&record:recordsList->records){std::cout<<"Record username "<<record.username<<" and score "<<record.score<<std::endl;}};stringleaderboardId="level1";client->listLeaderboardRecords(session,leaderboardId,{},opt::nullopt,recordsList->nextCursor,successCallback);}};stringleaderboardId="level1";client->listLeaderboardRecords(session,leaderboardId,{},opt::nullopt,opt::nullopt,successCallback);
Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
finalStringleaderboard="level1";LeaderboardRecordListrecords=client.listLeaderboardRecords(session,leaderboard);for(LeaderboardRecordrecord:records.getRecordsList()){System.out.format("Record for %s score %d",record.getUsername(),record.getScore());}// If there are more results get next page.
if(records.getCursor()!=null){varc=result.NextCursor;records=client.listLeaderboardRecords(session,leaderboard,null,100,records.getNextCursor());for(LeaderboardRecordrecord:records.getRecordsList()){System.out.format("Record for %s score %d",record.getUsername(),record.getScore());}}
Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
varleaderboard_id="level1"varresult:NakamaAPI.ApiLeaderboardRecordList=yield(client.list_leaderboard_records_async(session,leaderboard_id,null,null,100),"completed")ifresult.is_exception():print("An error occurred: %s"%result)returnforrinresult.records:varrecord:NakamaAPI.ApiLeaderboardRecord=rprint("Record username %s and score %s"%[record.username,record.score])ifresult.next_cursor:result=yield(client.list_leaderboard_records_async(session,leaderboard_id,null,null,100,result.next_cursor),"completed")ifresult.is_exception():print("An error occurred: %s"%result)returnforrinresult.records:varrecord:NakamaAPI.ApiLeaderboardRecord=rprint("Record username %s and score %s"%[record.username,record.score])
You can use a bunch of owner IDs to filter the records to only ones owned by those users. This can be used to retrieve only scores belonging to the user’s friends.
Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>?owner_ids=some&owner_ids=friends"\
-H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
6
varleaderboardId="level1";varownerIds=["some","friends","user ids"];varresult=awaitclient.listLeaderboardRecords(session,leaderboardId,ownerIds);result.records.forEach(function(record){console.log("Record username %o and score %o",record.username,record.score);});
Client
1
2
3
4
5
6
7
conststringleaderboardId="level1";varownerIds=new[]{"some","friends","user ids"};varresult=awaitclient.ListLeaderboardRecordsAsync(session,leaderboardId,ownerIds);foreach(varrinresult.OwnerRecords){System.Console.WriteLine("Record for '{0}' score '{1}'",r.Username,r.Score);}
Client
1
2
3
4
5
6
7
8
9
10
11
12
autosuccessCallback=[](NLeaderboardRecordListPtrrecordsList){for(auto&record:recordsList->records){std::cout<<"Record username "<<record.username<<" and score "<<record.score<<std::endl;}};vector<string>ownerIds={"some","friends","user ids"};stringleaderboardId="level1";client->listLeaderboardRecords(session,leaderboardId,ownerIds,opt::nullopt,opt::nullopt,successCallback);
Client
1
2
3
4
5
6
Stringleaderboard="level1";String[]ownerIds=newString[]{"some","friends","user ids"};LeaderboardRecordListrecords=awaitclient.ListLeaderboardRecordsAsync(session,leaderboard,ownerIds);for(LeaderboardRecordrecord:records.getRecordsList()){System.out.format("Record for %s score %d",record.getUsername(),record.getScore());}
Client
1
2
3
4
5
6
7
8
9
varleaderboard_id="level1"varowner_ids=["some","friend","user id"]varresult:NakamaAPI.ApiLeaderboardRecordList=yield(client.list_leaderboard_records_async(session,leaderboard_id,owner_ids),"completed")ifresult.is_exception():print("An error occurred: %s"%result)returnforrinresult.records:varrecord:NakamaAPI.ApiLeaderboardRecord=rprint("Record username %s and score %s"%[record.username,record.score])
Expired records after each leaderboard reset are removed from the leaderboard rankings but they are not deleted. A user can list expired records for their desired time period.
For example, to list records expired in the past week:
Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>?overrideExpiry=604800"\
-H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
6
varleaderboardId="<leaderboardId>";varoverrideExpiry=604800;varresult=awaitclient.listLeaderboardRecords(session,leaderboardId,overrideExpiry);result.records.forEach(function(record){console.log("Record username %o and score %o expired on %o",record.username,record.score,record.expiryTime);});
Client
1
2
3
4
5
6
7
conststringleaderboardId="<leaderboardId>";constint64overrideExpiry=604800;varresult=awaitclient.ListLeaderboardRecordsAsync(session,leaderboardId,overrideExpiry);foreach(varrinresult.Records){System.Console.WriteLine("Record for '{0}' and score '{1}' expired on '{2}'",r.Username,r.Score,r.ExpiryTime);}
Client
1
2
3
4
5
6
7
8
9
10
11
12
autosuccessCallback=[](NLeaderboardRecordListPtrrecordsList){for(auto&record:recordsList->records){std::cout<<"Record username "<<record.username<<" and score "<<record.score<<" expired on "<<record.expiryTime<<std::endl;}};stringleaderboardId="<leaderboardId>";intoverrideExpiry=604800;client->listLeaderboardRecords(session,leaderboardId,overrideExpiry,{},opt::nullopt,opt::nullopt,successCallback);
Client
1
2
3
4
5
6
finalStringleaderboard="<leaderboardId>";finalLongoverrideExpiry=604800;LeaderboardRecordListrecords=client.listLeaderboardRecords(session,leaderboard,overrideExpiry).get();for(LeaderboardRecordrecord:records.getRecordsList()){System.out.format("Record for %s score %d",record.getUsername(),record.getScore());}
Client
1
2
3
4
5
6
7
8
9
varleaderboard_id="<leaderboardId>"varoverride_expiry=604800varresult:NakamaAPI.ApiLeaderboardRecordList=yield(client.list_leaderboard_records_async(session,leaderboard_id,override_expiry),"completed")ifresult.is_exception():print("An error occurred: %s"%result)returnforrinresult.records:varrecord:NakamaAPI.ApiLeaderboardRecord=rprint("Record username %s and score %s expired on %s"%[record.username,record.score,record.expiry_time])
stringleaderboardId="level1";stringownerId="some user ID";int32_tlimit=100;client->listLeaderboardRecordsAroundOwner(session,leaderboardId,ownerId,limit,successCallback);