Cat Survivors: Как мы упаковали целую игру в 13КБ для js13k 2025

cat survivors game

Привет! Каждый год конкурс js13k бросает разработчикам вызов: создать игру на заданную тему всего за 1 месяц, причём так, чтобы ZIP-архив с ней весил не больше 13 312 байт. В 2025 году темой стал Чёрный Кот. Мы с энтузиазмом приняли вызов и поставили себе несколько целей: работать над игрой с умом, а не до изнеможения, добиться максимум геймплея при минимуме затрат и, конечно, сфокусироваться на главном — самой игре!

Оглавление


Итоги: ключевые выводы и уроки

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


Процесс создания: от идеи до релиза

Идея и кото-база

Идея родилась мгновенно: авто-шутер в духе Vampire Survivors, но с очаровательным чёрным котом в главной роли.

vampire survivors game

Чтобы выделиться, мы отказались от пиксель-арта в пользу векторной графики. Начали с процедурной анимации кота — это заняло немного времени, но сразу задало игре качественный и плавный вид, как в мобильных хитах.

black cat sketch

💪 Мне кажется, всё получилось так хорошо только потому, что у нас есть реальный прототип — наша любимая чёрная кошка Чичи. Она лично контролировала каждый этап разработки!

Следом добавили базовый геймплей: первого врага — Муху (выбрали голосованием!), стартовый экран и экран поражения. На тот момент мы ещё не знали, будет ли в игре концовка.

Наращивание контента

Дальше игра начала обрастать контентом: пассивные умения, оружие, бонусы, новые враги и, конечно, баланс прокачки. Появились экраны выбора улучшений и открытия сундуков. С каждым новым элементом игра становилась всё более целостной.

⚠️ Страх перед размером. В оригинальной Vampire Survivors есть эволюция оружия — мощный апгрейд из комбинации оружия и пассивки. Я с сожалением отключил эту механику, опасаясь, что она раздует игру сверх лимита в 13 КБ. Каждое новое оружие, описание и уникальная система стрельбы линейно увеличивали размер архива.

gameover screen

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

Система улучшений тоже не идеальна. Вначале игра заставляет выбрать три новых вида оружия, а затем предлагает случайные апгрейды. В будущем мы хотим это доработать: например, давать на выбор улучшение одного из имеющихся оружий и два новых.

Есть ли концовка? Да!

Мы очень рады, что смогли добавить в игру понятную цель и романтическую концовку. В оригинальной Vampire Survivors победить смерть невозможно, и это, честно говоря, немного расстраивает. Когда игрок понимает, что финала нет, он может испытать разочарование. У нас же есть шанс на победу!

Я также рад, что мы проработали ситуацию, когда все улучшения уже получены. В этом случае игра автоматически начинает прокачивать базовые характеристики персонажа. Этот момент было легко упустить.

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

Финальным штрихом стала настройка волн врагов и балансировка сложности. Мы специально сделали так, чтобы игрок, даже проиграв несколько раз, мог быстро освоиться и в итоге насладиться своей мощью в течение 10-минутной сессии.

Оптимизация вычисления характеристик

Изначально характеристики рассчитывались по классической формуле: value = base * (1 + mod) + add. Улучшения давали прирост в процентах, что требовало хранения избыточных данных и усложняло расчёты. В интерфейсе это выглядело как непонятное "+20%".

Я решил всё упростить. В новой схеме характеристики оружия и персонажа просто суммируются. Это позволило показывать игроку точный прирост к параметрам оружия и, что самое приятное, уменьшило размер архива на 100 байт! Неожиданный и впечатляющий результат.

stat effect ui

Определение столкновений

В игре много врагов, которые толкают друг друга. Мы намеренно выбрали простейший и медленный алгоритм — перебор «каждый с каждым»:

for (let i = 0; i < N; ++i) {
  for (let j = i + 1; j < N; ++j) {
    // check enemy[i] vs enemy[j]
  }
}

💡 Почему не тормозит? Обычно для таких задач используют пространственное разбиение (например, сетку), чтобы проверять столкновения только в пределах одной области. Но ради экономии места я от него отказался. Я ожидал серьёзных тормозов, но современные устройства справились. В качестве подстраховки можно было бы ограничить число врагов геймдизайном или отключать коллизии для тех, кого игрок не видит.

Чтобы разгрузить систему, я сделал летающих врагов (мух) на отдельном слое — они просто пролетают над остальными.


База знаний: что внутри игры?

Оружие (11 штук):

Пассивные умения (12 штук):

Враги (5 архетипов):

В планах были ещё Голуби и Еноты.

Из чемпионов и боссов выпадает Консерва, которая даёт 1 или сразу 3 улучшения. А из Коробки с сюрпризом можно получить крест (убить всех), магнит, здоровье или опыт.


