image

У всех была детская мечта. Лично я мечтал создать игру для моей первой консоли: Nintendo Game Boy. Сегодня моя мечта реализовалась — я выпустил первую игру для Game Boy на настоящем картридже: Sheep It Up!

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

  • Часть 1: использованные инструменты/технические сложности/сложности с графикой
  • Часть 2: ограничения звука/создание картриджей/отзывы игроков (эта часть пока пишется)

Игра


"Sheep It Up!" — это аркадная игра, в которой овца должна взбираться вверх, цепляясь за летающие застёжки-липучки. Концепция проста, но сама игра стремительно становится всё сложнее: как высоко вам удастся забраться, не упав?

Геймплей Sheep It Up!

Я сам коллекционер, поэтому хотел, чтобы проект создавался в духе старых игр Game Boy. Поэтому всё было разработано специально для этой игры: печатная плата, ROM, оболочка, защитный корпус и даже наклейка! Также мы стремились сохранить разумную цену, чтобы игрой мог насладиться каждый: 15 долларов (+ доставка). Она запустится на любой модели Game Boy, от самой первой до GBA SP, в том числе и на Super Game Boy.



Если у вас всё ещё есть Game Boy, то вы можете купить картридж на веб-сайте издателя:

https://catskullgames.com/sheep-it-up

Инструменты: спасибо, 2017 год!


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

В 2017 году по-прежнему нужно приложить много труда, чтобы сделать игру для 8-битной консоли. Но благодаря чудесным сообществам любителей у нас есть множество инструментов, облегчающих жизнь! Без них такой новичок, как я, не смог бы в одиночку создать игру для Game Boy. Так что же это за инструменты?

Для начала, расскажу о языке программирования. В те времена всё специализированное игровое оборудование программировалось на ассемблере. Это до сих пор возможно (и даже рекомендуется). Но теперь это не единственный вариант, потому что многие комплекты разработки для 8-битных и 16-битных консолей основаны на языке C. Для Game Boy этот потрясающий инструмент называется Game Boy Developers Kit (GBDK).

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

  • Game Boy Tile Designer (GBTD). Он позволяет рисовать спрайты и тайлы, а затем экспортировать их в двоичный формат, понимаемый Game Boy.
  • Game Boy Map Builder (GBMB). Этот инструмент позволяет строить уровни и фоновые изображения на основании тайлов, нарисованных в GBTD (это как Tiled, но для Game Boy).


Последнее, но тоже важное — нам нужен какой-то способ для тестирования игры. И здесь тоже важны современные инструменты. В 90-е разработчикам приходилось использовать дорогущие комплекты ICE, но сегодня у нас есть мощные программные эмуляторы, которые можно запустить на любом компьютере. Для тестирования собственной игры лучшим выбором будет BGB. Это очень точный эмулятор Game Boy с мощным отладчиком — обязательный инструмент для создания работающей игры!


Сама игра находится в правом верхнем углу, а все остальные пугающие окна — это разные инструменты отладки!

Но будьте готовы к тому, что для обеспечения работоспособности игры придётся тестировать её на настоящем оборудовании. В 90-х люди записывали свою программу на чип EPROM и использовали специальные картриджи для вставки этого чипа в настоящий Game Boy. Этот процесс эффективен, но довольно длителен и дорог. Сегодня у нас есть так называемые «Flashcarts» — картриджи, в которые можно вставлять SD-карты с образом ROM, запускаемый на Game Boy. Идея похожа, но новые инструменты более удобны и быстры в использовании. Существуют разные Flashcarts для Game Boy, но по-моему лучший создан Krikzz: Everdrive Game Boy. Более новая и улучшенная модель была выпущена этим летом, но я пользовался старой версией, которую купил раньше.




Flashcart из 90-х и из 2010-х

Несмотря на существование в 2017 году всех этих замечательных инструментов, создание игры для игровой консоли 1989 года всё равно остаётся сложной задачей. Особенно для тех, кто привык использовать «современные инструменты», такие как Unity, Unreal Engine или Godot. Я расскажу об основных трудностях, с которыми столкнулся в процессе разработки Sheep It Up!, в том числе о том, как меня удивили принципы работы Game Boy.

