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

Данный способ я назвал «блочным» (оговорюсь, что многие термины в статье были придуманы мной, так как материалов на данную тему на русском нет; после исследования нескольких игр я занялся изучением англоязычных материалов и документации к редакторам игр для старых платформ, тогда уже нашлись некоторые аналогии, в таких случаях буду приводить свои термины с объяснением их значения и их английские версии). В качестве примеров я буду приводить уровни из игры «Darkwing Duck», а также других игр компании «Capcom», разобранных мной несколько лет назад.

Я постараюсь пропустить описание использования дизассемблера и техническую часть исследования (если будет интерес, можно сделать на эту тему отдельную статью), а остановлюсь на описании, как именно разработчики хранили данные. Зная, что именно искать, найти это внутри образа ROM станет намного проще. Бонусом я покажу готовый редактор уровней и несколько созданных на нём хаков классических NES-игр.

Итак, начнём описание, как положено исследователям кода, снизу вверх.

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

Я лишь опишу в нескольких предложениях происходящее тут и перейду к более интересным вещам.
Видеопроцессор NES имеет несколько экранных страниц — одна из них отображается на экране. В экранной странице хранятся номера тайлов размером 8x8, которые нужно отобразить (30 рядов по 32 тайла, всего 960 байт) и их атрибуты (дополнительные биты цвета тайлов), на всю страницу уходит 1 килобайт описания, так компактно описывается целый экран. Сами тайлы берутся из знакогенератора (на 256 тайлов размером 8x8 расходуется 4 килобайта памяти, по 16 байт на один тайл), они могут быть расположены как в отдельном видеобанке картриджа, так и копироваться в видеопамять из обычного банка с данными. Для исследователя место их хранения практически не важно. Желающим более подробно разобраться с этой темой могу посоветовать почитать статью от MiGeRa на русском языке

Просмотреть содержимое знакогенераторов видеопроцессора можно с помощью любого эмулятора NES, я буду использовать наиболее продвинутый для исследования игр — FCEUX (на момент написания статьи последняя версия 2.2.2), в нём для этого нужно выбрать пункт меню Debug->PPU Viewer:

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

Как уже упоминалось выше, на описание одного экрана с помощью тайлов уходит 960 байт (30x32 тайлов). Но посмотрите на полную карту первого уровня «Darkwing Duck».

Она состоит из 20 экранов. Если хранить описание всех экранов потайлово, то на сохранение одного уровня уйдёт примерно 18 килобайт, а на всех семи игровых уровней — 131 килобайт. Напоминаю, что это не сама видеопамять в образе игры, а только описание с помощью тайлов видеопамяти игровых экранов! Это больше всего суммарного размера банков данных во всём образе ROM «Чёрного Плаща» (там всего 128 кб суммарно на код и данные и ещё 128 кб на видеобанки). Более того, уровни «Duck Tales 2» содержат до 32 экранов, при том, что образ весит вдвое меньше.

Тут стоит задать себе вопрос, как можно хранить описание экранов экономнее? Что бы вы сделали на месте разработчиков?

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

Сжатие.
Почти не применяется в NES-играх (зато постоянно применяется в играх на Sega Mega Drive и Snes) в основном по причине малого количества доступной оперативной памяти, которая требуется для хранения распакованных данных, а также медленного процессора. Тем не менее, изредка встречается RLE-сжатие
Пример — первая «Contra» (совместно с описанием экранов с помощью блоков, см. дальше 3-й способ про блоки):

Выделенные красным блоки сохранены в образе ROM в виде «7 раз повторить блок с платформой», а синим, соответственно, «3 раза повторить блок с платформой». Можно убедиться в этом, скачав редактор для этой игры.

Стоит заметить, что RLE на NES всё же используют, но не для сжатия описания уровня, а для более подходящих для этого сущностей. Например, сжимают им тайлы, хранящиеся в этом случае в банках данных («Duck Tales 2», та же «Contra»). Распаковка при этом происходит сразу в видеопамять. Также иногда сжимают текстовые данные (подходящими для этого алгоритмами), но для описания уровня на данной платформе это всё же экзотика.

