В 2018 году я подал заявку на участие в уже не существующем соревновании по гольфу JS1k. Темой того года была Неустойчивая шахта для монет, и я представил простую игру, в которой вам нужно было собирать монеты из нестабильной шахты, избегая при этом валунов. Я занял 10-е место, и это все еще для меня гордость.



Volatile Coin Mine - JS1k 2018
JS1k 2018 demo: 'Coins and Boulders' - Шахта рушится! Получите монеты. Избегайте валунов. mock.xyz



Как вы, наверное, догадались, записи JS1k должны быть не более 1 КБ. Придется много работать, чтобы выжать все до последней капли из каждого байта. Есть много действительно забавных техник сохранения байтов, некоторые из которых вы можете увидеть в несжатом исходном коде моей статьи. Многое заставит вас съежиться - это не для реального мира! ¹

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

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

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

Каждый кадр представлен строкой RLE. Например, первый кадр представлен с помощью x2d3x3dbx4b3x3bcx4bcx4c2x3ac2x4a2.

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

  • Каждая буква представляет собой цвет. В моем коде x прозрачный, a черный, b коричневый и так далее.
  • Число представляет, сколько раз следует повторить предыдущий цвет. Например, a2 будет шириной 2 единицы.
  • Единицы длиной 1 не содержат числа.
  • Ширина спрайта игрока - 6 единиц. Это означает, что строка RLE со значением a6 будет представлять собой черную линию, которая является полной шириной спрайта.
  • Вы можете думать о RLE как о потоке пикселей. Например, в спрайте шириной 6 единиц вы описываете третью форму ниже, используя x3a6, а не x3a3a3. Последний работает, но нам не нужны 2 лишних байта. Аналогичным образом a36 нарисовал бы черный квадрат.

Я также использовал RLE для всех остальных поз персонажей и игровых ресурсов. Некоторые из них имеют размеры, отличные от размеров спрайта игрока, например, монета и валун имеют ширину 4 единицы вместо 6.

Обратите внимание, что RLE также может быть выражено иначе, чем показано здесь. В некоторых реализациях положение буквы меняется, то есть число стоит перед буквой.

Рендеринг спрайтов

Код рендеринга спрайтов в моей записи не очень удобочитаем, поскольку он оптимизирован для очень специфического случая использования. Давайте перепишем его для общего пользования.

Сначала нужно расшифровать строку. Эта функция преобразует строку вида x3b3 в xxxbbb. Функция использует replace и простое регулярное выражение, которое будет соответствовать любому символу слова, за которым следует одна или несколько цифр. Второй аргумент replace - это функция, которая повторяет соответствующий символ слова (\w) необходимое количество раз (\d+).

После декодирования строки отрендерить наш спрайт относительно просто.

Первый шаг - преобразовать декодированную строку в массив и перебрать каждый символ. Цвет текущего пикселя можно найти, сопоставив символ, например a, палитре. Затем необходимо найти координаты x и y. Они рассчитываются с использованием текущего индекса и ширины (в единицах) спрайта.

Наконец, на экране отображается текущий «пиксель». В этом примере я использую HTML Canvas и fillRect.

Параметр width позволяет функции отображать спрайты разных размеров, например. 4 единицы шириной. Параметр scale используется для увеличения размера каждого «пикселя», например scale из 5 отобразит каждый из «пикселей» спрайта как блок 5 × 5 пикселей.

Вы можете увидеть пример кода для примера полного цикла ходьбы на Repl.it. Если вы хотите увидеть, как я с этим справился в своей записи JS1k, вы можете просмотреть несжатый исходный код на Github.

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

[1] Например, ~~n можно использовать вместо Math.floor(n) и +'1' вместо parseInt('1', 10). Подчеркивается Can - я бы никогда не стал этим заниматься, кроме кодового гольфа.