Существует несколько серий статей про программирование под NES, одна из них уже даже переведена на русский язык на Хабре. Однако ни одна из них не заходит дальше обзора базовых возможностей консоли – рассматривается общая архитектура консоли, основы ассемблера, вкратце рассказывается, как выводить спрайты на экран, упоминается что-то о музыке и мапперах, и цикл заканчивается.

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

Поэтому перед тем, как запрограммировать что-нибудь, предстоит разобраться с тем, как устроены те или иные эффекты для NES, и статья будет посвящена тому, как это сделать. Существует множество статей из разряда «Игры, выжавшие из NES максимум», попробуем разобраться в том, как сделаны все основные эффекты в этих играх, а также создадим инструменты, которыми можно найти другие игры, не менее технологичные по эффектам.

Дисклеймер


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

Краткие сведения о возможностях вывода графики NES


Предположим, что вы прочитали статьи nesdoug’а в переводе от BubaVV, и уже знаете базовые вещи о программировании графики для NES. Я не буду писать об этом детально, а лишь опишу кратко и упрощённо (можно пропустить эту часть и возвращаться к ней, когда будут возникать вопросы, почему какой-либо эффект сделан так, а не иначе):

  1. Видеопроцессор NES имеет четыре экранные страницы (и несколько режимов работы с ними, чаще всего две страницы просто копируют содержимое двух других), которые содержат данные об выводимых тайлах и атрибуты этих тайлов. Блок размером 2x2 тайла может быть окрашен всего 3 разными цветами + использовать 1 фоновый цвет.
  2. Также видеопроцессор умеет делать аппаратный скроллинг картинки между несколькими экранными страницами. Это значит, что на экране может рисоваться часть из первой экранной страницы и часть из второй. Экранные страницы при рендеринге «зациклены» в порядке 1-2-1-2-1-2.., т.е. если видеопроцессор дошёл до конца одной из страниц, он берёт данные из начала следующей. Это проще, чем звучит, можно открыть в эмуляторе какую-нибудь игру, и включить отладочное окно отображения экранных страниц, чтобы наглядно посмотреть, как это работает.
    (разбор устройства скроллинга с картинками здесь)
  3. Картинка в экранной странице рисуется тайлами из CHR-банка памяти, в котором в один момент находятся 256 тайлов памяти. Видеопроцессор «видит» два таких банка, один для отрисовка спрайтов, другой для отрисовки фона.

    При этом есть режим отрисовки спрайтов размером 8x16, при котором можно рисовать спрайты данными сразу из обоих банков.

    Эта память находится на картридже, поэтому в зависимости от «начинки» картриджа банки могут быть устроены по разному – переключаемые программно банки с ROM-памятью (чаще всего частями по 1, 2 или 4 килобайта), либо RAM-память, в которую можно записывать данные (чаще всего происходит копирование данных из банков с кодом).
  4. Тайлы из банков — это картинки размером 8x8 пикселей 2-битной цветности. К этим двум битам прибавляется два бита цвета атрибута тайла (общие для всех пикселей тайла). В итоге получается 4х битный индекс цвета в палитре из 16 цветов. В один момент времени в видеопроцессоре активны две палитры – для тайлов фона и спрайтов.
  5. Кроме фона, видеопроцессор отрисовывает до 64 спрайтов. Существует также ограничение на количество спрайтов, выводимое на одной строке, при превышении которого видеопроцессор пропускает спрайты, которые не успеет отрисовать.

Я не буду разбирать детально эти особенности NES, они «разжёваны» в обзорных статьях, например, тут. Я привёл их только для того, чтобы по ним перечислить 5 основных способов создания анимации и эффектов на NES – анимацию записью в экранную страницу, анимацию сменой позиции скроллинга, анимацию сменой содержимого CHR-банка, анимацию сменой палитры и анимацию отрисовкой спрайтов.

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

А теперь, внимание, все графические эффекты на NES делаются одним из этих способов или их сочетанием!

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

Синхронизация центрального процессора консоли и видеопроцессора


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

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

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

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

