31 декабря 2017

    Скроллинг в Evo SDK
Hippiman/Conscience

   Итак, ребята,сегодня мы снова поговорим
о том, как  выжать  все  соки из Evo SDK и 
выйти  за  его ограничения. Я думаю, те из 
вас, кто пытался сделать какую-либо игру в 
этой  среде,  сталкивались  с  отсутствием 
функции скроллинга. Это достаточно большая 
проблема, т.к.делать скроллинг путем пере─ 
рисовки  кучи  тайлов  каждый  кадр - дело 
неблагодарное и очень тормозное. 
   Можно, конечно,делать так,как я в своей
игре "Nomad": резать большой фон на неско─ 
лько  маленьких полос и двигать их по оче─ 
реди,имитируя параллаксный эффект. Но этот 
метод  больше  похож  на "танцы с бубном", 
доставляет много головной боли при рисовке 
фонов, все равно отъедает много процессор─ 
ного  времени и не очень приятен глазу иг─ 
рока. 
   Остается  ещё  два  выхода:  забить  на
скроллинг и делать игру поэкранной, или же 
писать его на ассемблере, напрямую изменяя 
видеопамять.Вот этим мы сегодня и займёмся 
на  примере  вертикального скроллинга. А в 
конце  статьи  я  расскажу  про один очень 
неприятный подводный камень, и если его не 
обойти,то все труды по работе с видеопамя─ 
тью будут напрасны. 

            Организация памяти

   Evo SDK  выдаёт  картинку  в разрешении
320*200 в16 цветов  из  64 возможных. Это 
стандартный ATM'овский видео-режим со все─
ми вытекающими.
   Каждый из двух экранов занимает 2 стра─
ницы ОЗУ,причём каждая страница разбита на
две области:

  Страница #05 (#07):"нечётные"
   #2000...#ЗFЗF -
пары пикселей3,7,11...155,159(8000 байт) 
   #0000...#1FЗF -
пары пикселей1,5,9...153,157(8000 байт) 

  Страница #01 (#03):"чётные"
   #2000...#ЗFЗF -
пары пикселей2,6,10...154,158(8000 байт) 
   #0000...#1FЗF -
пары пикселей0,4,8...152,156(8000 байт) 

   Каждый  байт  содержит цвет двух пиксе─
лей, причем биты в нем перемешаны:

  6,2,1,0- цвет левого пикселя,
  7,5,4,3- цвет правого пикселя.

            Пример скроллинга

   Для  примера  разберем простейшую функ─
цию.Эта функция просто сдвигает весь экран
вниз на несколько строк (указывается в па─
раметре shift ). Сначала он определяет, в
каком  "экране" нужно сделать сдвиг, затем
вычисляет адрес начала сдвига и адрес,куда
нужно  записать данные, а затем при помощи
командыlddr производит пересылку данных в
фоновом экране.

