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

Структура проекта игры для Яндекс Игр

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

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

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

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

Жёсткие требования к архиву

Всё, что ниже, взято из документации Яндекса (проверено в сентябре 2026 года). Это не рекомендации — это условия приёмки.

ТребованиеФормулировкаНомер
ФорматZIP-архив
Точка входаВ корне архива ровно один index.html
РазмерДо 100 МБ в распакованном виде1.21
Имена файлов и папокБез пробелов и без кириллицы1.22
СсылкиНикаких абсолютных URL на серверы S3 Яндекса в коде1.7
Независимость от адресаИгра не должна зависеть от URL, по которому её открыли1.18
Внешние хостыЛюбой внешний домен — только через вкладку CSP в консоли

Отдельно стоит запомнить: одновременно к игре прикреплён только один архив, повторная загрузка заменяет предыдущий. Так что «залью второй архив с патчем» не сработает — собирайте полный билд каждый раз.

Про максимальный размер самого ZIP (сжатого) в документации ничего не сказано — ограничение сформулировано только для распакованного содержимого. Ориентируйтесь на 100 МБ после распаковки.

Частая ошибка

Самая частая ошибка первой загрузки — заархивировать папку, а не её содержимое. Тогда внутри архива получается my-game/index.html, а в корне архива index.html нет. Консоль такой архив не примет. Проверяйте: откройте готовый ZIP и убедитесь, что index.html виден сразу, без единой папки над ним.

Рабочая структура папок

Для игры без бандлера достаточно четырёх сущностей: точка входа, стили, код, ресурсы.

my-game/
├── index.html          ← ровно один, строго в корне
├── styles/
│   └── main.css
├── src/
│   ├── main.js         ← точка входа, склеивает всё вместе
│   ├── game.js         ← игровой цикл, состояние
│   ├── yandex.js       ← вся работа с SDK в одном месте
│   ├── storage.js      ← сохранения (setData/getData + локальный фолбэк)
│   └── ui.js
└── assets/
    ├── images/
    ├── audio/
    └── fonts/

Почему именно так:

  • src/yandex.js отдельным файлом. Весь SDK — инициализация, реклама, лидерборды, сохранения — живёт в одном модуле с понятным интерфейсом (initYandex(), showRewarded(), saveProgress()). Остальной код не должен знать, что он работает под Яндексом. Это окупается сразу: игра запускается локально без SDK, и её можно портировать на другую площадку, переписав один файл.
  • assets/ по типам, а не по уровням. Ресурсы вы будете чистить и сжимать пачками — легче, когда все звуки в одной папке.
  • Плоско, а не глубоко. Три уровня вложенности — потолок. Чем длиннее относительные пути, тем больше шансов промахнуться.

Обратите внимание: sdk.js в этом дереве нет и быть не должно. В документации это сказано прямо — скачивать файл sdk.js и класть его в архив не нужно, лоадер отдаёт платформа.

index.html

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
  <title>My Game</title>
  <link rel="stylesheet" href="./styles/main.css">
  <!-- Yandex Games SDK -->
  <script src="/sdk.js"></script>
</head>
<body>
  <canvas id="game"></canvas>
  <script type="module" src="./src/main.js"></script>
</body>
</html>
// src/main.js
import { createGame } from './game.js';
import { initYandex } from './yandex.js';

const canvas = document.getElementById('game');
const game = createGame(canvas);

initYandex(game).catch((error) => {
  // SDK может быть недоступен — локально или при сетевом сбое.
  // Игра обязана запуститься в любом случае.
  console.warn('SDK недоступен, играем без него', error);
  game.start();
});