При этом используется несколько возможных вариантов синхронизации центрального и видеопроцессора консоли.

  • Использование маппера, который генерирует прерывания в конце отрисовки каждой строки, по таймеру либо по каким-либо другим условиям. Прерывание говорит центральному процессору «брось все дела, только сейчас можно быстро записать несколько байт в видеопамять, это важнее, к остальному вернёшься потом». Программа может установить обработчик этого прерывания – и выполнить какой-то код за то время, пока луч на экране гасится для перехода из одной строки на другую. Это самый простой для разработчика подход. Самый распространённый из таких мапперов – MMC3. «Железная» часть организации этого очень интересна, вдумайтесь – с помощью микросхемы на КАРТРИДЖЕ добавлялась новая возможность использования видеопроцессора на КОНСОЛИ.
  • В случае если маппер не позволяет генерировать прерывания, начинаются извращения. Программа должна сделать цикл простоя, ожидая появления какого-либо специально подстроенного условия, сигнализирующего о том, что видеопроцессор дошёл до отрисовки определённого места на экране (для этого могут применяться спрайты или вообще предназначенные для вывода звука функции консоли).

    Другой путь — отмерять количество тактов на инструкции, которые были выполнены (в этом случае программа должна обеспечивать равенство по времени выполнения двух веток условного оператора, и следить, чтобы инструкции, работающие с динамически составленными адресами, не выполняли переход за границы 256-байтной страницы памяти). Но даже при этом случае такты CPU и PPU не совпадают в точности, поэтому разработчики стараются делать такие трюки в тех местах экрана, где пользователь не заметит мерцания пикселей. Посмотрите на скриншот:



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

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

Анимация сменой палитры


Межкадровая


Смена палитры между кадрами – один из самых простых в реализации эффектов. Есть несколько разновидностей эффектов, которые реализуются с помощью анимации палитры.

Самый простой из них – обычное мерцание, когда один цвет сменяются другим. Другой эффект – «бегущие волны цвета», например, волны водопада, зыбучих песков или огня. Наконец, третий тип эффектов – высвечивание одним из цветов в палитре одного кадра изображения с одновременным гашением других цветов, таким образом можно показать движущуюся частицу дождя, снега или другого эффекта с частицами. Если вы внимательно читали теорию, то можете подсчитать, что у такой анимации будет максимально три кадра, если не совмещать её с анимацией записью в экранную страницу (тайлы ограничены двумя битами цвета, из которых один занят под фон). Все три вида такой анимации можно увидеть в разных уровнях игры Duck Tales 2:

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



Опознать такой тип анимации очень легко, и для этого не нужны специальные инструменты. Открываем окно PPU Viewer в эмуляторе и следим за тем, меняется ли палитра или нет. Для простоты изучения «высвечивания» кадров с каплями дождя замедляем скорость.

Посреди кадра (мидфреймовая)


Изменить палитру полностью посреди кадра сложно, но можно.

Для анимации в игровом процессе такой способ не применяется, но изредка используется в статических частях кадра. Очевидный эффект – отрисовать интерфейс палитрой, отличной от палитры уровня.



Менее очевидный – менять цвет с одного на другой постоянно для создания эффекта градиента.

Как распознать

Эмулятор позволяет посмотреть, какая палитра была загружена в момент отрисовки какой-либо строки кадра. Смотрим заставку Indiana Jones and the Last Crusade:


Индиана Джонс и градиентная заливка

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

Анимация записью в экранную страницу


Межфреймовая


Это самая обычная анимация фона изменением номера тайла, который будет отрисовывать видеопроцессор, чаще всегда выполняется один раз для одного объекта. Особо разбирать такие анимации не интересно, да и бессмысленно – почти любая обычная игровая анимация сделана так (но чаще интерактивную часть, например, крышку открывающегося сундука, дорисовывают спрайтом). Пример – отображение взрываемых объектов в Contra или Super C.


В этой игре при взрыве пушки, дверей или ворот, обновляется логический «блок», размером 4x4 тайла.

Создание иллюзии тайлов размером меньше, чем 8x8

Хорошо описано в статье с разбором Battle City. Вкратце, для каждого тайла размером 8x8 создаётся несколько вариантов такого же типа с отсутствующими частями 4x4, в итоге программной заменой тайлов можно создавать иллюзию того, что игра использует тайлы размером 4x4 пиксела:

image

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

Мидфреймовая


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

Анимация спрайтами


Межфреймовая


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

Спрайтами можно также отрисовывать частицы, а также добавлять дополнительные детали к фону. Тайлы фона должны быть выровнены на 8 пикселей, спрайты же можно рисовать в любом месте экрана, вдобавок, они рисуются из второго банка и другой палитрой – за счёт этого увеличиваются лимиты на количество цветов в картинке.

Существует и нецелевое применение спрайтов – для того, чтобы с помощью них синхронизировать процессор и видеопроцессор (например, Sprite Zero Hit и Sprite Overflow).

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

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


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

Рендеринг частиц

