Я впервые участвовал в конкурсе JS13KGAMES, месячном соревновании, которое проходило с 13 августа по 13 сентября и посвящено созданию HTML5-игры, размер которой в сжатом виде не превышает 13 килобайт (игровой джем, смешанный с соревнованием по коду в гольф).

Я сделал Стрелок по лезвию, 8-битный демейк научно-фантастического фильма 1982 года Бегущий по лезвию. (официальная запись JS13KGAMES на http://js13kgames.com/entries/blade-gunner и ее все еще развивающееся альтер-эго на моем веб-сайте http://herebefrogs.com/bladegunner/).

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

Идея игры

В этом году темой JS13KGAMES был Glitch. Визуальный сбой был очевиден, но я не чувствовал себя достаточно уверенно, чтобы придумать что-то впечатляющее. Глюк, как и баги, должен был произойти сам по себе и не доставить большого удовольствия, поэтому мне пришлось искать что-то еще. Это оставило меня с глюком как игровой механикой (можно ли это вообще считать глюком, если это сделано намеренно?) и глюком как элементом повествования. Мне потребовалось 2 недели, чтобы представить себе управляющего фабрикой, пытающегося не дать глючным роботам повредить других роботов на производственной линии, что почему-то напомнило мне сцену погони в культовом научно-фантастическом фильме 1982 года «Бегущий по лезвию лезвия: Декард пытается подобраться к убегающий репликант на переполненной улице, прежде чем он разрядит пистолет, чтобы избежать ранения прохожих. Казалось бы, сбалансированный геймплей, в котором игрок должен действовать быстро, чтобы ограничить потери, вызванные глючными андроидами, и в то же время не действовать небрежно, чтобы не нанести больше урона при этом.

За 2,5 недели до окончания конкурса я поставил перед собой цель создать с нуля одноэкранную андроид-охотничью игру. Только когда у меня заработала большая часть основной механики, я задумался о реиграбельности. Я не большой поклонник игр типа «бесконечный бегун», поскольку они, как правило, кажутся наименее конкурентоспособными игроками довольно повторяющимися, но я чувствовал, что добавление еще одного андроида для нейтрализации после каждого пройденного уровня было все еще самым простым вариантом для первой попытки игры. . Я постараюсь создать лучший кураторский опыт в следующем году.

Арт-директор

Я практиковал свои навыки пиксельной графики около года, участвуя в конкурсе Pixel Daily Challenge в Твиттере (https://twitter.com/Pixel_Dailies), и JS13KGAMES ощутил идеальное подтверждение моего обучения. Мне нужно было быстро создать красивый набор плиток с небольшим следом за короткое время. Мне нравились маленькие спрайты, такие как спрайты из Super Mario или The Legend Of Zelda, и я не хотел тратить время на долгие часы, зацикливаясь на выборе цвета (правда о пиксельной графике: сложная часть не столько в размещении пикселей, сколько в подбор правильных цветов для создания света и теней). Поэтому я принял ограничения виртуальной консоли Pico8: ее цветовая палитра ограничена 16 вариантами, которые хорошо сочетаются друг с другом, а ее спрайты имеют размер 8 x 8 пикселей (я использовал 9 x 9, чтобы дать себе немного больше творческой свободы).

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

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

Технические решения

Поскольку времени было мало, я не стал инвестировать в изучение игрового микрофреймворка, а не в изобретение совершенно новой игры. Вместо этого я полагался на ванильный JavaScript, который является отличным способом изучения основ.

Я использовал довольно типичный игровой цикл и визуализировал состояние игры за кадром на холсте размером 198 x 153 пикселей (со спрайтами 9 x 9 это квадратная сетка 22 x 17). Затем я визуализировал это изображение на экранном холсте, который был масштабирован, чтобы соответствовать окну браузера, сохраняя при этом соотношение сторон внеэкранного холста. Таким образом, мой пиксельарт был увеличен в 2-4 раза в зависимости от размеров окна браузера, но не искажен. Мне пришлось отключить сглаживание, чтобы сохранить четкие края спрайтов, что я сначала попытался сделать с помощью CSS-свойства "image-rendering: pixelated", но только Chrome учитывает это значение, так что в итоге я так и сделал. программно, установив "imageSmootingEnabled = false" в двухмерном контексте моего экранного холста (который необходимо устанавливать каждый раз, когда размер экранного холста изменяется, чтобы соответствовать новым размерам окна браузера).

Я использовал 42 глобальные переменные и 35 функций, что является простым, но не очень удобным способом структурирования кода. До оптимизации Blade Gunner состоит из:
 – 187 байт HTML-разметки,
 – 718 байт тайлового листа в формате PNG,
 – 23 422 байт кода игры на JavaScript (включая 983 байта base64). закодированный тайловый лист)
— 2517 байт уменьшенного кода JavaScript для JSFXR, библиотеки звукового синтезатора
Я использовал UglifyJS для минимизации своего кода JavaScript и конвейер Gulp для встраивания его в HTML-страницу. После этих основных оптимизаций Blade Gunner состоял из одного HTML-файла размером 16 953 байта, который был сжат до Zip-файла размером 6402 байта, оставив 52% от ограничения в 13 КБ для дополнительных функций.

Но единственное, чего мне не хватало, так это времени, а не пространства: я тратил все свое свободное время после работы и перед сном (или около 2,5–3 часов в день) в течение 14 дней подряд, что в сумме составляет примерно 40 часов разработки. время.

Уроки выучены

# 1 — Управляйте проектом, черт возьми, из списка функций

Поскольку до закрытия конкурса оставалось 14 дней, я знал, что должен победить в исполнении Blade Gunner, и не мог позволить себе терять время, заходя в тупик. Я составил исчерпывающий список функций, каждая из которых описана просто (например, герой может двигаться, андроид может стрелять…). Затем я разделил функции на 3 группы:

  1. то, что было абсолютно необходимо, чтобы иметь самую простую, но играбельную игру
  2. то, что нужно, чтобы немного оживить игру
  3. что было бы хорошо иметь, если бы все остальное было сделано

Наконец, я упорядочил функции в каждой группе на основе их зависимостей (например, герой должен уметь стрелять, прежде чем андроид сможет умереть). В ходе разработки я несколько раз пересматривал эти списки, отмечая завершенные функции и меняя местами некоторые из них между группами в зависимости от моего прогресса и знаний.

Первая группа была крайне важна, потому что, если все остальное потерпело неудачу и у меня закончилось время, по крайней мере, у меня была играбельная игра (хотя и очень голая), которую я мог представить. Из этой основы я удалил все функции из моей второй группы, постепенно улучшая Blade Gunner. Третья группа стала синонимом второстепенных игровых механик, которые я хотел бы изучить, когда конкурс закончится.

№2 – Делайте это с удовольствием

Я слышал слишком много историй о том, как разработчики игр начинали с таких вещей, как движок 3D-рендеринга; все их время втягивается в это, и через 6 месяцев у них все еще нет ничего играбельного. У меня была твердая решимость использовать противоположный подход, сосредоточив внимание только на функциях игрового процесса, используя самые простые графические заполнители (например, простые цветные фигуры), с обоснованием того, что, если бы игровая механика была хорошей, в игру было бы интересно играть даже без графика.

В первый день я выполнил свой план, и вот как выглядел Blade Gunner:

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

Во второй день я работал над спрайтами и анимацией двух главных героев:

На третий день я заменил заполнители окончательным рисунком, но использовал только первый спрайт анимации ходьбы:

Они жестко скользили по экрану, но этого было достаточно, чтобы передать дух моей игры. Затем последовали пули и экран «игра окончена». Я не начал внедрять анимацию спрайтов, пока не смог сыграть и выиграть Blade Gunner (что было легко, потому что глючные андроиды еще не стреляли в ответ ;p). Это определенно выглядело лучше, когда все персонажи начали двигаться убедительно, но ни один плейсхолдер не смог бы вызвать возбуждение от того, что даже самые простые спрайты двигаются туда-сюда, занимаясь своим бизнесом по стрельбе из клинков.

№ 3 — Разработайте системные правила, регулирующие игру

Я только что закончил читать Spelunky, потрясающую книгу по разработке игр, изданную Boss Fight Books (https://bossfightbooks.com/collections/books/products/spelunky-by-derek-yu) и написанную Дереком Ю, создателем игры. сам творец. В своем рассказе о том, как появилась Spelunky, Дерек объяснил свою любовь к игровым системам, в которых несколько правил последовательно управляют и применяются ко всем объектам игры. Изначально я перечислил перемещение героя, андроидов и свидетелей как 3 отдельных элемента, стрельбу — 2 (герой, андроиды) и смерть — 2 (андроиды, свидетели). Помня о принципах Дерека, я быстро понял, что герой, андроиды и наблюдатели — это одно и то же существо с разными прикрепленными спрайтами и некоторыми включенными или отключенными способностями (прохожие не могут стрелять, и изначально я даже не думал, что герой может умереть). , что сэкономило мне много времени на разработку многих правил, специфичных для сущностей.

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

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

#4 — Загрузка ресурсов

Когда дело дошло до загрузки моего набора плиток в объект изображения, чтобы я мог отобразить спрайты персонажей на холсте HTML, у меня было 2 варианта: URL-адрес PNG-файла набора плиток или URL-адрес данных (строка символов ASCII, представляющая двоичные данные PNG). закодировано в base64)

// option #1 file URL
var i = new Image();
i.addEventListener('load', function() { // image can be drawn });
i.src = 'assets.png';
// option #2 data URL
var i = new Image();
i.src = 'data:image/png;base64,iVBORw0KGgoAAAANS...RU5ErkJggg==');
// image can be drawn

Довольно легко найти примеры кода или инструменты, которые преобразуют ваш PNG-файл в строку в кодировке base64. Однако этот URL-адрес данных обычно вдвое превышает размер исходного изображения. Зачем мне выбирать этот вариант в соревновании по игре в гольф, таком как JS13KGAMES, где важен каждый байт?

Ну, скорость. Из того, что я прочитал, привлекательность URL-адреса данных заключается в том факте, что браузерам № 1 не нужно делать отдельный HTTP-запрос для загрузки изображения, поскольку URL-адрес данных уже встроен в мою HTML-страницу, а № 2 объект изображения доступен для рисования, как только для его атрибута src задан URL-адрес данных.

Оказывается, Chrome — единственный браузер, в котором верно № 2. В Firefox, Safari и IE/Edge мне приходилось ждать, пока изображение вызовет событие «загрузки», как обычно при использовании URL-адреса файла, прежде чем я мог безопасно отобразить его на своем холсте HTML. Тем не менее, это ожидание ничтожно мало по сравнению с двусторонним обращением по сети, необходимым для извлечения файла внешнего актива. Поскольку у меня было намного меньше предела в 13 КБ, я решил оптимизировать свою игру для времени загрузки, а не для размера, и использовал эту версию:

// safest way to load data URLs on all browsers
var i = new Image();
i.addEventListener('load', function() { // image can be drawn });
i.src = 'data:image/png;base64,iVBORw0KGgoAAAANS...RU5ErkJggg==');

#5 — Остановка игрового цикла

Некоторые примеры игровых циклов, с которыми я столкнулся, выглядели так:

// what I read online
function loop() {
   requestAnimationFrame(loop); // repeat ASAP once this loop ends
   render(); // the game state
   update(); // the game state
}

Казалось нелогичным запускать игровой цикл, запрашивая постановку в очередь другого игрового цикла, пока я даже не обновил или не отрендерил свое игровое состояние, поэтому я переместил вызов requestAnimationFrame() в конец цикла.

// what I tried instead
var requestId;
function loop() {
   update(); // the game state
   render(); // the game state
   requestId = requestAnimationFrame(loop);
}
function update() {
   ...
   if (gameOver) {
      cancelAnimationFrame(requestId);
   }
}

Однако стало ясно, почему это было ошибкой, когда я попытался остановить игровой цикл, когда игра закончилась: я сохранил идентификатор запроса, возвращаемый requestAnimationFrame(), в глобальную переменную и передал его в cancelAnimationFrame(), что предотвращает анимацию в очереди. запрос кадра от выполнения следующего. Это не сработало: поскольку requestAnimationFrame() выполнялся последним в моем игровом цикле, при следующем запуске update() requestId фактически был установлен на запрос кадра анимации, в котором в данный момент выполнялся update(), и, следовательно, не мог быть исключен из очереди.

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

// what I read online was correct but not the complete picture
function loop() {
   var requestId = requestAnimationFrame(loop);
   render();          // the game state
   update(requestId); // the game state
}
function update(requestId) {
   ...
   if (gameOver) {
      cancelAnimationFrame(requestId);
   }
}

# 6  — HTML для игры в гольф

Вот как выглядела моя HTML-страница:

<style> body { margin: 0; background: #121212; ... } </style>
<canvas></canvas>
<script>
  document.title = "Blade Gunner";
  ...
</script>

Оказывается, это все, что вам нужно! Современные браузеры сгенерируют для вас отсутствующие теги ‹html›, ‹head› и ‹body›, переместят тег ‹style› под ‹head›, а остальные поместят под ‹body›. Что касается заголовка страницы, который вы обычно выражаете с помощью тега ‹title› под ‹head›, document.title позволяет вам установить его программно.

Что я буду делать по-другому

#1 — Четко объясните цели игры

Я полагал, что все видели «Бегущего по лезвию» раньше, и помнил, что копы «Бегущего по лезвию» очень старались никогда не увольнять человека по ошибке, а это значительный риск в их положении. Поэтому я подумал, что будет очевидно, что стелс — это стиль игры. В финальном варианте игры я намеренно не использовал одну вариацию спрайта для всех андроидов, поскольку это сделало бы их легкой мишенью (как я сделал во время разработки по той же самой причине, что это облегчило мне отладку). Вместо этого я использовал те же спрайты, что и прохожие, спрятав среди них андроидов, как в фильме.

Я знал, что это будет проблемой, когда разрабатывал стартовый экран, потому что не мог выразить это кратким предложением, которое уместилось бы на экране. Два спрайта справа были моей попыткой предупредить игрока, что у глючных андроидов красные глаза и вытащенное оружие. Как и ожидалось, ранние игровые тесты показали, что, хотя некоторые игроки получили это, другие уничтожили всю популяцию наблюдателей и не поняли, что они сделали не так, чтобы заслужить экран «игра окончена».

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

№2 — Пиксельный шрифт

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

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

№ 3 — Подсчет очков

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

В этом году я видел много записей JS13KGAMES, которые успешно это сделали, и предложили простой способ дразнить других игроков в Твиттере их рекордами, повышая вирусный фактор игры.

#4 — Обнаружение столкновений

В моем первоначальном видении игрок должен был пробиваться сквозь толпу, чтобы добраться до андроида, как это делал Декард в «Бегущем по лезвию»: он преследовал репликанта по переполненной улице.

Я знал, что обнаружение и разрешение столкновений будет чем-то довольно сложным для разработки, чтобы все выглядело как надо (герой должен был оставаться на курсе, пока его замедляли, а прохожие силой отклонялись от их траектории). Я продолжал отодвигать его вниз по списку и обменивать на более легкие «выигрыши», на разработку которых ушло меньше времени. Я согласен с этим решением, так как игра выиграла больше от всех этих небольших улучшений, но я позабочусь о том, чтобы не сохранять столкновение в последний день моей следующей попытки JS13KGAMES, потому что это определенно имело бы большое значение, если бы был частью игры.

#5 — Мобильная поддержка

Эта игра направлена ​​на увеличение шансов выиграть приз JS13KGAMES. Для простоты я реализовал управление клавиатурой только в Blade Gunner. Я быстро подумал о сенсорном управлении (перетаскивание, чтобы переместить героя, касание, чтобы прицелиться и выстрелить), но не стал тратить время на работу над ним, так как подозревал, что это потребует гораздо больше усилий, чтобы оно чувствовалось хорошо (перетаскивание героя означает, что большой палец, вероятно, закрывает спрайт и, следовательно, не позволяет точно двигаться).

Тем не менее, когда я смотрю на исторические записи игр из последних пяти конкурсов JS13KGAMES, настольные компьютеры являются самой популярной категорией (в среднем ~ 120 заявок) по сравнению с мобильными (~ 40) и серверными (~ 10). Сервер требует гораздо больше работы, но я считаю, что добавление поддержки мобильных устройств — это легкий плод, если игровая механика легко переводится на сенсорное управление, что увеличивает мои шансы быть замеченным судьями, поскольку моя игра входит в менее переполненную категорию.

В заключении

Моей целью участия в JS13KGAMES было создание игры от начала до конца. Забавно: дайте мне произвольный крайний срок, и я найду время для создания этой игры, на создание которой у меня не было бы времени, если бы я сам установил крайний срок.

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

Если вы искали предлог или мотиватор, чтобы приступить к реализации своей игровой идеи , JS13KGAMES – это прекрасная возможность: в ней очень весело участвовать и есть отличное место, где можно поучиться у других (есть сотни приемов). обнаружить, просматривая код прошлых записей). Ограничение в 13 КБ заставит новичков представить короткую игру, а не неоправданно большую, и бросит вызов опытным разработчикам игр, чтобы они втиснули весь контент и уровни, о которых они мечтали, в свои конкурсные работы.