Рисование на чистом холсте.
Данный подход подразумевает то, что большая часть экрана остаётся чистой, поэтому описывать её не надо. Описывается только по каким координатам должны быть нарисованы конкретные объекты. Яркий пример такого подхода — игры серии «Марио»:




При таком подходе в памяти хранятся записи, которые расшифровываются в виде «нарисовать по координатам X,Y объект ЯЩИК». (Вместо «ЯЩИК» может быть любой игровой объект). Всё остальное пространство остаётся залито фоновым цветом и тратить драгоценные байты на его описание не нужно. Получается, что на одну такую запись расходуется всего 3 байта, а на одном экране будет нарисовано всего 5-6 объектов. Конечно, нужно потратить ещё несколько десятков байт на описание самих объектов, но это не идёт ни в какое сравнение с тем, чтобы хранить почти килобайт данных при описании всего экрана тайлами. А если вы присмотритесь к скриншотам повнимательнее, то узнаете страшную тайну «Super Mario Bros.» Облако и куст — это один и тот же объект, но нарисованный с разной палитрой. На что только не пойдут разработчики ради экономии нескольких байт.

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

Блочный
Основной и самый часто встречаемый способ экономии места для сохранения данных об игровых уровнях — блочный, при котором уровень описывается не тайлами 8x8, а большими единицами данных. Сами единицы данных (блоки) могут быть разного размера — самый часто встречаемый для NES размер в 2x2 тайла, т.е. размер блока 16x16 пикселей (в играх на Sega Mega Drive часто встречаются и блоки размером 4x4 тайлов). При этом сами блоки могут быть организованы в большие структуры — макроблоки (2x2 блока, 32x32 пикселей в большинстве случаев).


В левой части картинки показано объединение четырёх тайлов в один блок, в правой части — объединение четырёх блоков в один макроблок луны из первого уровня «Darkwing Duck».
Из скриншота должен быть понятен основной принцип объединения.

Примечание: ромхакеры часто называют блоки «Tiles», а макроблоки — «Tile Sprite Assembly (TSA)», что создаёт путаницу в понятиях тайла как символа/иконки в знакогенераторе и тайла как объединения нескольких других тайлов в одну структуру (TSA первого уровня и второго). Поэтому я позволю себе сохранить введённые мной названия.

В разных играх могут быть разные, но похожие системы блоков и макроблоков. В «Batman» размер макроблока — 2x1, за счёт чего фон выглядит менее блочным, во «Flintstones: Rescue Dino and Hoppy» макроблоки огромны (по 16 блоков), а в «New Ghostbusters 2» нету макроблоков, а комнаты составлены из обычных блоков. Принцип не меняется — уровень сохраняется как массив из чисел, кодирующий номера больших по размеру структур, составленных из более маленьких.

Например, описание первого экрана первого уровня в «Darkwing Duck» начинается в образе ROM по адресу 0x10 (это самое начало образа после 16 байт заголовка). Первые 8 байт — это первая строка экрана, 8 номеров макроблоков, которые будут отображены первыми, можете попробовать изменить их вручную, запустить игру, начать первый уровень и посмотреть результат. Следом описывается вторая строка, третья и так далее, один экран занимает 8 строк, дальше следует описание второго экрана. Описание экранов может идти не в том порядке, в котором они будут встречаться в игре. В каком-то смысле сами игровые экраны тоже можно представить огромными структурами из 8x8 макроблоков, из которых лепится сам уровень (весь уровень в этом случае называется «раскладкой»(англ. layout) игровых экранов). Экран не обязательно может иметь размер 8x8, зачастую встречаются экраны размером 8x6, верхняя и нижняя строка используются игрой для отрисовки интерфейса.

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