Технические сложности


Ограничения по размеру


Давайте начнём с очевидного: Sheep It Up! — это довольно простая игра. Это объясняется одной причиной — вся игра весит всего 32КБ. Именно — весь код, изображения и даже звуки уместились в крошечное пространство 32КБ. Для сравнения могу сказать, что 32КБ — это размер логотипа Википедии в очень маленьком разрешении:


Полная игра: 32КБ


160x146 пикселей (PNG-24): 32КБ

Разумеется, не каждая игра для Game Boy умещается в 32КБ. Для меня это было техническим ограничением, чтобы выпустить игру на настоящем картридже (подробнее об этом во второй части). Лучшие и самые известные игры для Game Boy на самом деле весят гораздо больше:

  • Pokemon Red / Blue занимают 1024КБ (огромные игры!)
  • Wario Land и Zelda Link's Awakening — 512КБ
  • Kirby Dream Land — 256КБ
  • Gargoyle's Quest — 128КБ

В реальности, только несколько игр для Game Boy занимали всего 32 КБ. В основном они были созданы на ранних этапах жизни консоли. Например, и Alleyway (одна из игр, выпущенных вместе с Game Boy в Японии) и Tetris (выпущенная вместе с консолью в США и Европе) умещаются в 32КБ. Обе эти игры замечательны, но из-за малого размера картриджа они довольно «ограничены» в масштабе: всего один экран, малое количество графических и звуковых ресурсов и т.д.



Alleyway и Tetris были играми на 32КБ, как и Sheep It Up!

Процессор: играем с портативной мощью!


Game Boy имеет процессор на 4МГц, специально разработанный для консоли (это сочетание процессоров Zilog Z80 и Intel 8080). В целом, вычислительная мощность Gameboy сравнима с NES, и даже немного выше благодаря меньшему размеру экрана и количеству цветов (подробнее об этом позже). Несмотря на низкую скорость в 4МГц, все игры для Game Boy обеспечивали стабильные 60fps. Учитесь, PS4 Pro и Xbox One X!

Но для только что выпустившегося из колледжа программиста самым большим ограничением была восьмибитность процессора. Как вы наверно знаете, внутри все компьютеры для обработки данных используют нули и единицы. Одна цифра, которая может иметь значение 0 или 1, называется «битом» (сокращённо от «binary digit», «двоичное значение», потому что она может иметь всего два значения). Процессор называется «8-битным», если он может за одну операцию обрабатывтаь 8 бит данных.

Как это влияет на создание видеоигр?

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

При 8 битах данных целое число может хранить меньшие значения по сравнению с 16-битными переменными:

  • 8-битная целая переменная: от -127 до 128 (или от 0 до 255 без использования знака)
  • 16-битная целая переменная: от -32768 до 32767 (или от 0 до 65535 if без использования знака)

Если вам всё ещё это непонятно, давайте возьмём конкретный пример из Sheet It Up!


Как вы видите на картинке, переменная очков содержит 5 разрядов и может изменяться от 0 до 99999. К сожалению, это намного больше, чем может храниться в 8-битном целом числе. На самом деле, даже 16-битной переменной будет недостаточно для хранения такого «большого» счёта!

Итак, для отслеживания количества очков в Sheep It Up! мне пришлось использовать не одну, а пять разных 8-битных целых переменных. Представьте, насколько «весело» обрабатывать все эти переменные, особенно если учесть, что я добавил возможность сохранения рекордных результатов. Мне приходилось сравнивать два значения, хранящихся в пяти разных переменных. Программирование на 8-битной системе заставляет постоянно искать подобные сложные решения, в то время как современные системы просто используют 32-битные или 64-битные переменные, и никогда не доставляют своим программистам таких проблем.

