Info Guide
#13
01 апреля 2021 |
|
Games - устройство игры Super Mario Bros от Gogin
Super Mario Bros. На пати CAFe'2019 была представлена демоверсия игры Super Mario Bros. 128K от Gogin'а (Сергея Смирнова). Эта демоверсия поддерживает основные фишки оригинальной игры, включая плавание, поедание грибов, битвы с Боузером и, конечно, сцену "прин─ цесса в другом замке". При этом прекрасно работает с частотой 50 fps. Причём по сра─ внению с версией 2002 года от того же автора был полностью переписан движок - теперь уже в цвете. Это не просто игра, а ещё и специализи─ рованная система подготовки ресурсов. В частности, существует вариант "COVID edition" с другой графикой. Нашей редакции стало интересно,как уст─ роен изнутри движок игры,а Сергей согласи─ лся дать интервью. * * * Alone Coder> Привет! Расскажи,как работает твой дви─ жок Марио? Мы с Shiru в своё время писали похожий движок, но без атрибутов, там пе─ рерисовывалось только то, что не пустое. Как я понимаю, у тебя есть какие-то прод─ виги и в плане атрибутов, и в плане иннер─ лупов, и в плане управления всем этим. В оригинальном SMB (который портирован в NedoOS ) в ОЗУ лежит карта видимых мета─ тайлов именно как метатайлов,а у тебя как? список объектов? А как хранится уровень? В оригинальном SMB есть стандартные столбцы и большие объекты. Я в своё время думал, как бы хранить карту полностью из объектов,которая допус─ кает скролл во все стороны. Но в итоге так и не решил, как сделать быстро, и вернулся к метатайлам... Gogin> У меня карта изначально представлена сеткой 256х16. Далее эта сетка компилиру─ ется в два блока байт. - Первый блок представляет собой байт- поток для отрисовки карты на экране. В нём данные уложены так, чтобы сразу было поня─ тно, какой спрайт (тайл) печатать в какой строке экрана, и сколько таких одинаковых тайлов подряд нужно напечатать. Формат хранения байт-карты разрабатывался неско─ лько месяцев, собственно, с него началась вторая реинкарнация движка в 2016 г. Из-за того, что распаковка байт-потока карты реализована только в одну сторону, то возможен скролл только вправо. Если ну─ жен скролл в обе стороны, то этот способ не подходит. Зато он позволяет за очень малое число тактов подготовить все регистровые пары для печати тайлов.Враги,которые появляются (создаются) с правой стороны экрана - тоже директивно зашиты в эту карту. - Второй блок байт - это обычная таблица 256х16, которая хранит физические свойства тайлов, и используется только для обработ─ ки логики и коллизий в игре. Изображение (спрайт) тайла и его физические свойства - это разные вещи. Они связываются на этапе описания уровня из тайлов, через свойства самих тайлов. По сути, любой блок в Марио - это неко─ торый "признак" активности/неактивности + название спрайта. А палитра задается для всей локации целиком, в привязке к имени блока. Таким образом, я могу иметь четыре раз─ ных блока: brick1, brick2, brick3, brick4. Они могут выглядеть одинаково (использо─ вать один и тот же спрайт),но первый будет "вышибаемым", второй будет фоном, третий будет содержать приз или монеты, а четвёр─ тый может быть цветным и полосатым, потому что палитра прописана только для блока brick4. Одно из самых сложных мест во всём тайл-движке - это удаление блока из карты, восстановление блока в карте или замена одного блока на другой. Эти операции тре─ буют точечных изменений сразу в двух кар─ тах - в байт-коде для рендеринга и в таб─ личной карте для обработки логики. И если с табличной картой, в принципе, всё понятно, то чтобы изменить байт-поток для рендера - в итоге,достаточно нетривиа─ льные алгоритмы получились. Alone Coder> Можно ли на твоём движке реализовать, например, игру North Star ? Gogin> Да, это легко сделать. Ничего менять не нужно. Придётся только переписать печать тайлов 2х2 на печать тайлов 3х3. Работа логики и все расчёты в системе координат мира - не изменятся. Alone Coder> А как выглядит иннерлуп вывода, включая выборку метатайла? (Ред.: иннерлуп - самый внутренний цикл.) Gogin> В цикле печатаем в экран тайлы по коло─ нкам слева направо, тайлы "чистят" сами себя,поэтому нахлёст идет слева и направо. Иннерлуп печати тайлов выглядит так: - читаем байт-код из карты, в нем прямо написано: какой тайл куда печатать и ско─ лько раз подряд. В байт-коде каждый бит там несёт смысл: 1 бит - признак команды: либо напечатать тайл, либо создать врага; 4 бита - номер тайла; 3 бита - кол-во повторений тайла; 4 бита - номер знакоместа по вертикали; 2 бита - смещение первого блока в цепочке, относительно текущей колонки; 1 бит - маркер очищения цвета после блоков (если требуется). Биты в байт-коде расположены таким об─ разом, что не нужно делать rrca или rlca, чтобы дотащить их до "удобной" позиции для использования. Далее: - всю информацию получили; - выводим сразу несколько тайлов одной процедурой; есть разные варианты процедур под разное количество одинаковых подряд тайлов; - красим в цвет первую колонку слева (вертикальную полосу 2 знакоместа); - очищаем последнюю колонку справа, 2 знакоместа, если выставлен соответствующий флаг; - если встречается байт-код "следующая колонка", сдвигаемся на два знакоместа и повторяем... - до тех пор, пока не упрёмся в правую границу экрана. Alone Coder> Сколько бит субпиксельных координат есть у героя и врагов? В оригинальном SMB субпиксели у героя какие-то странные и, похоже, влияют на скорость. (Ред.:субпиксели - дробная часть координат (мельче пикселя).) Gogin> Всё считается в системе fixed point 12.4: 12 бит целая часть, 4 бита дробная. Если взять 16-ричную координату #abcd в мире, то: - #ab - это координата тайла, - c - номер пикселя в тайле, - d - номер субпикселя Alone Coder> Как Марио входит в трубу? В оригинале у каждого спрайта был режим - печатать по─ верх фона или под фоном. Gogin> У меня в движке все спрайты печатаются поверх тайлов всегда,по OR. Марио входит в трубу ровно так же, как Piranha Plant из этой трубы вылезает. В цикле уменьшается высота runtime-объекта, и сдвигается вниз вертикальная координата.На экран печатает─ ся спрайт, обрезанный снизу. Alone Coder> А как входить в трубу, которая сбоку? Или только за счёт порядка вывода, и при этом должна быть гарантия,что труба выров─ нена по знакоместу? Gogin> В боковые трубы входить не умеем :) Это не реализовано. В принципе, если отметить зоны,где это возможно, и реализовать это в движке, то: - только на момент анимации входа в тру─ бу - меняем порядок печати (сначала глав─ ный герой, затем тайлы), - тайлы печатаются в лоб, без каких-либо OR, так что они целиком перекроют часть героя, - для красоты,чтобы 100% не было клэшин─ га, можно выровнять карту по знакоместу в момент входа, - а можно и не выравнивать, так как мак─ симальное убегание героя вправо и так вы─ ровнено по знакоместу, и чтобы получить клэшинг,нужно будет сначала убежать вправо чуть дальше, чем труба, затем вернуться назад, и затем залезть в неё. Вот тут шанс НЕ попасть в знакоместо - высокий. Alone Coder> Как работает клипирование с боков, сверху, снизу? Gogin> Клипирования слева-справа вообще нет. Клипирование сверху и снизу работает так же,как заход в трубу и как растения.Просто печатается часть спрайта,обрезанная сверху или снизу. Alone Coder> Как распределяется память под спрайты и тайлы? Gogin> Память под тайлы и спрайты распределяе─ тся полностью автоматически,и по окончании билда, я,по сути,не знаю,где какие спрайты лежат. За это отвечает сборщик-компилятор, который автоматически всё рассчитывает и распределяет по свободной памяти. Когда компилируются спрайты для уровня, то на входе:предоставляются описатели всех сущностей в игровом мире (ресурсы отдель─ но, спрайты отдельно),карта мира и различ─ ные настройки - это тоже ресурсы. Получается своего рода реляционная мо─ дель игры. Загрузив в память модель мира, можно прямо вычислить,какие спрайты потре─ буются, и в каких объектах они будут испо─ льзованы. Всё это автоматом конвертируется в нуж─ ный для движка формат, складывается в па─ мять,строятся таблицы смещений.Если объект может двигаться попиксельно и медленно, то для него создаётся 8 копий спрайта со сме─ щениями. Если скорость движения объекта кратна двум, то создаётся 4 копии со сме─ щениями и т.п. В итоге в рамках одной локации на выхо─ де имеем готовые банки со спрайтами и го─ товые таблицы для всех спрайтов тайлов и всех объектов. Спрайты, конечно же, испо─ льзуются повторно, если возможно. Alone Coder> Если графика лежит в страничке, то как она выводится на два экрана? Gogin> Графика для 5-го экрана лежит в 5-й странице и частично в 4-й странице.Графика для 7-го экрана лежит в 7-й странице и ча─ стично во 2-й странице. Alone Coder> Как движок достаёт нужный спрайт? Прос─ то по номеру, по сгенерированной метке или там многоуровневая система класс объекта - номер анимации - номер фазы? Gogin> Когда уровень компилируется, все иерар─ хии объектов разворачиваются максимально в плоские структуры, вплоть до физического адреса спрайта. Для врагов,призов и других объектов - адреса спрайтов (физические) лежат открыто в неизменяемых структурах, наподобие static свойств в классе.Для тай─ лов адреса спрайтов - это просто таблица, в которой адрес спрайта зависит от номера тайла и текущего положения карты мира на экране. Вот кусок конвейера обработки объекта. DY = DY + Gravity; Y = Y + DY; X = X + DX; и далее анимация по таймеру. ;Process Y = Y + dY wpf_process_y: ld e,a ;A = (ix+RO_Y) ld d,0 bit 7,e jr z,wpf_process_y_2 dec d ;DE = object dY (12.4) wpf_process_y_2: ld hl,(ix+RO_Y) ;HL=object Y (12.4) add hl,de ld (ix+RO_Y),hl ;check out of screen below ld a,h cp #fe ;Y = -32px jp z,wpf_delete_object ;Process X = X + dX wpf_process_x: ld hl,(ix+RO_X) ;coord X (12.4) ld c,l ;C = coord X low -> will ;be used below for animation ld e,(ix + RO_DX) ;dX (4.4) ld b,e ;B = dX -> will ;be used below for animation ld d,0 bit 7,e ;move left or right jr z,wpf_process_x_2 dec d ;D = #ff wpf_process_x_2: add hl,de ;X = X + dX ;check out of map at left ld a,h inc a ;cp #ff jp z,wpf_delete_object ld (ix+RO_X),hl ;Process animation wpf_process_animation: ifdef OPTIMIZE_RUNTIME_OBJECTS ld a,(internal_timer) and %00000011 jr nz,wpf_process_updown endif ;B = dX (4.4), set above ;B<=0 - move left, B>0 - move right ld d,RDEF_SPRITE_NO_LEFT_1 ld a,b dec a rla ifdef BACKWARDS ccf endif jr c,wpf_pa_1 inc d inc d ;D = RDEF_SPRITE_NO_RIGHT_1 wpf_pa_1: ;check dX == 0 ? ld a,b and a jr nz,wpf_pa_2 ;use timer if dX == 0 ld a,(internal_timer) rlca rlca rlca rlca ld c,a wpf_pa_2: ;C = coord X, set above ;bit 7 of C = sprite 1/2 bit 7,c jr nz,wpf_pa_3 inc d wpf_pa_3: ld a,d ;A = sprite cell index ;in definition ld hl,(ix+RO_DEF_ADDR) or l ld l,a ld a,(hl) ld (ix+RO_SPRITE_NO),a ;Process 'updown slide' bit wpf_process_updown: Alone Coder> А почему в версии,которая была предста─ влена на CAFe, иногда оставалось белое знакоместо,когда маленький Марио бил снизу по кирпичу? Gogin> Это некорректно работает восстановление цвета фона тайла, если игрок сначала взял разбег, затем ударился головой о блок, а затем затормозился. Если за это время карта мира успеет уехать сильно влево, то серединка первого блока остается незакрашенной. Позиция вышибленного блока будет сильно отличаться от позиции этого же блока чуть ранее, и атрибуты восстанавливаются не в том месте, где должны. Баг проявлялся только в движении и с разбега. Аналогичный баг был с правой сто─ роны от тайлов,когда нужно чистить атрибу─ ты после тайлов. Это исправляется перерисовкой полосы атрибутов целиком,по длине,равной смещению карты за время "подпрыгивания" блока. * * * Сейчас Gogin пишет новую игру на совер─ шенно новом движке. Ждём чего-то не менее интересного!
Другие статьи номера:
Похожие статьи:
В этот день... 21 ноября