Как я участвовал в конкурсе маленьких игр js13kGames

в 10:13, , рубрики: game development, javascript

Как я участвовал в конкурсе маленьких игр js13kGames

В программерских конкурсах широко принято и приветствуется написание постмортемов. Никаких похорон: фактически, это сочинение на тему «Что я узнал, участвуя в конкурсе».
Контест маленьких игр на джаваскрипте js13kGames не исключение, и я хотел бы поделиться с Хабром накопившимися ощущениями начинающего игродела.

(Для тех, кто хочет поиграть, но не хочет читать откровения, вот ссылка на игру.)

Для начала о самом конкурсе

читатель printf в своём постмортеме уже описал его, но я для полноты всё-таки повторюсь:
Нужно написать игру на js, которая после упаковки в zip будет занимать не больше 13 килобайт (точнее будет «кибибайт», но не все ещё привыкли к этой единице). Само собой, никаких ресурсов, подключаемых извне.

Кстати, оказалось, что 13 кб в zip для любителей код-гольфа — это просто огромное количество места. Забегаю вперёд скажу, что из 13 кб удалось использовать только чуть больше 8, отчего у меня остался очень непрятный осадок — игра недоделана, нужно было стараться лучше, место-то позволяет, ме-ме-ме!... Но это было потом, а теперь к самому началу.

Месяц до конкурса

Да, работа началась уже тогда. Но скорее, исследовательская и координационная. Я (Россия), Maxime Euziere (Франция) и Jim Herrero (США) ранее работали вместе над маленькими гольферскими проектами. Ну, и решили пилить игру вместе.

Пока неизвестен ни формат игры, ни тем более, тематика. Известно только, что игра будет зазипована, а ZIP можно сжать лучше или хуже. Проводились вялотекущие тесты эффективности различных способов архивации, на каких платформах и в каких распаковщиках они поддерживаются, и того, как вообще следует писать код под zip. Краткий отчёт о проделанной работе находится на gist.github.com (на английском языке). Вкратце: нормально поддерживается только сжатие Deflate.

А теперь немного о том, что в отчет не попало (но лежало на поверхности). С дефлейтом можно развернуться на полную и приложить zopfli! Я вообще люблю прикладывать zopfli к разным вещам. Как оказалось, даже не пришлось писать своих велосипедов для оптимизации zip (хотя я их, ессно, написал) — есть шикарный готовый перепаковщик AdvZip. Только тс-с-с!

Итак, заранее вооружившись мы стали ждать начала конкурса и объявления темы.

«Элементы: земля, вода, воздух и огонь»

