Как написать игру на ассемблере для ZX Spectrum/Глава 07

Материал из Emuverse
Версия от 17:31, 6 августа 2013; Panther (обсуждение | вклад)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)
Данный материал защищён авторскими правами!

Использование материала заявлено как добросовестное, исключительно для образовательных некоммерческих целей.

Автор: А. Евдокимов, А. Капульцевич, И. Капульцевич, ИД «Питер»

ГЛАВА СЕДЬМАЯ,
в которой вы научитесь создавать все элементы игрового пространства

Как вы уже знаете, игровое пространство составляют перемещающиеся спрайты, которые появляются и исчезают на экране во время игры, и неподвижный или медленно перемещающийся пейзаж. В предыдущих главах мы частично показали, каким образом можно создавать спрайты, используя привычные символы 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

Рассмотрим пример вывода спрайта произвольной конфигурации (рис. 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;
  • задайте окно, по размерам покрывающее этот спрайт и выполните скроллинг при помощи одной из четырех рассмотренных выше процедур. Спрайт довольно резво устремится в нужную сторону, так что в программе необходимо предусмотреть небольшую задержку.
Рис. 7.2. Перемещение спрайта скроллингом окна

Сразу же виден и недостаток этого способа, который состоит в том, что спрайт перемещается по экрану вместе с фоном, поскольку скроллинг захватывает все изображение в окне. Отсюда ясно, что применять такой метод можно лишь в тех случаях, когда фон как таковой отсутствует или, по крайней мере, мелкие детали не попадают в сдвигаемое окно. В качестве иллюстрации к сказанному приведем небольшую программку, которая плавно перемещает по экрану симпатичный паровозик, позаимствованный нами из спрайт-файла 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 показана часть игрового пространства, состоящего из протяженного пейзажа и микроавтобуса, проезжающего мимо домов. На переднем плане мелькают дорожные фонари. Выехав за пределы города, автомобиль останавливается на пару секунд, а затем начинает свое движение с исходной точки трассы. Просмотрев этот мультфильм несколько раз, вы обнаружите, что дома появляются не каким-то случайным образом, а занимают строго определенное положение на пути следования автомобиля. То есть в программе создается вполне конкретный пейзаж и каждый момент времени на экране виден только небольшой его фрагмент.

Рис. 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