Как узнать из каких структур (макроблоков) состоит уровень конкретной игры?
Для этого нужно найти описание уровня внутри образа ROM с помощью дизассемблера или другим способом и изменить один или несколько байт в этом описании, чтобы посмотреть, что произойдёт на экране:



На картинках — примеры разных размеров макроблоков в разных играх (2x2 тайла в «Chip & Dale 2», 4x4 тайла в «Jungle Book», 4x8 тайлов в «Flintstones Surprise of Dinosaur Peak»).

Ещё раз — всё в уровнях игр на NES описывается блоками (ну, и макроблоками). При этом описание макроблоков чаще всего состоит просто из индексов отдельных блоков (при размере макроблока 2x2 — 4 индекса блока, всего 4 байта, для «Чёрного Плаща» слева-направо и сверху-вниз), а вот описания блоков включают в себя дополнительную информацию — цвет всего блока и его характеристику, является ли блок фоном, платформой, на которой можно стоять, подбираемым предметов или шипами, которые наносят урон и т.п. Разумеется, встречаются игры, в которых данное правило не соблюдается (например, в «Ninja Cats» цвет задаётся сразу для всего макроблока, а в «Chip & Dale 2» информация о типе блока закодирована просто в самом его номере). Другое отличие — порядок хранения частей макроблоков в памяти, они могут идти последовательно (4 байта на описание первого макроблока, затем 4 байта на описание следующего и т.д. зачастую по 256 штук на уровень), либо же хранится отдельно (например, в «Tiny Toon Adventures» сначала хранятся все левые верхние кусочки макроблоков, за ним все левые правые кусочки, потом нижние левые и правые четвертинки соответственно).

Однако общие принципы блочного построения соблюдаются везде, что позволяет, во-первых, быстро находить похожие структуры в разных играх, во-вторых, изучать, в каких играх использовались похожие движки. Так, например, уровни «Darkwing Duck» с точностью до указателей на наборы блоков и макроблоков соотвествуют таковым в игре «Tale Spin» (хотя сам движок взят из «MegaMan 4», в котором наборы блоков и макроблоков были разделены по разным банкам, но с сохранением одинаковых указателей на них), и очень похожи на уровни «Chip & Dale» (отличия только в способах хранения вспомогательной информации уровня — в том, как записывается способ скроллинга экранов и кодов дверей между комнатами). Вторые же «Chip & Dale» сделаны совсем по другому, экраны в них описываются не макроблоками, а обычными блоками размером 2x2, и поэтому описание занимается намного больше места, так что сами экраны на уровнях регулярно повторяются, хотя благодаря дизайнерской работе неподготовленный игрок этого не замечает (в первой зоне первого уровня, например, циклически повторяются всего 3 экрана).

Исследуя игры, я писал для proof-of-concept программу CadEditor, которая отображала бы уровни из образов ROM так, как они выглядят в ходе прохождения игры на консоле.

Со временем она обрастала функционалом редактора, и ромхакеры даже сделали с её помощью несколько замечательных хаков (в основном на «Capcom»-классику), а также с десяток демок.

Вот одно из прохождений хака «Darkwing Duck In Edoropolis»:


Текущая версия редактора позволяет изменять уровни для 50 игр на платформы NES и Sega Mega Drive (для многих игр только по одному уровню, и часто требуется «доработка напильником», так что потребуются знания в ромхакинге).

Как упоминалось выше, код писался для себя, поэтому не отличается хорошим качеством, многие вещи сделаны топорно. Редактором я почти не занимаюсь из-за нехватки времени, но с радостью объяснил бы код кому-либо, кто захотел бы дорабатывать его или писать конфиги для подключения новых игр.