void do_scrolldown(u8 shift) __naked
{
__asm
  push ix
  ld ix,#0
  add ix,sp
  ld a,(_SCREENACTIVE)
//ищем нужную страницу 
  bit 1,a
  jr z,secpaged
frstpaged:
  ld a,#0x1
  jpdopaged
secpaged:
  ld a,#0x3
dopaged:
  ld (_gl_page),a

//В переменной SCREENACTIVE SDK хранит 
//номер активного экрана. Код выше читает 
//эту переменную и устанавливает нужный 
//номер страницы в _gl_page, которая была 
//заранее объявлена в C коде. 

begd:
//page 
  ld a,(_gl_page)
  xor #0x7f
  ld bc,#Oxbff7
  ld (_MEMSLOT2),a
  out (c),a

//Этот код включает выбранную ранее 
//страницу во 2-е окно. _MEMSLOT2 - 
//единственное окно, которое мы можем 
//более-менее свободно использовать. 
//Остальные постоянно заняты. 

beg2d:
//--------------------------- 
//writeaddr 
//Вычислим адрес того места, куда будем 
//перемещать часть экрана. 
  ld c,#0x0
  ld b,4 (ix)
  dec b
//Получим количество строк, на которые 
//нужно сдвинуть экран. 
//4 (ix) - это 1-й параметр, 
//переданный в С функцию. 

  ld hl,#0x28
  ld de,#0x28
  jr z,zerd
muld:
  add hl,de
  djnz muld
zerd:
//Умножим кол-во строк на 40 байт 
//(сложением в цикле), получим сдвиг от 
//начала страницы. 

  ld b,h
  ld c,l
  ld de,(_gl_addr)
  add hl,de
  ld de,#0x1f40
  add hl,de
  push hl
//Получим реальный адрес, куда следует 
//копировать данные: начало сектора памяти 
//+ размер копируемой области 
//+ размер экрана. 

//Каждый байт хранит информацию о 
//двух пикселях 
//- получаем не 320 байт в ряду, а 160. 
//Каждый ряд разделен на 2 страницы 
//- получаем по 80 байт на страницу 
//каждая страница поделена ещё раз пополам 
//- получаем по 40 байт на область. 

//readaddr 
  ld hl,(_gl_addr)
  add hl,de
  push hl
//Рассчитаем адрес, откуда будем 
//копировать память: 
//начало сектора памяти + размер экрана. 

//чтение и запись 
  ld hl,#0x1f40
  sbc hl,bc
  ld b,h
  ld c,l
  pop hl
  pop de
  lddr
//Воспользуемся командой lddr для 
//перемещений большой области памяти. 
//Эта команда пересылает блоки снизу вверх 
//(в HL должен находиться адрес начала 
//пересылаемых данных, 
//в DE - адрес назначения пересылки, 
//в BC - размер пересылаемой области). 

//Далее следует несколько проверок на 
//номер страницы и адрес начала области. 
//Ничем не примечательный код, служит для 
//того, чтобы обработать все 4 экранных 
//отрезка памяти. 
  ld hl,(_gl_addr)//проверим адрес
  ld bc,#0x8000
  sbc hl,bc
  jr z,addrifd
  jp nxtd
addrifd:
  ld hl,#OxaOO0
  ld (_gl_addr),hl
  jp beg2d
nxtd:
  ld hl,#0x8000
  ld (_gl_addr),hl
  ld a,(_gl_page)
  ld b,#0x1
  sub b
  jr z,frst_chngd
  jp next_page_ifd
frst_chngd:
  ld a,#0x5
  ld (_gl_page),a
  jp begd
next_page_ifd:
  ld a,(_gl_page)
  ld b,#0x3
  sub b
  jr z,sec_chngd
  jp endd
sec_chngd:
  ld a,#0x7
  ld (_gl_page),a
  jp begd
endd:
  ld bc,#Oxbff7
  ld a,#0x71
  out (c),a
  pop ix
  ret
__endasm;
}

   Можно  заметить, что между двумя облас─
тями памяти #0000...#1FЗF и #2000...#ЗFЗF
есть немного свободного места(192 байта).
Этого места в аккурат хватает для хранения
4 строк. В данном примере это не нужно, но
для  зацикленного  скроллинга, в  качестве
буфера придётся очень кстати.

         Обман спрайтового движка

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

   Должно быть так:



   А получается так:



   Спрайты восстанавливают область экрана,
над которой движутся.
   Но и это решаемо.

   Как вообще  работает спрайтовый движок?
Помимо  основных  двух  экранов, каждый из
которых занимает по2 страницы, в SDK есть
ещё  специальный  буфер фона для спрайтов,
который занимает4 страницы.
   Этот буфер нужен для того,чтобы спрайты
при  перемещении  восстанавливали  фон под
предыдущим своим местоположением.
   При каждом обновлении экрана вызывается
функцияupdateTilesToBuffer , которая све─
ряется  с картой обновления тайлов и копи─
рует  нужные  тайлы в буфер восстановления
спрайтов.
   А карта обновления  тайлов, в свою оче─
редь,изменяется каждый раз при перерисовке
какого-либо тайла.
   Эта  схема  с  одной  стороны  выглядит