Именно так гласила тема. В скайпе начался брейнсторм. Предлагались различные варианты — от бега с препятствиями (Turtles Can't Skate поразительно похож по механике на то, что обсуждалось) до игры типа Леммингов (опять же. мы были не одни с этой идеей). Но в итоге (на 3 или 4 день) было решено сделать что-то типа Bejeweled.

И тут швах: Максим решил уволиться с работы, и переехать в другой город. Он не мог принимать активного участия в написании кода, и предполагалось, что он будет помогать в гольфинге — уменьшении размера кода. Который, кстати, даже не пригодился. Но его мысли, предложения и наметки алгоритмов сильно помогли мне, и упоминание в «credits» он полностью заслужил. Все таки, одна голова хорошо, а две лучше. «Две» — потому что Джим свалил сразу после брейнсторма в неизвестном направлении по неизвестным мне причинам.

Управлять программистами — всё равно, что пасти котов. А если они находятся на разных сторонах земного шара — тут ваще пипец.

Разработка

Итак, идея проста: на доске есть плиточки (тайлы), которые можно объединять. Объединяем Воду и Землю, получаем Грязь. Собираем при плитки с грязью в ряд, они исчезают, и начисляются очки. Далее грязь нельзя объединить ещё раз с водой и землёй, иначе количество вариантов просто зашкаливало, и игрок вряд ли разобрался когда-нибудь в этой таблице алхимических элементов.

Тайлы

Для начала генерируем сами плиточки: форма, выпуклось и лёгонькая песчаная текстурка из Math.random(). Для каждой плитки эта текстурка создается заново, чтобы не было «эффекта батареи», когда глаз подсознательно начинает находить похожие элементы. Кстати, тайлы и фавыкон после каждой загрузки страницы слегка отличаются.

Это всё оказалось гораздо проще сделать, манипулируя с сырыми данными канваса: createImageData / putImageData. Правда, уголки у плиток не совсем скруглённые — это была ранняя наметка, когда я ещё не знал, что укладываюсь в доступное место с запасом. Но позже мне показалось, что так даже интереснее.

Сверху рисуется кривыми Безье иконки, уже силами и средствами API канваса.

Из полученного создается изображение (new Image()). Можно было бы оставить в виде канваса, но так и отлаживать легче, и операция копирования пикселей в канвас быстрее.

Задник генерируется так же, процедурной манипуляцией с сырыми пикселями. Задник чуть-чуть синеват, а тайлики желтоваты, это старый прикол, широко применяющийся в пиксель-арте: желтое кажется ближе. Задник этот ставится элементу canvas в качестве фона в css: незачем все это дело перерендеривать вручную, пусть браузер мучается. Кстати, отладчик Хрома как раз и мучался. Совет: не ставьте в background-image здоровенные картинки в data:image/*, дебаггер виснет ну просто неприлично.

Обработка координат курсора

Ну тут всё просто. Если вы когда-нибудь писали драг-н-дроп вручную, то тут всё то же самое. И из-за крайней долбанутости зоопарка браузеров координаты приходится вычислять как «координаты относительно экрана минус координаты левого верхнего угла элемента». И это в 2014 году.

Обработка мышиных и пальцетычковых событий в принципе одинакова. Единственное — пришлось заглушить Pointer Events, чтобы в Windows Phone всё работало. Да-да, Windows Phone, игра изначально разрабатывалась с расчетом на мобильные устройства, а мобильное устройство — это не только айпад.

Рендер и анимация

В простеньких игрушках обычно перерисовка обычно происходит по setTimeout / setInterval. При таком подходе фрейм-рейт низкий и почти постоянный, и «физика» обрабатывается покадрово. Да, в js таймеры могут проскальзывать при нагрузке на событийный цикл, но этим можно пренебречь.

Но я решил сделать «как большие мальчики», и использовать requestAnimationFrame (где он есть). А это ставило процесс рендера и анимации с ног на голову — фрейм срабатывал вообще когда угодно и как угодно часто от 60 раз в секунду до парочки раз в час. Но, господа, это круто. Результаты гораздо приятней.

Движок (если можно так назвать) работает следующим образом: есть массив игрового поля, «модель». Когда что-то в модели меняется, это происходит мгновенно, но в массив blockingAnimations кладётся объект, определяющий анимацию. Пока все анимации не завершились, requestAnimationFrame крутится постоянно. Когда завершились — производится проверка, может ли тайлики упасть под действием гравитации, взорваться или исчезнуть. Если нет — не запрашиваем requestAnimationFrame, незачем жечь процессор без нужды.

Тут я хочу сделать небольшую ремарку (если меня ещё кто-то читает): игра рассчитывалась как казуальная, и случай, когда пользователь поигрался и оставил её открытой в другой вкладке — более чем реален. А в какой-нибудь старой Опере под Windows 98 постоянный перерендер в соседней вкладке не вызывает ничего, кроме желания закрыть игру. Заботьтесь о своих пользователях, даже если это не пользователи, а игроки!

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

Бета. Вернее, Альфа

Тут я созрел на первый публичный тест. Получил массу отзывов и предложений.

Первый реквест — ввести очки поскорее. Это было сделано.

Второй — сделать все анимации неблокирующими. Тут пришлось ответить отказом, я не волшебник, я только учусь.

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

Добавлены очки, поправлены страшные баги при отрисовке. Настало время беты. Я проводил её честно — на людях, которые её раньше не видели. И все они говорили, что не понимают, что надо делать. Пришлось запиливать «how to play» (который, кажется, оказался невнятным). Это в свою очередь добавило багов при отрисовке. Короче, разработка, как она есть.

Но даже после «how to play» оставались вопросы: пояснения-то на инглише! Пришлось идти на решительный шаг — локализацию. В итоге в игре три языка — английский, русский и французский.

Мобильные устройства

То, что было «изначально заложено» пришло время вытаскивать наружу. С кодом всё шикарно, а с отображением — не очень.

Прежде всего, meta-viewport. Эта сволочь не работает. Вернее, работает, но вообще не так, как надо. Сказать при помощи него, что на эране должна отображаться область как минимум 750 × 770 можно, но ни один браузер этого не сделает. Пришлось указывать width=device-width,height=device-height, и эмулировать всё это через CSS transform (где он поддерживается).

И тут порадовал айпад — оказывается, document.documentElement.clientHeight / .clientWidth там всегда равны 1024. Что, согласитесь, абсолютно бесполезно. Пришлось подтыкивать костыли конкретно для айфона/айпада и определять через window.innerHeight.

Опять же, всплыла ещё одна проблема — при уменьшении канваса всё смотрится клёво, но при увеличении… гхм… как минимум, так себе. Всё замылено и неаккуратно. Ещё один костыль, и увеличение производится только со множителем кратным 2 (уменьшение — с любым). И пометка на будущее: канвас фиксированного размера — путь вникуда.

Звуки

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

Брать готовую либу не хотелось. Треккер я, к сожалению, не умею, и к написанию музыки не особо способен. Да и зачем фоновая музыка на веб-странице?

Иное дело — звуки. В качестве основы я взял генератор WAV от p01. Чтобы звуки не были похожи на Денди или спецэффекты из Sci-Fi восьмидесятых, модифицировал его на 16 бит и 44100 Гц. И начал экспериментировать.

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

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

Итог

А знаете, господа, инди-игродел — это интересно! В итоге — с интересом проведённый месяц, много опыта (причем в основном формата «надо было делать по-другому») и, главное, рабочая игра.

Вот она, кстати: Quintessence на js13kGames.

Автор: subzey

Источник

* - обязательные к заполнению поля