nzeemin.github.io / Портирование Highway Encounter

Оригинал на ZX Spectrum

«Highway Encounter» на World of Spectrum

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

Вы управляете Vorton, всего их пять, текущий с белой шапкой это Main Vorton, остальные это Auto-Vorton, все они толкают вперёд Lasertron, который нужно довести до конца зоны 0. Мир игры разбит на зоны, всего их 31., визуально они нумеруются 30..0, внутри наоборот 0..30 и больше.

Мир игры описывается в виде массива записей длиной по 16 байт, всего их 260 (с адреса 7B00). Это все объекты всех зон, то есть таблица игрового мира.
Объекты мира – это препятствия и враги.
Поля каждой записи объекта:
; +$00 – горизонтальное смещение объекта; FE – пустая запись, FF – конец таблицы
; +$01,+$02 – позиция объекта
; +$03,+$04 – смещение объекта; это значение плюс смещение текущей зоны ($8F03) даёт адрес на теневом экране
; +$05 – базовый номер спрайта – индекс в таблице 7A00
; +$06 – паттерн движения: 000 = Vorton, 006 = Fire, 012 = Block, 026 = Flat blocker, 032 = Barrel
; +$07 – направление движения, копия из (IX+$0С) AND $07, значения 0..7
; +$08 – флаги движения; 0 = останов, 1 = движение; bit 7 = 1: взрыв
; +$09 – горизонтальное смещение (только биты 2-3)
; +$0A – фаза спрайта, смещение от $8F00: 015,017,020,021,022,023
; +$0B – номер точки продолжения по таблице $9000
; +$0C – начальное направление движения, копируется в +$07
; +$0D,+$0E,+$0F – начальные значения для +$00,+$01,+$02

Дальше, есть блок игровых объектов с которыми мы работаем сейчас, формат записей тот же что и выше. Начинается он по адресу 8A00, сначала идут 5 Vortons, затем Lasertron, затем три fireball (выстрелы/пули). Затем за этим блоком из девяти зафиксированных игровых объектов идут текущие игровые объекты, числом до 44 записей. При смене текущей зоны обновляется и набор текущих игровых объектов – перебираются все объекты игрового мира, выбираются только те которые вблизи текущей зоны, с некоторым порогом. Поэтому получается что объекты мира "живут" только когда они находятся внутри текущей зоны или вблизи её. При помещении в таблицу текущих объектов, копируются 14 байт записи, в остальные два байта записывается адрес откуда взята запись в игровом мире. При переходе в другую зону (пересоздание списка текущих объектов), текущие записи копируются обратно по этому сохранённому адресу – возвращаются в общую таблицу игрового мира.

Отрисовка.
В памяти держится два теневых экрана, оба 1 бит на пиксель (чёрно-белые). Первый теневой экран это фон, рисунок зоны без объектов. Этот экран формируется один раз при заходе в зону, меняется только при смене зоны. Но в этот экран выводятся плоские площадки-препятствия: они не двигаются и не могут загораживать другие объекты, поэтому их можно рисовать на фоне.
Второй теневой экран это копия первого, но поверх ещё рисуются спрайты объектов. Спрайты имеют размер 24x24 пикселей, каждый спрайт имеет маску.
При отрисовке перебираются все текущие игровые объекты (включая Vortons, Lasertron и выстрелы). Сначала по адресу 8D52 собирается список объектов – гориз.смещение + адрес записи, причём эта таблица собирается сортированной от дальних объектов к ближним – сделано для того чтобы при отрисовке спрайты правильно наложились друг на друга. Объекты не попадающие в экран не попадают в эту таблицу. После составления таблицы сразу выполняется рисование спрайтов на втором теневом экране, при этом в отдельной таблице отмечаются знакоместа в которые попал спрайт (таблица флагов отрисовки, байт на знакоместо 8x8).
Следующий шаг – отрисовка второго теневого экрана на реальный экран. Причём полностью он рисуется только один раз при входе в зону, после этого отрисовка опирается на таблицу флагов отрисовки, т.е. выводим только то что реально изменилось.
И последний шаг отрисовки – восстановление второго теневого экрана, т.е. стирание спрайтов. Это делается копированием знакомест 8x8 с первого теневого экрана, опираясь на таблицу флагов отрисовки, т.е. тоже восстанавливается только то что изменялось.
Сама отрисовка спрайтов выполняется с горизонтальной точностью в 2 пиксела – есть 4 процедуры отрисовки спрайтов, со смещениями +4, +2, 0, -2. Т.е. точность движения объектов по вертикали – 1 пиксел, по горизонтали 2 пиксела.