ES-модули работают в браузере без сборки, но с двумя условиями: расширение в импорте обязательно (./game.js, а не ./game), и страница должна открываться по HTTP, а не двойным кликом по файлу (file:// модули блокирует). Для локального запуска используйте официальный npx @yandex-games/sdk-dev-proxy -p <путь к папке игры> — он и статику раздаст, и SDK подставит.

Важно

/sdk.js — единственный путь в вашем HTML, который начинается со слэша, и так и должно быть. Лоадер отдаётся с корня хоста, на котором крутится игра. Все ваши файлы подключаются относительными путями через ./. Если игра интегрируется через собственный домен или iframe, вместо /sdk.js документация предписывает абсолютный https://sdk.games.s3.yandex.net/sdk.js — это тоже штатный вариант, а не исключение из правила 1.7.

Относительные пути: где легко промахнуться

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

Где написан путьОтносительно чего резолвится
Атрибуты в HTML (src, href)адрес документа, то есть index.html
url() внутри CSSсам CSS-файл
import в ES-модулесам JS-модуль
img.src, fetch(), new Audio() в JSадрес страницы, а не JS-файла

Последняя строка — источник классического бага. В src/game.js вы пишете img.src = './assets/images/hero.png' — и это правильно, потому что путь считается от index.html, а не от src/. Интуиция подсказывает '../assets/...', и вот это как раз не сработает. Если путаетесь, соберите пути в один модуль-константу или используйте new URL('../assets/images/hero.png', import.meta.url) — эта форма считается от модуля и не зависит от того, откуда её вызвали.

Осторожно

Требование 1.7 запрещает абсолютные URL на серверы S3 Яндекса в коде игры. Соблазн возникает после первой публикации: вы открываете опубликованную игру, копируете из DevTools адрес своей картинки и вставляете его в код. Так делать нельзя — адрес меняется при следующей загрузке архива, игра ломается, а модерация такое отклоняет. То же самое касается путей от корня вида /assets/hero.png: они привязывают игру к конкретному адресу размещения, а требование 1.18 прямо запрещает зависеть от URL.

Внешние домены — отдельная история. Подключить шрифт с публичного CDN или ходить за данными на свой API не запрещено, но каждый внешний хост нужно заранее заявить на вкладке CSP в консоли с обоснованием и дождаться согласования. Правила там строгие: без путей, без портов, без IP-адресов, только https и wss, и основная масса данных игры всё равно должна лежать в архиве. Для шрифтов и картинок это лишняя работа и лишняя точка отказа — положите их в assets/.

Имена файлов

Требование 1.22 запрещает пробелы и кириллицу. Практика добавляет ещё одно правило, которого в документации нет, но которое ломает игры чаще всего остального: только нижний регистр и дефисы.

Причина в том, что Windows не различает регистр в именах файлов, а сервер, который раздаёт вашу распакованную игру, различает. Локально img.src = 'assets/images/hero.png' найдёт файл Hero.png без единого возражения. После публикации это станет 404 — и, скорее всего, ошибкой в консоли, а JS-ошибки при запуске входят в список типовых причин отказа (требование 1.14).

Совет

Договоритесь с собой один раз: nizhniy-registr-cherez-defis.png. Никаких Фон уровня 2.png, Sprite Sheet (final).png, герой_v2.PNG. Это дешевле, чем потом искать один файл из трёхсот, который сломал сборку. Если проект уже разросся — переименование делается скриптом, а Claude Code хорошо справляется с задачей «переименуй все файлы в assets по kebab-case и почини все ссылки на них в коде».

Заодно проследите, чтобы в архив не попали .git/, node_modules/, исходники PSD/Aseprite, README.md и .DS_Store. Они съедают лимит в 100 МБ, а среди них легко проскакивает файл с пробелом в имени.

Нужен ли бандлер

Короткий ответ: для простой игры на чистом JavaScript — нет.

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

Для игры уровня «арканоид, три-в-ряд, кликер, платформер на canvas» этого достаточно: несколько ES-модулей, один CSS, папка с ассетами.

Есть один нюанс совместимости. Требование 1.20 перечисляет поддерживаемые платформы, включая iOS 9.0+, а ES-модули появились в Safari только начиная с iOS 10.3. Если вы хотите буквального соответствия этому пункту без сборки — подключайте скрипты классическими тегами в нужном порядке:

<script defer src="./src/game.js"></script>
<script defer src="./src/yandex.js"></script>
<script defer src="./src/main.js"></script>

defer гарантирует и порядок выполнения, и запуск после парсинга HTML. Модульность вы при этом потеряете, но для трёх-пяти файлов это терпимо. Альтернатива — бандлер с транспиляцией под старый таргет.

Когда бандлер действительно нужен

  • Зависимости из npm. Phaser, three.js, matter.js, howler — их нужно во что-то собрать.
  • TypeScript. В том числе если хотите официальные типы SDK: npm install --save @types/ysdk.
  • Много кода. С двух-трёх десятков модулей ручное подключение и отсутствие минификации начинают ощущаться.
  • Конвейер ассетов. Автоматическая упаковка атласов, сжатие картинок, инлайн мелких файлов — когда вы упираетесь в 100 МБ.

Практический выбор по умолчанию — Vite. Но с одной обязательной настройкой:

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  base: './',              // без этого пути будут вида /assets/... — игра сломается
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
  },
});

