Как написать игру на ассемблере для ZX Spectrum/Глава 07
- ГЛАВА СЕДЬМАЯ,
- в которой вы научитесь создавать все элементы игрового пространства
Как вы уже знаете, игровое пространство составляют перемещающиеся спрайты, которые появляются и исчезают на экране во время игры, и неподвижный или медленно перемещающийся пейзаж. В предыдущих главах мы частично показали, каким образом можно создавать спрайты, используя привычные символы UDG и средства ассемблера. Однако такой способ пригоден лишь для небольших изображений, да и то, если их общая площадь не превышает двух десятков знакомест. Теперь пора нам подробно познакомиться с общим случаем, когда размеры спрайтов могут быть практически любыми, а их количество ограничено лишь сюжетом игры, вашей фантазией и трудолюбием. Есть и другая, не менее важная задача — создание таких процедур вывода спрайтов на экран и рисования пейзажей, которые бы требовали на это минимального времени и, желательно, были не слишком громоздкими.
БЫСТРЫЙ ВЫВОД СПРАЙТОВ
Что бы ни появлялось на экране во время игры — спрайты или какие-либо тексты — каждое изображение состоит из отдельных символов. Таким образом, чтобы быстро выводить сложные картинки, нужно начать с самого простого — печати произвольного символа в текущую позицию экрана. Раньше мы поручали эту задачу процедуре RST 16, которая неплохо справлялась со своими обязанностями до тех пор, пока отдельные кадры изображения не слишком быстро сменяли друг друга. Безусловно, ее и дальше вполне можно использовать в подобных ситуациях. Однако, когда речь заходит о создании динамических картинок, а именно такие мы чаще всего наблюдаем после загрузки наиболее интересных игровых программ, она уже перестает нас удовлетворять. Изображения начинают временами пропадать и, конечно же, теряется естественное восприятие событий.
Во 2-й главе мы приводили пример небольшой программки на Бейсике, которая печатала букву A, и сказали, что по такому принципу работает любая процедура вывода символов на экран. Теперь перепишем ее на ассемблере и как основу используем для составления подпрограмм вывода спрайтов. До того, как этот фрагмент появится в программе, необходимо в регистровой паре DE задать адрес символа в наборе, в HL — рассчитанный начальный адрес знакоместа:
......... LD B,8 MET1 LD A,(DE) LD (HL),A INC DE INC H DJNZ MET1 .........
Ниже приводится процедура PRSYM, которая, так же как и RST 16, выводит на экран отдельные символы в текущее знакоместо экрана с учетом заданных атрибутов, но работает она приблизительно в 10 раз быстрее. Конечно же даром ничего не дается и увеличение быстродействия достигается за счет урезания выполняемых ею функций. Например, с ее помощью невозможно выводить тексты на принтер, печатать символы UDG и псевдографики, а также ключевые слова Бейсика. Не «воспринимает» она и управляющие коды. Тем не менее, PRSYM в тех или иных модификациях используется дальше в нескольких программах. Например, в одной из них показывается, как рисовать на экране лабиринты, различные орнаменты или рамки произвольной конфигурации.
Описание процедуры начнем с команды BIT, которая здесь встречается впервые и выполняет проверку состояния отдельных битов. Значение бита отражает флаг нуля — если бит установлен, выполняется условие NZ, в противном случае — Z (т. е. если проверяемый бит равен нулю, то Z=1). Команда имеет формат BIT n, s, где n — номер бита, задаваемый числом от 0 (младший) до 7 (старший), а s — операнд, которым может быть один из регистров общего назначения или (HL), (IX+d) и (IY+d). Попутно стоит сказать о еще двух командах этой группы: SET n, s — установка бита с номером n и RES n, s — сброс бита. В первом случае в бит n записывается единица, во втором — нуль. В этих командах операнды и формат такие же, как и в команде BIT, но флаги остаются без изменений.
PRSYM PUSH BC PUSH DE PUSH HL LD L,A ;по коду символа вычисляем его адрес LD H,0 ; в текущем наборе ADD HL,HL ADD HL,HL ADD HL,HL LD DE,(23606) ADD HL,DE LD DE,(23684) ;адрес текущей позиции печати ; в видеобуфере EX DE,HL PUSH HL LD B,8 PRS1 LD A,(DE) BIT 3,(IY+87) ;режим INVERSE 1 JR Z,PRS2 CPL ;если включен, инвертируем байт PRS2 BIT 1,(IY+87) ;режим OVER 1 JR Z,PRS3 XOR (HL) ;если включен, объединяем ; с изображением на экране PRS3 LD (HL),A INC DE INC H DJNZ PRS1 POP HL PUSH HL LD A,H ;вычисляем адрес в области атрибутов AND #18 RRCA RRCA RRCA ADD A,#58 LD H,A LD A,(23695) LD (HL),A ;записываем байт атрибутов в видеобуфер POP HL INC L ;переходим к следующей позиции печати JR NZ,PRS4 ;если 0, то это означает, что позиция ; печати перешла в следующую треть экрана LD A,H ;в этом случае увеличиваем ADD A,8 ; старший байт адреса на 8 LD H,A CP #58 JR C,PRS4 LD H,#40 ;если выход из последней трети, то ; возвращаемся в начало видеобуфера PRS4 LD (23684),HL POP HL POP DE POP BC RET
Используем сначала эту процедуру для вывода буквенной или цифровой информации. Конечно, можно представить себе ситуацию, когда печатается всего один символ, например, уровень игры, но все же значительно чаще приходится иметь дело с текстовыми строками или даже целыми страницами и тут процедуры PRSYM явно недостаточно. Во-первых, необходимо уметь позиционировать курсор подобно тому, как это выполняет AT в Бейсике, во-вторых, желательно иметь возможность в любой момент переводить курсор на следующую строку, а в-третьих, — вспомним еще несколько атрибутов печати, которые применяются в команде RST 16: INK, PAPER, BRIGHT, FLASH, INVERSE и OVER. Для того чтобы вывод текстов на экран не вызывал особых проблем, необходима процедура, хорошо «понимающая» все управляющие коды, используемые при печати, и настраивающая в соответствии с ними системные переменные, используемые подпрограммой PRSYM. Ниже приводится текст такой процедуры, которую мы назвали WRITE, с краткими комментариями к отдельным группам строк.
Прежде чем привести ее текст, объясним смысл вновь встретившейся здесь команды. Это инструкция EX (SP),HL, которая обменивает содержимое регистровой пары HL со значением, находящимся на вершине машинного стека: то, что было в HL, помещается на вершину стека, а два байта со стека перемещаются в HL. Значение регистра SP при этом не изменяется. Обратите внимание на то, как в подпрограмме WRITE2 осуществляется переход по выбранному адресу: число заносится в стек командой EX (SP),HL, а затем выполняется команда RET.
Другой момент, требующий пояснения, это процедура, расположенная в ПЗУ по адресу 8, благодаря чему ее можно вызывать командой RST. Она используется интерпретатором для выдачи различных сообщений и с ее помощью можно в любой момент остановить практически любую программу в кодах при возникновении критической ошибки. Код сообщения, уменьшенный на единицу (например, −1 для 0 OK), записывается в директиве DEFB непосредственно за командой RST 8.
WRITE LD A,(HL) ;берем очередной символ из строки INC HL AND A RET Z ;вывод символов до кода 0 CP " " JR C,WRITE2 ;если управляющий код (< 32) WRITE1 CALL PRSYM JR WRITE ; Выбор из таблицы адреса перехода в зависимости от кода WRITE2 PUSH HL PUSH BC LD HL,TABLE ;адрес таблицы LD C,A ;сохраняем код в C WRITE3 LD A,(HL) ;читаем код из таблицы INC HL AND A JR Z,WRITE5 ;если встречен маркер конца таблицы CP C JR Z,WRITE4 ;если код найден INC HL ;пропускаем 2 байта адреса INC HL JR WRITE3 ;проверяем следующий код WRITE4 POP BC LD A,(HL) ;берем из таблицы адрес перехода INC HL LD H,(HL) ;помещаем его в HL LD L,A EX (SP),HL ;восстанавливаем HL, а адрес записываем ; на вершину стека RET ;переходим по адресу с вершины стека WRITE5 POP BC ;если код отсутствует в таблице, POP HL ; то восстанавливаем регистры LD A,"?" ; и печатаем символ ? JR WRITE1 ; Перевод строки PR_13 PUSH HL LD HL,(23684) ;адрес текущей позиции печати LD A,L AND %11100000 ;возвращаемся к началу строки ADD A,#20 ;переходим к следующей строке LD L,A JR NZ,PR13_1 ;если не перешли в следующую треть LD A,H ADD A,8 ;старший байт адреса увеличиваем на 8 LD H,A CP #58 ;проверяем, был ли выход за пределы экрана JR C,PR13_1 LD H,#40 ;переводим позицию печати в верхнюю ; строку экрана PR13_1 LD (23684),HL ;возвращаем рассчитанный адрес ; позиции печати POP HL JR WRITE ; Цвет «чернил» INK PR_16 LD A,(HL) ;читаем следующий байт из строки AND %111 ;выделяем 3 младших бита (значения 0...7) PUSH BC LD B,%11111000 ;в регистре B - маска битов для INK PR16_1 LD C,A LD A,(IY+85) ;23695 AND B ;освобождаем биты предыдущего атрибута OR C ; и записываем на их место новое значение LD (IY+85),A ;возвращаем байт атрибутов PR16_2 POP BC INC HL JR WRITE ; Цвет «бумаги» PAPER PR_17 LD A,(HL) ;с цветом PAPER аналогично INK AND %111 RLCA ; только предварительно сдвигаем RLCA ; биты на свое место RLCA PUSH BC LD B,%11000111 JR PR16_1 ; Мерцание FLASH PR_18 LD A,(HL) AND 1 PUSH BC LD B,%01111111 PR18_1 RRCA JR PR16_1 ; Яркость BRIGHT PR_19 LD A,(HL) AND 1 PUSH BC LD B,%10111111 RRCA JR PR18_1 ; Инверсия INVERSE (3-й бит в системной переменной P_FLAG) PR_20 LD A,(HL) AND 1 PUSH BC LD B,%11110111 RLCA RLCA PR20_1 RLCA LD C,A LD A,(IY+87) AND B OR C LD (IY+87),A JR PR16_2 ; Режим наложения OVER (1-й бит в системной переменной P_FLAG) PR_21 LD A,(HL) AND 1 PUSH BC LD B,%11111101 JR PR20_1 ; Позиционирование курсора AT PR_22 LD A,(HL) ;берем номер строки CP 24 ;если больше 24, то выход позиции JR NC,OUTSCR ; печати за пределы экрана INC HL PUSH DE PUSH HL CALL 3742 ;вычисляем адрес начала строки POP DE LD A,(DE) ;берем номер столбца CP 32 ;если больше 32, то выход позиции JR NC,OUTSCR ; печати за пределы экрана INC DE ADD A,L LD L,A LD (23684),HL ;запоминаем адрес новой позиции POP HL EX DE,HL JP WRITE OUTSCR RST 8 ;сообщение Бейсика DEFB 4 ; «Out of screen» PRSYM ......... ; Таблица переходов на процедуры для управляющих кодов TABLE DEFB 13 DEFW PR_13 DEFB 16 DEFW PR_16 DEFB 17 DEFW PR_17 DEFB 18 DEFW PR_18 DEFB 19 DEFW PR_19 DEFB 20 DEFW PR_20 DEFB 21 DEFW PR_21 DEFB 22 DEFW PR_22 DEFB 0
Покончив с текстами, перейдем к описанию программы, которая выводит на экран спрайт произвольной конфигурации, но сначала стоит сказать несколько слов о том, что собой представляют спрайты с точки зрения программиста. Мы уже описывали работу со спрайтами и вам известно, что в принципе — это блоки данных, организованные определенным образом. Надо сказать, что существует множество различных форматов спрайтов. Например, формат спрайтов, принятый в Laser Basic отличается от того, который используется в Beta Basic, а тот, который хотим предложить мы, в свою очередь, не похож ни на первый, ни на второй, и все они отличаются от того, который мы продемонстрировали в предыдущих главах. Главным критерием в выборе формата блока данных является способ вывода спрайта на экран. Представляемый нами способ, быть может, не самый оптимальный в плане быстродействия, но зато программа имеет минимальные размеры при максимальном количестве возможностей. Так, например, спрайт может частично или даже полностью выходить за пределы экрана, а вывод может быть осуществлен по любому известному принципу объединения изображений (то есть с замещением либо по AND, OR или XOR). Для упрощения программы спрайт будет выводиться по символам, как и в описанной ранее процедуре PUT.
Начнем с разработки формата спрайтов, который зависит от способа вывода графики, а затем, привязываясь к формату, напишем соответствующую процедуру вывода спрайтов на экран (строго говоря, разработка формата спрайтов и процедуры их вывода должны протекать параллельно, так как одно от другого неотделимо).
Блок данных, описывающий каждый спрайт, будет состоять из двух частей: заголовка, включающего в себя относительные координаты и атрибуты для каждого знакоместа (эта часть будет напоминать формат спрайтов для процедуры PUT), и данных о состоянии пикселей (по 8 байт на знакоместо — как в символьном наборе). Заголовок будет начинаться указанием общей площади спрайта, или иначе — количества знакомест, составляющих спрайт. Для этого достаточно одного байта, что позволит создавать спрайты площадью до 255 знакомест. Затем для описания каждого символа потребуется по 3 байта, как и в процедуре PUT:
- 1-й байт — относительная вертикальная координата данного знакоместа в спрайте;
- 2-й байт — относительная горизонтальная координата данного знакоместа в спрайте;
- 3-й байт — суммарные атрибуты знакоместа.
Таким образом, можно составить примерно такой заголовок:
SPRITE DEFB 7 DEFB 0,2,15 DEFB 1,0,6, 1,1,6, 1,2,6, 1,3,6 DEFB 2,1,6, 2,2,6
Вторую часть блока данных составляют уже знакомые вам описания пикселей знакомест (по 8 байт на каждое), причем они должны быть перечислены в том порядке, в котором указаны в заголовке, например (всего должно быть 7 строк — по количеству символов, входящих в спрайт):
DEFB 33,39,62,255,0,127,127,127 DEFB 246,73,146,255,0,11,222,251 ................................ DEFB 35,216,225,228,3,16,148,35
Теперь разберемся с числовыми параметрами, необходимыми для вывода спрайта. Перед обращением к процедуре вывода, которую мы назвали PTBL, в регистре B нужно задать верхнюю границу описывающего прямоугольника (ROW), в C — левую границу описывающего прямоугольника (COL), в HL — адрес блока данных спрайта (метка SPRITE), а в аккумуляторе — код команды, определяющей способ вывода спрайтов, который в самом начале работы процедуры будет вставлен в основной цикл вывода и тем самым вместо составления четырех похожих друг на друга подпрограмм можно пользоваться одной. Для обычного вывода с замещением предыдущего изображения нужно задать команду NOP — отсутствие операции, которая кодируется байтом 0; для осуществления вывода по принципу OR, AND или XOR необходимо вставить в процедуру команды OR (HL), AND (HL) или XOR (HL), имеющие коды #B6, #A6 и #AE соответственно. Чтобы не держать в голове все эти коды, имеет смысл определить их как константы с помощью директивы EQU и присвоить им удобочитаемые имена:
- SPRPUT — вывод с уничтожением предыдущего изображения;
- SPROR — вывод по принципу OR;
- SPRAND — вывод по принципу AND;
- SPRXOR — вывод по принципу XOR.
В данном примере вновь применена команда EX (SP),HL, но здесь вершину стека можно рассматривать в качестве временной переменной, и такой прием может быть рассмотрен как один из способов борьбы с нехваткой регистров.
PTBL SPRPUT EQU 0 ;код команды NOP SPROR EQU #B6 ;код команды OR (HL) SPRAND EQU #A6 ;код команды AND (HL) SPRXOR EQU #AE ;код команды XOR (HL) PUSH HL LD (MODE),A ;устанавливаем способ объединения ; изображений LD A,(HL) ;количество знакомест в спрайте INC HL PUSH HL ;умножаем на 3 (результат в HL) LD L,A LD H,0 LD E,L LD D,H ADD HL,HL ADD HL,DE POP DE ADD HL,DE ;начало данных, описывающих пиксели EX DE,HL PTBL1 PUSH AF PUSH BC LD A,(HL) ;вертикальная координата в спрайте INC HL PUSH HL ADD A,B CP 24 JR NC,PTBL4 ;если знакоместо выходит за пределы экрана PUSH DE CALL 3742 ;получаем адрес строки экрана POP DE EX (SP),HL LD A,(HL) ;горизонтальная координата в спрайте EX (SP),HL ADD A,C CP 32 JR NC,PTBL4 ;если знакоместо выходит за пределы экрана ADD A,L LD L,A LD B,8 PUSH HL ;сохраняем адрес экрана PTBL2 LD A,(DE) MODE NOP LD (HL),A INC DE INC H DJNZ PTBL2 POP BC ;восстанавливаем адрес экрана в BC LD A,B ;определяем адрес атрибутов AND #18 SRA A SRA A SRA A ADD A,#58 LD B,A POP HL ;восстанавливаем адрес данных INC HL LD A,(HL) ;переносим байт атрибутов в видеобуфер DEC HL LD (BC),A PTBL3 POP BC POP AF INC HL INC HL DEC A JR NZ,PTBL1 POP HL RET PTBL4 LD HL,8 ADD HL,DE EX DE,HL POP HL JR PTBL3
Рассмотрим пример вывода спрайта произвольной конфигурации (рис. 7.1). Не правда ли, многие узнали в нем одного из персонажей игры FIST. Все дело в том, что этот спрайт как нельзя лучше демонстрирует эффективность предложенной нами процедуры PTBL, поскольку его форма заметно отличается от прямоугольной. Управляющая часть программы получилась довольно короткой:
ORG 60000 ENT $ LD A,48 ;INK 0: PAPER 6 LD (23693),A XOR A ;BORDER 0 CALL 8859 CALL 3435 LD A,2 CALL 5633 LD B,10 ;ROW LD C,15 ;COL LD A,SPRPUT ;вывод с уничтожением предыдущего ; изображения LD HL,SPR1 ;чтение адреса спрайта SPR1 CALL PTBL RET PTBL ......... ; Заголовок спрайта SPR1 DEFB 22 DEFB 0,3,48, 0,4,48, 1,3,48, 1,4,48, 2,2,48 DEFB 2,3,48, 2,4,48, 2,5,48, 2,6,48, 3,2,48 DEFB 3,3,48, 3,4,48, 4,0,48, 4,1,48, 4,2,48 DEFB 4,3,48, 4,4,48, 5,0,48, 5,1,48, 5,2,48 DEFB 5,3,48, 5,4,48 ; Данные спрайта DEFB 0,0,0,0,3,12,209,46 DEFB 0,0,0,0,192,32,224,240 DEFB 249,34,38,43,43,48,35,248 DEFB 16,112,56,8,16,48,16,240 DEFB 3,6,31,61,61,59,59,27 DEFB 254,255,255,255,255,255,255,255 DEFB 64,224,255,255,255,255,255,255 DEFB 0,0,255,192,224,243,252,240 DEFB 0,248,4,2,2,250,50,28 DEFB 31,7,5,6,7,15,15,31 DEFB 255,255,254,229,3,247,247,235 DEFB 240,0,0,0,252,254,254,254 DEFB 0,0,0,0,0,62,103,77 DEFB 0,0,0,0,0,0,249,255 DEFB 31,31,63,63,127,255,255,255 DEFB 235,219,219,219,252,224,193,131 DEFB 254,254,254,126,254,252,252,248 DEFB 65,67,71,69,34,30,0,0 DEFB 255,255,255,255,63,7,0,0 DEFB 255,254,252,248,240,224,0,0 DEFB 3,3,1,2,2,1,0,0 DEFB 248,248,152,198,1,255,0,0
Есть смысл немного прокомментировать приведенные выше числовые данные, которые полностью соответствуют описанному выше формату. В заголовке перечислены (например, для первой строки):
- 22 — общее количество знакомест в спрайте,
- 0 — координата Y первого выводимого на экран знакоместа, взятая относительно левого верхнего угла описывающего прямоугольника,
- 3 — координата X того же знакоместа,
- 48 — суммарные атрибуты знакоместа: PAPER 6, INK 0.
Далее идут тройки чисел, относящиеся к другим знакоместам спрайта и в последовательности, указанной выше: координата Y, координата X, суммарные атрибуты.
СПРАЙТ-ГЕНЕРАТОР
Можно по разному создавать блоки данных для спрайтов, начиная с самого простого способа, когда изображение сначала рисуется на бумаге, а затем выписываются его коды байт за байтом. Можно воспользоваться приведенной нами в четвертой главе программой, для которой сначала создается фонт (например, в Art Studio), соответствующий одному или сразу нескольким спрайтам, после чего коды все равно требуется записать и только затем уж вводить в программу. Оба варианта требуют затрат большого труда и времени и оправдывают себя лишь в случаях небольших спрайтов (порядка 1 — 6 знакомест). Учитывая все это, мы сочли необходимым предложить программу, которая полностью исключает какие-либо записи, а формируемые ею кодовые блоки можно сразу встраивать в создаваемые вами игры.
Программа состоит из двух частей — бейсиковской и кодовой. Если вы работаете с магнитофоном, то программу на Бейсике можно исполнять сразу, если же с дисководом, то три строки текста следует заменить (какие именно, сказано ниже). Затем введите и оттранслируйте ассемблерную часть, создав соответствующий кодовый файл. Теперь можно работать со спрайтами. После ввода и старта программы на вашем экране появится меню. Если вы предварительно просмотрите текст программы, то легко обнаружите, какие функции она может выполнять, тем не менее коротко прокомментируем эти опции.
- Load Screen — загрузка экранного файла
- Create Sprite — создание спрайта
- Save Sprite — сохранение спрайт-файла
- New Sprite — удаление спрайтов из памяти для начала создания нового спрайт-файла
- View Table — просмотр таблицы смещений спрайтов в спрайт-файле
- Quit Program — выход из программы
Нажимая клавиши Q и A, можно перемещать курсор в виде инвертированной полоски вверх или вниз по строчкам меню. Отметив курсором нужный пункт, нажмите клавишу M для выполнения функции.
Прежде всего необходимо загрузить экранный файл, для чего предназначен первый пункт меню Load Screen. Внизу экрана появится запрос Screen name:, на который нужно ввести имя загружаемой картинки со спрайтами. После загрузки экранного файла программа снова выйдет в меню.
После этого можно «вырезать» с картинки спрайты, выбрав следующий пункт Create Sprite. Окно с меню исчезнет с экрана и останется только загруженная картинка и маленький пунктирный квадратик. С помощью клавиш Q, A, O и P поместите его в верхний левый угол выбранного спрайта и нажмите клавишу M, чтобы зафиксировать местоположение квадратика на экране. Затем, управляя теми же клавишами, расширьте его до нужных размеров, чтобы спрайт полностью поместился внутри отмеченной пунктиром области и еще раз нажмите клавишу M. Возврат в меню покажет, что спрайт успешно закодирован — можно создавать следующий. Если создано уже достаточно много спрайтов и все они имеют значительные размеры, то памяти может не хватить. В этом случае программа выдаст сообщение Out of memory! Вы можете сохранить полученный спрайт-файл, вызвав опцию Save Sprite, и начать создание следующего, предварительно очистив память, выбрав пункт New Sprite.
Перед сохранением спрайт-файла нужно будет ввести его имя, под которым он будет записан на внешний носитель, а перед удалением спрайтов из памяти потребуется подтвердить свое намерение, нажав клавишу Y.
Последняя опция Quit Program в особых комментариях не нуждается, поэтому скажем только, что во избежание случайного выхода (а следовательно, и потери данных) нужно будет также подтвердить или опровергнуть выбор.
Сообщим еще общие «эксплуатационные» характеристики спрайт-генератора: каждый вновь создаваемый спрайт может занимать площадь до 255 знакомест, если вам захочется чуть больше — описывающий прямоугольник все равно не позволит, сколько бы ни старались. Максимальное количество спрайтов для одного спрайт-файла — 22, после чего его необходимо сохранить. И последнее, создаются спрайты только прямоугольной формы.
Для того чтобы вам легче было разобраться в этой сервисной программе, приведем расшифровку используемых обозначений, а также дадим краткое описание ее основной части — функции создания спрайта:
Массивы:
- m$(6,13) — наименования опций меню
- s(22) — смещения спрайтов относительно начала спрайт-файла
Переменные:
- spr — количество созданных спрайтов
- addr — адрес следующего спрайта
- col, row — координаты окон и спрайтов (переменная row используется также для определения позиции курсора меню)
- len, hgt — размеры окон и спрайтов
- pap — цвет PAPER окон
- k$ — символ нажатой клавиши
Константы:
- scr — адрес «теневого» экрана
- ad0 — адрес начала спрайт-файла
- ramka, svscr, restor, clsv, setv, * gtbl — адреса одноименных процедур
Опишем «центральную» подпрограмму ГЕНЕРАТОРА СПРАЙТОВ — подпрограмму создания спрайтов:
- 2010 — если создано 22 спрайта, сообщение о том, что спрайт-файл завершен. Необходимо его сохранить и начать новый.
- 2020 — вывод экранной картинки.
- 2030 — определение начальных значений переменных «вырезаемого» спрайта.
- 2040 — вывод по заданному размеру и в заданном месте экрана пунктирной рамки, отмечающей будущий спрайт.
- 2045..2090 — установка рамки в верхний левый угол «вырезаемого» спрайта.
- 2100 — удаление рамки и звуковой сигнал после нажатия клавиши M.
- 2110 — вывод рамки.
- 2130..2170 — выбор желаемого размера спрайта.
- 2200 — кодирование спрайта.
- 2210 — если процедура gtbl возвращает ненулевое значение, то рассчитывается величина смещения спрайта от начала спрайт-файла, а переменная addr указывает на конец спрайт-файла.
- 2220..2240 — выдается сообщение о нехватке памяти для создания спрайта заданных размеров. Можно сохранить спрайт-файл и начать новый или попытаться создать спрайт меньших размеров.
10 POKE 23693,40: BORDER 5: CLS 20 DIM m$(6,13): FOR n=1 TO 6: READ m$(n): NEXT n 30 DIM s(22): LET spr=0 40 LET scr=30000: LET ad0=36912: LET addr=ad0 50 LET ramka=65000: LET svscr=65003: LET restor=65006: LET clsv=65009: LET setv=65012: LET gtbl=65015 60 RANDOMIZE USR svscr 100 REM --- МЕНЮ --- 110 RANDOMIZE USR restor: LET row=6: LET col=8: LET len=15: LET hgt=13: LET pap=7: GO SUB 8000 120 LET row=0 130 IF row<0 THEN LET row=5 135 IF row>5 THEN LET row=0 140 FOR n=0 TO 5: PRINT PAPER 7; BRIGHT 1;AT 7+n*2,9;m$(n+1): NEXT n 150 PRINT INVERSE 1; BRIGHT 1;AT 7+row*2,9;m$(row+1) 160 PAUSE 0: LET k$=INKEY$ 170 IF k$="a" OR k$="A" THEN BEEP .01,20: LET row=row+1: GO TO 130 180 IF k$="q" OR k$="Q" THEN BEEP .01,20: LET row=row-1: GO TO 130 190 IF k$<>"m" AND k$<>"M" THEN GO TO 150 200 BEEP .01,20: GO SUB (row+1)*1000 210 GO TO 100 1000 REM --- ЗАГРУЗКА ЭКРАННОЙ КАРТИНКИ --- 1010 INPUT "Screen name: "; LINE n$ 1020 LOAD n$CODE 16384,6912 1030 RANDOMIZE USR svscr: RETURN 2000 REM --- СОЗДАНИЕ СПРАЙТОВ --- 2010 IF spr=22 THEN LET row=11: LET col=4: LET len=23: LET hgt=3: LET pap=3: GO SUB 8000: PRINT AT row+1,col+1; PAPER pap; BRIGHT 1;"Sprite-file complete!": BEEP 1,-20: PAUSE 0: RETURN 2020 RANDOMIZE USR restor 2030 LET spr=spr+1: LET row=12: LET col=15: LET len=1: LET hgt=1: POKE 23303,len: POKE 23304,hgt 2040 POKE 23301,col: POKE 23302,row: RANDOMIZE USR ramka 2045 PAUSE 0: LET k$=INKEY$ 2050 IF (k$="q" OR k$="Q") AND row>0 THEN RANDOMIZE USR ramka: LET row=row-1: GO TO 2040 2060 IF (k$="a" OR k$="A") AND row<24-hgt THEN RANDOMIZE USR ramka: LET row=row+1: GO TO 2040 2070 IF (k$="o" OR k$="O") AND col>0 THEN RANDOMIZE USR ramka: LET col=col-1: GO TO 2040 2080 IF (k$="p" OR k$="P") AND col<32-len THEN RANDOMIZE USR ramka: LET col=col+1: GO TO 2040 2090 IF k$<>"m" AND k$<>"M" THEN GO TO 2045 2100 RANDOMIZE USR ramka: BEEP .01,20 2110 POKE 23303,len: POKE 23304,hgt: RANDOMIZE USR ramka 2120 PAUSE 0: LET k$=INKEY$ 2130 IF (k$="a" OR k$="A") AND hgt<24-row AND len*(hgt+1)<256 THEN RANDOMIZE USR ramka: LET hgt=hgt+1: GO TO 2110 2140 IF (k$="q" OR k$="Q") AND hgt>1 THEN RANDOMIZE USR ramka: LET hgt=hgt-1: GO TO 2110 2150 IF (k$="o" OR k$="O") AND len>1 THEN RANDOMIZE USR ramka: LET len=len-1: GO TO 2110 2160 IF (k$="p" OR k$="P") AND len<32-col AND (len+1)*hgt<256 THEN RANDOMIZE USR ramka: LET len=len+1: GO TO 2110 2170 IF k$<>"m" AND k$<>"M" THEN GO TO 2120 2200 BEEP .01,20: POKE 23300,hgt*len: RANDOMIZE addr: LET ad=USR gtbl 2210 IF ad THEN LET s(spr)=addr-ad0: LET addr=ad: RETURN 2220 LET col=6: LET row=11: LET hgt=3: LET len=20: LET pap=2 2230 GO SUB 8000: PRINT AT row+1,col+3; PAPER pap; BRIGHT 1;"Out of memory!" 2240 LET spr=spr-1: BEEP 1,-20: PAUSE 0: RETURN 3000 REM --- СОХРАНЕНИЕ СПРАЙТ-ФАЙЛА --- 3010 IF NOT spr THEN RETURN 3020 INPUT "Sprite name: "; LINE n$ 3030 SAVE n$CODE ad0,addr-ad0 3040 RETURN 4000 REM --- УДАЛЕНИЕ СПРАЙТ-ФАЙЛА ИЗ ПАМЯТИ --- 4010 IF NOT spr THEN RETURN 4020 GO SUB 7000: IF k$<>"y" THEN RETURN 4030 LET spr=0: LET addr=ad0: RETURN 5000 REM --- ПРОСМОТР ТАБЛИЦЫ СМЕЩЕНИЙ СПРЙТОВ --- 5010 CLS : IF NOT spr THEN RETURN 5020 FOR n=1 TO spr: PRINT "Sprite No ";n,"Offset == ";s(n): NEXT n 5030 PAUSE 0: RETURN 6000 REM --- ВЫХОД ИЗ ПРОГРАММЫ --- 6010 GO SUB 7000: IF k$<>"y" THEN RETURN 6020 CLEAR : STOP 7000 REM --- ЗАПРОС --- 7010 LET row=11: LET col=5: LET len=21: LET hgt=3: LET pap=6: GO SUB 8000 7020 PRINT AT row+1,col+1; PAPER pap; BRIGHT 1; "Are you shure (Y/N)?" 7030 PAUSE 0: LET k$=INKEY$: IF k$="Y" THEN LET k$="y" 7040 BEEP .01,20: RETURN 8000 REM --- ОКНА --- 8010 POKE 23301,col: POKE 23302,row: POKE 23303,len: POKE 23304,hgt 8020 RANDOMIZE USR clsv: PRINT PAPER pap; BRIGHT 1;: RANDOMIZE USR setv 8030 LET l=len*8-1: LET h=hgt*8-1 8040 PLOT col*8,175-row*8: DRAW l,0: DRAW 0,-h: DRAW -l,0: DRAW 0,h 8050 RETURN 9000 REM --- ДАННЫЕ МЕНЮ --- 9010 DATA "Load Screen" 9020 DATA "Create Sprite" 9030 DATA "Save Sprite" 9040 DATA "New Sprite" 9050 DATA "View Table" 9060 DATA "Quit Program" 9900 REM --- АВТОСТАРТ ПРОГРАММЫ --- 9910 POKE 23693,40: BORDER 5: CLEAR 29999 9920 LOAD "sptgen"CODE 9930 RUN
Если вы работаете в системе TR-DOS, то несколько строк этой программы следует заменить на приведенные ниже:
1020 RANDOMIZE USR 15619: REM : LOAD n$CODE 16384,6912 3030 RANDOMIZE USR 15619: REM : SAVE n$CODE ad0,addr-ad0 9920 RANDOMIZE USR 15619: REM : LOAD "sptgen"CODE
и только после этого использовать.
Некоторые процедуры, как вы заметили, написаны на ассемблере и вызываются функцией USR. При использовании ряда подпрограмм в машинных кодах из Бейсика возникает проблема, как определить адреса обращения к ним. Можно, конечно, оттранслировать каждую из них отдельно, задав для каждой определенный начальный адрес или в одном исходном файле указать несколько директив ORG. Но при этом возникнут другие сложности, связанные с компоновкой программы. Можно также, оттранслировав весь пакет процедур как единое целое, просмотреть затем полученные коды с помощью дизассемблера и найти точки входа в каждую подпрограмму. Но при этом, если потребуется внести в текст какие-либо изменения (а особенно часто это придется делать на этапе отладки), то всю работу по определению адресов придется повторять с начала. В связи с этим мы предлагаем вам наиболее простой способ, часто применяемый в подобных ситуациях: в начале ассемблерного текста нужно вставить ряд команд JP, передающих управление всем процедурам пакета, к которым имеется обращение из Бейсика (либо из другого языка). Зная, что команда JP в памяти занимает 3 байта, несложно вычислить адрес любой процедуры по ее «порядковому номеру». Впоследствии мы еще не раз воспользуемся этим методом, поэтому мы и обратили на него ваше внимание.
Основная часть пакета — это подпрограмма GTBL, сохраняющая в памяти образ экрана в принятом для процедуры PTBL формате спрайтов. Подпрограмма OUT_BT также относится к ней. При вызове GTBL в переменной __SP сохраняется начальное состояние указателя стека SP. Делается это для корректного выхода в Бейсик в случае возникновения ошибки (Out of memory — нехватка памяти).
Подпрограмма RAMKA выводит на экран пунктирный прямоугольник, отмечающий границы создаваемого спрайта. Вывод производится по принципу XOR, поэтому при повторном обращении к процедуре прежний вид экрана полностью восстанавливается.
Подпрограмма SVSCR нужна для сохранения экранного изображения в памяти для последующего его восстановления процедурой RESTOR.
В пакет включены также две описанные ранее процедуры CLSV и SETV для очистки окна экрана и установки в нем постоянных атрибутов.
ORG 65000 ORIGIN EQU $ ;верхняя допустимая граница спрайт-файла ADDR EQU 23670 ;текущий адрес в спрайт-файле ATTR EQU 23695 ;значение атрибутов окна SCREEN EQU 30000 ;адрес «теневого» экрана N_SYM EQU 23300 ;рассчитанная в Бейсике площадь спрайта COL EQU 23301 ;координаты спрайта ROW EQU 23302 LEN EQU 23303 ;размеры спрайта HGT EQU 23304 ; 65000 JP RAMKA ; 65003 JP SVSCR ; 65006 JP RESTOR ; 65009 JP CLSV ; 65012 JP SETV ; 65015 GTBL CALL RESTOR ;восстанавливаем экранную картинку LD (__SP),SP ;запоминаем состояние стека для ; возврата при возникновении ошибки LD IX,(ADDR) ;адрес конца спрайт-файла ; Формирование заголовка LD A,(N_SYM) ;количество знакомест ; в создаваемом спрайте CALL OUT_BT ;записываем первый байт в спрайт-файл LD A,(ROW) ;вычисляем адрес атрибутов LD L,A LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL LD A,#58 ADD A,H LD H,A LD A,(COL) ADD A,L LD L,A LD DE,(LEN) LD BC,0 GTBL1 PUSH BC PUSH DE PUSH HL GTBL2 LD A,B CALL OUT_BT ;позиция по вертикали внутри спрайта LD A,C CALL OUT_BT ;позиция по горизонтали LD A,(HL) CALL OUT_BT ;байт атрибутов INC HL INC C DEC E JR NZ,GTBL2 POP HL LD DE,32 ;переходим к следующей строке ADD HL,DE POP DE POP BC INC B DEC D JR NZ,GTBL1 ; Данные состояния пикселей LD A,(HGT) LD B,A LD A,(ROW) GTBL3 PUSH AF PUSH BC CALL 3742 ;вычисляем адрес начального знакоместа LD A,(COL) ADD A,L LD L,A LD A,(LEN) LD B,A GTBL4 PUSH BC PUSH HL LD B,8 ;переписываем в спрайт-файл 8 байт ; знакоместа GTBL5 LD A,(HL) CALL OUT_BT INC H DJNZ GTBL5 POP HL INC HL ;переходим к следующему знакоместу POP BC DJNZ GTBL4 POP BC POP AF INC A ;переходим к следующей строке DJNZ GTBL3 PUSH IX ;возвращаем в Бейсик адрес POP BC ; конца спрайт-файла RET OUT_BT PUSH BC ;запись в спрайт-файл байта из A PUSH HL ; Проверка наличия свободной памяти PUSH IX POP HL LD BC,ORIGIN ;адрес конца свободной памяти ; для спрайт-файла AND A ;очистка флага CY перед вычитанием ; (если этого не сделать, результат будет неверен!) SBC HL,BC ;если текущий адрес достиг ORIGIN, JR NC,OUTRAM ; происходит выход в Бейсик POP HL POP BC LD (IX),A ;записываем байт в спрайт-файл INC IX ;увеличиваем адрес размещения кодов RET OUTRAM LD SP,(__SP) ;восстанавливаем значение стека LD BC,0 ;возвращаем в Бейсик код ошибки RET __SP DEFW 0 ;переменная для сохранения указателя стека ; Рисование прямоугольной пунктирной рамки RAMKA LD A,(ROW) PUSH AF CALL 3742 ;вычисляем адрес экрана LD A,(COL) ADD A,L LD L,A CALL HOR ;проводим верхнюю линию CALL VERT1 ;рисуем боковые стороны в первой ; строке окна LD A,(HGT) DEC A JR Z,RAMK2 ;обходим, если единственная строка LD B,A ;иначе рисуем боковые стороны по всей ; высоте окна POP AF RAMK1 PUSH AF CALL VERT ;заканчиваем предыдущую строку POP AF INC A ;переходим к следующей PUSH AF CALL 3742 ;вычисляем адрес экрана LD A,(COL) ADD A,L LD L,A CALL VERT ;ставим верхние точки CALL VERT1 ;заканчиваем вертикальный пунктир POP AF DJNZ RAMK1 ;повторяем PUSH AF RAMK2 POP AF ; Горизонтальная пунктирная линия HOR PUSH BC PUSH HL LD A,(LEN) ;рисуем пунктир по ширине окна LD B,A HOR1 LD A,%10011001 ;фактура пунктирной линии XOR (HL) ;объединяем с экранным изображением LD (HL),A ;возвращаем на экран INC HL DJNZ HOR1 POP HL POP BC RET ; Рисование двух точек для боковых сторон рамки VERT PUSH HL LD A,128 ;левая точка XOR (HL) LD (HL),A LD A,(LEN) ;ищем адрес правой стороны окна DEC A ADD A,L LD L,A LD A,1 ;правая точка XOR (HL) LD (HL),A POP HL RET ; Боковые стороны рамки по высоте знакоместа VERT1 INC H ;пропускаем 3 ряда пикселей INC H INC H CALL VERT ;ставим точки на левой и правой ; сторонах прямоугольника INC H CALL VERT ;повторяем для следующего ряда INC H ;делаем следующий промежуток INC H INC H RET ; Сохранение области видеобуфера в «теневом» экране SVSCR LD HL,16384 LD DE,SCREEN LD BC,6912 LDIR RET ; Восстановление изображения на экране RESTOR LD HL,SCREEN LD DE,16384 LD BC,6912 LDIR RET ; Подпрограмма очистки окна CLSV ......... ; Подпрограмма установки атрибутов в окне SETV .........
МУЛЬТИПЛИКАЦИЯ
В большинстве компьютерных игр персонажи беспрерывно передвигаются по экрану, создавая неповторимые ситуации, чем собственно и привлекают к себе внимание многочисленной армии почитателей ZX Spectrum. В пятой главе мы уже слегка затронули проблему движения изображений, прояснив с помощью нескольких примеров те принципы, которые лежат в основе любого перемещения по экрану, будь то тексты или спрайты. Теперь настало время внести в этот вопрос полную ясность, показав способы, наиболее часто используемые в игровых программах.
У вас может возникнуть вопрос, а зачем, собственно, рассматривать несколько разных способов, не проще ли ограничиться одним, только очень хорошим, и применять его во всех случаях компьютерной «жизни». Все дело в том, что возможны совершенно отличные друг от друга ситуации, определяемые конкретным замыслом. Скажем, для простых сюжетов нет смысла использовать сложные способы передвижения спрайтов, а в насыщенных играх простые способы уже могут оказаться неэффективными.
Первый способ основан на скроллинге окон, и если вы припомните программу, которая раздвигает в разные стороны створки железной решетки, то получите о нем полное представление. Тем не менее, мы вновь к нему возвращаемся, поскольку в нашем распоряжении появилась удобная процедура для вывода спрайтов. Перечислим действия, характерные для этого способа:
- поместите на экран спрайт, например, с помощью процедуры PTBL, установив режим SPRPUT;
- задайте окно, по размерам покрывающее этот спрайт и выполните скроллинг при помощи одной из четырех рассмотренных выше процедур. Спрайт довольно резво устремится в нужную сторону, так что в программе необходимо предусмотреть небольшую задержку.
Сразу же виден и недостаток этого способа, который состоит в том, что спрайт перемещается по экрану вместе с фоном, поскольку скроллинг захватывает все изображение в окне. Отсюда ясно, что применять такой метод можно лишь в тех случаях, когда фон как таковой отсутствует или, по крайней мере, мелкие детали не попадают в сдвигаемое окно. В качестве иллюстрации к сказанному приведем небольшую программку, которая плавно перемещает по экрану симпатичный паровозик, позаимствованный нами из спрайт-файла SPRITE2B пакета Laser Basic (рис. 7.2).
ORG 60000 ENT $ XOR A CALL 8859 LD A,5 LD (23693),A CALL 3435 LD A,2 CALL 5633 ; Начальная установка регистров процедуры PTBL LD B,10 LD C,0 LD A,SPRPUT LD HL,PAROW ; Вывод на экран «паровозика» CALL PTBL ; Задание параметров окна LD HL,#A00 ;COL = 0, ROW = 10 LD (COL),HL LD HL,#320 ;LEN = 32, HGT = 3 LD (LEN),HL LD B,0 ;задание длины пробега «паровозика» ; (0 = 256 пикселей) MOVE PUSH BC CALL SCR_RT ;обращение к процедуре скроллинга вправо POP BC DJNZ MOVE RET ; Подпрограмма скроллинга окна вправо SCR_RT ......... ; Подпрограмма вывода спрайта PTBL ......... ; Переменные к процедуре скроллинга COL DEFB 0 ROW DEFB 0 LEN DEFB 0 HGT DEFB 0 ; Заголовок данных для «паровозика» PAROW DEFB 14,0,0,5,0,1,5,0,2,5,0,3,5 DEFB 1,0,5,1,1,5,1,2,5,1,3,5,1,4,5 DEFB 2,0,5,2,1,5,2,2,5,2,3,5,2,4,5 ; Данные DEFB 0,0,0,3,15,63,64,95 DEFB 0,0,0,255,255,254,1,255 DEFB 0,0,0,192,160,70,201,73 DEFB 0,0,0,0,30,33,26,18 DEFB 24,248,152,253,133,181,181,181 DEFB 33,39,62,255,0,127,127,127 DEFB 246,73,146,255,0,254,252,251 DEFB 230,83,134,189,133,133,173,214 DEFB 0,192,96,160,160,160,160,96 DEFB 181,133,253,0,255,38,20,15 DEFB 127,0,255,0,255,83,138,7 DEFB 244,11,247,0,255,41,69,131 DEFB 35,216,228,3,249,148,35,192 DEFB 192,32,216,176,96,192,128,0
Второй способ не многим сложнее первого. Он основан на многократном выводе спрайта на экран. Если на каждом шаге изменять на единицу одну из координат, то спрайт будет двигаться параллельно соответствующей границе экрана, если же менять сразу обе, то он начнет перемещаться по диагонали. Основное требование к спрайту — он должен иметь по краям пустое пространство шириной в одно знакоместо, иначе изображение, помещенное на экран на предыдущем шаге, не будет полностью затираться следующим выводимым спрайтом и по экрану потянется не предусмотренный программистом след. Таким образом, если пустые места сделаны вокруг всего спрайта, то его можно спокойно передвигать в любом направлении, если же заранее известно, что он будет перемещаться вправо и никуда больше (как в примере ниже), то достаточно оставить пустую полоску шириной в одно знакоместо только слева.
К сожалению, этот способ перемещения спрайта тоже не лишен недостатка, видного невооруженным глазом: вместе с изображением, действительно подлежащим удалению, спрайт как ластик сотрет вообще весь фон позади себя. Справиться с этим можно точно так же, как мы рекомендовали выше, — выводить спрайт на сплошной фон, лишенный сложного пейзажа, что приемлемо для ограниченного числа игровых сюжетов. В качестве иллюстрации приведем программу, которая передвигает по экрану слева направо маленького динозавра, перебравшегося к нам из игры LITTLE PUFF:
ORG 60000 ENT $ XOR A CALL 8859 LD A,7 LD (23693),A CALL 3435 LD A,2 CALL 5633 ; Основная часть программы LD C,-4 MOVE LD B,10 LD A,SPRPUT LD HL,DIN PUSH BC CALL PTBL LD BC,10 CALL 7997 POP BC INC C LD A,C CP 32 JP M,MOVE ;если координата меньше 32 (с учетом знака) RET ; Подпрограмма вывода спрайта PTBL ......... ; Заголовок данных для «динозавра» DIN DEFB 16,0,0,4,0,1,4,0,2,4,0,3,4 DEFB 1,0,4,1,1,4,1,2,4,1,3,4 DEFB 2,0,4,2,1,4,2,2,4,2,3,4 DEFB 3,0,4,3,1,4,3,2,4,3,3,4 ; Данные: DEFB 0,0,0,0,0,0,0,0 DEFB 0,0,0,0,0,0,0,0 DEFB 0,0,0,0,0,70,117,42 DEFB 0,0,0,0,0,64,160,64 DEFB 0,0,0,0,0,0,0,0 DEFB 0,0,2,6,11,21,46,31 DEFB 78,72,110,52,123,111,96,31 DEFB 192,128,219,173,71,254,127,0 DEFB 0,0,0,0,0,0,0,0 DEFB 53,100,85,181,20,4,0,0 DEFB 205,188,63,119,103,231,55,185 DEFB 248,0,0,192,224,240,208,224 DEFB 0,0,0,0,0,0,0,0 DEFB 0,0,0,0,0,0,1,0 DEFB 28,57,67,167,156,191,157,0 DEFB 24,120,184,48,38,140,96,0 DEFB 0,0,0,0,0,0,0,0
Программа содержит всего один цикл и настолько проста, что в дополнительных комментариях не нуждается.
Третий способ использует вывод спрайтов на экран по принципу XOR, который заложен в процедуре PTBL (как, впрочем, и другие). Применение принципа XOR для объединения изображений позволяет легко справиться с проблемой восстановления фона при перемещении спрайтов. Перечислим вначале все операции, которые должна выполнить программа:
- спрайт помещается на экран процедурой PTBL, в режиме XOR;
- через некоторое время (должен же спрайт немного побыть на экране) вторично выполняется процедура PTBL для того же спрайта, опять же по принципу XOR, при этом он стирается;
- координаты спрайта (или одна из них) изменяются и все повторяется сначала, при этом спрайт появляется на экране уже в другом месте.
Проделывая все это многократно, можно получить неплохой эффект мультипликации с сохранением фона.
Продемонстрируем этот способ на примере программы, которая в действии выглядит следующим образом. По дороге с ружьем на изготовку двигается солдат, медленно, но верно приближаясь к стенке из белого кирпича. Можно легко заметить, что движение состоит из двух фаз, каждой из которых, очевидно, должен соответствовать один спрайт: первый — ноги вместе и ружье чуть-чуть приподнято и второй — ноги расставлены в шаге, а ружье при этом опускается немного вниз. Когда солдат проходит мимо стенки, их картинки начинают смешиваться по принципу XOR, что мы и наблюдаем на экране — появляется какое-то хаотическое изображение, как будто человек продирается сквозь стену, а не идет мимо нее. Однако после того как стенка оказывается позади солдата, мы обнаруживаем, что оба изображения полностью восстановились.
ORG 60000 ENT $ LD A,4 LD (23693),A XOR A CALL 8859 CALL 3435 LD A,2 CALL 5633 ; Рисование пейзажа, состоящего из дороги и стены CALL GRUNT ; Вывод первой фазы спрайта «солдат» в режиме XOR LD B,9 ;задаем координату Y LD C,-3 ;начальное значение координаты X LOOP LD A,SPRXOR ;устанавливаем режим вывода LD HL,SOLD1 ;задаем адрес спрайта 1 фазы PUSH HL PUSH BC CALL PTBL LD BC,20 ;задержка спрайта на экране CALL 7997 POP BC POP HL ; Повторный вывод первой фазы спрайта в режиме XOR LD A,SPRXOR PUSH HL PUSH BC CALL PTBL POP BC POP HL INC C ;увеличиваем координату X ; Вывод второй фазы спрайта «солдат» в режиме XOR LD A,SPRXOR LD HL,SOLD2 ;задаем адрес спрайта 2 фазы PUSH HL PUSH BC CALL PTBL LD BC,20 CALL 7997 POP BC POP HL ; Повторный вывод второй фазы спрайта в режиме XOR LD A,SPRXOR PUSH HL PUSH BC CALL PTBL POP BC POP HL INC C ;увеличиваем координату X LD A,C ;количество шагов солдата CP 32 ;проверка условия конца «дороги» JP M,LOOP RET ; Подпрограмма рисования пейзажа GRUNT EXX PUSH HL LD BC,#4700 ;начало горизонтальной линии (B=71, C=0) CALL 8933 ; Рисование «дорожки» LD DE,#101 LD BC,250 CALL 9402 ; Рисование «стены» LD BC,#915 ;B = 9, C = 21 STEN PUSH BC LD A,SPRPUT ;устанавливаем режим вывода LD HL,STENA ;задаем адрес спрайта «стена» CALL PTBL POP BC INC B LD A,B CP 13 JR C,STEN POP HL EXX RET PTBL ......... ; Заголовок данных первой фазы спрайта «солдат» SOLD1 DEFB 10 DEFB 0,0,7, 0,1,7, 1,0,7, 1,1,7 DEFB 2,0,7, 2,1,7, 2,2,7 DEFB 3,0,7, 3,1,7, 3,2,7 ; Данные первой фазы спрайта «солдат» DEFB 1,4,13,27,91,35,28,3 DEFB 128,32,112,184,182,140,56,192 DEFB 12,16,19,15,15,7,3,27 DEFB 8,32,12,156,192,152,172,192 DEFB 60,63,114,45,31,47,112,123 DEFB 250,7,0,183,160,44,192,56 DEFB 0,0,2,255,96,224,0,0 DEFB 51,7,1,6,12,19,30,15 DEFB 220,220,216,33,27,11,135,134 DEFB 0,0,0,128,64,192,128,0 ; Заголовок данных второй фазы спрайта «солдат» SOLD2 DEFB 9 DEFB 0,0,7, 0,1,7, 1,0,7, 1,1,7 DEFB 2,0,7, 2,1,7, 2,2,7 DEFB 3,0,7, 3,1,7 ; Данные второй фазы спрайта DEFB 3,8,26,55,183,71,56,7 DEFB 0,64,224,112,108,24,112,128 DEFB 24,32,38,31,31,15,7,55 DEFB 16,64,24,56,128,48,104,128 DEFB 121,254,239,200,178,127,62,204 DEFB 244,14,251,0,239,65,91,0 DEFB 0,0,0,8,252,128,128,0 DEFB 243,111,15,0,6,6,1,15 DEFB 184,184,184,0,48,214,173,239 ; Заголовок спрайта «стена» STENA DEFB 2 DEFB 0,0,7, 0,1,7 ; Данные спрайта «стена» DEFB 0,223,223,223,0,253,253,253 DEFB 0,223,223,223,0,253,253,253
Четвертый способ основан на принципе записи части экранного изображения в буферную область памяти с последующим его возвратом на экран. Сначала поясним суть этого способа, а затем приведем небольшую программу, которая его иллюстрирует. В программе ГЕНЕРАТОР СПРАЙТОВ для сохранения образа экрана в памяти использовалась процедура GTBL и чтобы не писать еще одну подпрограмму, применим ее же для пересылки в буфер фрагмента экранного изображения. Вставляя эту процедуру в программу, следует предварительно внести в нее небольшие изменения:
- убрать первые четыре строки (до CALL OUT_BT);
- все команды CALL OUT_BT заменить на
LD (IX),A INC IX
- в самом конце процедуры GTBL (непосредственно перед инструкцией RET) убрать команды PUSH IX и POP BC;
- внутренняя подпрограмма OUT_BT также не нужна, поэтому ее можно опустить.
В остальном все остается без изменений. Поскольку экранное изображение сохраняется в памяти, нужно позаботиться о выделении для этих целей некоторого рабочего буфера. Чтобы буфер не перекрыл занятую программой память, следует знать не только адрес его начала (который, кстати, понадобится для возврата полученного процедурой GTBL изображения), но и его размер. Вычислить его можно исходя из принятого нами формата спрайтов: количество знакомест (N_SYM) плюс заголовок (N_SYM×3) плюс данные (N_SYM×8). Итого получится N_SYM×11+1. Для небольших спрайтов вполне можно включить буфер в саму программу, воспользовавшись директивой ассемблера
BUFFER DEFS N_SYM*11+1
Добавим, что при таком способе задания буфера размер его может быть больше рассчитанного, но ни в коем случае не меньше, иначе сохраняемые коды уничтожат часть программы! Кроме этого, надо сказать, что DEFS имеет смысл использовать только для сохранения небольших участков экрана, а при работе с большими изображениями (или когда одновременно сохраняются много окон) лучше выделить для этих целей некоторый участок памяти вне программы, чтобы сократить размер исполняемого модуля.
Для восстановления изображения нужно воспользоваться процедурой PTBL, задав в качестве адреса спрайта метку BUFFER или абсолютный адрес буфера, если он находится вне программы (хотя в этом случае его удобнее задать как константу).
Перечислим основные этапы реализации описываемого способа передвижения спрайта:
- процедурой GTBL забираем в буфер часть экранного изображения в форме прямоугольного окна;
- в это же место процедурой PTBL (в режиме SPRPUT) выводим спрайт;
- делаем небольшую задержку;
- ранее сохраненное окно с изображением части экрана переносим процедурой PTBL (в режиме SPRPUT) обратно на экран и в то же самое место;
- изменяем координаты спрайта (либо одну из них, как в примере) и повторяем перечисленные выше действия.
При составлении программы игры необходимо следить за тем, чтобы спрайт не выходил за пределы экрана, по крайней мере влево и вверх, так как этого не допускает процедура GTBL.
Эффективность способа продемонстрируем с помощью приведенной ниже программы, которая передвигает по экрану человечка из игры EXPRESS. Он пробегает мимо кустов, закрывая их собой по очереди, однако за его спиной кусты вновь появляются. Добежав до лежащего на дороге камня, человечек спотыкается и падает, на этом действие микромультфильма заканчивается (но вы можете попытаться его продолжить).
ORG 60000 ENT $ N_SYM EQU 6 ;задаем количество знакомест окна LD A,6 LD (23693),A XOR A CALL 8859 CALL 3435 LD A,2 CALL 5633 ; Начало программы CALL GRUNT ;рисуем пейзаж LD C,0 ;начальное значение координаты X для ; «бегущего человечка» ; Перенос окна с частью изображения в буфер LOOP LD B,10 ;COL = C, ROW = 10 LD (COL),BC LD HL,#302 ;LEN = 2, HGT = 3 LD (LEN),HL LD IX,BUFFER ;в IX заносим начальный адрес буфера LD A,N_SYM ;в A заносим площадь окна PUSH BC CALL GTBL ;образ окна переносим в память POP BC ; Выводим человечка на то место, где было окно LD HL,MAN1 ;задаем адрес спрайта «человечек» LD A,SPRPUT ;задаем режим вывода PUSH BC CALL PTBL ; Вводим небольшую задержку LD BC,10 CALL 7997 POP BC ; Ранее запомненное окно с изображением части экрана переносим ; из буфера обратно на экран LD HL,BUFFER ;задаем начальный адрес буфера ; с изображением части экрана LD A,SPRPUT ;устанавливаем режим вывода PUSH BC CALL PTBL ;вывод окна на экран POP BC INC C ;увеличиваем координату X человечка LD A,C CP 20 JR C,LOOP ;(или JP M,LOOP) ; Вывод человечка, споткнувшегося о камень LD BC,#B16 ;B = 11, C = 22 LD A,SPRPUT ;устанавливаем режим вывода LD HL,MAN2 ;задаем адрес спрайта «упавший человечек» CALL PTBL RET PTBL ......... ; измененная процедура GTBL GTBL LD (IX),A INC IX LD A,(ROW) LD L,A LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL LD A,#58 ADD A,H LD H,A LD A,(COL) ADD A,L LD L,A LD DE,(LEN) LD BC,0 GTBL1 PUSH BC PUSH DE PUSH HL GTBL2 LD A,B LD (IX),A INC IX LD A,C LD (IX),A INC IX LD A,(HL) LD (IX),A INC IX INC HL INC C DEC E JR NZ,GTBL2 POP HL LD DE,32 ADD HL,DE POP DE POP BC INC B DEC D JR NZ,GTBL1 LD A,(HGT) LD B,A LD A,(ROW) GTBL3 PUSH AF PUSH BC CALL 3742 LD A,(COL) ADD A,L LD L,A LD A,(LEN) LD B,A GTBL4 PUSH BC PUSH HL LD B,8 GTBL5 LD A,(HL) LD (IX),A INC IX INC H DJNZ GTBL5 POP HL INC HL POP BC DJNZ GTBL4 POP BC POP AF INC A DJNZ GTBL3 RET ; Рисование пейзажа. Еще раз хотим напомнить вам о необходимости ; сохранения HL' при использовании подпрограммы 9402 и аналогичных ; ей. При вызове программы из GENS это не критично, а вот в Бейсик ; будет уже не вернуться. GRUNT EXX PUSH HL LD BC,#4700 ;B = 71, C = 0 CALL 8933 ; Рисование «дорожки» LD DE,#101 LD BC,250 CALL 9402 ; Установка «камня» LD BC,#C15 ;B = 12, C = 21 LD A,SPRPUT LD HL,KAM CALL PTBL ; Вывод двух «кустов» LD BC,#B05 ;B = 11, C = 5 LD A,SPRPUT LD HL,KUST CALL PTBL LD BC,#B10 ;B = 11, C = 16 LD A,SPRPUT LD HL,KUST CALL PTBL POP HL EXX RET ; Графические переменные COL DEFB 0 ROW DEFB 0 LEN DEFB 0 HGT DEFB 0 ; Буфер для сохранения окна экрана BUFFER DEFS N_SYM*11+1 ; Заголовок спрайта «бегущий человечек» MAN1 DEFB 6,0,0, 6,0,1, 6,1,0, 7,1,1,7 DEFB 2,0,3, 2,1,3 ; Данные спрайта «бегущий человечек» DEFB 83,111,255,127,255,255,127,255 DEFB 127,252,254,249,254,243,249,48 DEFB 190,31,31,15,15,7,3,3 DEFB 208,220,248,112,128,224,192,80 DEFB 27,123,231,7,15,14,12,30 DEFB 40,172,230,224,240,112,56,60 ; Заголовок спрайта «упавший человечек» MAN2 DEFB 6,0,0, 3,0,1, 7,0,2,6 DEFB 1,0,3, 1,1,7, 1,2,6 ; Данные спрайта «упавший человечек» DEFB 0,0,0,0,0,7,22,59 DEFB 3,1,7,31,63,127,255,255 DEFB 164,127,254,253,254,254,255,255 DEFB 123,124,124,63,79,112,63,15 DEFB 63,31,10,13,6,2,0,0 DEFB 127,127,255,255,255,47,237,89 ; Заголовок и данные спрайта «камень» KAM DEFB 1, 0,0,7 DEFB 0,0,0,30,103,159,254,124 ; Заголовок спрайта «куст» KUST DEFB 4, 0,0,6, 0,1,4, 1,0,6, 1,1,4 ; Данные спрайта «куст» DEFB 33,12,102,242,185,13,53,121 DEFB 56,100,192,218,160,134,157,56 DEFB 29,204,110,38,54,182,87,91 DEFB 50,112,119,238,236,234,90,84
Пятый способ лучше всего начать с описания картинки, которую формирует приведенная нами программа, а не с особенностей метода, как мы это делали раньше. Дело в том, что здесь придется ввести некоторые новые понятия, такие, например, как маска, и, как говорится в таких случаях — «лучше один раз увидеть…» После запуска программы перед вами появится «морской» пейзаж, состоящий из синей поверхности воды, черного ночного неба, на котором, тем не менее, видны белые облака (скорее всего их освещает луна), а между водой и небом — живописные острова, вблизи которых медленно проплывает военный корабль. Особенность этого способа состоит в том, что изображение корабля закрывает остров только по контуру спрайта, который не обязательно будет обозначен прямыми линиями, проходящими по границам знакомест. Достигается это благодаря использованию маски, поэтому необходимо сказать несколько слов о том, что же это такое и как ее сделать.
Представим себе, что с помощью графического редактора вы создали какой-то спрайт, скажем, изобразили корабль (рис. 7.3, а). Затем, воспользовавшись функцией Cut & paste window, перенесите полученный спрайт немного правее и обведите его по контуру, закрасив всю внешнюю область и оставив зазор в один пиксель (рис. 7.3, б). Наконец, удалите все, что расположено внутри контура как показано на рис. 7.3, в. Так вот, то, что получилось на последнем рисунке, и есть маска для исходного спрайта «корабль».
а | б | в |
Рис. 7.3. Изготовление маски спрайта |
Перейдем к реализации способа перемещения спрайтов с восстановлением фона, основанного на применении маски:
- процедурой GTBL забираем в буфер часть экранного изображения;
- процедурой PTBL по принципу AND в то же место экрана помещаем маску корабля, в результате чего будет очищена не вся прямоугольная область экрана, а только внутренняя часть, которую вы оставили незаштрихованной (вспомните, в чем заключается принцип AND);
- процедурой PTBL по принципу OR внутрь маски помещаем спрайт корабля (объединение по OR не стирает предыдущего изображения, поэтому и нужно почистить экран маской);
- ранее сохраненное окно с изображением части экрана переносим из буфера обратно на экран, используя процедуру PTBL в режиме SPRPUT;
- изменяем координаты спрайта на новые, после чего повторяем перечисленные действия в том же порядке.
ORG 60000 ENT $ N_SYM EQU 36 ;задаем количество знакомест окна LD A,6 LD (23693),A XOR A CALL 8859 CALL 3435 LD A,2 CALL 5633 ; Начало программы CALL MORE ;рисуем «морской» пейзаж LD C,0 ;начальная координата Y корабля LOOP LD B,8 ;задаем параметры окна, в которое ; поместим изображение части экрана ; (COL = C, ROW = 8) LD (COL),BC LD HL,#409 ;LEN = 9, HGT = 4 LD (LEN),HL LD IX,BUFFER ;в IX заносим начальный адрес буфера LD A,N_SYM ;в регистр A заносим площадь окна PUSH BC CALL GTBL ;образ окна переносим в память POP BC ; По принципу AND на экран помещаем маску LD B,8 LD HL,MASKA ;задаем адрес маски LD A,SPRAND ;устанавливаем режим вывода PUSH BC CALL PTBL POP BC ; По принципу OR в маску помещаем спрайт «корабль» LD B,8 INC C ;увеличиваем координату X «корабля» LD HL,KORAB ;задаем адрес спрайта «корабля» LD A,SPROR ;устанавливаем режим вывода PUSH BC CALL PTBL ;выводим корабль LD BC,20 ;вводим задержку CALL 7997 POP BC ; Ранее запомненное окно с изображением части экрана переносим ; из буферной области памяти обратно на экран LD B,8 DEC C LD HL,BUFFER ;устанавливаем начальный адрес буфера LD A,SPRPUT ;устанавливаем режим вывода PUSH BC CALL PTBL POP BC INC C ;увеличиваем координату X корабля LD A,C ;конечная координата X корабля CP 32 ;проверяем, не вышел ли корабль ; за пределы экрана JR C,LOOP RET ; Рисование «моря» синим цветом MORE LD A,14 LD (23693),A CALL 3435 LD A,2 CALL 5633 LD BC,320 LD HL,#5800 ; Изображение черного «неба» NEBO LD (HL),7 INC HL DEC BC LD A,B OR C JR NZ,NEBO LD BC,#606 ;B = 6, C = 6 LD A,SPRPUT ;устанавливаем режим вывода LD HL,OBL ;задаем адрес спрайта «облако» CALL PTBL ;печать «облака» LD BC,#414 ;B = 4, C = 20 LD A,SPRPUT LD HL,OBL CALL PTBL ;печать еще одного «облака» ; Вывод на экран спрайта «остров» OSTROV LD BC,#908 ;B = 9, C = 8 - координаты LD A,SPRPUT LD HL,LAND ;задаем адрес спрайта CALL PTBL ;выводим его на экран RET PTBL ......... GTBL ......... ;Графические переменные COL DEFB 0 ROW DEFB 0 LNG DEFB 0 HGT DEFB 0 ; Резервирование памяти для окна BUFFER DEFS N_SYM*11+1 ; Заголовок данных спрайта «корабль» KORAB DEFB 13 DEFB 0,3,7, 1,1,4, 1,2,7, 1,3,7, 1,4,7 DEFB 1,5,4, 2,0,15, 2,1,15, 2,2,15, 2,3,15 DEFB 2,4,15, 2,5,15, 2,6,15 ; Данные спрайта «корабль» DEFB 0,0,0,16,16,16,16,24 DEFB 0,0,0,0,0,0,0,31 DEFB 0,0,0,0,3,4,7,197 DEFB 60,38,30,60,149,159,179,191 DEFB 0,0,0,0,128,128,111,229 DEFB 0,0,0,0,0,0,240,0 DEFB 0,1,0,127,85,43,31,0 DEFB 2,251,84,255,85,187,255,0 DEFB 183,254,201,255,85,255,255,0 DEFB 238,12,191,213,127,255,255,0 DEFB 63,217,255,85,255,255,255,0 DEFB 127,40,255,85,186,255,255,0 DEFB 128,0,254,20,184,240,224,0 ; Заголовок маски MASKA DEFB 14 DEFB 0,3,7, 1,1,4, 1,2,4, 1,3,4, 1,4,4 DEFB 1,5,4, 1,6,4, 2,0,15, 2,1,15, 2,2,15 DEFB 2,3,15, 2,4,15, 2,5,15, 2,6,15 ; Данные маски DEFB 255,255,199,199,199,199,195,129 DEFB 255,255,255,255,255,255,192,192 DEFB 255,255,255,248,240,240,16,0 DEFB 128,128,128,0,0,0,0,0 DEFB 255,255,255,63,63,0,0,0 DEFB 255,255,255,255,255,7,7,0 DEFB 255,255,255,255,255,255,255,63 DEFB 252,252,0,0,0,0,128,192 DEFB 0,0,0,0,0,0,0,0 DEFB 0,0,0,0,0,0,0,0 DEFB 0,0,0,0,0,0,0,0 DEFB 0,0,0,0,0,0,0,0 DEFB 0,0,0,0,0,0,0,0 DEFB 63,0,0,0,1,3,7,15 ; Заголовок спрайта «облако» OBL DEFB 4 DEFB 0,0,7, 0,1,7, 0,2,7, 0,3,7 ; Данные спрайта «облако» DEFB 0,48,218,255,255,98,56,0 DEFB 60,255,86,251,247,247,46,112 DEFB 0,58,127,207,123,239,118,57 DEFB 0,0,56,238,207,117,24,0 ; Заголовок спрайта «остров» LAND DEFB 14 DEFB 0,0,4, 0,1,4, 0,2,4, 0,3,4 DEFB 0,4,4, 0,5,4, 0,6,4, 0,7,4 DEFB 0,8,4, 0,9,4, 0,10,4, 0,11,4 DEFB 0,12,4, 0,13,4 ; Данные спрайта «остров» DEFB 0,0,0,0,0,0,28,255 DEFB 0,0,0,0,0,28,255,255 DEFB 0,0,0,0,48,255,255,255 DEFB 0,0,0,0,0,0,252,255 DEFB 0,0,0,0,0,3,63,255 DEFB 0,0,48,252,255,255,253,252 DEFB 0,0,0,0,193,243,255,255 DEFB 0,0,0,0,193,243,255,255 DEFB 0,0,0,0,0,3,63,255 DEFB 0,0,48,252,255,255,253,252 DEFB 0,0,0,0,193,243,255,255 DEFB 0,0,48,252,255,255,253,252 DEFB 0,0,0,0,193,243,255,255 DEFB 0,0,0,0,0,192,252,255
ПОСТРОЕНИЕ ПЕЙЗАЖЕЙ
Деление изображения на пейзаж и спрайты, с точки зрения его формирования на экране, вообще-то является достаточно условным, поскольку все процедуры, используемые для рисования спрайтов, применимы и при создании пейзажей. Однако если рассматривать этот вопрос с игровой точки зрения, то здесь уже можно увидеть существенные различия, которые позволяют выделить создание пейзажа как самостоятельную задачу. Прежде всего, он в большинстве игр занимает всю площадь экрана, а спрайты, хотя и бывают довольно большими, но все же ограничены в размерах. Другое важное отличие — пейзаж, как правило, неподвижен или, если и изменяется, то незначительно.
Рассмотрим несколько приемов формирования пейзажа и проиллюстрируем их небольшими программами и рисунками. Начать нужно, по-видимому, с самого простого — с голубого неба (синей глади воды, сплошного ковра зеленой травы, желтого песка пустыни и т. д.). С этой целью достаточно в самом начале программы задать:
LD A,40 ;INK 0, PAPER 5 LD (23693),A CALL 3435
и требуемый фон готов. Несколько сложнее обстоят дела с космическим пространством. То есть, задать черный цвет «бумаги» никакой проблемы не составит, но вот со звездами не все так просто. Можно, конечно, нарисовать и закодировать маленькие спрайты и даже заставить их случайным образом вспыхивать (особенно хорошо выглядит задание повышенной яркости в фазе максимума). Но ведь каждый спрайт-звездочка — это целое знакоместо, поэтому много ли их можно разместить на экране. Значительно лучше смотрятся звездочки размером в пиксель, как, например, в игре ELITE, но тут уже требуется небольшая программка, которую мы предлагаем вам написать самостоятельно.
Простые пейзажи можно сформировать из отдельных спрайтов, либо фрагментов, составленных из повторяющихся спрайтов. Здесь «трава» содержит восемь спрайтов, каждый из которых занимает четыре знакоместа, «куст» — из трех спрайтов, а «человечек» — это отдельный спрайт из шести знакомест. Ниже приводится соответствующая этому пейзажу программа:
ORG 60000 ENT $ LD A,5 LD (23693),A XOR A CALL 8859 CALL 3435 ; Вывод восьми спрайтов «трава» LD B,8 ;задаем количество выводимых ; спрайтов «трава» LD HL,DATA1 ;читаем адрес блока координат LD DE,TRAW CALL POSIT ; Вывод трех спрайтов «куст» LD B,3 LD HL,DATA2 LD DE,KUST CALL POSIT ; Вывод спрайта «человечек» LD B,1 LD HL,DATA3 LD DE,MAN ; Подпрограмма начальной установки регистров для процедуры PTBL POSIT PUSH BC LD B,(HL) ;устанавливаем Y INC HL LD C,(HL) ;устанавливаем X INC HL PUSH HL PUSH DE EX DE,HL LD A,SPRPUT ;вывод с наложением CALL PTBL POP DE POP HL POP BC DJNZ POSIT RET PTBL ......... ; Задание пар координат (Y,X) для вывода восьми спрайтов «трава» DATA1 DEFB 17,0,17,4,17,8,17,12,17,16 DEFB 17,20,17,24,17,28 ; Заголовок спрайта «трава» TRAW DEFB 4, 0,0,4, 0,1,4, 0,2,4, 0,3,4 ; Данные для спрайта «трава» DEFB 193,106,191,253,255,235,181,36 DEFB 18,170,247,255,255,94,107,41 DEFB 101,200,255,219,255,255,87,10 DEFB 18,170,247,255,255,95,107,41 ; Задание пар координат для печати трех спрайтов «куст» DATA2 DEFB 11,14,13,14,15,14 ; Заголовок спрайта «куст» KUST DEFB 4, 0,0,6, 0,1,4, 1,0,6, 1,1,4 ; Данные для спрайта «куст» DEFB 33,12,102,242,185,13,53,121 DEFB 56,100,192,218,160,134,157,56 DEFB 29,204,110,28,54,182,87,91 DEFB 50,112,119,238,236,234,90,84 ; Пара координат для вывода «человечка» DATA3 DEFB 14,23 ; Заголовок спрайта «человечек» MAN DEFB 6, 0,0,5, 0,1,5, 1,0,5 DEFB 1,1,5, 2,0,5, 2,1,5 ; Данные для спрайта «человечек» DEFB 3,15,17,36,48,16,26,31 DEFB 192,240,248,56,132,60,120,240 DEFB 25,14,7,24,31,59,55,55 DEFB 240,192,184,124,244,246,250,194 DEFB 56,27,3,0,6,6,7,0 DEFB 26,104,160,144,48,36,184,0
Задание в виде отдельных блоков данных (DATA1…DATA3) пар координат, первая из которых соответствует вертикальной позиции спрайта, а вторая — горизонтальной, позволяет легко «разбрасывать» спрайты по всему экрану, создавая любые их комбинации. Вывод спрайтов осуществляется процедурой PTBL.
Пейзажи в игровых программах довольно часто формируются из многократно повторяющихся фрагментов, причем последние могут занимать как одно знакоместо, так и состоять из нескольких. Взять, к примеру, всевозможные лабиринты, разрезы зданий и других сооружений, карты боевых действий и так далее, всего просто не перечесть. Составим программу, которая формирует картинки именно такого типа, причем для простоты будем считать, что все различающиеся фрагменты имеют размеры одного знакоместа. В этом случае для их быстрого вывода на экран можно воспользоваться способом, аналогичным тому, который использовался в рассмотренной раньше процедуре PRSYM.
Предположим, что мы хотим получить изображение, которое представляет собой внешнюю часть лабиринта к создаваемой игре. Соответствующая этой картинке программа выглядит так:
ORG 60000 LD IX,LAB_0 ;перед вызовом в IX заносится адрес ; данных лабиринта LABS1 LD C,(IX+1) ;позиция начального элемента по горизонтали LD B,(IX+2) ;позиция начального элемента по вертикали LD A,(IX+3) ;количество повторений и направление AND 31 JR Z,LABS5 ;если выводится одиночный элемент LD E,A LABS2 LD A,(IX) ;код символа (0...5) CALL PRINT ;вывод символа на экран BIT 7,(IX+3) ;проверка направления вывода JR NZ,LABS3 INC C ;слева направо JR LABS4 LABS3 INC B ;сверху вниз LABS4 DEC E ;следующий элемент JR NZ,LABS2 JR LABS6 LABS5 LD A,(IX) ;вывод одиночного элемента CALL PRINT LABS6 LD DE,4 ;увеличиваем адрес в блоке данных ADD IX,DE ; на 4 байта LD A,(IX) ;проверка на достижение конца блока данных INC A ;если -1 (255) JR NZ,LABS1 RET PRINT PUSH BC PUSH DE PUSH HL LD L,A ;по коду определяем адрес символа LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL LD DE,D_SYMB ADD HL,DE PUSH HL LD A,B ;вычисляем адрес видеобуфера CALL 3742 LD A,L ADD A,C LD L,A POP DE LD B,8 PUSH HL PRINT1 LD A,(DE) ;переносим на экран 8 байт LD (HL),A INC DE INC H DJNZ PRINT1 POP HL ;восстанавливаем начальный адрес экрана LD A,H ;рассчитываем адрес атрибутов ; соответствующего знакоместа AND #18 RRCA RRCA RRCA ADD A,#58 LD H,A LD A,(23695) ;записываем байт атрибутов в видеобуфер LD (HL),A POP HL POP DE POP BC RET ; Данные для построения лабиринта (рамки) ; IX+0 - номер символа в таблице D_SPR (0..5) ; IX+1 - начальная позиция экрана по горизонтали ; IX+2 - начальная позиция экрана по вертикали ; IX+3 - младшие 5 битов - количество повторений вывода символа, ; 7-й бит определяет направление вывода: ; установлен - сверху вниз (задается символами @#80), ; сброшен - слева направо LAB_0 DEFB 0,2,8,0, 1,3,8,2 DEFB 2,5,8,0, 3,5,9,0 DEFB 5,5,10,0, 1,6,10,20 DEFB 3,2,9,2@#80, 5,2,11,0 DEFB 1,3,11,0, 2,4,11,0 DEFB 3,4,12,8@#80, 4,4,20,0 DEFB 1,3,20,0, 0,2,20,0 DEFB 3,2,21,2@#80, 5,2,23,0 DEFB 1,3,23,2, 4,5,23,0 DEFB 3,5,22,0, 0,5,21,0 DEFB 1,6,21,20, 4,26,10,0 DEFB 3,26,9,0, 0,26,8,0 DEFB 1,27,8,2, 2,29,8,0 DEFB 3,29,9,2@#80, 4,29,11,0 DEFB 1,28,11,0, 0,27,11,0 DEFB 2,26,21,0, 3,26,22,0 DEFB 5,26,23,0, 1,27,23,2 DEFB 4,29,23,0, 3,29,21,2@#80 DEFB 2,29,20,0, 1,28,20,0 DEFB 5,27,20,0, 3,27,12,8@#80 DEFB 0,3,9,0, 2,4,9,0 DEFB 4,4,10,0, 5,3,10,0 DEFB 0,27,9,0, 2,28,9,0 DEFB 4,28,10,0, 5,27,10,0 DEFB 0,27,21,0, 2,28,21,0 DEFB 4,28,22,0, 5,27,22,0 DEFB 0,3,21,0, 2,4,21,0 DEFB 4,4,22,0, 5,3,22,0 DEFB -1 ;конец данных ; Данные символов (элементов лабиринта) D_SYMB DEFB 127,213,159,191,253,182,248,181 DEFB 255,85,255,255,85,170,0,255 DEFB 254,83,249,245,121,181,121,181 DEFB 249,181,249,181,249,181,249,181 DEFB 249,117,249,245,81,169,3,254 DEFB 249,189,255,191,213,170,192,127
Используя эту программу без всяких переделок и изменив только ее блоки данных, можно с успехом рисовать различные рамки для оформления кадров меню или окон, предназначенных для вывода всевозможной оценочной информации, правил игры и т. д.
Прежде чем перейти к следующему разделу, поясним встретившуюся в блоке данных LAB_0 не очень понятную запись @#80. В комментарии было сказано, что это означает установку 7-го бита в числе. Дело в том, что ассемблер GENS помимо простых арифметических операций сложения, вычитания, умножения и деления предоставляет возможность использования в выражениях поразрядных операций OR, XOR и AND. Обозначаются они соответственно символами @, ! и &. Поэтому, например, выражение 2@#80 (2 OR #80) примет значение #82 или 130, а запись %101!%110 (%101 XOR %110) после вычислений заменится числом %011 или 3.
ФОРМИРОВАНИЕ ИЗОБРАЖЕНИЯ В ПАМЯТИ
В данном разделе мы собираемся рассказать, пожалуй, о самом совершенном способе вывода спрайтов и пейзажа на экран. Разобравшись с приведенной здесь программой, вы поймете, как формируется игровое пространство в наиболее солидных фирменных игрушках, где общая площадь лабиринта, города или иной местности во многие десятки (а то и сотни) раз превышает тот клочок земли, который мы наблюдаем на экране.
Скажем несколько слов о принципе работы этой программы, о том, как создается протяженный пейзаж и как формируется изображение. В отличие от приведенных выше примеров здесь картинка не выводится сразу на экран, а строится в памяти. И только после того как наложен последний штрих, весь кадр быстро перебрасывается в видеобуфер как один спрайт. Мало того, что такой метод позволяет избежать мелькания картинок, здесь не нужно заботиться о восстановлении фона и размышлять на тему, как «подсунуть» один спрайт под другой. Строится изображение так: в предварительно очищенную область памяти, используемую в качестве так называемого «теневого» или виртуального окна, выводятся спрайты, занимающие самое «дальнее» положение, затем, последовательно приближаясь к ближнему плану, строится и все остальное изображение. Кстати, именно таким способом получена «трехмерная» графика в играх вроде ALIEN 8, KNIGHT LORE и им подобных.
В «серьезных» игровых программах «теневое» окно обычно занимает лишь немногим большее пространство, чем окно на экране, отмечающее игровое поле. Но вы прекрасно знаете, что, имея дело с окнами, приходится идти на определенные трудовые затраты. Обычно процедуры, выполняющие вывод в виртуальное окно, довольно хитроумны и отличаются весьма солидными размерами, поэтому поместить их в книге оказалось просто нереально. Чтобы выйти из положения и все же продемонстрировать такой прием, мы решили отойти от принципа экономии памяти и вместо окна использовали целый экран. Однако для программ, после трансляции которых исполняемый файл занимает меньше 30-35 килобайт, такое «расточительство», в общем, допустимо. Зато при правильном выборе адреса «теневого» экрана работа с ним практически не отличается от вывода в физический видеобуфер, поэтому к нему применимы все приведенные ранее процедуры лишь с несущественной доработкой.
Для облегчения всех расчетов виртуальный экран должен располагаться по «ровному» шестнадцатеричному адресу: #6000, #7000, #8000 и т. д. Тогда все вычисления будут такими же, как при работе с физическим экраном, только к полученному адресу нужно добавлять смещение (достаточно только разницу старших байтов). В нашей программе адрес «теневого» экрана равен #8000, следовательно, разность старших байтов будет #80-#40=#40. Зададим это смещение константой V_OFFS, чтобы при желании несложно было перенести виртуальный экран в любое другое место.
На рис. 7.4 показана часть игрового пространства, состоящего из протяженного пейзажа и микроавтобуса, проезжающего мимо домов. На переднем плане мелькают дорожные фонари. Выехав за пределы города, автомобиль останавливается на пару секунд, а затем начинает свое движение с исходной точки трассы. Просмотрев этот мультфильм несколько раз, вы обнаружите, что дома появляются не каким-то случайным образом, а занимают строго определенное положение на пути следования автомобиля. То есть в программе создается вполне конкретный пейзаж и каждый момент времени на экране виден только небольшой его фрагмент.
Поясним, как это достигается. В блоке данных, описывающих пейзаж (в программе он обозначен меткой D_LAND), указывается положение каждого дома (или другого объекта) на трассе, протяженность дома, его «удаленность» от дороги и адрес другого блока, задающего его внешний вид. Программа просматривает данные пейзажа и, исходя из положения автомобиля (он всегда выводится в центре окна), выбирает только те дома, которые целиком или частично попадают в окно экрана, то есть если правый угол дома не выходит за левую границу и левый — за правую, а остальные пропускает. После этого начинается вывод объектов в «теневой» экран: сначала дома, затем рисуется дальний тротуар, автомобиль, ближний тротуар и, наконец, фонари. Как только построение закончено, окно «теневого» экрана переносится в физический видеобуфер и тем самым изображение становится видимым.
Так как объем книги не безграничен, в программе заданы только два типа зданий, а протяженность пейзажа измеряется 200 знакомест. Но вы можете дополнить графику, составив из имеющихся спрайтов другие дома. Попробуйте увеличить пробег автомобиля, введите новые спрайты, изображающие деревья, решетки ограды, арки, попытайтесь создать загородный пейзаж. Для этого вам нужно будет только расширить блоки данных, формат которых подробно описан в тексте программы. Не бойтесь экспериментировать, и результат, уверены, порадует вас.
Несмотря на относительную сложность программы, в ней не встретится неизвестных доселе команд. Единственное нововведение — это процедура ПЗУ, находящаяся по адресу 8020, которая служит для проверки нажатия клавиши Break (Caps Shift/Space). Подпрограмма не требует никаких входных параметров и сообщает о том, что Break нажата установкой на выходе флага переноса. В противном случае выполняется условие NC.
До того, как мы приведем текст программы, обращаем ваше внимание, что вызывается она только из Бейсика, так как ассемблер GENS пользуется своим собственным внутренним стеком и поскольку он может оказаться в любом месте (все зависит от адреса загрузки GENS), то в результате «теневой» экран может перекрыть стек, а что из этого следует, догадаться нетрудно.
ORG 60000 ; Адрес «теневого» (или виртуального) экрана = #8000 V_OFFS EQU #40 ;старший байт смещения адреса ; «теневого» экрана относительно ; начального адреса физического экрана LD A,69 LD (23693),A XOR A CALL 8859 CALL 3435 LD A,2 CALL 5633 ;здесь необходима только для правильного ; вывода атрибутов основного экрана ; Инициализация переменных MAIN LD A,2 LD (X_LAMP),A LD HL,0 LD (X_LAND),HL CALL FRAME ;рамка вокруг окна MAIN1 CALL CLS_V ;очистка «теневого» экрана ; Формирование игрового поля в «теневом» экране CALL LAND ;вывод заднего плана (дома) CALL LINE1 ;рисование дальнего тротуара CALL PUTCAR ;автомобиль CALL LINE2 ;ближний тротуар CALL LAMPS ;фонари ; ---------------- CALL PUTVRT ;вывод окна виртуального экрана ; на физический CALL 8020 ;проверка нажатия клавиши Break RET NC ;выход из программы, если нажата LD HL,(X_LAND) ;изменение координаты автомобиля INC HL LD (X_LAND),HL LD DE,200 ;если пройдено расстояние AND A ; меньше 200 знакомест, SBC HL,DE JR NZ,MAIN1 ; то движение продолжается LD BC,100 ;иначе - пауза 2 сек. CALL 7997 JR MAIN ; и переход к началу пути ; Рисование горизонтальных линий белого цвета, изображающих тротуары LINE1 LD L,#40 LD H,#4A+V_OFFS JR LINE3 LINE2 LD L,#60 LD H,#4C+V_OFFS LINE3 PUSH HL ;адрес экрана здесь не рассчитывется, ; а задается в явном числовом виде LD D,H LD E,L INC DE LD (HL),255 ;сплошная линия (если изменить число, ; получится пунктирная) LD BC,31 LDIR ;проводим линию POP HL LD A,H ;расчет адреса атрибутов SUB V_OFFS AND #18 RRCA RRCA RRCA ADD A,#58+V_OFFS LD H,A LD D,H LD E,L INC DE LD (HL),7 ;INK 7, PAPER 0 LD BC,31 LDIR RET ; Вывод автомобиля в виртуальный экран PUTCAR LD BC,#90E ;координаты фиксированы (B = 9, C = 14) LD HL,SPR7 ;маска LD A,SPRAND ;по принципу AND CALL PTBL LD HL,SPR6 ;автомобиль LD A,SPROR ;по принципу OR JP PTBL ; Рисование в виртуальном экране дорожных фонарей LAMPS LD HL,X_LAMP LD C,(HL) ;горизонтальная координата самого левого ; на экране фонаря LD B,7 ;вертикальная координата фиксирована PUSH HL LAMPS1 LD HL,SPR9 ;маска LD A,SPRAND CALL PTBL LD HL,SPR8 ;фонарь LD A,SPROR CALL PTBL LD A,20 ;следующий через 20 знакомест ADD A,C LD C,A CP 28 JR C,LAMPS1 ;закончить вывод? POP HL DEC (HL) ;уменьшение координаты на 2 DEC (HL) RET P ;выход, если начальная координата ; не отрицательна LD (HL),18 ;иначе задаем начальную координату RET X_LAMP DEFB 0 ; Подпрограмма очистки виртуального экрана CLS_V LD H,#40+V_OFFS LD L,0 LD D,H LD E,L INC DE LD (HL),0 LD BC,6144 LDIR ; Атрибуты LD (HL),1 ;INK 1, PAPER 0 LD BC,767 LDIR RET ; Формирование изображения здания, скомбинированного из нескольких ; спрайтов (адрес соответствующего блока данных в паре HL) PUT_B LD A,(HL) ;количество блоков в доме INC HL PUT_B1 PUSH AF PUSH BC LD A,(HL) ;горизонтальная координата блока INC HL ADD A,C CP 2 ;проверка выхода за пределы окна JR C,PUT_B3 CP 29 JR NC,PUT_B3 LD C,A LD A,(HL) ;вертикальная координата блока INC HL ADD A,B ;проверка выхода за пределы окна JP M,PUT_B4 LD B,A LD E,(HL) ;адрес спрайта, изображающего блок INC HL LD D,(HL) INC HL PUSH HL EX DE,HL LD A,SPRPUT CALL PTBL ;выводим спрайт в «теневой» экран POP HL PUT_B2 POP BC POP AF DEC A JR NZ,PUT_B1 ;следующий блок RET PUT_B3 INC HL ;обход, если блок выходит за пределы окна PUT_B4 INC HL INC HL JR PUT_B2 ; Вывод помещающейся в окно части пейзажа X_LAND DEFW 0 ;координата автомобиля на трассе LAND LD HL,D_LAND ;блок данных, описывающих ; местоположение и внешний вид домов LAND1 LD BC,(X_LAND) LD E,(HL) ;горизонтальная координата дома на трассе INC HL LD D,(HL) INC HL LD A,D OR E RET Z ;0 - маркер конца блока данных LD A,(HL) ;протяженность дома в знакоместах INC HL PUSH HL PUSH DE EX DE,HL ;вычисляем координату дальнего конца дома ADD A,L LD L,A LD A,H ADC A,0 LD H,A INC HL AND A SBC HL,BC ;сравниваем с текущим положением ; автомобиля POP DE JR C,LAND3 ;если дом выходит за левый край окна LD HL,28 ADD HL,BC SBC HL,DE JR C,LAND3 ;если дом выходит за правый край окна EX DE,HL SBC HL,BC ;вычисление экранной координаты дома LD C,L POP HL LD B,(HL) ;вертикальная экранная координата дома INC HL LD E,(HL) ;адрес блока данных, описывающих дом INC HL LD D,(HL) PUSH HL EX DE,HL CALL PUT_B ;вывод изображения дома в «теневой» экран POP HL LAND2 INC HL JR LAND1 ;следующий дом LAND3 POP HL ;обход, если изображение дома не ; попадает в окно экрана INC HL INC HL JR LAND2 ; ВНИМАНИЕ! В процедуре PTBL есть изменения! PTBL SPRPUT EQU 0 SPROR EQU #B6 SPRAND EQU #A6 SPRXOR EQU #AE PUSH HL LD (MODE),A LD A,(HL) INC HL PUSH HL LD L,A LD H,0 LD E,L LD D,H ADD HL,HL ADD HL,DE POP DE ADD HL,DE EX DE,HL PTBL1 PUSH AF PUSH BC LD A,(HL) INC HL PUSH HL ADD A,B CP 24 JR NC,PTBL4 PUSH DE CALL 3742 POP DE ; +++ Добавление +++ LD A,V_OFFS ADD A,H LD H,A ; +++ EX (SP),HL LD A,(HL) EX (SP),HL ADD A,C CP 32 JR NC,PTBL4 ADD A,L LD L,A LD B,8 PUSH HL PTBL2 LD A,(DE) MODE NOP LD (HL),A INC DE INC H DJNZ PTBL2 POP BC LD A,B AND #18 SRA A SRA A SRA A ; +++ Изменение: вместо ADD A,#58 +++ ADD A,#58+V_OFFS ; +++ LD B,A POP HL INC HL LD A,(HL) DEC HL LD (BC),A PTBL3 POP BC POP AF INC HL INC HL DEC A JR NZ,PTBL1 POP HL RET PTBL4 LD HL,8 ADD HL,DE EX DE,HL POP HL JR PTBL3 ; Вывод окна «теневого» экрана на физический PUTVRT LD A,8 ;отступ сверху (в пикселях) LD B,12*8 ;высота окна (в пикселях) PUT_V1 PUSH AF PUSH BC LD C,4*8 ;отступ слева (в пикселях) CALL 8880 ;адрес физического экрана LD D,H ;сохраняем в DE LD E,L LD A,V_OFFS ADD A,H ;в HL - соответствующий адрес LD H,A ; «теневого» экрана LD BC,24 ;ширина окна - 24 знакоместа LDIR POP BC POP AF INC A ;следующий ряд пикселей DJNZ PUT_V1 ; Перенос атрибутов LD DE,#5824 ;адрес верхнего левого угла окна LD L,E ; в области атрибутов LD A,D ADD A,V_OFFS ;вычисляем соответствующий адрес LD H,A ; в «теневом» экране LD B,12 PUT_V2 PUSH BC LD BC,24 LDIR ;переносим 24 байта (по ширине окна) LD BC,8 ;переходим к следующей ADD HL,BC ; строке экрана (32-24 = 8) EX DE,HL ADD HL,BC EX DE,HL POP BC DJNZ PUT_V2 RET ; Рисование рамки вокруг окна ; (подпрограммы LABS1 и PRINT описаны в предыдущем разделе) FRAME LD IX,DFRAME ;Данные для построения рамки LABS1 ......... PRINT ......... ; Блоки дома: ; Окно 1 (с рамой) SPR1 DEFB 4 DEFB 0,0,65, 0,1,65, 1,0,65, 1,1,65 DEFB 255,193,193,193,255,193,193,193 DEFB 255,7,7,7,7,7,7,7 DEFB 193,193,193,255,255,255,255,255 DEFB 7,7,7,255,255,255,255,255 ; Окно 2 (без рамы) SPR2 DEFB 4 DEFB 0,0,65, 0,1,65, 1,0,65, 1,1,65 DEFB 255,252,252,252,252,252,252,252 DEFB 255,63,63,63,63,63,63,63 DEFB 252,252,252,255,255,255,255,255 DEFB 63,63,63,255,255,255,255,255 ; Окно с балконом SPR3 DEFB 4 DEFB 0,0,65, 0,1,65, 1,0,65, 1,1,65 DEFB 255,193,193,193,193,193,193,193 DEFB 255,7,7,7,7,7,7,7 DEFB 191,182,182,182,182,182,128,255 DEFB 251,219,219,219,219,219,3,255 ; Духовые окна на крыше дома SPR4 DEFB 4 DEFB 0,0,65, 0,1,65, 1,0,65, 1,1,65 DEFB 0,0,255,255,254,254,254,254 DEFB 0,0,255,255,127,127,127,127 DEFB 255,255,255,255,153,153,255,255 DEFB 255,255,255,255,153,153,255,255 ; Подвальные окна SPR5 DEFB 2 DEFB 0,0,65, 0,1,65 DEFB 255,255,255,255,240,255,255,255 DEFB 255,255,255,255,15,255,255,255 ; Автомобиль SPR6 DEFB 10 DEFB 0,0,66, 0,1,66, 0,2,66, 0,3,66, 0,4,66 DEFB 1,0,66, 1,1,66, 1,2,66, 1,3,66, 1,4,66 DEFB 0,15,24,48,56,62,63,57 DEFB 0,255,48,48,56,60,255,249 DEFB 0,255,48,48,56,60,255,249 DEFB 0,255,48,48,56,60,255,243 DEFB 0,192,96,32,32,48,248,248 DEFB 63,63,62,29,1,0,0,0 DEFB 255,255,239,183,240,224,0,0 DEFB 255,255,255,255,0,0,0,0 DEFB 255,255,247,237,15,7,0,0 DEFB 248,252,124,184,128,0,0,0 ; Маска для спрайта «автомобиль» SPR7 DEFB 10 DEFB 0,0,66, 0,1,66, 0,2,66, 0,3,66, 0,4,66 DEFB 1,0,66, 1,1,66, 1,2,66, 1,3,66, 1,4,66 DEFB 224,192,128,131,128,128,128,128 DEFB 0,0,0,135,129,0,0,0 DEFB 0,0,0,135,129,0,0,0 DEFB 0,0,0,135,129,0,0,0 DEFB 31,15,15,15,135,3,3,3 DEFB 128,128,128,128,192,252,254,255 DEFB 0,0,0,0,0,7,15,255 DEFB 0,0,0,0,0,255,255,255 DEFB 0,0,0,0,0,224,240,255 DEFB 1,1,1,1,3,63,127,255 ; Фонарный столб SPR8 DEFB 7 DEFB 0,0,70, 0,1,70, 0,2,70, 1,1,70 DEFB 2,1,70, 3,1,70, 4,1,70 DEFB 0,0,127,64,63,0,0,0 DEFB 0,0,239,146,17,16,16,16 DEFB 0,0,252,4,248,0,0,0 DEFB 16,16,16,16,16,16,16,16 DEFB 16,16,16,16,16,16,16,16 DEFB 16,16,16,56,56,56,56,56 DEFB 56,56,56,56,56,56,56,56 ; Маска для спрайта «фонарный столб» SPR9 DEFB 7 DEFB 0,0,70, 0,1,70, 0,2,70, 1,1,70 DEFB 2,1,70, 3,1,70, 4,1,70 DEFB 255,0,0,0,0,128,255,255 DEFB 255,0,0,0,0,68,199,199 DEFB 255,1,1,1,1,3,255,255 DEFB 199,199,199,199,199,199,199,199 DEFB 199,199,199,199,199,199,199,199 DEFB 199,199,131,131,131,131,131,131 DEFB 131,131,131,131,131,131,131,131 ; Данные для формирования зданий: ; 1-й байт - количество блоков в доме ; Далее следуют данные для каждого блока ; 1-й байт: горизонтальная координата блока в доме относительно ; верхнего левого угла изображения ; 2-й байт: вертикальная координата блока в доме ; 3-й и 4-й байты: адрес спрайта блока D_BLD1 DEFB 25 DEFW #000,SPR4,#002,SPR4,#004,SPR4,#006,SPR4,#008,SPR4 DEFW #200,SPR3,#202,SPR2,#204,SPR1,#206,SPR2,#208,SPR3 DEFW #400,SPR3,#402,SPR2,#404,SPR1,#406,SPR2,#408,SPR3 DEFW #600,SPR1,#602,SPR2,#604,SPR1,#606,SPR1,#608,SPR1 DEFW #800,SPR5,#802,SPR5,#804,SPR5,#806,SPR5,#808,SPR5 D_BLD2 DEFB 15 DEFW #000,SPR3,#002,SPR1,#004,SPR2 DEFW #200,SPR3,#202,SPR1,#204,SPR2 DEFW #400,SPR3,#402,SPR1,#404,SPR2 DEFW #600,SPR1,#602,SPR1,#604,SPR2 DEFW #800,SPR5,#802,SPR5,#804,SPR5 ; Данные для формирования пейзажа: ; 1-й и 2-й байты (слово): горизонтальная координата дома ; 3-й байт: протяженность дома в знакоместах ; 4-й байт: вертикальная координата дома ; 5-й и 6-й байты (слово): адрес данных для формирования дома D_LAND DEFW 1,#10A,D_BLD1,14,#FE0A,D_BLD1,25,#FE0A,D_BLD1 DEFW 39,#106,D_BLD2,46,#10A,D_BLD1,63,6,D_BLD2 DEFW 70,10,D_BLD1,90,#10A,D_BLD1,101,10,D_BLD1 DEFW 112,#FF0A,D_BLD1,123,#FE0A,D_BLD1,138,#106,D_BLD2 DEFW 145,#FE06,D_BLD2,152,#FF06,D_BLD2,159,6,D_BLD2 DEFW 166,#106,D_BLD2,178,#FE0A,D_BLD1,190,#FE06,D_BLD1 DEFW 0 ; Данные рамки вокруг окна (формат описан в предыдущем разделе) DFRAME DEFB 0,3,0,0, 1,4,0,24, 2,28,0,0 DEFB 3,3,1,12@#80, 3,28,1,12@128 DEFB 5,3,13,0, 1,4,13,24, 4,28,13,0 DEFB -1 ; Элементы рамки D_SYMB DEFB 127,213,159,191,253,182,248,181 DEFB 255,85,255,255,85,170,0,255 DEFB 254,83,249,245,121,181,121,181 DEFB 249,181,249,181,249,181,249,181 DEFB 249,117,249,245,81,169,3,254 DEFB 249,189,255,191,213,170,192,127