Движение и столкновения объектов.
Пробегаем по таблице текущих игровых объектов, сравнивая записи каждый-с-каждым. Если выявили столкновение, делается такая штука. Берётся значение паттерна движения обоих объектов (поле +$06), они складываются, полученное значение используется как индекс в таблице адресов продолжений – просто происходит переход по указанному в таблице адресу продолжения. Например, для статичных препятствий все адреса продолжений одинаковы, по ним код который говорит что объект должен остановится. Или например, у «солнышка» при столкновении с препятствием получаем адрес продолжения, где направление движения меняется на противоположное.
Движение объектов обрабатывается вместе с расчётом столкновений, если нет столкновений, то отрабатывается то что указано байтом «паттерн движения» в записи объекта. Обычно это просто движение без изменений в текущем направлении, но есть и необычные паттерны, например «взять случайное число и с вероятностью 10/256 повернуть влево или вправо».

В таблице объекта есть два байта про спрайт – базовый номер спрайта, плюс смещение фазы спрайта. Реальный номер спрайта получается так. Берём смещение фазы спрайта, добавляем адрес 8F00 – получаем адрес где лежит фаза спайта, это значение складываем с базовым номером спрайта, получаем индекс в таблице 7A00, где уже лежит реальный адрес спрайта. Что это даёт? во-первых, фазу спрайта по 8F00+смещение можно крутить – и там есть несколько таких переменных, которые крутятся по-разному. Во-вторых, базовый номер спрайта задаётся отдельно – можно иметь разные по виду объекты, у которых фаза крутится одинаково.

Что сделано отлично:

  • Идея с общим миром в 260 записей из которого выбирается набор в максимум 44 текущих записи + 9 фиксированных записей. Причём мир легко восстанавливается в исходное состояние из тех же самых записей – очень эффективно.
  • Отрисовка с двумя теневыми экранами плюс таблицей флагов отрисовки – получилась довольно быстрой.
  • 9 фиксированых записи игровых объектов + 44 переменных идут одни за другим – однородная таблица игровых объектов, что упрощает логику работы с объектами и отрисовки. Начало этого списка хранится в переменной 8F01. Смысл тут в том, что когда Main Vorton умирает, то просто сдвигается адрес начала таблицы в 8F01.
  • Все переменные собраны в один блок по адресу 8F00, инициализация всех переменных при старте игры выполняется копированием всего блока целиком из блока констант.
  • Расчёт столкновений через таблицу адресов продолжений – это круто, прям магия.

Набор спрайтов:

Портирование на УКНЦ

Работа над портом начата в апреле 2017 года, закончена в январе 2018. Общие затраты времени – человеко-месяц, плюс-минус дни. Выполнялось по-командное переписывания кода с ассемблера Z80 на MACRO-11.

Отличия порта от оригинала:

  • более слабая графика в плане цветов,
  • медленнее примерно в 1.5 раза,
  • нет звука,
  • финальная анимация упрощена,
  • нет обновления high score и нет таблицы рекордов.

Скриншоты портированой версии:

Демо-режим (первоначально я делал основное поле в зелёном цвете, но позже перешёл на жёлтый, т.к. этот цвет на всех УКНЦ выглядит одинаково и ярче на ч/б мониторе):

Проверено что игра работает и на реальной машине (спасибо hobot с форума zx-pk.ru):

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

  • skoolkit-5.4 на начальном этапе для декомпиляции оригинальной игры, разобраться что к чему, из полученного HTML потом копировал по кусочкам исходный код для Z80;
  • эмулятор EmuZWin и его отладчик, для того чтобы понять как работает оригинал, и для параллельной отладки;
  • эмулятор UKNCBTL, специально для этого порта сделал вьювер спрайтов и несколько доделал отладчик;
  • Visual Studio 2013 для работы над UKNCBTL и для написания на C# небольшой утилиты SpriteRotate с целью подготовки спрайтов и прочих массивов, взятых из дампа оригинала;
  • Эмулятор RT11 для консоли Windows от Patron, постоянно использую для компиляции и линковки;
  • эмулятор EmuStudio от Titus для тестирования.