запутанной, а с другой стороны - позволяет
достичь  максимального  быстродействия  за
счёт  расхода  лишней  памяти  (которой на
ATM/ZX Evo предостаточно). 
   Первое, что  приходит  в  голову: нужно
как-то дать SDK знать,что тайлы скроллиру─
емой  области обновились и их нужно скопи─
ровать в буфер. Чуть подумав,понимаешь,что
не обязательно обновлять всю область. Ведь
необходимо  обновить только те тайлы в из─
менённой области, над которыми есть спрай─
ты, это  позволит  повысить быстродействие
программы.
   Итак,у нас есть задача:каким-то образом
заставить  движок обновить тайлы в буфере.
В исходниках  SDK есть замечательная функ─
ция -setTileUpdateMapF , которая устанав─
ливает флаг обновления тайла, а координаты
берёт из регистровой парыBC . Это то, что
нам и нужно. Однако эта функция недоступна
из  C'шного  кода. Но  никто не мешает нам
вызывать  её  по  адресу, ведь  в конечной
программе  код  движка  всегда находится в
одном и том же месте.
   Адрес вхождения в функцию можно опреде─
лить, банально продебажив конечный код, но
мы  ведь  не извращенцы, тем более что так
нужно будет делать каждый раз при внесении
изменений в код SDK. Мы пойдем другим, бо─
лее простым путем.

   Открываем директорию с исходниками SDK.
Находим  файл lib_startup.asm. Листаем в 
самый конец и находим код,который отвечает 
за экспорт функций. Дописываем туда на но─ 
вой строчке вот этот код: 
       export setTileUpdateMapF
   Теперь делаем
toolssjasmplus
            sjasmplus.exe lib_startup.asm
   На   выходе   получаем startup.bin  и
lib_startup.exp . Второй нам  и нужен. От─
крываем  его в текстовом редакторе и нахо─ 
дим 
    setTileUpdateMapF: EQU 0x0000E644
(у вас адрес может быть другим). 
  0x0000E644 есть адрес вхождения в нашу
функцию. 
   Далее  знатоки асма могут написать свою
обвязку вызова этой функции из C кода, для 
остальных я приведу пример такой обвязки. 

void setTileUpdateMap(u8 x,u8 y) __naked
{
__asm
  push ix
  ld ix,#0
  add ix,sp
  ld a,#0x1

  ld c,4 (ix)
  ld b,5 (ix)
  call (#OxeбЧ4)
  inc b
  call (#OxeбЧ4)
  inc b
  call (#OxeбЧ4)
  inc c
  call (#OxeбЧ4)
  inc c
  call (#OxeбЧ4)
  dec b
  call (#OxeбЧ4)
  dec b
  call (#OxeбЧ4)
  dec c
  call (#OxeбЧ4)
  inc b
  call (#OxeбЧ4)
  pop ix
  ret
__endasm;
}

  #OxeбЧ4 подмените на свой адрес.

   Спрайт в Evo SDK имеет размер16*16 px,
т.е. в лучшем случае  нужно будет обновить
4 тайла. Но  практически  всегда обновлять
придется 9 тайлов  вокруг центра спрайта.
Эта функция как раз так и делает: на входе
получает координаты спрайта (левый верхний
угол) и обновляет 9 тайлов  вниз и вправо
от него.
   Теперь  каждый  второй кадр нужно вызы─
вать  эту  функцию, и  будет вам счастье -
спрайты не будут портить фон. "Третьей ко─
смической скорости" при сдвиге всего экра─
на  у вас  всё равно не получится, спрайты
тормозят  рендер  более чем в два раза, но
простор  для творчества и задел на будущее
у вас теперь есть.

  P.S.:в приложении к журналу вы найдёте
проект mem_test, а в нем несколько функций 
вертикального скроллинга,включая зациклен─ 
ный скроллинг всего экрана вверх и вниз, а 
также  зацикленный скроллинг определённого 
сектора экрана. 



Other articles:


Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Similar articles:
Vedem - Description, prohodilka games: Dizzy 5: Spelbound Dizzy.
Thoughts readers - Another plan to save the Spectrum.

В этот день...   23 November