Рендеринг дождя в Mitsume ga tooru.



Тут видно, что игра использует спрайты размером 8x16 (такие спрайты использовать выгоднее в плане количества одновременно показанной на экране графики, потому что вне зависимости от размера спрайтов – 8x8 или 8x16, можно нарисовать 64 спрайта). Также можно заметить, что игра рисует капли дождя через кадр, из-за чего игроку кажется, что капель в два раза больше, чем есть на самом деле – ведь спрайты нужны игре ещё и для отрисовки главного персонажа, снарядов и босса, который тоже состоит множества частиц-пчёл.

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

Похожий эффект — рендеринг звёзд в Galaxian или в Addams Family.



Отображение теней

В 2,5-D играх, таких как Jurassic Park, спрайты используются, чтобы отобразить тени под объектами – так игрок может определить высоту, на которой находится объект.



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





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

Маскировка спрайтов под фон

Особая хитрость дизайнеров, одна из тех, за которые игру записывают в «лучшую графику для nes». Заметить её сложно, но со скриптом обнаружения спрайтов не составляет труда. Чаще всего разработчики создавали спрайтами иллюзию отдельного слоя фона (видеопроцессор NES не поддерживает слоёв, из-за чего не знакомым с устройством консоли людям кажется, что игра делает что-то невозможное для консоли).

Примеры:


В Mitsume ga Tooru, помимо раздельной скорости движения грузовика и фона, для усиления эффекта спрайтами дорисовывается столб, как будто находящийся на отдельном слое, движущемся с медленной скоростью.


В известной заставке Megaman 2 спрайты создают эффект, что весь дом находится на отдельном слое, хотя это не так.


В Bucky O’Hare присутствует как эффект параллакса (движущийся отдельно слой выделен зелёными линиями), так и дополнительно для иллюзии движения воды ниже добавлены спрайты на неподвижный слой.


Большое колесо вначале башни после первого уровня в Castlevania 3. Кажется, будто колесо вращается, но движущиеся шестерёнки – это спрайты, ездящие так, как будто составляют единое целое с основой колеса. В дополнение мелкие шестерёнки также анимированы переключением банков памяти, про этот эффект будет рассказано далее, в примере с Power Blade 2.

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

Отдельно упомяну игры, в которых спрайты переходят в фон и обратно незаметно для игрока.

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



Эта техника часто используется в играх от Capcom, чтобы показать взаимодействие с интерактивными блоками – разбиваемые камни в Duck Tales 1-2, ящики в Chip and Dale Rescue Rangers 1-2, ракушки в Little Mermaid.


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

В Prince of Persia техника используется более продвинуто. В те моменты (и только в них), когда игрок пробегает в «ворота», нарисованные на фоне, и должен пройти между двумя столбами, поверх персонажа спрайтами дорисовывается такой же самый столб, как и в фоне, который закрывает игрока. Так что оба столба в фоне остаются за персонажем, но перед персонажем временно появляется третий столб, чтобы сохранить иллюзию правильного порядка объектов.


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

Эффект полупрозрачного/непрозрачного фона

Зато показать полупрозрачность фона возможно. Для каждого спрайта на экране в памяти видеопроцессора отведён флаг «рисовать ли спрайт за фоном или перед ним». Если все пиксели тайла фона непрозрачные – такой спрайт будет полностью невидим за фоном. Если же часть пикселей фона будет прозрачной, а другая – нет, то персонаж за фоном будет «просвечивать».


Мой любимый пример полупрозрачного фона – секретные проходы в Duck Tales 2.

Если выходить из прохода в замедлении покадрово, можно заметить проблему организации полупрозрачности таким образом – бит «за фоном/перед фоном» устанавливается для целого спрайта 8x8 – поэтому Скрудж ненадолго проваливается за факелы в том месте, где он частично стоит в проходе, а частично уже вышел из него.

Решение этой проблемы – закрыть спрайт частично другим спрайтом – так появится возможность увеличить точность отображения вплоть до 1 пиксела. Этот эффект можно наблюдать детально в Mystery World Dizzy или Nightshade.


Персонаж отображается частично за колонной, а частично перед стеной, как и должен. Детально этот эффект разобран здесь

Эффект сумки Кота Феликса
Напоследок, приём из разряда «грязных хаков» из игры Felix the Cat. Посмотрите, как Феликс прячется в сумку – нижняя часть спрайта плавно пропадает в сумке, а верхняя остаётся видимой перед фоном.



Если включить скрипт отображения спрайтов, получится такая картинка:



