![image](https://habrastorage.org/getpro/habr/post_images/4b2/91d/b8a/4b291db8a27692c6649a7b2c6ffb5be3.png)
Параллакс-скроллинг (создание иллюзии глубины в 2D-сцене перемещением фона и переднего плана с разной скоростью) стал сегодня стандартным элементом платформеров и других 2D-игр. Этот эффект, популяризированный аркадной игрой Moon Patrol 1982 года, к началу 90-х уже был довольно распространённым на аркадных автоматах и домашних консолях. Игры для PC — это совсем другая история. Из сайдскроллеров для DOS, выпущенных в 1990-1993 годах, параллакс-скроллинг имелся у немногих (к 1993 году он стал более популярным, но всё равно встречался редко). Были и очень ранние примеры применения параллакс-скроллинга, например, в порте 1983 года вышеупомянутой Moon Patrol для PC, но в нём присутствовал только контур фона, а не графическое изображение. Поэтому благодаря наличию этой особенности Duke Nukem II выделялся на фоне каталога платформеров Apogee, вместе со своим предшественником Duke Nukem 1991 года и игры Cosmo’s Cosmic Adventure 1992 года (в которой большая часть кода и форматов файлов были такими же, как в Duke 2).
![](https://habrastorage.org/getpro/habr/post_images/989/4f7/9eb/9894f79ebafc992c78947acab28e5518.gif)
Параллакс-скроллинг Duke Nukem II в действии
Стоит заметить, что для подавляющего большинства первых игр Apogee целевым был стандарт EGA, чтобы они были доступны для людей со старым оборудованием (VGA обратно совместим с EGA). Первые игры для DOS с параллакс-скроллингом обычно были VGA-играми, и на то, как мы увидим ниже, были логичные причины.
Игра Duke Nukem стала своего рода гибридом. Для неё требуется VGA-карта, и в ней присутствуют 256-цветные сцены, но сам геймплей использует 16-цветный режим EGA, а палитра VGA применяется для получения цветов, которые невозможно создать в EGA. Однако сам движок рендеринга фундаментально основан на EGA, и игра на самом деле могла работать на EGA-картах, только с неправильными цветами.
Прежде чем мы приступим к изучению того, как параллакс-скроллинг был реализован в Duke Nukem, давайте разберёмся, почему этот эффект было сложно создать в EGA-играх.
Что сложного было в параллакс-скроллинге?
Что требуется для параллакс-скроллинга с технической точки зрения? Концептуально он довольно прост: нам нужно отрисовывать фон отдельно от переднего плана и использовать для них разные смещения скроллинга. На консолях и аркадных автоматах это обычно реализовывалось при помощи аппаратного ускорения: программист настраивал несколько уровней изображений, а оборудование выполняло их композитинг в готовый результат (в отличие от SNES, у NES не было аппаратных слоёв, но всё равно имелись аппаратные функции, немного упрощавшие реализацию параллакс-скроллинга). Без аппаратной поддержки композитинг приходилось выполнять полностью программно, например, создавая объединённое изображение в буфере кадров графической карты. Проще всего это сделать, сначала отрисовав фон, а затем изобразить всё поверх него, перерисовывая те части фона, которые перекрыты передним планом.
В разработке игр для EGA-карт при этом процессе возникают две проблемы. Во-первых, старые PC были недостаточно быстрыми, чтобы перерисовывать весь экран в каждом кадре с сохранением полной частоты кадров (т.е. нативной частоты обновления дисплея). PC в основном проектировались для офисной работы и бизнес-приложений, где быстрая и плавная графика не была приоритетом (однако когда популярными стали графический интерфейс пользователя и высокие разрешения, потребность в быстрой графике увеличилась, что привело к созданию шины VLB). По сравнению с процессорами игровых консолей процессоры PC были быстрее, но скорость особо не помогала, когда все пиксели нужно было пропихнуть через узкую шину ISA. Звуковые карты, контроллеры жёстких дисков и другие устройства хотя бы имели поддержку DMA, то есть данные можно было передавать между системным ОЗУ и устройством без вмешательства ЦП. Однако EGA-/VGA-карты не поддерживали эту функцию. Поэтому процессору приходилось «вручную» копировать все пиксели из основной памяти в видеопамять. Также существовала возможность копирования из видеопамяти в видеопамять (latch copy, подробнее об этом ниже), но этот процесс всё равно должен был вручную управляться процессором.
Насколько же плохой была ситуация? Максимальная теоретическая пропускная способность шины ISA составляет 8 МБ/с, и этого более чем достаточно для передачи изображения размером 320×200 пикселей с глубиной цвета 4 бита на пиксель (16 цветов) с частотой 70 Гц — для этого необходимо около 2,2 МБ/с. Но что это означает на практике? Чтобы получить конкретные значения, я написал бенчмарк и запустил его на нескольких машинах эпохи DOS. Вот результаты отрисовки в буфер кадров одного полноэкранного изображения наиболее оптимальным (насколько я знаю) способом:
Процессор | Графическая карта | Время, мс | Достигаемый FPS |
80486 DX2, 66 МГц | Paradise Autoswitch EGA2 (EGA, 8-битная ISA) | 72 | 14 |
80486 DX2, 66 МГц | bit-design PCELC V1.3 (EGA, 8-битная ISA) | 54 | 19 |
80286, 16 МГц | Cirrus Logic CL-GD5422 (16-битная ISA) | 11 | 90 |
80386 DX, 40 МГц | Cirrus Logic CL-GD5420 (16-битная ISA) | 9 | 111 |
80486 DX2, 66 МГц | Cirrus Logic CL-GD5422 (16-битная ISA) | 10 | 100 |
80486 DX2, 66 МГц | Tseng Labs ET4000/W32 (VLB) | 3 | 333 |
Первыми идут две EGA-карты, и их производительность просто отвратительна. Их задача заключалась в отображении одного полноэкранного изображения на очень хорошем для того времени ЦП, и всё равно падение частоты кадров оказалось огромным. Всё неожиданно плохо; любопытно, насколько это вызвано тем, что эти карты имеют только 8-битные интерфейсы ISA, что снижает полосу пропускания вдвое, и насколько это зависит от того, что само оборудование менее мощное. К сожалению, у меня нет VGA-карт с 8-битным интерфейсом ISA, так что не могу сравнить их. Далее у нас идут две VGA-карты с 16-битной ISA, протестированные на разных ЦП. В целом эти карты проявляют себя неплохо, но нужно учитывать, что пока они отображают только одно полноэкранное изображение. Реальной игре нужно ещё отрисовывать спрайты и игровой мир, считывать ввод, выполнять логику игры и так далее. Если для одной только отрисовки экрана требуется 11 мс, то, если мы хотим достичь 70 FPS, на всё прочее остаётся лишь 3 мс, что на практике для этих машин кажется невозможным. Поэтому хотя ситуация гораздо лучше, чем у EGA-карт, она всё равно не идеальна, и для обеспечения хорошей производительности в играх требуется много работы. Также любопытно видеть, насколько мало на результаты влияет ЦП. С точки зрения мощности между 286 (16 МГц) и 486 (66 МГц) огромная пропасть, последний не только имеет более высокую тактовую частоту, но и обладает кэшем и другими архитектурными улучшениями. Однако разница в производительности графики относительно невелика. Это показывает, насколько узким «бутылочным горлышком» является шина ISA. И если это не было очевидно раньше, то становится абсолютно ясно, когда мы видим последний пример, где использовалась графическая карта VLB: она примерно в три раза быстрее графической карты ISA. Благодаря новому стандарту ситуация для разработчиков игр стала гораздо лучше, однако VLB появилась только в 1992 году и в основном использовалась на дорогих материнских платах 486. Поэтому система с таким уровнем производительности была доступна не каждому и возможность работы игры на обычных ISA-картах привлекала к ней более широкую аудиторию.
Как мы видим из этих результатов, при наивной реализации параллакс-скроллинга, когда мы сначала рендерим фон, а затем поверх него отрисовываем остальные элементы игры, невозможно было бы достичь высокой частоты кадров на графических картах до VLB. Обеспечить хорошую скорость рендеринга было непросто даже без параллакс-скроллинга. Из-за таких скоростных ограничений разработчики многих игр отрисовывали только те части экрана, которые изменились с предыдущего кадра. Однако такие техники оптимизации противоречили параллакс-скроллингу, поскольку для него необходимо перерисовывать большие части экрана.
Ну ладно, а если мы согласимся на более низкую частоту кадров, например, 35 вместо 70? Или всего 20-25, в конце концов, в те времена игры частенько работали с таким низким FPS. Сработает ли это? Здесь в дело вступает EGA и ещё больше усложняет ситуацию.
Планарная схема памяти EGA
![](https://habrastorage.org/getpro/habr/post_images/694/94f/3f0/69494f3f046ded309addc2b7f5037bd8.jpg)
Карта IBM EGA.
EGA использует палитру из 16 цветов. Это значит, что значение пикселя в буфере кадров обозначает не цвет, а индекс палитры, который задаёт цвет. При сканировании буфера кадров для генерации сигнала на монитор оборудование графической карты автоматически на лету преобразует эти индексы палитры в цветовые значения. Для хранения значения от 0 до 15 достаточно 4 битов, поэтому, по сути, можно хранить в байте два пикселя. Поэтому можно ожидать, что буфер кадров будет представлять собой линейную последовательность байтов, и каждый из байтов будет обозначать два последовательных пикселя. Однако на самом деле EGA работает совершенно иначе.
![](https://habrastorage.org/getpro/habr/post_images/736/80d/ada/73680dada70e60df263460101ecdc0d7.png)
Линейная и планарная память
Данные распределены на четыре так называемых плоскости (plane). В первой плоскости хранятся все первые биты всех пикселей, во второй — все вторые биты всех пикселей, и так далее. То есть на каждой отдельной плоскости каждый байт представляет 8 последовательных пикселей, но только один из четырёх битов каждого из этих пикселей.
(Зачем стандарт был реализован таким образом? В то время чипы памяти были слишком медленными для того, чтобы получать данные пикселей со скоростью, достаточной для управления экраном с частотой 60 или 70 Гц. Благодаря разделению данных на плоскости четыре бита можно было считывать параллельно, а затем комбинировать, что оказывалось достаточно быстро).
Более того: процессор за раз может получать доступ только к одной из плоскостей. Буфер кадров размером 320×200 с четырьмя битами на пиксель — это суммарно 32000 байтов, однако ЦП видит только окно в 8000 байтов через видеопамять, отражённую в основную память. По сути, для доступа ко всем данным ему нужно выполнять переключение банков. Это переключение банков выполняется установкой аппаратного регистра графической карты при помощи ввода-вывода через порты (ассемблерной командой
OUT
). То есть для копирования изображения в буфер кадров нам нужно:- С помощью ввода-вывода через порты выбрать плоскость 0
- Записать все данные для плоскости 0
- С помощью ввода-вывода через порты выбрать следующую плоскость
- Повторять, пока не будет выполнена запись во все четыре плоскости
Как вы можете догадаться, преобразованием данных изображений из линейного в планарный формат не стоило заниматься на лету в процессе отрисовки изображений, поэтому игры той эпохи обычно хранили данные изображений в планарной структуре. И Duke Nukem II в этом не является исключением.
Однако схема памяти влияет не только на форматы файлов, но и на то, как работает отрисовка изображений в буфер кадров. Итак, давайте представим, что мы уже заполнили буфер кадров фоном, а теперь хотим отрисовать что-то поверх него. Простейший случай — это отрисовка изображения с шириной, кратной 8, в позиции, координата X которой тоже кратна 8. В таком случае всё удобно совпадает и мы можем просто побайтово скопировать пиксели спрайта, одна плоскость за другой. Из-за переключения между плоскостями производительность немного снижается, однако нам достаточно скопировать ровно столько байтов, сколько есть в исходном изображении.
![](https://habrastorage.org/getpro/habr/post_images/05b/10c/295/05b10c295f598a1ebd0f49e0caf826be.png)
Скоординированная запись в память EGA
Если же мы хотим переместить этот спрайт вправо на три пикселя, всё становится сложнее. Теперь нам нужно работать с отдельными битами в байте, потому что мы больше не ссылаемся на начало восьмипиксельного блока:
![](https://habrastorage.org/getpro/habr/post_images/b4b/26f/d4f/b4b26fd4f43a3f65007134d3e35eeafa.png)
Невыровненная запись в память EGA
К счастью, у оборудования EGA есть поддерживающие функции, немного упрощающие работу. Для изменения отдельных битов в байте нужны побитовые операции: необходимо сдвинуть исходные данные так, чтобы они были выровнены относительно целевых битов, применить битовую маску, чтобы переписать только требуемые биты, а затем скомбинировать исходные данные с целевыми, и, наконец, записать их обратно. В нашем примере со смещением на три пикселя для первого байта данных это будет выглядеть примерно так (в коде на C):
uint8_t mask = 0xE0; // 0b11100000
*target = (*target & mask) | (*source >> 3);
EGA может выполнять все эти побитовые операции вместо нас, снимая часть нагрузки с ЦП. Но нам всё равно нужно сначала считывать целевые данные, прежде чем записывать исходные данные. При считывании из видеопамяти EGA должна поместить копию считанных данных во внутреннее хранилище, называемое регистром-защёлкой (latch register). При записи данных в EGA записываемые данные можно подвергнуть побитовому сдвигу, маскированию и комбинировать с данными в регистре-защёлке, а уже потом записывать их в видеопамять.
К несчастью, нам всё равно нужно заставлять оборудование EGA делать всё правильно; например, нужно подобрать необходимую битовую маску и значения сдвига для позиции, в которой мы хотим выполнять отрисовку, а также ввод-вывод через порты, необходимые для соответствующей настройки оборудования. Ещё нам нужно считывать из видеопамяти для заполнения регистра-защёлки перед каждой операцией записи. Наконец, из-за отсутствия выравнивания с адресами байтов нам теперь нужно записывать больше байтов, чем ранее, поскольку 8 битов исходного изображения (один байт данных) теперь нужно распределить по двум байтам адресного пространства видеопамяти. То есть для изображения шириной 16 пикселей нам теперь нужно записывать и считывать не менее трёх байтов вместо записи только двух. И не забудьте о том, что всё описанное выше должно происходить четыре раза, по одному для каждой плоскости. Всё это сильно повышает сложность кода и отрицательно сказывается на производительности.
Даже если мы ограничимся только значениями, кратными 8, то всё равно столкнёмся с подобным усложнением, как только нам понадобится отрисовывать изображения шириной, не кратной 8, или если нам нужно отрисовать часть исходного изображения, начиная со смещения, не кратного 8. Последнее как раз необходимо для выполнения плавного скроллинга изображения фона — иными словами, для параллакс-скроллинга.
Стоит заметить, что все эти сложности существуют только на горизонтальной оси. По вертикали мы всегда работаем с адресами байтов, что гораздо проще. Предположу, что именно благодаря этому в шутере с вертикальным скроллингом Major Stryker, выпущенном Apogee в начале 1993 года, присутствует множество слоёв параллакса, несмотря на то, что целевой платформой для него является EGA.
Ранее я также говорил, что параллакс чаще встречается в VGA-играх, и что VGA было проще программировать, чем EGA. В основном это связано с 256-цветным режимом VGA, в котором каждый пиксель всегда занимает один байт. Поэтому вся сложность, вызываемая необходимостью адресации отдельных байтов, исчезает. VGA тоже имеет планарную схему памяти, однако она используется для байтов, а не для битов.
Похоже, разработчики Duke Nukem посмотрели на весь этот запутанный хаос, который представляет собой EGA, и сказали: «Нет уж, спасибо, с этим мы связываться не будем». Игра имеет ограничение: она работает в сетке пикселей 8×8, что позволяет обойти большинство сложностей (за одним исключением: эффекты частиц отрисовываются как отдельные пиксели и могут перемещаться свободно). Эта методика уже использовалась в первой игре Duke Nukem, а затем её сохранили в Cosmo’s Cosmic Adventure и в Duke Nukem II. Разумеется, нас интересует вопрос: как разработчикам удалось реализовать параллакс-скроллинг, учитывая эти ограничения? Мы узнаем об этом чуть позже, но сначала давайте разберёмся, как работает отрисовка мира в целом.
Отрисовка мира и спрайтов
Уровни (карты) Duke Nukem II, как и большинства платформеров, составлены из тайлов. Тайлы могут находиться спереди или сзади спрайтов Дюка, врагов и других объектов. Некоторые тайлы частично прозрачны (имеют «маску»). Их можно размещать поверх других тайлов (с некоторыми ограничениями) или просто отдельно, чтобы через них просвечивал фон.
![](https://habrastorage.org/getpro/habr/post_images/4b2/91d/b8a/4b291db8a27692c6649a7b2c6ffb5be3.png)
Игра перерисовывает в каждом кадре мир целиком, то есть фон, тайлы и спрайты. Код отрисовки спроектирован так, чтобы максимально избегать перерисовки (многократной отрисовки в одинаковой позиции пикселя), чтобы уменьшить количество необходимых операций записи в видеопамять.
Первыми отрисовываются тайлы карты: построчно с левого верхнего угла экрана к правому нижнему в сетке из блоков пикселей 8×8. Для каждой ячейки сетки, вне зависимости от того, отрисовывается ли тайл, часть фона или оба, существует тайл с маской. На этом этапе также отрисовываются сплошные тайлы (без маски), отображаемые поверх спрайтов — это может показаться странным, но вскоре мы к этому вернёмся. Камера выполняет скроллинг только с шагом в 8 пикселей, поэтому тайлы карты всегда выровнены с сеткой.
![](https://habrastorage.org/getpro/habr/post_images/b83/8d7/673/b838d767324b952b4fd8fe64f3b79932.png)
Структура тайлов спрайта Дюка
После отрисовки тайлов и фона начинается этап отрисовки спрайтов. Сама спрайтовая графика тоже разбита на группы тайлов и рендерится почти аналогично тайлам с масками. То есть, по сути, всё в Duke 2 основано на тайлах, даже спрайты (как говорилось выше, исключением являются эффекты частиц).
При отрисовке спрайта игра проходит по его тайлам построчно, начиная с левого верхнего до правого нижнего. Для каждого тайла она проверяет, находится ли он на экране (спрайт может частично быть вне экрана), и если в этом месте находится тайл карты, то он должен находиться поверх спрайтов. Если он не на экране или если поверх должен отображаться тайл карты, то этот тайл спрайта пропускается. Благодаря такой системе спрайты никогда не перерисовываются сплошными тайлами карты, что уменьшает количество данных, которые нужно передавать по шине ISA в графическую карту. Так как спрайты могут располагаться в ячейках сетки тайлов, они никогда не бывают между двумя ячейками тайлов карты, поэтому тайл спрайта или полностью видим, или полностью скрыт. Спрайты также могут частично перекрываться тайлами с масками переднего плана, но в такой ситуации считается, что спрайт полностью видим и частичная перерисовка допускается.
![](https://habrastorage.org/getpro/habr/post_images/b58/44d/b3f/b5844db3fc21144514b3f2f22b29af1d.png)
Пример частичного перекрытия спрайта тайлами карты переднего плана. Самый левый столбец тайлов спрайта не отрисовывается, поэтому спрайт как будто находится за деревянной колонной, которая на самом деле отрисована до отрисовки спрайта.
При помощи такой системы уже можно реализовать простейший вид параллакса: фон может оставаться статическим, а карта и спрайты — скроллиться. Именно такое решение использовано в первой части Duke Nukem, но в Duke 2 (и Cosmo) разработчики пошли дальше, заставив скроллиться и фон.
Как заставить двигаться фон
Как говорилось ранее, тайлы (и спрайты) и карты всегда скроллятся с шагом в 8 пикселей. Фон скроллится с шагом в 4 пикселя. Именно эта разница в скорости скроллинга и создаёт эффект параллакса. Но как мы увидели в разделе о структуре памяти EGA, отрисовка изображений со смещением в 4 пикселя — непростая задача. И даже если бы нам не приходилось учитывать сложность работы с оборудованием EGA, это всё равно было бы непросто из-за того, как организована графика фона (но об этом позже). Как бы то ни было, авторы игры не стали заморачиваться с созданием отдельных процедур отрисовки фонов. Вместо этого они использовали небольшой трюк: при загрузке карты игра создаёт копии изображения фона с пикселями, смещёнными вверх/влево на 4.
![](https://habrastorage.org/getpro/habr/post_images/24f/cb3/dc8/24fcb3dc8c705a91dea8d998c8eeeb32.png)
Немодифицированное изображение фона
![](https://habrastorage.org/getpro/habr/post_images/e57/1b6/95e/e571b695e6c5c007425b5bd3372db49d.png)
Копия фона, смещённая влево на 4 пикселя
Создав эти копии, игра может переключаться между обычными и смещёнными версиями при каждом шаге скроллинга. Если конкретнее, то она использует смещённые версии для нечётных позиций камеры и обычные — для чётных.
Чтобы фон скроллился после первых 4 пикселей, начальный столбец/строка в исходном изображении фона меняется. По сути, области экрана, в которых отображается фон, используются как окна размером в тайл, показывающие изображение фона, и в зависимости от позиции скроллинга они могут показывать разные части изображения. Область экрана, используемая для геймплея, составляет 256×160 пикселей, то есть 32×20 тайлов. Изображение фона имеет размер 320×200 или 40×25 тайлов. Допустим, камера находится в позиции 160, что кратно 40. То есть рендеринг начинается с левого верхнего тайла версии фона без сдвига. Далее он переключается на версию со сдвигом, по-прежнему начиная с левого верхнего тайла. Затем игра возвращается к версии без сдвига, но теперь самый левый столбец тайлов на экране отображает второй столбец тайлов изображения фона, пропуская первый тайл в каждой строке, и так далее. Вот как это выглядит в анимации:
![](https://habrastorage.org/getpro/habr/post_images/4ff/eb2/ea9/4ffeb2ea9faa358a620de6c8d8aa956a.gif)
Над экраном игры показана верхняя строка двух версий изображения фона. Голубым прямоугольником обозначена часть изображения фона, отображаемая на экране.
После того, как достигнут конец изображения фона, следующие экранные тайлы после него переходят к отображению тайлов из начала изображения фона, благодаря чему фон повторяется:
![](https://habrastorage.org/getpro/habr/post_images/cf8/7fa/862/cf87fa862c2376447360fcfcccf5ab2c.png)
Голубым прямоугольником показана часть фона, отображаемая в левой половине изображения, жёлтым прямоугольником — часть, используемая после того, как изображение начинает повторяться.
То же самое происходит и по вертикали. Для фонов, которые могут скроллиться вертикально и горизонтально, игра создаёт четыре версии: без изменений, со сдвигом влево, со сдвигом вверх, со сдвигом влево и вверх.
На некоторых уровнях игры фон скроллится постоянно, вне зависимости от позиции камеры. Это работает аналогично параллакс-скроллингу, единственная разница заключается в значении счётчика, используемого для определения смещения скроллинга вместо позиции камеры. Инкремент счётчика связан со временем. Для горизонтальной версии этого автоматического скроллинга фон дополнительно скроллится с шагом в 2 пикселя, а не в 4. Принцип остаётся тем же, но в данном случае игра создаёт 4 версии фона, смещённые на 0, 2, 4 и 6 пикселей. Эти 4 изображения отображаются по порядку, после чего выполняется инкремент начального столбца тайла исходного изображения.
Рассмотренные нами техники позволяли игре достаточно эффективно реализовывать параллакс-скроллинг благодаря системе, предотвращающей перерисовку. Однако для повышения производительности разработчики провели дополнительную работу. Давайте теперь поговорим о ней.
Ускоряем отрисовку тайлов
Наборы тайлов и графика фона выстроены так, чтобы отрисовка отдельных блоков пикселей 8×8 была быстрой. В форматах изображений, представленных как битовые карты, данные обычно выстроены по строкам пикселей: все пиксели самой верхней строки хранятся первыми, затем идёт вторая строка, и так далее. Однако фоны и наборы тайлов выстроены в тайлы. Сначала хранятся 8 строк по 8 пикселей каждая, представляющие левый верхний блок 8×8 пикселей изображения. За ними идут 8 строк по 8 пикселей, представляющие второй блок, и так далее. Каждая 8-пиксельная строка состоит из 4 байтов, хранящих 4 битовых плоскостей EGA для этих 8 пикселей. Такая схема позволяет искать нужную позицию тайла в изображении, а затем считывать по порядку 32 байта, чтобы получить все 8×8 пикселей, составляющих этот тайл.
Когда игра загружает уровень, набор тайлов и фон загружаются в видеопамять. Благодаря этому можно использовать технику под названием latch copy. При помощи неё игра может скопировать тайл из фона или набора тайлов, считывая и записывая всего 8 байтов. По сравнению с 32 байтами, необходимыми для копирования тайла из основной памяти в видеопамять, это гораздо быстрее, к тому же нам не нужно выполнять ввод-вывод через порты для переключения между плоскостями, что также экономит время.
Мы уже говорили о регистре-защёлке EGA, когда обсуждали отрисовку изображений. Однако я не сказал о том, что на самом деле существует четыре регистра-защёлки — по одному на каждую плоскость. Каждый раз, когда ЦП считывает байт из видеопамяти, он получает только данные текущей выбранной плоскости, но внутри графической карты в регистры-защёлки загружаются данные для всех четырёх плоскостей. И теперь мы узнаём, что оборудование можно настроить так, чтобы во время операций записи оно игнорировало данные от ЦП и использовало только значения из защёлок. Таким образом ЦП может считать один байт, который заполняет защёлки данными для всех четырёх плоскостей, а затем записать их обратно по другому адресу, что позволяет сохранить данные из защёлок во все четыре плоскости в этом целевом адресе. Записываемое ЦП значение неважно, правильным должен быть только адрес. Благодаря этому механизму мы можем копировать 4 байта по цене одного байта.
Для отрисовки спрайтов и тайлов с масками игра вынуждена копировать все четыре плоскости из основной памяти по отдельности, поскольку для применения маски прозрачности ей нужно выполнить побитовые операции. Однако отрисовка тайлов и фона составляют подавляющее большинство данных, записываемых в каждом кадре, поэтому благодаря этой оптимизации игра получает существенное повышение скорости. Насколько же оно существенно? Я выполнил бенчмарк, отрисовав полностью заполненный тайлами экран с использованием техники latch copy и без неё. Вот результаты:
ЦП | Графическая карта | Latch copy | Обычное копирование | Ускорение |
80486 DX2, 66 МГц | Paradise Autoswitch EGA2 (EGA, 8-битная ISA) | 36 мс / 28 FPS | 86 мс / 12 FPS | 58 % |
80486 DX2, 66 МГц | bit-design PCELC V1.3 (EGA, 8-битная ISA) | 34 мс / 29 FPS | 87 мс / 11 FPS | 61 % |
80286, 16 МГц | Cirrus Logic CL-GD5422 (16-битная ISA) | 20 мс / 50 FPS | 50 мс / 20 FPS | 60 % |
80386 DX, 40 МГц | Cirrus Logic CL-GD5420 (16-битная ISA) | 13 мс / 77 FPS | 32 мс / 31 FPS | 59 % |
80486 DX2, 66 МГц | Cirrus Logic CL-GD5422 (16-битная ISA) | 12 мс / 83 FPS | 28 мс / 36 FPS | 57 % |
80486 DX2, 66 МГц | Tseng Labs ET4000/W32 (VLB) | 5 мс / 200 FPS | 10 мс / 100 FPS | 50 % |
За исключением случая VLB-карты, ускорение которой оказалось чуть меньше, оптимизация latch copy во всех случаях повышает производительность примерно на 60 %. Любопытно, что EGA-карты могут при помощи этой техники выполнять отрисовку быстрее по сравнению с отрисовкой целого экрана за раз, хотя для всех остальных карт последняя выполняется быстрее. Кроме того, теперь мы видим большее влияние скорости ЦП: при переходе с 286 на 386 чётко видно улучшение на 7 мс. Это логично, поскольку скорость, с которой ЦП может выполнять latch copy, теперь более важна, чем сырая полоса пропускания шины для отрисовки целого экрана за раз.
Подведём итог
На архитектуру первой части Duke Nukem сильно повлияли ограничения EGA, и игра разрабатывалась с их учётом: привязка всех элементов к сетке блоков 8×8 пикселей позволяла избегать сложностей, вызываемых невыровненным записям в память EGA. Отрисовка всего мира как сетки тайлов, в том числе фона и спрайтов, до минимума снижала перерисовку и уменьшала необходимость в полосе пропускания. Это позволило реализовать в 1990 году параллакс-скроллинг, который в то время был лишь в очень немногих играх для DOS. Но за это приходилось платить цену: так как всё двигалось с шагом в 8 пикселей, частота обновления должна была оставаться довольно низкой, иначе всё двигалось бы слишком быстро. Как видно из бенчмарков, при использовании оптимизации latch copy даже 286 способен рендерить заполненный тайлами экран с частотой 50 FPS. Поэтому кажется вполне реалистичным, что Duke Nukem мог достигать приличной скорости в 35 FPS. Но из-за этого игра была бы слишком быстрой, поэтому частоту кадров пришлось уменьшать. В Cosmo’s Cosmic Adventure и Duke Nukem II произошла дальнейшая эволюция движка: при помощи трюков был добавлен скроллинг фонов. Однако фундаментальная структура, требовавшая снижения частоты кадров, оставалась такой же.
Стоило ли оно того? Duke Nukem и Cosmo были очень успешными, оставаясь в десятке самых продаваемых Shareware-игр ещё долго после своего релиза. Пока мне не удалось найти подробной информации, но я думаю, что Duke Nukem II тоже проявила себя хорошо. Для того времени все эти игры были привлекательны с точки своего совершенства, скорости геймплея и графики. Также стоит заметить, что дёрганая частота кадров на ЭЛТ-мониторе была менее заметна. Но насколько приятно было смотреть на такую графику? По сравнению с другими играми наподобие Commander Keen, отдававшими приоритет не параллаксу, а плавному скроллингу, низкая частота кадров и дёрганый скроллинг Duke Nukem/Cosmo могут казаться некомфортными. Лично я по-прежнему люблю играть в Duke Nukem II, но знаю людей, которые так и не смогли взяться за эту игру из-за того, что дёрганая графика слишком напрягала глаза.
К счастью, движок RigelEngine больше не отягощён сложностями старого оборудования EGA. На современном компьютере с дискретным GPU, способным рендерить сложную 3D-графику, отрисовка нескольких слоёв 2D-изображений поверх друг друга — тривиальная и очень быстрая задача. Даже очень слабые системы, например, Raspberry Pi первого поколения, могут легко справляться с параллакс-скроллингом. Это позволяет RigelEngine (опционально) улучшать игровой процесс, добавляя режим плавного скроллинга и движения, благодаря чему игра работает с частотой 60 FPS (или выше) без изменения скорости геймплея. Эффект параллакса полностью использует все возможности дополнительных кадров, выполняя движение с более частым шагом в 1 пиксель вместо 4 пикселей, происходящих реже. Игра выглядит гораздо плавнее; вероятно, именно так бы сделали разработчики оригинала, если бы у них имелась техническая возможность. Может быть, они бы даже добавили дополнительные слои параллакса…
Вот видео, в котором показана разница:
На этом мы завершаем разбор параллакс-скроллинга Duke Nukem II и сложностей, из-за которых в ранних играх для DOS этот эффект было трудно реализовать. Сегодня разработка игр так же сложна, как и в те времена, однако характер сложностей поменялся, и некоторые вещи, которые были сложными — например, параллакс-скроллинг — стали повседневностью. Пусть нам интересно разбирать сложности, с которыми сталкивались разработчики игр в 90-х, например, с программированием для EGA, однако, вероятно, хорошо, что вывод изображений на экран стал гораздо более лёгким процессом.