Ringo Render 64x48
UPD(20.12.2022): Исходники оптимизированного рендера доступны на GitHub
Всем привет!
Сегодня я немного расскажу про то как устроен рендер в нашей совсем недавно вышедшей игре Ringo.
Если вдруг пропустили то посмотреть/поиграть можно здесь
К сожалению исходники настолько запутаны, уплотнены и пестрят ошибочными комментариями из прошлых итераций движка что вряд ли кому-то помогут разобраться. Да и сам я на данный момент уже плохо помню как там и что работает. Но так как уже несколько человек попросили меня рассказать про рендер то я попробую осветить хотя бы базовые моменты которые помогли достигнуть результата.
Режим 64х48
Самый лёгкий способ сделать 64х48 на спектруме это воспользоваться двумя экранами и сделать мультиколор 8х4. Пиксели двух экранов заполняются паттерном в виде столбиков, в моём случае это 11110000b, т.е. левая часть знакоместа ink правая papper. Далее мы переключаем экраны каждые 4 строки во время прохода луча в области пикселей чтобы атрибутами второго экрана менять цвета в нижней части знакоместа независимо от верхней.
Традиционно в моих рендерах всё происходит в два кадра. Сначала в буфер рисуются тайлы, затирая старую картинку, а потом сверху накладываются спрайты и буфер кидается на экран. Изначально я сделал буфер в манках ( munk сокращение от Multicolor Chunk). Этот термин придумал vivid и подробнее про них можно почитать здесь Если коротко то это честный буфер в точках которые потом парами схлапываются в нужный атрибут с помощью pop hl: ldi Это очень красиво и позволило легко анимировать тайлы делая картинку живой, да и скролл по горизонтали с шагом в нашу виртуальную точку 4х4 получался сам собой, но оказалось медленно для моих целей. Я уже смирился с мыслью что придется делать всё в три кадра, но результат мне совсем не понравился. Поэтому всё ушло ко второму варианту — сделать буфер в уплотненном виде, т.е. сразу в формате атрибутов.
Чтобы вырваться на свободу в двух фреймах я решил что нарисую уровень в две быстрые страницы, в одной из них он будет сдвинут и я просто буду копировать в рабочий буфер нужный сдвиг в зависимости от х координаты камеры и накладывать спрайты уже на атрибутный буфер. У меня получилось сделать это всё в два фрейма и осталось куча времени под игру. Я даже сделал физику главного героя и логику камеры, однако в какой-то момент стало ясно что картинка выглядит мёртвой т.к. задний фон статичен. К тому же чтобы его менять нужно потратить много времени, рисуя всё в два буфера фона. После непродолжительной внутренней борьбы было решено перейти к третьему варианту, который является комбинацией первых двух. А именно: перерисовывать весь буфер тайлами и накладывать поверх спрайты, но делать это всё в уплотненном(атрибутном) виде.
В общем виде вырисовывалась такая картина:
Первый кадр:
В верхнем бордюре происходит переброска рабочего буфера в атрибуты двух экранов. Так как времени в верхнем бордюре недостаточно чтобы перебросить все 768х2 байта то продолжаем дальше кидать уже по медленной памяти в нужный момент переключая экраны каждые 4 строки и учитывая задержки памяти. После того как мы закончим кидать атрибуты мы начинаем рисовать тайлы в рабочий буфер также параллельно переключая экраны каждые 4 строки в нужные моменты времени. Здесь уже легче так как рабочий буфер лежит в быстрой памяти. Как только мы проходим весь экран мы заканчиваем рисовать тайлы в буфер, а весь нижний бордер отдаём под игру, т.к. только тут у нас есть возможность использовать код с произвольным временем выполнения.
Второй кадр:
В верхнем бордюре мы рисуем оставшиеся тайлы которые не влезли в предыдущем кадре и выполняем разный служебный код с постоянным временем выполнения (патчинг рендера, камеры итп). Затем мы накладываем на рабочий буфер спрайты также параллельно переключая экран каждые 4 строки пока не дойдём до нижнего бордюра, который опять отдаём по игру.
Итак осталось самое сложное, нарисовать максимально быстро 63 тайла в атрибутный буфер и наложит на него спрайты.
Спрайты
В процессе формирования визуального стиля и понимания моих возможностей как художника, я пришел к размеру спрайтов 12х10 точек (на самом деле чистыми 11х10, т.к. каждый спрайт ещё дублируется в сдвинутом виде на одну точку). Спрайт приблизительно 8х8 плюс чёрная окантовка. Спрайт храниться в следующем виде: байт чистого инк+64, байт чистого папера+64… итак 120 байт. 0 считается прозрачным, а +64 нужно чтобы была возможность рисовать чёрным и делать обводку.
Так как спрайты будут накладываться параллельно с переключением экранов нужен код с постоянным временем выполнения. Также нужно было прийти к какой-то минимальной единице вывода чтобы всё легче «параллелить». Спуститься на максимально адовый уровень (сгенерировать код и всунуть в нужные моменты ауты) я не решился.
Пришлось прикидывать как это всё раскидать между линиями в блокноте, это больше походило на записки сумасшедшего. Я перепробовал разные размеры спрайтов и таких записей было куча.
В итоге такой единицей вывода у меня стала печать 2-х точек спрайта или 1-го обычного атрибута.
В виде макроса это выглядело так:
Спрайты выводятся в цикле, что очень сильно сэкономило памяти. Вывод одного спрайта выглядит так:
С учётом обвязки вышло что за одно переключение экранов мы успеваем положить 8 точек спрайта (4 атрибута). Так как спрайт шириной 12 точек (6 атрибутов), то нам надо шагать на строку вниз каждые 12 точек. В названии макроса зашифрована эта логика. Например STEP62 — положить 6 атрибутов, шагнуть вниз, положить ещё 2 и.т.д Конечно идеально бы паралеллились спрайты 8х8, но это уже слишком мелко, даже для меня. Для примера приведу один макрос, чтобы была понятна идея:
Тайлы
От быстрого вывода тайлов зависел успех всей задумки. Основная проблема было в этом долбаном сдвиге по горизонтали на пол знакоместа. Из-за него приходилось накладывать края тайлов друг на друга смешивая паперы и инки от разных тайлов. Надо было сделать это максимально быстро, параллельно переключению экранов и с постоянным временем. Ну вы в курсе.
В результате экспериментов мне пришлось познать дзен и абстрагироваться от формата тайлов в принципе и придумать как вообще я смогу совей головой сгенерировать самый быстрый вывод тайлика с наложением левого края на предыдущий тайл. В итоге я пришёл к странной связке pop af: or (hl) и подгонкой формата тайла под него. В итоге я занял последние две быстрые страницы под раскрянченый код вывода тайлов. В одной странице тайлы сдвинуты и накладываются ором. Фрагмент вывода выглядит так:
В другой странице не сдвинуты, но код такой же, за исключением адресов. Это нужно чтобы просто подменять страницу в зависимости от сдвига карты и оставаться с теми же тактами:
Я надеюсь вы что-нибудь поняли из моих сумбурных записок, потому что к этому моменту я уже устал писать и решил закруглиться :)
Если вдруг будут вопросы или нужны какие-то подробности то пишите в комментариях или в личку. Будет вообще здорово если вы предложите варианты ускорения или другие подходы, т.к. меня не покидает ощущение что я пошёл по странному пути и что-то упустил!
See ya \0/
Всем привет!
Сегодня я немного расскажу про то как устроен рендер в нашей совсем недавно вышедшей игре Ringo.
Если вдруг пропустили то посмотреть/поиграть можно здесь
К сожалению исходники настолько запутаны, уплотнены и пестрят ошибочными комментариями из прошлых итераций движка что вряд ли кому-то помогут разобраться. Да и сам я на данный момент уже плохо помню как там и что работает. Но так как уже несколько человек попросили меня рассказать про рендер то я попробую осветить хотя бы базовые моменты которые помогли достигнуть результата.
Режим 64х48
Самый лёгкий способ сделать 64х48 на спектруме это воспользоваться двумя экранами и сделать мультиколор 8х4. Пиксели двух экранов заполняются паттерном в виде столбиков, в моём случае это 11110000b, т.е. левая часть знакоместа ink правая papper. Далее мы переключаем экраны каждые 4 строки во время прохода луча в области пикселей чтобы атрибутами второго экрана менять цвета в нижней части знакоместа независимо от верхней.
Традиционно в моих рендерах всё происходит в два кадра. Сначала в буфер рисуются тайлы, затирая старую картинку, а потом сверху накладываются спрайты и буфер кидается на экран. Изначально я сделал буфер в манках ( munk сокращение от Multicolor Chunk). Этот термин придумал vivid и подробнее про них можно почитать здесь Если коротко то это честный буфер в точках которые потом парами схлапываются в нужный атрибут с помощью pop hl: ldi Это очень красиво и позволило легко анимировать тайлы делая картинку живой, да и скролл по горизонтали с шагом в нашу виртуальную точку 4х4 получался сам собой, но оказалось медленно для моих целей. Я уже смирился с мыслью что придется делать всё в три кадра, но результат мне совсем не понравился. Поэтому всё ушло ко второму варианту — сделать буфер в уплотненном виде, т.е. сразу в формате атрибутов.
Чтобы вырваться на свободу в двух фреймах я решил что нарисую уровень в две быстрые страницы, в одной из них он будет сдвинут и я просто буду копировать в рабочий буфер нужный сдвиг в зависимости от х координаты камеры и накладывать спрайты уже на атрибутный буфер. У меня получилось сделать это всё в два фрейма и осталось куча времени под игру. Я даже сделал физику главного героя и логику камеры, однако в какой-то момент стало ясно что картинка выглядит мёртвой т.к. задний фон статичен. К тому же чтобы его менять нужно потратить много времени, рисуя всё в два буфера фона. После непродолжительной внутренней борьбы было решено перейти к третьему варианту, который является комбинацией первых двух. А именно: перерисовывать весь буфер тайлами и накладывать поверх спрайты, но делать это всё в уплотненном(атрибутном) виде.
В общем виде вырисовывалась такая картина:
Первый кадр:
В верхнем бордюре происходит переброска рабочего буфера в атрибуты двух экранов. Так как времени в верхнем бордюре недостаточно чтобы перебросить все 768х2 байта то продолжаем дальше кидать уже по медленной памяти в нужный момент переключая экраны каждые 4 строки и учитывая задержки памяти. После того как мы закончим кидать атрибуты мы начинаем рисовать тайлы в рабочий буфер также параллельно переключая экраны каждые 4 строки в нужные моменты времени. Здесь уже легче так как рабочий буфер лежит в быстрой памяти. Как только мы проходим весь экран мы заканчиваем рисовать тайлы в буфер, а весь нижний бордер отдаём под игру, т.к. только тут у нас есть возможность использовать код с произвольным временем выполнения.
Второй кадр:
В верхнем бордюре мы рисуем оставшиеся тайлы которые не влезли в предыдущем кадре и выполняем разный служебный код с постоянным временем выполнения (патчинг рендера, камеры итп). Затем мы накладываем на рабочий буфер спрайты также параллельно переключая экран каждые 4 строки пока не дойдём до нижнего бордюра, который опять отдаём по игру.
Итак осталось самое сложное, нарисовать максимально быстро 63 тайла в атрибутный буфер и наложит на него спрайты.
Спрайты
В процессе формирования визуального стиля и понимания моих возможностей как художника, я пришел к размеру спрайтов 12х10 точек (на самом деле чистыми 11х10, т.к. каждый спрайт ещё дублируется в сдвинутом виде на одну точку). Спрайт приблизительно 8х8 плюс чёрная окантовка. Спрайт храниться в следующем виде: байт чистого инк+64, байт чистого папера+64… итак 120 байт. 0 считается прозрачным, а +64 нужно чтобы была возможность рисовать чёрным и делать обводку.
Так как спрайты будут накладываться параллельно с переключением экранов нужен код с постоянным временем выполнения. Также нужно было прийти к какой-то минимальной единице вывода чтобы всё легче «параллелить». Спуститься на максимально адовый уровень (сгенерировать код и всунуть в нужные моменты ауты) я не решился.
Пришлось прикидывать как это всё раскидать между линиями в блокноте, это больше походило на записки сумасшедшего. Я перепробовал разные размеры спрайтов и таких записей было куча.
В итоге такой единицей вывода у меня стала печать 2-х точек спрайта или 1-го обычного атрибута.
В виде макроса это выглядело так:
;маски перед началом рендера в BC
;ld b,00111000b
;ld c,00000111b
ld bc,56*256+7
MACRO SPRITE_LINE_FAST
pop de ;10t байт папера байт инка
xor a ;4
cp e ;4
ld a,(hl);7t берём с экрана
jr z,1f ;7\12
and b ;4t оставляем инк
or e ;4t накладываем папер из спрайта
jp 2f ;10t
1:
;13 tdelay
ld (00000),a ;13t
2:
;50t
;в аккумуляторе либо байт с экрана либо уже со вставленным папером из выше кода
ld e,a ; 4t сохраняем eго в е - уже ненужно е
xor a ;4t
cp d ;4t
ld a,e ;4t восстанавливаем
jr z,1f ;12/7
and c ;4
or d ;4
jp 2f ;10
1:
;13t delay
ld (00000),a ;13t
2:
ld (hl),a ;7t
;48t
ENDM
;98t(!!!!!!!!!!!!!!!!!!!!)
Спрайты выводятся в цикле, что очень сильно сэкономило памяти. Вывод одного спрайта выглядит так:
exx : out (c),e : exx
STEP62
exx : out (c),d : exx
STEP44
exx : out (c),e : exx
STEP26
exx : out (c),d : exx
STEP62
exx : out (c),e : exx
STEP44
exx : out (c),d : exx
STEP26
exx : out (c),e : exx
STEP62
exx : out (c),d : exx
STEP40
С учётом обвязки вышло что за одно переключение экранов мы успеваем положить 8 точек спрайта (4 атрибута). Так как спрайт шириной 12 точек (6 атрибутов), то нам надо шагать на строку вниз каждые 12 точек. В названии макроса зашифрована эта логика. Например STEP62 — положить 6 атрибутов, шагнуть вниз, положить ещё 2 и.т.д Конечно идеально бы паралеллились спрайты 8х8, но это уже слишком мелко, даже для меня. Для примера приведу один макрос, чтобы была понятна идея:
MACRO STEP62
;20t
SPRITE_LINE_FAST ;98t for 2 pixels
inc hl ;6t next screen pos
SPRITE_LINE_FAST ;98t for 2 pixels
inc hl ;6t next screen pos
SPRITE_LINE_FAST ;98t for 2 pixels
inc hl ;6t next screen pos
SPRITE_LINE_FAST ;98t for 2 pixels
inc hl ;6t next screen pos
SPRITE_LINE_FAST ;98t for 2 pixels
inc hl ;6t next screen pos
SPRITE_LINE_FAST ;98t for 2 pixels
;step down
; 10t 11t
ld de,attrBufferWidth-5 : add hl,de ; next line
SPRITE_LINE_FAST ;98t for 2 pixels
inc hl ;6t next screen pos
SPRITE_LINE_FAST ;98t for 2 pixels
inc hl;6t
;ok 867
;45t to 912
ld (0),bc
nop : nop : nop : nop
ld a,r
ENDM
Тайлы
От быстрого вывода тайлов зависел успех всей задумки. Основная проблема было в этом долбаном сдвиге по горизонтали на пол знакоместа. Из-за него приходилось накладывать края тайлов друг на друга смешивая паперы и инки от разных тайлов. Надо было сделать это максимально быстро, параллельно переключению экранов и с постоянным временем. Ну вы в курсе.
В результате экспериментов мне пришлось познать дзен и абстрагироваться от формата тайлов в принципе и придумать как вообще я смогу совей головой сгенерировать самый быстрый вывод тайлика с наложением левого края на предыдущий тайл. В итоге я пришёл к странной связке pop af: or (hl) и подгонкой формата тайла под него. В итоге я занял последние две быстрые страницы под раскрянченый код вывода тайлов. В одной странице тайлы сдвинуты и накладываются ором. Фрагмент вывода выглядит так:
pop af ; 10t papper in a
ld hl,ADDR ;10t
or (hl) ;7t
ld (hl),a ;7t
pop hl : ld (ADDR+1),hl ;26
pop hl : ld (ADDR+3),hl ;26
В другой странице не сдвинуты, но код такой же, за исключением адресов. Это нужно чтобы просто подменять страницу в зависимости от сдвига карты и оставаться с теми же тактами:
pop af ; 10t papper in a
ld hl,ADDR ;10t
or (hl) ;7t
ld (hl),a ;7t
pop hl : ld (ADDR),hl ;26
pop hl : ld (ADDR+2),hl ;26
Я надеюсь вы что-нибудь поняли из моих сумбурных записок, потому что к этому моменту я уже устал писать и решил закруглиться :)
Если вдруг будут вопросы или нужны какие-то подробности то пишите в комментариях или в личку. Будет вообще здорово если вы предложите варианты ускорения или другие подходы, т.к. меня не покидает ощущение что я пошёл по странному пути и что-то упустил!
See ya \0/
20 комментариев
О, уже 1 :)
В общем, Денис, спасибо! По существу пока сказать нечего, надо ещё раз перечитать)
Но я почти всё понял с первого раза! А это редко быавет)
Получается ты сначала пробовал накодить движок и уже потом прикидывал стиль игры, графику, геймплей?
Под какой размер монитора/тв расчитана игра?
Игра расчитана под любой размер монитора/тв, чем больше размер тем дальше отодвигаться надо )
Если коротко то формат спрайта меняется на байт маски+уплотненный байт спрайта, в конверторе генерируем маску зная где у нас прозрачный пиксел а где нет. вывод такой:
Итого 32т на две точки. По итогу сумасшедшее ускорение от моего первоначального варианта.
Я постараюсь всё это оформляю в отдельный опенсорсный движок-TSU, будет 8 спрайтов против 6 в Ringo, размер вырастет на две линии в высоту и плюс ещё 16 маленьких спрайтов 5х5. Также полностью доступны два нижних бордюра на код игры.