Leaderboards

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.

Tip

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.

Leaderboard object

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. 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.

Reset schedules

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.

Reset schedules are defined in CRON format when the leaderboard is created. If a leaderboards has no reset schedule set it will never expire.

Leaderboard records

Each leaderboard contains a list of records ordered by their scores.

Records belong to an owner 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. A record has some builtin fields like lang, location, and timezone. These values are used to filter leaderboard results.

Custom fields

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.

string id = leaderboard.Id; // an INLeaderboard Id.
var score = 1200;

// add custom fields with each record.
var metadata = "{\"race_conditions\": [\"sunny\", \"clear\"]}";

var message = new NLeaderboardRecordWriteMessage.Builder(id)
    .Set(score)
    .Metadata(metadata)
    .Build();
client.Send(message, (INResultSet<INLeaderboardRecord> results) => {
  foreach (var r in list.Results) {
    Debug.LogFormat("Record handle '{0}' score '{1}'.", r.Handle, r.Score);
  }
}, (INError err) => {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
});
let id = leaderboard.id //a Leaderboard ID.
let score = 1200
let metadata = "{\"race_conditions\": [\"sunny\", \"clear\"]}".data(using: .utf8)!

var recordWrite : LeaderboardRecordWrite(leaderboardID: id)
recordWrite.set = score
recordWrite.metadata = metadata