В левой части есть 3 странных спрайта (стоит отметить, что скрипт отображает спрайты немного неверно – игра использует спрайты 8x16, а скрипт рисует только 8x8 пикселей, так что на самом деле они идут друг за другом по вертикали непрерывно).

Если разобраться немного детальнее, а именно включить логгирование координат спрайтов в скрипте, то станет заметно, что это не 3 спрайта, а 24, по 8 в каждой строке. Это максимум, который может успеть отрисовать видеопроцессор в одной строке. Он делает это слева направо, поэтому после того, как 8 спрайтов отрисованы в невидимой зоне экрана, спрайты Феликса, залезающего в сумку, в этих строках отрисовываться уже не успевают. Таким хитрым образом сделана маскировка той части спрайтов, которые находятся ниже границы погружения.

Игр с таким эффектом маскировки не так уж и мало (см. здесь раздел Use of excess sprites for masking effects)

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

Анимации изменением содержимого CHR-банка


Для начала стоит чуть более детально рассмотреть отличия между картриджами с CHR-ROM и CHR-RAM (бывают и такие, на которых присутствуют оба типа памяти). Для программиста разница в том, что игра с CHR-ROM позволяет быстро переключать целые банки памяти. Разновидности мапперов имеют разные размеры «банков» — по 1, 2, 4 килобайта. Это даёт возможность иметь общую часть банка и переключаемую. Переключение осуществляется несколькими командами мапперу, и может быть сделано со скоростью несколько раз за кадр.

Картриджи с CHR-RAM требуют для «переключения» непосредственной записи нужного числа байт в память адресного пространства PPU (для программы – через запись по определённому адресу CPU). То есть для изменения одного целого тайла видеопамяти потребуется 16 команд записи.

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

Межфреймовая анимация


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

Пример такой анимации – механические объекты на заводах в уровнях Power Blade 2


Анимированные механизмы


Отдельные кадры анимации банками (переключается нижняя половина банка).

Изучить содержимое CHR-ROM банков можно с помощью любого тайлового редактора, например, TLP. Это позволяет также определить и размер переключаемого игрой банка.

Анимация в CHR-RAM требует непосредственной записи тайлов в память и её сложнее отследить статически – данные могут храниться в сжатом виде или даже генерироваться на лету процедурно. Поэтому для отслеживания таких эффектов я написал ещё пару скриптов.

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

Одна из самых «экстремальных» игр, использующих анимации CHR-RAM – Battletoads.
Во-первых – она использует изменение содержимого CHR-RAM для анимации персонажей! Т.е. в памяти хранится только один кадр для каждой жабы и он постоянно обновляется. Такой эффект позволяет хранить в банке памяти больше данных.



Теперь запустим скрипт подсчёта кол-ва переданных за кадр байтов. Скрипт написан для эмулятора Mesen, потому что он единственный позволяет отслеживать нужные события видеопроцессора из lua.


Как видно, на 2-м уровне, игра передаёт до 256 байт за кадр. Причём на чётных кадрах анимируется жаба, а на нечётных – фон (Игре ещё нужно выполнять обновления экранных страниц при скроллинге, они тоже делаются записью в PPU, а также анимацию второго игрока).

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



Пример процедурного изменения памяти CHR-RAM – зеркальное отражение блоков размером 2x2 тайла в Gun Smoke. Посмотреть на варианты содержимого памяти «до» и «после» можно в редакторе уровней CadEditor:



Этот простой приём позволяет увеличить количество игровых элементов почти в 2 раза. Если смотреть на содержимое PPU во время прохождения уровня, можно заметить, насколько медленно это работает, зеркалирование всего банка памяти занимает несколько секунд.

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

Более детально стоит разобрать самый красивый эффект анимации переключением банков (возможный как с CHR-ROM, так и CHR-RAM), достигаемый в сочетании с эффектом скроллинга – симуляцию параллакса (отдельный слой, движущийся с другой скоростью, чем основной).

За наличие этого эффекта игра автоматически попадает в рейтинги «лучшие графические игры на nes».

Определить его, при наличии некоторого опыта, можно визуально – отдельный слой состоит всего из нескольких зацикленных анимированных блоков. Но для надёжности стоит запустить скрипт dump_animated_chr.lua, который я упоминал выше, чтобы точно не ошибиться с тем, что параллакс сделан именно таким способом (существует и другой способ реализации параллакса, с другими ограничениями по возможностям).

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

Примеры игр:


Bucky O'Hare. Параллакс в окошках – иллюзия отдельного слоя.


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


