Лидерборд — это соревновательная таблица, которая живёт не только внутри игры: основной лидерборд платформа показывает прямо на карточке игры в каталоге. Технически это четыре метода SDK, но вокруг них есть ровно те детали, на которых спотыкается большинство реализаций: обязательная авторизация, лимит в один запрос в секунду и полностью переименованный API. Все факты в этой главе сверены с официальной документацией в сентябре 2026 года.
Что умеет и чего не умеет лидерборд SDK
Главное ограничение нужно понять до того, как вы начнёте писать код: лидерборды SDK учитывают только результаты авторизованных пользователей. Метод setScore() требует авторизации, и гость в таблицу не попадёт никак. При этом требование 1.2 запрещает заставлять игрока входить в аккаунт — значит, игра обязана нормально работать и без лидерборда.
Документация предлагает два способа и прямо разрешает их сочетать:
| Способ | Кого учитывает | Кто хранит данные |
|---|---|---|
Лидерборды SDK (ysdk.leaderboards) | Только авторизованных | Яндекс |
| Кастомный лидерборд в коде игры | Всех, включая гостей | Вы (технология не ограничена) |
Практичная схема для большинства игр: локальный рекорд в сохранениях показываем всем, а таблицу SDK — как бонус, с кнопкой «Войти, чтобы попасть в рейтинг». Если в вашей механике игрок ожидает увидеть свой результат в таблице сразу, документация рекомендует делать собственный лидерборд — SDK эту задачу не закрывает.
Создание лидерборда в консоли
Без записи в консоли методы SDK будут возвращать 404. Идём в Консоль разработчика → нужная игра → вкладка Лидерборды и заполняем поля.
| Поле | Что задаёт |
|---|---|
| Техническое название лидерборда | Строка, которую вы передаёте первым аргументом во все методы SDK |
| Локализованное отображаемое название | Что видит игрок; для русского — поле [ru], для остальных языков добавляется локализация |
| Является ли основным лидербордом | Основной может быть только один — именно он показывается на карточке игры. Назначая основным новый, вы автоматически снимаете флаг со старого |
| Тип лидерборда | numeric (число) или time (время в миллисекундах) |
| Направление сортировки | По убыванию (первыми идут большие значения) или по возрастанию (первыми — меньшие) |
| Размер десятичной части счёта | Сколько знаков целого числа показывать после запятой: при размере 2 значение 5712 отображается как 57.12 |
Классическая связка для гонки на время — тип time плюс сортировка по возрастанию. Для очков — numeric и сортировка по убыванию.
Осторожно
Имя удалённого лидерборда переиспользовать нельзя, и два лидерборда с одинаковым именем не создать. Попытка даёт ошибку Object already exists. Продумайте название заранее: main, score_v1, time_classic — потом останется только завести новое имя.
После создания можно отредактировать техническое название, локализованные названия и флаг «основной». Изменить сами рекорды, скрыть или удалить конкретного пользователя из таблицы через консоль нельзя — это делается только через поддержку.
Актуальный API: обращаемся напрямую к ysdk.leaderboards
Никакой инициализации не нужно — методы вызываются прямо на объекте SDK.
Частая ошибка
const lb = await ysdk.getLeaderboards() — устаревший способ, так прямо написано в документации. Вместе с ним устарели и длинные имена методов. Если вы копируете код из статьи 2021 года или получили его от нейросети, обученной на таких статьях, вы почти наверняка получите именно этот вариант.
Карта переименования — ровно та, что приведена в документации:
| Устаревший вызов | Актуальный вызов |
|---|---|
lb.getLeaderboardDescription() | ysdk.leaderboards.getDescription() |
lb.setLeaderboardScore() | ysdk.leaderboards.setScore() |
lb.getLeaderboardPlayerEntry() | ysdk.leaderboards.getPlayerEntry() |
lb.getLeaderboardEntries() | ysdk.leaderboards.getEntries() |
Лимиты и требования к авторизации у методов разные, и это важнее, чем кажется:
| Метод | Лимит | Авторизация |
|---|---|---|
ysdk.leaderboards.setScore() | 1 запрос за 1 секунду | Обязательна |
ysdk.leaderboards.getPlayerEntry() | 60 запросов за 5 минут | Обязательна |
ysdk.leaderboards.getEntries() | 20 запросов за 5 минут | Не нужна |
| Остальные запросы | 20 запросов за 5 минут | — |
Обратите внимание на асимметрию: показать таблицу можно кому угодно, а вот записать результат — только авторизованному игроку.
Отправка результата: setScore
await ysdk.leaderboards.setScore('leaderboard2021', 120);
// Третий параметр — необязательное описание пользователя.
await ysdk.leaderboards.setScore('leaderboard2021', 120, 'Прошёл без подсказок');Правила по значению score:
- тип
number, не может быть отрицательным; - максимум ограничен только логикой JavaScript (
Number.MAX_SAFE_INTEGER); - для лидерборда типа
timeзначение передаётся в миллисекундах и тоже считается целым числом; - дробные результаты передаются целым числом, а запятую рисует параметр «Размер десятичной части счёта».
Перед отправкой документация рекомендует проверять доступность метода:
const canSetScore = await ysdk.isAvailableMethod('leaderboards.setScore');Метод возвращает Promise<Boolean>. Связка isAvailableMethod() + setScore() — это и есть штатный способ не получать ошибку у гостей: неавторизованные пользователи просто не попадают в лидерборд.
Не бейтесь в лимит 1 запрос/сек
Лимит в одну секунду легко нарушить в аркаде, где очки капают каждый кадр. Наивный setScore() в обработчике набора очков отправит десятки запросов, и часть из них платформа отклонит с ошибкой. Правильная схема: отправлять только улучшение результата и не чаще, чем разрешено.
// leaderboard.js
const LB_NAME = 'leaderboard2021';
const MIN_INTERVAL = 1100; // с запасом к лимиту «1 запрос в секунду»
let ysdk = null;
let canSetScore = false;
let description = null; // ILeaderboardDescription
let bestSent = null;
let pending = null;
let timerId = null;
let lastSentAt = 0;
export async function initLeaderboard(sdk) {
ysdk = sdk;
try {
canSetScore = await ysdk.isAvailableMethod('leaderboards.setScore');
description = await ysdk.leaderboards.getDescription(LB_NAME);
} catch (err) {
console.warn('[lb] лидерборд недоступен', err);
canSetScore = false;
}
}
function isBetter(next, prev) {
// invert_sort_order === true → побеждает меньшее значение (например, время).
return description?.description.invert_sort_order ? next < prev : next > prev;
}
export function submitScore(score) {
if (!canSetScore) return;
if (!Number.isFinite(score) || score < 0) return;
if (bestSent !== null && !isBetter(score, bestSent)) return;
if (pending !== null && !isBetter(score, pending)) return;
pending = Math.round(score);
schedule();
}
function schedule() {
if (timerId !== null) return;
const wait = Math.max(0, MIN_INTERVAL - (Date.now() - lastSentAt));
timerId = setTimeout(flush, wait);
}
async function flush() {
timerId = null;
const score = pending;
pending = null;
if (score === null) return;
lastSentAt = Date.now();
try {
await ysdk.leaderboards.setScore(LB_NAME, score);
bestSent = score;
} catch (err) {
console.warn('[lb] setScore не прошёл', err);
if (pending === null) pending = score; // вернём в очередь
}
if (pending !== null) schedule();
}Вызывать submitScore() можно хоть каждый кадр — наружу уйдёт максимум один запрос в 1,1 секунды и только при реальном улучшении. Направление «лучше» берётся из invert_sort_order, поэтому один и тот же модуль работает и для очков, и для времени.
Важно
Ошибка отправки результата не должна ломать игру. Все вызовы лидерборда оборачивайте в try/catch и логируйте, а не роняйте игровой цикл: игра, падающая с необработанной ошибкой в консоли, отклоняется на модерации по пункту 1.14.
Место игрока: getPlayerEntry
try {
const entry = await ysdk.leaderboards.getPlayerEntry(LB_NAME);
console.log(entry.rank, entry.score, entry.player.publicName);
} catch (err) {
if (err.code === 'LEADERBOARD_PLAYER_NOT_PRESENT') {
// У игрока ещё нет записи — это нормальная ситуация, а не сбой.
showPlaceholder('Сыграйте, чтобы попасть в таблицу');
} else {
console.warn('[lb] getPlayerEntry', err);
}
}Отдельный код LEADERBOARD_PLAYER_NOT_PRESENT — то, что чаще всего забывают обработать. Он срабатывает у каждого нового игрока при первом открытии таблицы, и без ветки на него интерфейс показывает «ошибку» там, где ошибки нет.
Поля записи (ILeaderboardEntry): score, rank, extraData, а внутри player — publicName, uniqueID, getAvatarSrc(size) и getAvatarSrcSet(size) с размерами small, medium, large.
Таблица целиком: getEntries
const res = await ysdk.leaderboards.getEntries(LB_NAME, {
quantityTop: 10, // 1..20, по умолчанию 5
includeUser: true, // по умолчанию false
quantityAround: 3, // 1..10, по умолчанию 5
});| Опция | Диапазон | По умолчанию |
|---|---|---|
quantityTop | 1–20 | 5 |
quantityAround | 1–10 | 5 |
includeUser | true / false | false |
В ответе: entries (массив записей того же вида, что у getPlayerEntry), userRank (место пользователя; 0, если его нет в выдаче), ranges с интервалами мест и — важная деталь — leaderboard с полным описанием лидерборда. То есть отдельный вызов getDescription() для отрисовки не нужен: тип счёта и десятичная часть уже приехали вместе с записями.
function renderLeaderboard(container, res) {
container.textContent = '';
const list = document.createElement('ol');
for (const entry of res.entries) {
const li = document.createElement('li');
const avatar = document.createElement('img');
avatar.src = entry.player.getAvatarSrc('small');
avatar.srcset = entry.player.getAvatarSrcSet('small');
avatar.alt = '';
avatar.width = 32;
avatar.height = 32;
const name = document.createElement('span');
// Имя может быть пустым, если игрок скрыл профиль.
name.textContent = entry.player.publicName || 'Пользователь скрыт';
const score = document.createElement('b');
score.textContent = formatScore(entry.score, res.leaderboard);
li.append(avatar, name, score);
if (res.userRank && entry.rank === res.userRank) li.classList.add('is-me');
list.append(li);
}
container.append(list);
}Имя и extraData приходят от других пользователей — вставляйте их через textContent, а не innerHTML. Это не паранойя, а обычная гигиена работы с чужими строками.
Совет
Лимит getEntries — 20 запросов за 5 минут, то есть в среднем один раз в 15 секунд. Запрашивайте таблицу при открытии экрана рейтинга и кэшируйте ответ в памяти на 20–30 секунд, а не обновляйте по таймеру в фоне. Кнопка «Обновить» с блокировкой на несколько секунд надёжнее автообновления.
Форматирование счёта
Платформа сама применяет десятичную часть к своим карточкам, но если вы рисуете таблицу внутри игры — форматирование на вашей стороне.
function formatScore(score, lb) {
const format = lb.description.score_format;
const offset = format.options.decimal_offset || 0;
if (format.type === 'time') {
const min = Math.floor(score / 60000);
const sec = Math.floor((score % 60000) / 1000);
const ms = score % 1000;
return `${min}:${String(sec).padStart(2, '0')}.${String(ms).padStart(3, '0')}`;
}
return offset > 0 ? (score / 10 ** offset).toFixed(offset) : String(score);
}Нумерация мест: документация отдельно оговаривает, что в ranges[].start счёт ведётся с нуля и первое место считается нулевым элементом. Про rank такой оговорки нет — выводите его как есть и один раз сверьте с реальной таблицей в режиме черновика, прежде чем добавлять к нему единицу «на всякий случай».
Сценарий для гостя
Гость видит таблицу (getEntries авторизации не требует), но не видит себя. Аккуратный порядок действий:
async function onLoginClick() {
await ysdk.auth.openAuthDialog();
// После входа объект Player нужно получить заново — старый не обновляется.
player = await ysdk.getPlayer();
await initLeaderboard(ysdk); // пересчитываем isAvailableMethod
submitScore(localBestScore); // отправляем накопленный локальный рекорд
refreshTable();
}Кнопку входа показывайте по явному действию игрока и с понятной выгодой («Войдите, чтобы ваш результат попал в таблицу») — этого требует пункт 1.2.1. Диалог авторизации, всплывающий сам при старте игры, — причина отказа.
Что проверить перед отправкой
- Игра запускается и полностью играбельна без авторизации.
- У нового игрока экран рейтинга показывает заглушку, а не ошибку (
LEADERBOARD_PLAYER_NOT_PRESENTобработан). - В консоли нет необработанных ошибок при выключенном интернете и при быстром наборе очков.
- Техническое название в коде совпадает с полем в консоли: если методы отдают 404, проверять нужно именно его.
- Для типа
timeвsetScore()уходят миллисекунды, а не секунды. - Значение
scoreникогда не бывает отрицательным и не дробным.
Что дальше
Лидерборд — первая часть игры, которая зависит от внешнего сервиса и умеет падать: нет сети, нет авторизации, превышен лимит, нет лидерборда с таким именем. Дальше разберём обработку ошибок и отладку в режиме черновика: debug-панель, что именно смотреть в консоли и как проверять сценарии, которые на локальной машине не воспроизводятся.