var message = LeaderboardRecordWriteMessage()
message.leaderboardRecords.append(recordWrite)
client.send(message: message).then { records in
  for record in records {
    NSLog("Record handle %@ and score %d.", record.handle, record.score)
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}
var id = leaderboard.id //a Leaderboard ID.
var score = 1200
var metadata = {"race_conditions": ["sunny", "clear"]}

var message = new nakamajs.LeaderboardRecordWriteRequest();
message.set(id, score, metadata);
client.send(message).then(function(records) {
  records.foreach(function(record) {
    console.log("Record handle %o and score %o", record.handle, record.score);
  });
}).catch(function(error){
  console.log("An error occured: %o", error);
})

Create a leaderboard

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.

local metadata = {
  weather_conditions = "rain"
}
local id = "4ec4f126-3f9d-11e7-84ef-b7c182b36521"
nk.leaderboard_create(id, "desc", "0 0 * * 1", metadata, false)

List leaderboards

All leaderboards can be listed by a user.

var message = new NLeaderboardsListMessage.Builder().Build();
client.Send(message, (INResultSet<INLeaderboard> list) => {
  Debug.LogFormat("Found '{0}' leaderboards.", list.Results.Count);

  foreach (var leaderboard in list.Results) {
    Debug.LogFormat("Leaderboard id '{0}' and sort '{1}'", leaderboard.Id, leaderboard.Sort);
    Debug.LogFormat("Leaderboard metadata '{0}'", leaderboard.Metadata);
  }
}, (INError err) => {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
});
var message = LeaderboardsListMessage()
client.send(message: message).then { leaderboards in
  NSLog("Found %d leaderboards.", leaderboards.count)
  for leaderboard in leaderboards {
    NSLog("Leaderboard id %@ and sort %d.", leaderboard.id, leaderboard.sort)
    NSLog("Leaderboard metadata %@", leaderboard.metadata)
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}
var message = new nakamajs.LeaderboardsListRequest();
client.send(message).then(function(leaderboards) {
  console.log("Found %o leaderboards.", leaderboards.length)
  leaderboards.leaderboards.forEach(function(leaderboard) {
    console.log("Leaderboard id %o and sort %o", leaderboard.id, leaderboard.sort);
    console.log("Leaderboard metadata %o", leaderboard.metadata);
  });
}).catch(function(error){
  console.log("An error occured: %o", error);
})

When you have more than 100 leaderboards you can fetch the next set of results with a cursor.

var errorHandler = delegate(INError err) {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
};

var messageBuilder = new NLeaderboardsListMessage.Builder();
client.Send(messageBuilder.Build(), (INResultSet<INLeaderboard> list) => {
  // Lets get the next page of results.
  INCursor cursor = list.Cursor;
  if (cursor != null && list.Results.Count > 0) {
    var message = messageBuilder.Cursor(cursor).Build();

    client.Send(message, (INResultSet<INLeaderboard> nextList) => {
      foreach (var l in nextList.Results) {
        Debug.LogFormat("Leaderboard id '{0}' and sort '{1}'", l.Id, l.Sort);
        Debug.LogFormat("Leaderboard metadata '{0}'", l.Metadata);
      }
    }, errorHandler);
  }
}, errorHandler);
var message = LeaderboardsListMessage()
client.send(message: message).then { leaderboards in
  // Lets get the next page of results.
  if let cursor = leaderboards.cursor && leaderboards.count > 0 {
    message.cursor = cursor
    client.send(message: message).then { ls in
      NSLog("Found %d leaderboards.", ls.count)
      for leaderboard in ls {
        NSLog("Leaderboard id %@ and sort %d.", leaderboard.id, leaderboard.sort)
        NSLog("Leaderboard metadata %@", leaderboard.metadata)
      }
    }.catch { err in
      NSLog("Error %@ : %@", err, (err as! NakamaError).message)
    }
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}
var message = new nakamajs.LeaderboardsListRequest();
client.send(message).then(function(leaderboards) {
  // Lets get the next page of results.
  if (leaderboards.cursor && leaderboards.length > 0) {
    message.cursor = leaderboards.cursor
    client.send(message).then(function(ls) {
      console.log("Found %o leaderboards.", ls.length)
      ls.records.forEach(function(leaderboard) {
        console.log("Leaderboard id %o and sort %o", leaderboard.id, leaderboard.sort);
        console.log("Leaderboard metadata %o", leaderboard.metadata);
      });
    }).catch(function(error) {
      console.log("An error occured: %o", error);
    })
  }
}).catch(function(error){
  console.log("An error occured: %o", error);
})

Submit a score

A user can submit a score to a leaderboard and update it at any time. When a score is submitted it's sent with an operator which indicates how the new value should change the current score stored. The operators are: "set", "best", "inc", "decr".

Set operator

The set operator will submit the score and replace any current value for the owner.

string id = leaderboard.Id; // an INLeaderboard Id.
var score = 1200;

var message = new NLeaderboardRecordWriteMessage.Builder(id)
    .Set(score)
    .Build();
client.Send(message, (INResultSet<INLeaderboardRecord> list) => {
  foreach (var r in list.Results) {
    Debug.LogFormat("Record handle '{0}' score '{1}'.", r.Handle, r.Score);
  }
}, (INError err) => {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
});
let id = leaderboard.id //a Leaderboard ID.
let score = 1200

var recordWrite : LeaderboardRecordWrite(leaderboardID: id)
recordWrite.set = score

var message = LeaderboardRecordWriteMessage()
message.leaderboardRecords.append(recordWrite)
client.send(message: message).then { records in
  for record in records {
    NSLog("Record handle %@ and score %d.", record.handle, record.score)
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}
var id = leaderboard.id //a Leaderboard ID.
var score = 1200

var message = new nakamajs.LeaderboardRecordWriteRequest();
message.set(id, score);
client.send(message).then(function(records) {
  records.records.forEach(function(record) {
    console.log("Record handle %o and score %o", record.handle, record.score);
  });
}).catch(function(error){
  console.log("An error occured: %o", error);
})

Best operator

The best operator will check the new score is better than the current score and keep which ever value is best based on the sort order of the leaderboard. If no score exists this operator works like "set".

string id = leaderboard.Id; // an INLeaderboard Id.
var score = 1200;

var message = new NLeaderboardRecordWriteMessage.Builder(id)
    .Best(score)
    .Build();
client.Send(message, (INResultSet<INLeaderboardRecord> list) => {
  foreach (var r in list.Results) {
    Debug.LogFormat("Record handle '{0}' score '{1}'.", r.Handle, r.Score);
  }
}, (INError err) => {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
});
let id = leaderboard.id //a Leaderboard ID.
let score = 1200

var recordWrite : LeaderboardRecordWrite(leaderboardID: id)
recordWrite.best = score

var message = LeaderboardRecordWriteMessage()
message.leaderboardRecords.append(recordWrite)
client.send(message: message).then { records in
  for record in records {
    NSLog("Record handle %@ and score %d.", record.handle, record.score)
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}
var id = leaderboard.id //a Leaderboard ID.
var score = 1200

var message = new nakamajs.LeaderboardRecordWriteRequest();
message.best(id, score);
client.send(message).then(function(records) {
  records.records.forEach(function(record) {
    console.log("Record handle %o and score %o", record.handle, record.score);
  });
}).catch(function(error){
  console.log("An error occured: %o", error);
})

Increment operator

The increment operator will add the score value to the current score. If no score exists the new score will be added to 0.

string id = leaderboard.Id; // an INLeaderboard Id.
var score = 1200;

var message = new NLeaderboardRecordWriteMessage.Builder(id)
    .Increment(score)
    .Build();
client.Send(message, (INResultSet<INLeaderboardRecord> list) => {
  foreach (var r in list.Results) {
    Debug.LogFormat("Record handle '{0}' score '{1}'.", r.Handle, r.Score);
  }
}, (INError err) => {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
});
let id = leaderboard.id //a Leaderboard ID.
let score = 1200

var recordWrite : LeaderboardRecordWrite(leaderboardID: id)
recordWrite.increment = score

var message = LeaderboardRecordWriteMessage()
message.leaderboardRecords.append(recordWrite)
client.send(message: message).then { records in
  for record in records {
    NSLog("Record handle %@ and score %d.", record.handle, record.score)
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}
var id = leaderboard.id //a Leaderboard ID.
var score = 1200

var message = new nakamajs.LeaderboardRecordWriteRequest();
message.increment(id, score);
client.send(message).then(function(records) {
  records.records.forEach(function(record) {
    console.log("Record handle %o and score %o", record.handle, record.score);
  });
}).catch(function(error){
  console.log("An error occured: %o", error);
})

Decrement operator

The decrement operator will subtract the score value from the current score. If no score exists the new score will be subtracted from 0.

string id = leaderboard.Id; // an INLeaderboard Id.
var score = 1200;

var message = new NLeaderboardRecordWriteMessage.Builder(id)
    .Decrement(score)
    .Build();
client.Send(message, (INResultSet<INLeaderboardRecord> list) => {
  foreach (var r in list.Results) {
    Debug.LogFormat("Record handle '{0}' score '{1}'.", r.Handle, r.Score);
  }
}, (INError err) => {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
});
let id = leaderboard.id //a Leaderboard ID.
let score = 1200

var recordWrite : LeaderboardRecordWrite(leaderboardID: id)
recordWrite.decrement = score

var message = LeaderboardRecordWriteMessage()
message.leaderboardRecords.append(recordWrite)
client.send(message: message).then { records in
  for record in records {
    NSLog("Record handle %@ and score %d.", record.handle, record.score)
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}
var id = leaderboard.id //a Leaderboard ID.
var score = 1200

var message = new nakamajs.LeaderboardRecordWriteRequest();
message.decrement(id, score);
client.send(message).then(function(records) {
  records.records.forEach(function(record) {
    console.log("Record handle %o and score %o", record.handle, record.score);
  });
}).catch(function(error){
  console.log("An error occured: %o", error);
})

List records

A user can list records from a leaderboard. This makes it easy to compare scores to other users and see their positions.

List by score

The standard way to list records is ordered by score based on the sort order in the leaderboard.

string id = leaderboard.Id; // an INLeaderboard Id.

var message = new NLeaderboardRecordsListMessage.Builder(id).Build();
client.Send(message, (INResultSet<INLeaderboardRecord> list) => {
  foreach (var r in list.Results) {
    Debug.LogFormat("Record handle '{0}' score '{1}'.", r.Handle, r.Score);
  }
}, (INError err) => {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
});
let id = leaderboard.id //a Leaderboard ID.
var message = LeaderboardRecordsListMessage(leaderboardID: id)
client.send(message: message).then { records in
  for record in records {
    NSLog("Record handle %@ and score %d.", record.handle, record.score)
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}
var id = leaderboard.id //a Leaderboard ID.

var message = new nakamajs.LeaderboardRecordsListRequest();
message.leaderboardId = id;
client.send(message).then(function(records) {
  records.records.forEach(function(record) {
    console.log("Record handle %o and score %o", record.handle, record.score);
  });
}).catch(function(error){
  console.log("An error occured: %o", error);
})

You can fetch the next set of results with a cursor.

var errorHandler = delegate(INError err) {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
};

string id = leaderboard.Id; // an INLeaderboard Id.

var messageBuilder = new NLeaderboardRecordsListMessage.Builder(id);
client.Send(message.Build(), (INResultSet<INLeaderboardRecord> list) => {
  // Lets get the next page of results.
  INCursor cursor = list.Cursor;
  if (cursor != null && list.Results.Count > 0) {
    var message = messageBuilder.Cursor(cursor).Build();

    client.Send(message, (INResultSet<INLeaderboardRecord> nextList) => {
      foreach (var r in nextList.Results) {
        Debug.LogFormat("Record handle '{0}' score '{1}'.", r.Handle, r.Score);
      }
    }, errorHandler);
  }
}, errorHandler);
let id = leaderboard.id //a Leaderboard ID.
var message = LeaderboardRecordsListMessage(leaderboardID: id)

client.send(message: message).then { records in
  if let cursor = records.cursor && records.count > 0 {
    message.cursor = cursor
    client.send(message: message).then { r in
      for record in r {
        NSLog("Record handle %@ and score %d.", record.handle, record.score)
      }
    }.catch { err in
      NSLog("Error %@ : %@", err, (err as! NakamaError).message)
    }
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}
var id = leaderboard.id //a Leaderboard ID.

var message = new nakamajs.LeaderboardRecordsListRequest();
message.leaderboardId = id;
client.send(message).then(function(records) {
  // Lets get the next page of results.
  if (records.cursor && records.length > 0) {
    message.cursor = records.cursor;
    client.send(message).then(function(rs) {
      rs.records.forEach(function(record) {
        console.log("Record handle %o and score %o", record.handle, record.score);
      });
    }).catch(function(error) {
      console.log("An error occured: %o", error);
    })
  }
}).catch(function(error){
  console.log("An error occured: %o", error);
})

List by filter

You can add a filter on one of "lang", "location", or "timezone".

string id = leaderboard.Id; // an INLeaderboard Id.

var message = new NLeaderboardRecordsListMessage.Builder(id)
    .FilterByLocation("San Francisco")
    .Build();
client.Send(message, (INResultSet<INLeaderboardRecord> list) => {
  foreach (var r in list.Results) {
    Debug.LogFormat("Record handle '{0}' score '{1}'.", r.Handle, r.Score);
  }
}, (INError err) => {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
});
let id = leaderboard.id // a Leaderboard ID.
var message = LeaderboardRecordsListMessage(leaderboardID: id)
message.filterByLocation = "San Francisco"
client.send(message: message).then { records in
  for record in records {
    NSLog("Record handle %@ and score %d.", record.handle, record.score)
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}
var id = leaderboard.id //a Leaderboard ID.

var message = new nakamajs.LeaderboardRecordsListRequest();
message.leaderboardId = id;
message.location = "San Francisco";
client.send(message).then(function(records) {
  records.records.forEach(function(record) {
    console.log("Record handle %o and score %o", record.handle, record.score);
  });
}).catch(function(error){
  console.log("An error occured: %o", error);
})

List by friends

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.

IList<string> ownerIds = new List<string>();
ownerIds.Add(user.Id); // an INUser Id.
string id = leaderboard.Id; // an INLeaderboard Id.

var message = new NLeaderboardRecordsListMessage.Builder(id)
    .FilterByOwnerIds(ownerIds)
    .Build();
client.Send(message, (INResultSet<INLeaderboardRecord> list) => {
  foreach (var r in list.Results) {
    Debug.LogFormat("Record handle '{0}' score '{1}'.", r.Handle, r.Score);
  }
}, (INError err) => {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
});
let id = leaderboard.id // a Leaderboard ID.
let ownerIds : [String] = []
ownersIds.append(user.id) // a user ID

var message = LeaderboardRecordsListMessage(leaderboardID: id)
message.filterByOwnerIds = ownerIds
client.send(message: message).then { records in
  for record in records {
    NSLog("Record handle %@ and score %d.", record.handle, record.score)
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}
var id = leaderboard.id //a Leaderboard ID.
var ownerIds = [];
ownerIds.push(user.id); // a user ID

var message = new nakamajs.LeaderboardRecordsListRequest();
message.leaderboardId = id;
message.ownerIds = ownerIds;
client.send(message).then(function(records) {
  records.records.forEach(function(record) {
    console.log("Record handle %o and score %o", record.handle, record.score);
  });
}).catch(function(error){
  console.log("An error occured: %o", error);
})

Find current user

A leaderboard can be "scrolled" to the page which contains a record owner. This can be used to give users a view of their own position within a leaderboard.

string id = leaderboard.Id; // an INLeaderboard Id.
string ownerId = user.Id;   // an INUser Id.

var message = new NLeaderboardRecordsListMessage.Builder(id)
    .FilterByPagingToOwnerId(ownerId)
    .Build();
client.Send(message, (INResultSet<INLeaderboardRecord> list) => {
  foreach (var r in list.Results) {
    Debug.LogFormat("Record handle '{0}' score '{1}'.", r.Handle, r.Score);
  }
}, (INError err) => {
  Debug.LogErrorFormat("Error: code '{0}' with '{1}'.", err.Code, err.Message);
});
let id = leaderboard.id // a Leaderboard ID.
string ownerID = user.id // a user ID.

var message = LeaderboardRecordsListMessage(leaderboardID: id)
message.filterByPagingToOwnerID = ownerID
client.send(message: message).then { records in
  for record in records {
    NSLog("Record handle %@ and score %d.", record.handle, record.score)
  }
}.catch { err in
  NSLog("Error %@ : %@", err, (err as! NakamaError).message)
}
var id = leaderboard.id //a Leaderboard ID.
ownerId = user.id; // a user ID

var message = new nakamajs.LeaderboardRecordsListRequest();
message.leaderboardId = id;
message.ownerId = ownerId;
client.send(message).then(function(records) {
  records.records.forEach(function(record) {
    console.log("Record handle %o and score %o", record.handle, record.score);
  });
}).catch(function(error){
  console.log("An error occured: %o", error);
})