(Примечание для опытных разработчиков игр: я знаю, что можно было найти более эффективный подход, чем хранение единственного разряда со значениями от 0 до 9 в целом int8. Но на самом деле мне нужно было, чтобы каждая переменная могла принимать значения от 0 до 255, потому что я храню не сами разряды очков, а идентификаторы тайлов, используемые для отображения этого числа на экране.)

Сложности с графикой


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


Все изображения на Game Boy состоят из двух элементов: фонового слоя (BKG) и нескольких подвижных объектов, называемых спрайтами (OBJ). На экране может быть не более 40 спрайтов. Кроме того, есть ещё одно ограничение: Game Boy не может отображать больше десяти спрайтов в одной строке.

Фон (BKG) и спрайты (OBJ)

На самом деле, всё немного сложнее, потому что существует дополнительный слой «окна», который может прокручиваться независимо от фона. Обычно он используется для пользовательского интерфейса (очков и т.д.). Но этот слой непрозрачен: он скрывает все графические данные слоя фона, находящиеся за ним. Поэтому ради простоты можно считать, что в Game Boy есть один «фоновый» слой, но при необходимости его часть может прокручиваться независимо.

Game Boy видит четыре цвета!


Давайте, наконец, познакомимся с самым очевидным: оригинальная модель Game Boy могла отображать всего четыре различных цвета.


Кто-то может возразить, что Light Grey (светло-серый) и Dark Grey (тёмно-серый), на самом деле скорее Light Green (светло-зелёный) и Dark Green (тёмно-зелёный), но это не важно: для рисования графики мы в любом случае можем использовать всего четыре цвета. По крайней мере, это относится к фону, потому что в случае со спрайтами ситуация другая!


И в самом деле, как видно на картинке, овца нарисована всего тремя цветами. Почему же? На самом деле я нарисовал спрайт четырьмя цветами. Но четвёртый цвет (здесь это чёрный) просто не отображается, потому что используется Game Boy как «прозрачный цвет». Благодаря этому не все спрайты становятся квадратными изображениями, они могут иметь различные формы, позволяющие видеть за ними фон.


Значит ли это, что нельзя использовать для рисования спрайтов чёрный цвет?

Разумеется нет, как видно ниже на примере спрайта «липучки». Для этого спрайта я использовал другую палитру из 3+1 цветов: «White» используется в качестве прозрачного цвета, а «Black» отображается на спрайте.


Веселье с палитрами


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


Как вы видите, есть одна палитра в четыре цвета для фонового слоя и две палитры в 3 цвета + 1 прозрачный цвет, которые могут использоваться спрайтами (каждый спрайт может использовать одну или другую палитру). Здорово то, что можно свободно назначать любому слоту палитры любой цвет. Это позволяет создавать очень интересные эффекты, например, постепенное снижение и увеличение яркости экрана, используемое во многих играх.


Чтобы реализовать на Game Boy затенение экрана, достаточно изменять цвета в трёх палитрах. На первом этапе все слоты палитр заполнены белым цветом. Затем на втором этапе самый тёмный слот цвета (чёрный) заполняется светло-серым, постепенно проявляя изображение. На третьем этапе светло-серый становится тёмно-серым, и так далее, пока в каждой палитре не отобразится все четыре цвета.

С помощью похожего способа мы можем заставить персонажа «мерцать», в каждом кадре меняя цвета в палитре. Например, такой способ используется, когда Марио в Super Mario Land ловит звёздочку и становится неуязвимым.

Создаём мир из тайлов!


Есть ещё одна, последняя, странность в том, как Game Boy отображает изображения, особенно она непривычна для разработчика игр, работающего с движками текущего поколения. Поскольку консоль не имеет «буфера кадра», мы должны задавать цвет каждого пикселя на экране по отдельности. Все экранные изображения собираются из множества «тайлов», т.е. пикселей 8x8. Это относится и к фонам, и к спрайтам:


Эта техника, предназначенная для снижения количества видеопамяти, необходимой для отображения графики, подразумевает, что мы не можем отрисовывать произвольные линии на экране Game Boy. Но это ещё и один из секретов, позволяющих Game Boy отображать такие красивые игры с процессором всего в 4МГц и видеопамятью в 8КБ (всё верно, килобайт, а не мегабайт или гигабайт).
Однако один из недостатков такого тайлового отображения заключается в том, что наши спрайты овцы и «липучки» состоят из нескольких спрайтов. Овца имеет размер 16x16 пикселей, поэтому в теории для её отображения необходимы четыре спрайта 8x8. К счастью, у Game Boy также есть режим отображения спрайтов 8x16, и это означает, что каждый объект в Sheep It Up! состоит всего из двух спрайтов, а не из четырёх.


Помните ли вы, что мы ограничены максимумом в 40 спрайтов на экране? Кроме того, Game Boy не может отображать более 10 спрайтов в одной строке.

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

Полноэкранные тайлы


Я не упоминал этого ранее, но разрешение экрана Game Boy равно 160x144 пикселям. Это значит, что для закрытия всего экрана нужно 20x18 = 360 тайлов.


К сожалению, видеопамять Game Boy ограничена (8КБ), и может хранить всего 256 разных спрайтов. Это означает, что без программных трюков невозможно отобразить полноэкранное изображение:


Секрет заключается в повторном использовании тайлов: как вы видите, в изображении тайла есть много «пустого пространства», то есть один «белый тайл» из ОЗУ располагается на изображении несколько раз. На экране заставки используется всего 178 разных тайлов, то есть при желании я мог бы добавить дополнительных деталей.


Я не знаю, какой способ повторного использования тайлов применялся в 90-х, но сегодня у нас есть для этого очень удобный инструмент под названием Game Boy Tile Data Generator. Мы передаём ему изображение PNG (в четырёх цветах), и он автоматически генерирует тайлы и карту тайлов, которую нужно отображать на настоящем Game Boy. Очевидно, что он также автоматически распознаёт и повторно использует одинаковые тайлы, чтобы сэкономить как можно больше видеопамяти консоли!


Для справки: большинство консолей 80-х и 90-х работали похожим образом и имели экран на основе тайлов 8x8: Nes, Master System, PC-Engine, Super Nintendo, Genesis, Game Boy, Game Gear... Первыми популярными домашними консолями с буфером кадра были PlayStation и Saturn, а первая портативная консоль с этой технологией была Game Boy Advance (Atari Lynx стала первой портативной консолью с управлением отдельными пикселями). То есть освоив «работу с тайлами» на Game Boy, вы можете использовать свои умения в дальнейшем на других ретроконсолях!

Советы профессионалов: создание четырёхцветных спрайтов!


Давайте закончим небольшим советом, которые дали талантливые разработчики из Nintendo. Посмотрите на скриншот из Wario Land:


Вы видите, что спрайт монетки на самом деле нарисован четырьмя цветами? Как им удалось это сделать, если я говорил, что Game Boy может отображать всего три цвета на спрайт?

GIF

Ответ прост: каждая монета состоит из двух спрайтов, и в каждом спрайте используется своя палитра. Как вы видите, когда мы разделяем монету на две половины, она на самом деле создана из двух спрайтов 8x16. Как обычно, Nintendo очень внимательна к деталям: они не просто выглядят двумя соединёнными «половинками спрайта», разработчики слегка сместили их и они перекрывают друг друга на один пиксеь, что делает монету больше похожей на единый элемент!

Заключение


На этом завершается первая часть постмортема моей игры. Благодарю за прочтение! Во второй части мы обсудим проблемы со звуком (там тоже есть немало странного!) и создание картриджей без уничтожения уже имеющейся игры для Game Boy. Также я разберу отзывы, полученные от игроков в Sheep It Up!


Надеюсь, что вам понравилась статья! И если у вас сохранилась консоль Game Boy, то не забудьте, что вы можете купить прекрасный картридж Sheep It Up всего за 15 долларов у Catskull games. Перед отправкой каждый картридж собирается вручную!