Код: агрессивная минификация

zip size history

Наш главный враг — байты. Мы использовали агрессивные методы минификации, иногда в ущерб читаемости и производительности. Но для js13k это оправдано.

Ключевые слова под запретом

Я запретил себе использовать некоторые ключевые слова JavaScript, чтобы сэкономить место:

💡 Трюк с локальными переменными. Чтобы избавиться от {} и return, можно объявлять локальные переменные прямо в аргументах стрелочной функции.

// Было
const fn = (x, y) => {
  let value = calc(x, y);
  return s(value);
};

// Стало
const fn = (x, y, value = calc(x, y)) => s(value);

Математика

Оператор ** отлично заменяет Math.sqrt(x) на x**.5 и Math.pow(x, y) на x**y. Все нужные функции из Math я ре-экспортирую, чтобы их можно было минифицировать. Например, export const {sin, cos, atan2, exp, hypot, random, ...} = Math;.

А для вычисления длины вектора я использую hypot(x, y) вместо sqrt(x*x + y*y).


Сборка: от TypeScript до ZIP

Наша схема сборки выглядела так:

  1. Rollup + TypeScript Transformers: Компиляция TypeScript с кастомными трансформациями (например, для переименования полей).
  2. esbuild: Первый проход минификации.
  3. Terser: Более агрессивная минификация.
  4. Roadroller: Упаковка HTML/CSS в JS и создание финального HTML-файла.
  5. advzip: Сжатие в ZIP-архив.
  6. Efficient Compression Tool: Дополнительное сжатие ZIP-файла.

🚀 Будущая идея: Написать Class Crasher — трансформер, который автоматически превращает простые классы в интерфейсы и набор функций. Это может дать ещё большую экономию.


Графика и управление

Canvas, а не WebGL

Я фанат WebGL, но в этот раз решил использовать чистый Canvas с учётом devicePixelRatio. Я опасался, что на больших экранах игра будет тормозить, но современные устройства справились на ура. Это было приятным сюрпризом и сэкономило много места.

Советы по Canvas:

Управление

Pointer Events отлично справился с мышью и тач-событиями. Кода получилось очень мало. Для клавиатуры я использовал KeyboardEvent.code и вот такой трюк для определения направления:

const x =
  (isDown["KeyD"] | isDown["ArrowRight"]) -
  (isDown["KeyA"] | isDown["ArrowLeft"]);
const y =
  (isDown["KeyS"] | isDown["ArrowDown"]) - (isDown["KeyW"] | isDown["ArrowUp"]);

Побитовое ИЛИ (|) здесь нужно, чтобы undefined | 1 превращалось в 1, а не в NaN.

😩 Главная ошибка: Я не добавил поддержку геймпада с самого начала. Когда я взялся за это в конце, оказалось, что нужно переписывать все экраны выбора улучшений. Времени уже не хватило.


Аудио: ZZFX и процедурная музыка

Для меня звук — это не опция, а половина атмосферы игры. 🎶 Кто-то вырезает аудио, чтобы влезть в лимит, но в этом году правила стали строже: нет звука — нет и оценки в соответствующей категории. Поэтому я всегда ищу способ добавить звук в игру, даже если это не звучит профессионально.

Для звуковых эффектов я использовал ZZFX — это, пожалуй, самый простой и быстрый способ добавить звук в игру. Музыку я пытался генерировать с помощью chatgpt и deepseek, но результаты были ужасны. В итоге я оставил простую ритм-машинку, написал генератор мелодии на основе случайных паттернов в Ля-мажоре и добавил простую аранжировку. Получилось немного назойливо, но лучше, чем ничего.

💡 Идея на будущее: Использовать семплы, сгенерированные в ZZFX, в качестве инструментов для процедурной музыки. Это может сэкономить ещё немного байт.


Заключение

Это была наша четвёртая игра на js13k, и с каждым годом становится только интереснее. Конкурс заставляет по-новому взглянуть на инструменты, оптимизацию и веб-стандарты.

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

Поздравляем всех с завершением конкурса! Мы очень рады, что существует этот конкурс, есть участники, которые как и мы, каждый год принимают в нём участие! Хотим выразить благодарность организаторам (в особенности Andrzej Mazur), которые дают нам возможность ставить себе вызов каждый год и собираться для обсуждения получившихся игр!

А что думаете вы? Сталкивались с подобными задачами? Какие у вас любимые трюки для минификации? Делитесь мыслями в моём Telegram-канале или пишите на почту!


Наши прошлые работы для js13k

← Пред.
🍄 The Last of Us: Игра после Сериала — Первые впечатления