GameDev Guide
Глава 1314 мин чтенияПроверено: 5 сентября 2026 г.

Лидерборды: таблицы рекордов через ysdk.leaderboards

Научитесь создавать лидерборд в консоли и работать с актуальным API ysdk.leaderboards — отправлять результат, показывать топ и место игрока, укладываться в лимиты частоты и не ломать игру у неавторизованных пользователей.

Содержание главы

Лидерборд — это соревновательная таблица, которая живёт не только внутри игры: основной лидерборд платформа показывает прямо на карточке игры в каталоге. Технически это четыре метода 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, а внутри playerpublicName, 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
});
ОпцияДиапазонПо умолчанию
quantityTop1–205
quantityAround1–105
includeUsertrue / falsefalse

В ответе: 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-панель, что именно смотреть в консоли и как проверять сценарии, которые на локальной машине не воспроизводятся.

Источники

Официальная документация платформы — единственный источник истины по её требованиям.

Нужно подробнее?

Полное обучение разбирает те же темы глубже: уроки с примерами, готовые структуры проектов, развёрнутые чек-листы и файл инструкций для Claude Code — он входит в стоимость. Бесплатные главы при этом остаются открытыми и продолжают дополняться.

Что входит в полное обучение