Mitsume ga Tooru. Стена отдельно от платформ.


Micro Mashines. Пол «шашечками» – с одновременным горизонтальным и вертикальным параллаксом.

Разберём эти красивые эффекты более детально.

Вот скрипт для редактора CadEditor, который берёт все сдампленные кадры видеопамяти и отрисует их в виде png-картинок. Результат работы:



Затем используем команду для ImageMagic, чтобы склеить кадры вместе:

convert -delay 1x24 -loop 0 *.png animation.gif

Получается такая гифка (для Bucky O’Hare):



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

Аналогично, можно посчитать, что для боевых жаб в колодце используется для эффекта вертикального параллакса анимация блока стены в 32 кадра, это 6 килобайт данных на 1 только блок (4x4 тайла) анимации!



Тут видно, что при использовании CHR-RAM (в Боевых Жабах) в отличие от CHR-ROM (в Баки) не нужно выделять целый банк или тщательно планировать место в других банках заранее. Однако плата за это – более сложный код и отсутствие возможности анимировать весь банк.

Ещё один способ изучения эффектов параллакса – разрушить иллюзию, поставив анимированный блок в такое место, где он не будет гладко сшиваться с другими блоками. Например, откроем уровень с колодцем в редакторе CadEditor и добавим «блок-индикатор»:



Загрузим изменённый уровень в эмулятор и посмотрим результат:



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

Ещё один «эффект», демонстрирует Jurassic Park сразу после запуска игры. Это просто запись красивой анимации в нескольких банках подряд покадрово и прокрутка её без всякой интерактивности. Не буду его демонстрировать из обиды на разработчиков, из-за того, что они так увлеклись демо-эффектами на старте игры, что у них не осталось места под красивую концовку, думаю, многие игроки были разочарованы этим.

Мидфреймовая анимация


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

Очевидное применение этому – отрисовать интерфейс одним банком, а игровой экран – другим. Можно также выделить какую-то часть экрана (лучше отделённую горизонтально от основной части). В Teenage Mutant Ninja Turtles 3 есть оба этих эффекта – фон пляжа нарисован тайлами из одного банка, пляж – другими, а красивый интерфейс с рожицами черепах – третьими.



Более технологичный эффект – переключить банк так, чтобы нарисовать одну большую красивую картинку. Пример – Robocop 3.



Больше примеров титульных экранов с переключением банков можно посмотреть в статье Shiru

Эффект отслеживается простым изучением окна Debug->Name Table Viewer в эмуляторе – в этом окне отображается только один из наборов тайлов, второй в этот момент выглядит как «каша».

Анимация изменением позиции скролла


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

Кол-во примеров в ней будет зависеть от результатов опроса.

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

Использованные инструменты


Fceux – эмулятор, необходимо брать сборку из последних версий исходников разработчиков, публичная версия давно не обновлялась и в ней отсутствуют часть нужных для отладки lua-функций.
render_sprites_numbers.lua – скрипт для fceux для наглядного отображения номеров тайлов спрайтов на экране, а также их позиций и флагов отображения.

dump_animated_chr.lua – скрипт для fceux для сохранения содержимого уникальных CHR-банков видеопроцессора в файлы для анализа того, как из них составляется анимация.

Mesen – эмулятор, который лучше всего подходит для отладки эффектов графики, для него написаны скрипты, которые невозможно реализовать для fceux.
mesen_chrRamWriteCounter.lua – скрипт для mesen для отображения строк, на которых игра изменяет содержимое памяти видеопроцессора и подсчёта количества таких изменения между кадрами и во время кадра.

mesen_makeScreensEveryFrame.lua – скрипт для mesen для сохранения скриншотов каждый кадр, использовался для создания gif-анимаций.

CadEditor – универсальный редактор уровней NES-игр, в данный момент содержит конфиги для отображения 1172 различных уровней для 118 игр (список периодически расширяется).

