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

Обновление опубликованной игры

Как выпустить новую версию: какая модерация нужна при изменении файлов и промо, почему обновления не входят в лимит заявок, как мигрировать сохранения без потери прогресса и что делать, если обновление сломало игру.

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

Первое обновление после релиза всегда выходит быстрее, чем стоило бы. Нашлась опечатка, баг на конкретном телефоне, слишком сложный третий уровень — хочется поправить сегодня. Механика обновления в Яндекс Играх этому не мешает: черновик создаётся в два клика, а пока идёт проверка, в каталоге остаётся предыдущая версия.

Опасность в другом. У ваших игроков уже есть прогресс, и он лежит не у них на диске, а в облаке платформы — в формате, который придумали вы месяц назад. Любое изменение структуры данных, сделанное без миграции, стирает этот прогресс. Не «может стереть» — стирает, у всех сразу, молча. Об этом основная часть главы.

Как выпустить обновление

Порядок из документации:

  1. Проверить, соответствует ли игра требованиям (они могли измениться с прошлой проверки).
  2. Консоль → Игры → нужная игра.
  3. Открыть черновик: если вы редактируете игру впервые, нажать Создать черновик; если черновик есть, он откроется сам; если у игры несколько неопубликованных версий — выбрать одну из них.
  4. Отредактировать поля и загрузить новый архив.
  5. Нажать Отправить на модерацию — черновик перейдёт в статус «Ожидает модерации».

Поле Версия заполняется вами (по умолчанию первая версия называется 0.0.0.1). Платформа его не проверяет и ни на что не влияет — это ваша внутренняя маркировка. Заводите привычку сразу: через год «версия от 14 марта» ничего вам не скажет, а 1.4.0 — миграция сохранений на v3 скажет всё.

После того как статус сменится на «Опубликовано», новая версия появится в каталоге, черновик исчезнет из дерева игры, а на вкладке «Черновик» снова появится кнопка Создать черновик.

Важно

Во время проверки в каталоге остаётся ранее загруженная версия. Игроки не видят «технических работ» и не теряют доступ к игре — обновление подменяет билд только в момент публикации. Поэтому отправлять сырую сборку «пусть пока проверяют» безопасно с точки зрения игроков, но опасно с точки зрения кулдауна: отказ по обновлению так же удваивает время ожидания.

Какая модерация нужна

Правило одно и зависит только от того, трогали ли вы файлы игры.

Что изменилосьВид модерацииСрок
Файлы игры (архив)Полная — тестируют билд3–5 рабочих дней
Только промоматериалы: иконка, обложка, скриншоты, текстыКонтентная — билд не тестируют1–2 рабочих дня

Отсюда практический вывод: не смешивайте правки промо с правками кода, если правка промо срочная. Замена одной обложки пройдёт за пару дней, а тот же черновик с новым архивом — за неделю.

Два ограничения, которые важны именно для обновлений:

  • Лимит в две одновременные заявки на публикацию новых игр на обновления не распространяется. Вы можете обновлять опубликованные игры сколько угодно, даже когда две новые игры уже стоят в очереди.
  • Единовременно у одной игры может проходить только одна модерация — черновика, A/B-теста креативов или A/B-теста версий. Пока идёт проверка черновика, остальные типы недоступны.

Миграция сохранений

Это главный технический риск обновления, и он не имеет отношения к платформе: платформа честно хранит то, что вы записали. Проблема в том, что старый объект прилетит из getData() в новый код.

Почему без миграции прогресс исчезает

Типичный сценарий. В первой версии сохранение выглядело так:

{ level: 12, money: 340, sound: true }

Во второй вы переименовали money в coins, добавили инвентарь и разложили настройки в отдельный объект. Новый код читает save.coins, получает undefined, подставляет значение по умолчанию — и записывает поверх. Игрок, накопивший 340 монет за неделю, открывает игру и видит ноль. Восстановить нечего: старое значение уже перезаписано.

Ошибка здесь не в переименовании поля, а в отсутствии слоя, который приводит любые старые данные к текущему формату до того, как игра к ним прикоснётся.

Версия внутри данных и цепочка миграций

Схема, которая работает и не требует ничего, кроме дисциплины: номер версии хранится внутри самого сохранения, а миграции применяются последовательно.

// migrations.js
const CURRENT_VERSION = 4;

const MIGRATIONS = {
  // 1 → 2: у первой версии номера не было вовсе.
  1: (s) => {
    s.coins = s.money ?? 0;
    delete s.money;
    return s;
  },
  // 2 → 3: настройки собраны в объект.
  2: (s) => {
    s.settings = { sound: s.sound !== false, music: s.music !== false };
    delete s.sound;
    delete s.music;
    return s;
  },
  // 3 → 4: инвентарь стал массивом идентификаторов.
  3: (s) => {
    s.inventory = Array.isArray(s.items) ? s.items.map((i) => i.id) : [];
    delete s.items;
    return s;
  },
};

export function migrate(raw) {
  if (!raw || typeof raw !== 'object') return createDefaultSave();

  let s = JSON.parse(JSON.stringify(raw)); // не мутируем оригинал
  let v = Number(s.version) || 1;

  if (v > CURRENT_VERSION) {
    // Данные из будущего: игрок успел поиграть в новую версию на другом
    // устройстве, а тут открылась старая. Ничего не ломаем и не пишем.
    console.warn('[save] версия из будущего:', v);
    return { ...s, __readOnly: true };
  }

  while (v < CURRENT_VERSION) {
    const step = MIGRATIONS[v];
    if (!step) {
      console.error('[save] нет миграции с версии', v);
      return createDefaultSave();
    }
    s = step(s);
    v += 1;
    s.version = v;
  }

  return s;
}