base: './' — не украшение. По умолчанию Vite генерирует ссылки от корня (/assets/index-a1b2c3.js), а это ровно тот случай, который запрещает требование 1.18: игра начинает зависеть от адреса размещения. С base: './' пути становятся относительными и архив работает где угодно.

И главное про бандлер: в архив идёт содержимое dist/, а не папка проекта. index.html должен оказаться в корне dist — у Vite так и происходит по умолчанию, но проверьте после первой сборки.

Проверка перед упаковкой

Три вещи, которые стоит прогонять каждый раз. PowerShell, из папки со сборкой:

# 1. Имена с пробелами или не-ASCII символами (должно быть пусто)
Get-ChildItem -Recurse | Where-Object { $_.Name -match '[^!-~]' } |
    Select-Object -ExpandProperty FullName

# 2. Размер в распакованном виде (лимит — 100 МБ)
'{0:N1} МБ' -f ((Get-ChildItem -Recurse -File | Measure-Object Length -Sum).Sum / 1MB)

# 3. Упаковка содержимого папки, а не самой папки
Compress-Archive -Path .\* -DestinationPath ..\build.zip -Force

То же самое в bash:

find . | LC_ALL=C grep '[^!-~]'          # 1. плохие имена — должно быть пусто
du -sh --exclude=.git .                  # 2. размер
zip -r ../build.zip . -x '.*' -x '__MACOSX/*' -x '*/.DS_Store'   # 3. архив

Диапазон !-~ — это все печатаемые ASCII-символы кроме пробела, поэтому одна проверка ловит сразу и пробелы, и кириллицу, и эмодзи в именах.

Дальше — откройте получившийся ZIP любым архиватором и посмотрите на первый экран. Видите index.html без папки над ним — можно грузить.

Как поручить это Claude Code

Проверка структуры — идеальная задача для агента, потому что она скучная и полностью формальная. Разовый запрос выглядит примерно так:

Проверь папку dist перед загрузкой в Яндекс Игры: в корне должен быть ровно один index.html; в именах файлов и папок не должно быть пробелов и не-ASCII символов; распакованный размер — до 100 МБ; в коде не должно быть путей, начинающихся со слэша (кроме /sdk.js), и абсолютных URL на домены Яндекса. Выведи список нарушений с путями к файлам, ничего не исправляя.

Ещё лучше — вынести эти правила в CLAUDE.md проекта, чтобы они действовали в каждой сессии и агент не создавал файлы с пробелами в именах с самого начала. О том, как устроен CLAUDE.md и что в него имеет смысл класть, речь пойдёт в главах про рабочий процесс с Claude Code.

Что дальше

Структура готова, архив собирается — пора наполнять его содержимым. В следующей главе подключаем SDK: правильный тег загрузчика (тот, который проходит требование 1.19.1, а не тот, что кочует по устаревшим туториалам), инициализация, порядок вызовов в жизненном цикле игры и проверка через debug-панель.

Источники

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

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

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

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