Script-ExportAllChrsToPng.cs – скрипт для CadEditor’а для построения тайлов-картинок из бинарных файлов с содержимым памяти PPU и палитры так, как это делают эмуляторы NES.

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


  1. AVX
    18.04.2018 18:14

    Интересно, спасибо!
    Всегда интересовало, как могли реализовать на NES некоторые достаточно сложные вещи. Если можно, объясните, как реализовали эффект «волн», например как в черепашках-ниндзя 3, в момент появления стартового меню. Там ещё голосом что-то выкрикивают, не знаю как написать. Ну и как реализуется логика 3D в этой игре, ведь большинство уровней в трёх измерениях сделано.


    1. spiiin Автор
      18.04.2018 18:37

      Если можно, объясните, как реализовали эффект «волн»


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


      Ну и как реализуется логика 3D в этой игре, ведь большинство уровней в трёх измерениях сделано.

      Ну, это называется 2,5D — для полноценного третьего измерения не хватает изменения размеров при удалении объектов от наблюдателя. Хотя там море во втором уровне сделано с симуляцией этого эффекта. Для 2,5D особенно делать ничего и не нужно, достаточно при рендере объектов сортировать в порядке от самых дальних к зрителю к самым ближним, см. Алгоритм художника


  1. kocmoc941
    18.04.2018 18:59

    Продолжайте в том же духе!
    Хотелось бы только понять как храниться палитра в памяти, везде написано как то муторно, киньте плз ссыль на нормальное описание палитры (или в статье осветите), а то: храниться по 8 байт каждые из которых формируют младший и старший бит палитры… такое ощущение что везде одно и тоже, либо перевод, либо копипаст…
    С гифками вроде всё ок, не много и не мало. Спс за статью!


    1. spiiin Автор
      18.04.2018 19:16
      +1

      Дык привёл же вроде…
      habrahabr.ru/post/348212
      dendy.migera.ru/nes/g02.html

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


      1. kocmoc941
        20.04.2018 18:58

        Не мигере и англоязычных сайтах смотрел всё равно не понимал, но нашёл старый журнал (в сети в djvu) «Игровые Приставки — Ремонт и Обслуживание», вот там всё нормально расписано с примерами, всё стало ясно, теперь уже когда мигеру читаешь, всё тоже ясно сразу становиться :)
        Только остался один непонятный момент, но это я тут почитаю ещё (по ссылке выше habrahabr.ru/post/348212) + поэсперементирую сам: фон закрашиваешся палитрой для первого спрайта (0x3F10), почему так хз, ведь везде пишут что должен закрашивать первым цветом из 0x3F00?
        Мой тесткейс:
        Я вывожу 960 пустых тайлов (16 байт, все 0), + в область атрибутов страницы пишу 64 нуля, в файле палитры (который гружу в 3F00 состоит из 32 байт) когда меняю первый байт — результата нуль, когда меняю байт 0x10 — то меняется фон (но это палитра спрайта!?)… Можете не отвечать сам дойду как нибудь, а так заранее спасибо! :)


        1. spiiin Автор
          20.04.2018 20:08

          wiki.nesdev.com/w/index.php/PPU_palettes

          Addresses $3F10/$3F14/$3F18/$3F1C are mirrors of $3F00/$3F04/$3F08/$3F0C. Note that this goes for writing as well as reading


          Нулевой цвет зеркалируется для обеих палитр, т.е. если вы в цикле записываете 32 байта в две палитры, то вы сначала пишете нужный цвет в палитру фона, а потом записью в 0x3F10 его затираете. И наоборот, записью в 0x3F10 вы меняете цвет в 0x3F00.

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


    1. loginsin
      18.04.2018 19:18
      +1

      Не то, чтобы она прям таки где-то хранится. Записывается она в PPU по адресам $3F00-$3F1F: первые 16 байт — палитра фона, затем 16 — палитра спрайтов.
      Здесь можете почитать подробно. Вообще, там много интересного по денди можно найти, но в некоторых статьях на ресурсе присутствуют довольно обидные ошибки (скорее опечатки).


      1. VEG
        19.04.2018 11:37

        Ага, на dendy.migera.ru отличный справочник для начинающих программировать под Dendy. Я как раз им (наряду с wiki.nesdev.com) пользовался когда делал Unchained Nostalgia.


  1. VEG
    19.04.2018 11:39

    Большое спасибо за статью. Приятно видеть статьи по такой исключительно гиковской тематике. Это вам не блокчейн, о котором и так везде говорят!


  1. LynXzp
    19.04.2018 12:08

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


  1. JTG
    19.04.2018 14:33

    Вот это мясо! Отдельное спасибо за скрипты.


  1. perfect_genius
    19.04.2018 21:23

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


    1. spiiin Автор
      20.04.2018 09:43

      Не согласен. Трюки и хитрости программисты графики использовали всегда, просто тогда это были трюки для того, чтобы сделать что-то, не умеет процессор. Позже — трюки, чтобы сделать то, что умеет последняя версия DirectX/OpenGL, а сейчас, чаще — трюки для обхода косяков Unity3D.

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