Комментарии (14)


  1. jaiprakash
    26.12.2017 11:32

    А что не так с восьмибитностью и 16/32-разрядными переменными? Переноса на процессоре нет?
    (Если я не разговариваю с переводом)


    1. AbstractGaze
      26.12.2017 11:35
      +1

      Что мешало проверить перевод это или нет? Тем более что обратили внимание? Да это перевод.

      Переводчик кстати тоже может ответить, если тема перевода в его компетенции.


      1. jaiprakash
        26.12.2017 11:38

        В приложении хабра в самой статье не видно что это. Но в принципе заметно. Потом вышел в список статей — там плашка есть.


    1. beeruser
      27.12.2017 05:10

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


      1. mobi
        27.12.2017 09:42

        Можно хранить в виде binary-coded decimal и использовать в 2 раза меньше памяти (если, конечно, из процессора не выкинули команду DAA, которая есть в 8080 и Z80).


        1. beeruser
          27.12.2017 21:24

          >> 2 раза меньше памяти
          DAA там есть. Но вам что, жалко 2(!) байта стало?
          Вы потратите куда больше чтобы достать старший ниббл.
          В GBCPU нет команды RLD, поэтому так:

          rrca
          rrca
          rrca
          rrca
          and #0f

          6 байт

          К тому же он хранит не число 0-9, а номер тайла, поэтому ещё 2 байта =)

          add a, start_tile_id


          1. mobi
            27.12.2017 23:09

            Вопрос же не только в удобстве вывода, но и в удобстве инкремента. Хотя, у меня в итоге получилось только 4 байта сэкономить (11 байт для BCD-кода, позволяющего добавлять 1-99 очков, и 15 байт для «обычного» кода, добавляющего 1-10 очков), так что возможно в данном случае такая оптимизация действительно не имеет смысла.


            1. beeruser
              28.12.2017 02:14

              15 это с установкой адреса?
              Попробовал ради интереса, получилось 14 без установки адреса (+3 байта)

              ;hl address
              ;a value 0-10
              
                  ld b,5
              l1: add a,(hl)
                  cp start_tile_id+10
                  jr c, l2
                  sub 10
              l2: ld (hl+),a ;постинкремент
                  sbc a,a ; эти две команды получают 
                  inc a   ; a = carry ? 0 : 1
                  djnz l1

              >> возможно в данном случае такая оптимизация действительно не имеет смысла.
              Это всего несколько десятков тактов и пара байт. Не стоит даже размышления над этим вопросом.
              Очки кратные 10 делаются просто — приписывается нолик справа (oldskool trick)
              10 очков явно лучше чем одно :-P


              1. mobi
                28.12.2017 17:01

                У меня на байт больше, так как вместо

                sbc a,a
                inc a
                используется
                ld a,0
                adc a,a
                (в A вроде бы должен быть флаг переноса, а не его инверсия). Для сравнения, код через DAA выглядит так:
                    ld   b,3
                    or   a
                L1: adc  a,(hl)
                    daa
                    ld   (hl),a
                    ld   a,0
                    inc  hl ; или dec hl - смотря в каком порядке хранить
                    djnz L1

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


                1. beeruser
                  29.12.2017 17:26

                  >> (в A вроде бы должен быть флаг переноса, а не его инверсия)
                  Перенос после проверки диапазона инвертирован (jr c, L2). А sub 10 оставляет после себя C=0.
                  Поэтому перенос нужно инвертировать ещё раз перед использованием.


  1. werklop
    26.12.2017 13:52

    15$ — это ппц просто! Оно того не стоит


  1. demoded
    26.12.2017 14:06

    Порекламируйте на новую зеландию, овечки и Velcro это символичные тут :) и достаточно много нинтендобоев.


  1. VolkZubamiSchelk
    26.12.2017 20:46

    10 минут смотрел на картинку с дебаггером, аж ностальгия накатилась. Как же он похож на ассемблер Z80.


    1. beeruser
      27.12.2017 04:34

      А это и есть обрезанный Z80, без альтернативных наборов и IX/IY. Зато есть автоинкремент, например.

      Фраза «сочетание процессоров Zilog Z80 и Intel 8080» является бредом, т.к. Z80 обратно совместим с 8080.