Надеюсь, данная статья позволит желающим немного разобраться в том, как были устроены уровни в старых играх (кстати, не только для NES, но и для остальных приставок с тайловой графикой — Sega Mega Drive, SNES, GBA и другие). Если у читателей возникнет интерес, могу написать ещё несколько статей похожей тематики, например: технический процесс поиска данных об уровнях (с помощью дизассемблера либо скриптов коррапта файлов), отличия в устройстве уровней для сеговских игр, устройство систем анимаций персонажей, поиск объектов на уровнях или создание вспомогательных инструментов для исследования.

Ссылки:
Исходники редактора
Тема на форуме с обсуждением редактора

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


  1. FreeS
    30.05.2015 19:07
    +7

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


    1. LinGG
      30.05.2015 20:37

      согласен! у меня nes появилась в 3 года в 91 году (китайская подделка в виде красной машины :) ) и с тех пор эта тема у меня в сердце навсегда! всегда казалось это чем-то недостижимым и сложным и люди, которые разрабатывали игры, ухищрались выжать из того, что было — я их всегда уважал и считал гениями (особенно японцы постарались в ту эпоху). коммент из серии «раньше трава зеленее была», но все же в этом есть что-то романтичное и душевное чтоли… за статью автору большое спасибо!


  1. Aingis
    30.05.2015 21:27
    +1

    > Вторые же «Chip & Dale» сделаны совсем по другому… так что сами экраны на уровнях регулярно повторяются, хотя благодаря дизайнерской работе неподготовленный игрок этого не замечает (в первой зоне первого уровня, например, циклически повторяются всего 3 экрана).

    Ох, сюда просто напрашивается скриншот!


    1. spiiin Автор
      31.05.2015 02:37
      +20


      Ящики, призы и враги маскируют то, что экраны одинаковые. Так почти в каждом подуровне.


      1. Newbilius
        31.05.2015 08:29
        +5

        Вот в этом месте удивили так удивили! :-)


      1. pushtaev
        02.06.2015 11:26

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


        1. spiiin Автор
          02.06.2015 14:29

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


  1. VBKesha
    31.05.2015 01:28
    +3

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

    Было бы интересно почитать например на примере игры JACKAL.


    1. spiiin Автор
      02.06.2015 15:20

      О, интересная игра, посмотрю, что в ней)


  1. cher11
    31.05.2015 02:07

    Спасибо за статью! Скриншот с моей любимой Darkwing Duck в Вашем редакторе привел в полный восторг. С радостью почитал бы еще.


  1. mifki
    31.05.2015 11:24

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


    1. pushtaev
      31.05.2015 14:56
      +3

      Ubisoft потеряли исходники дополнений к Третьим героям, а вы говорите NES :).


    1. DrMefistO
      31.05.2015 17:49

      Тогда, когда оно разрабатывалось (я имею ввиду игры), никто не задумывался о том, что: их будут переводить, их будут хакать. Именно из-за перевода своей любимой игрушки я и влился в тему ромхакинга.

      И почему вдруг авторы игры захотят раскрывать ее исходники?


      1. mifki
        31.05.2015 18:04

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


        1. DrMefistO
          31.05.2015 18:06

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


          1. mifki
            31.05.2015 18:11
            -1

            Или сговорились смотреть, как люди мучаются.


        1. DrMefistO
          31.05.2015 18:12

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


          1. mifki
            31.05.2015 18:15
            +1

            А в остальные разы — отказывали, не отвечали или не удавалось найти?


            1. DrMefistO
              31.05.2015 18:22
              +1

              Чаще всего — не удавалось найти хоть каких-либо контактных данных авторов игры.
              Иногда, обнаруживалось, что авторы присоединялись к более крупной конторе, например, SEGA, KONAMI.
              Ну, и, в именно моем опыте, был случай, когда авторы игры ответили, но отказом (это была игра Fantastic Dizzy на Sega Mega Drive).

              У моего товарища был успешный опыт входа в контакт с авторами Herc's Adventures (PSX) по поводу сжатия данных. Насколько я помню, они таки поделились с ним алгоритмом и описанием сжатия.

              Но, опять же, случаи успеха здесь единичные.


        1. spiiin Автор
          01.06.2015 12:25

          Во-первых, у разработчиков и сейчас есть куча информации под NDA, то, чем они делятся — верхушка айсберга.
          Во-вторых, информация об играх вполне может представлять коммерческую ценность. «Mario Maker» на Wii U по сути и есть просто редактор самого первого NES-овского Марио, и будет отлично продаваться. «Final Fantasy» ранние переиздаются под мобильные платформы без изменений геймплея. Зачем делиться тем, что можно ещё раз перепродать?
          Хотя, и не без приятных исключений, вроде выложенных недавно исходников «Prince of Persia» с пояснительным техническим документом


          1. mifki
            01.06.2015 12:30

            Еще раз, я говорю не о дележе исходниками, а вот, например, как они в какой-нибудь игре уместили кучу уровней в память или отрисовывали с нужной скоростью. Тогда эти секреты были конкурентном преимуществом, а сейчас-то?


  1. yizraor
    31.05.2015 12:22
    +1

    Спасибо автору за публикацию! (жаль плюсануть не могу, кармы не хватает)
    И технически любопытно, и поностальгировал немного по ушедшему детству, глядя видео… Музычка прям-таки навевает ))
    Весь первый уровень недоумевал, что не так с музыкой — вроде и классная, но как-будто должна быть другая. Потом понял — в этом и хак, что с другой игры музыку взяли… если не ошибаюсь — из «Ninja Cat»?


    1. spiiin Автор
      31.05.2015 13:04
      +2

      Весь хак — приключения Чёрного Плаща в мире «Ninja Cat» (музыка, карта, уровни, сюжет).


      1. yizraor
        31.05.2015 13:38

        ага, когда до 7:50 досмотрел, тогда и понял что уровни тоже переделаны ))
        а до того, казалось, что вроде как всё то же самое, планировка уровней совпадает же… видимо не вглядывался


  1. alan008
    31.05.2015 15:48

    А вот у меня был практически легендарный картридж 4-мя очень длинными и красивыми играми:
    1) The Jungle Book
    2) Jurassic Park
    3) Star Wars Episode IV (с обычными уровнями-бродиками и 3D полетами на звездолетах)
    4) Felix The Cat (примитивная и очень простая игра, но очень длинная)
    Как они умудрились всё это упихать в один картридж, до сих пор ума не приложу :-)


    1. DrMefistO
      31.05.2015 17:41
      +1

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


      1. DIHALT
        31.05.2015 23:07
        +1

        Cluster не так давно расковырял логику картриджей и родил собственный мапер на плисине.


  1. DrMefistO
    31.05.2015 17:31
    +2

    Таки знал, что тема ретро-игр жива! Давно ждал статьи на Хабре об этом.

    C меня статья о чем то таком на Sega Mega Drive (в частности, поиск упакованных данных, написание распаковщика, инструментарий ромхакера)!


  1. r0g3r
    31.05.2015 20:11
    +4

    Как ретрогеймер одобряю и всячески поддерживаю идею насчёт продолжения цикла статей!


  1. KaaPex
    02.06.2015 11:35

    Интересно было бы узнать, как реализовано в играх столкновение между тайлами, да и вообще логика самого игрового процесса.


    1. spiiin Автор
      02.06.2015 15:18
      +2

      Там навряд ли настолько общие элементы есть для всех игр, надо конкретные примеры разбирать.

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


      Логика игрового процесса слишком общая тема. Там внутри движков, грубо говоря, то же, что и сейчас (апдейт всех игровых объектов, за который они совершают какое-либо своё действие, иногда даже подобие встроенных скриптовых языков встречается, в «New Ghostbusters 2»), только с очень серьёзными ограничениями.
      Могу статью сделать, на примере того же «Чёрного Плаща» показать, как работает скроллинг и игровая логика одного из врагов.


      1. KaaPex
        02.06.2015 15:24

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