Применяется сразу после чтения и до любой игровой логики:

const raw = (await player.getData(['save'])).save;
const save = migrate(raw);

Четыре свойства этого кода стоят внимания:

  • Шаги атомарны и однонаправлены. Каждая функция переводит ровно на одну версию вперёд. Добавить пятую версию — значит дописать 4: (s) => ... и поднять константу, не трогая остальное.
  • Миграция идемпотентна. Повторный прогон уже мигрированных данных ничего не изменит, потому что version уже равен текущей.
  • Версия из будущего не ломает игру. Это реальный сценарий: игрок открыл обновлённую версию на телефоне, а на десктопе у него закешировалась старая. Помечаем данные как read-only и не перезаписываем — иначе старая сборка затрёт новый прогресс.
  • Отсутствие миграции — явная ошибка, а не тихий сброс. Лучше увидеть строку в консоли на тесте, чем узнать о проблеме из отзывов.

Правила, которые экономят нервы

Не удаляйте поля в том же обновлении, где перестали их использовать. Оставьте их лежать одну-две версии. Если обновление придётся откатывать, старая сборка найдёт свои данные на месте.

Сделайте резервную копию перед первой записью нового формата. Одно поле в сохранении, один раз:

if (!save.backupV3 && rawVersionWas(3)) {
  save.backupV3 = raw;                       // сырой снимок старых данных
  await player.setData({ save }, true);      // flush: true — пишем сразу
}

Помните про лимит: максимальный размер данных на игрока — 200 КБ. Бэкап должен быть маленьким, и держать его вечно не нужно — удалите в следующем обновлении.

Не переносите миграцию на сервер и не завязывайте на сеть. Она должна отработать на том, что вернул getData(), даже если это undefined.

Проверяйте миграцию на реальных старых данных. Порядок такой:

  1. Открыть игру в режиме черновика на старой версии, наиграть прогресс.
  2. Загрузить новый архив в черновик.
  3. Открыть черновик снова тем же аккаунтом — старое сохранение в облаке никуда не делось, миграция отработает на настоящих данных.
  4. Отдельно проверить чистый старт: debug-панель → SDK mocks ⚒️Clear cloud data, игра запускается как в первый раз.

Осторожно

Тестировать миграцию на объекте, который вы сами написали руками, недостаточно. Реальные сохранения бывают неполными: игрок вышел в момент записи, часть полей не успела дописаться, у кого-то лежат данные версии, которую вы уже забыли. Пишите миграции так, будто на входе может быть что угодно, — с ??, Array.isArray() и проверками типов.

Что ещё меняется вместе с форматом

Помимо setData, есть setStats — числовые данные с лимитом 10 КБ. Их формат тоже версионируется, но проще: ключи независимы, и появление нового ключа никого не ломает. Переименование — ломает. Если переименовываете счётчик, прочитайте старый ключ, запишите новый и оставьте старый на месте до следующего обновления.

Флаги вместо обновлений

Не всякое изменение требует новой сборки. Через ysdk.getFlags() игра может получить удалённую конфигурацию — плоский объект «ключ — значение», заданный в Консоли. Документация рекомендует запрашивать флаги один раз на старте.

const flags = await ysdk.getFlags({
  defaultFlags: { newShop: 'false', dailyBonus: '10' },
});

if (flags.newShop === 'true') openNewShop();

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

Если обновление сломало игру

Кнопки «откатить на предыдущую версию» в Консоли нет — это надо знать заранее, до того, как она понадобится. Варианты в порядке предпочтения:

  1. Выключить проблемную фичу флагом, если она была под флагом. Мгновенно и без модерации — ради этого флаги и нужны.
  2. Загрузить предыдущий архив как новое обновление. Работает всегда, но это полная модерация: 3–5 рабочих дней, в течение которых сломанная версия остаётся в каталоге.
  3. Снять игру с публикации. Крайняя мера: вкладка «Опубликовано» → Удалить. Последняя удалённая версия попадёт на вкладку «Черновик», и вернуть игру можно повторной отправкой на модерацию. Пока игра снята, трафика нет вовсе, а метрики проседают.

Именно поэтому обновления, затрагивающие формат данных, стоит выпускать отдельно от всего остального — небольшим изменением, которое легко проверить и не страшно откатывать.

Совет

Для крупных изменений в геймплее есть A/B-тесты версий: вы загружаете экспериментальную сборку, задаёте процент аудитории и продолжительность, а по окончании сравниваете метрики с основной версией. Запустить эксперимент можно только у уже опубликованной игры, и параллельно с ним нельзя публиковать новые версии — иначе результаты будут недостоверными. Это способ не гадать, стало ли лучше.

Что дальше

Обновления имеет смысл выпускать, опираясь на данные, а не на ощущения. Значит, данные нужно уметь читать: какие метрики Консоль считает сама, что означают показы, сессии и удержание, когда цифра — сигнал, а когда шум, как подключить внешнюю аналитику с учётом ограничений CSP и как при этом не собирать лишнего о своих игроках. Об этом — следующая глава.

Источники

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

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

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

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