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

         

Бегущая строка


Теперь покажем, как можно создать на ассемблере еще один эффект, который, несмотря на относительную простоту реализации, пользуется в игровых программах довольно большой популярностью. Вкниге [1] мы показывали, как добиться эффекта бегущей строки средствами Бейсика, однако интерпретатор не позволил нам добиться плавного движения букв. В ассемблере же можно без особого труда устранить этот недостаток, правда, для этого нам придется познакомиться еще с одним типом команд, выполняющих различные виды сдвигов битов в регистрах или в памяти.

Смысл сдвигов заключается в том, что все биты, не меняя своего относительного положения, смещаются вправо или влево. В зависимости от типа команды уходящие «за край» биты могут появляться с противоположной стороны (циклические сдвиги) либо теряться (нециклические или простые сдвиги). Например, после циклического сдвига влево числа 11001001 получится результат 10010011, а после простого сдвига вправо этого же значения - ?1100100. Знак вопроса на месте 7-го бита означает, что его значение зависит от результата предыдущей операции, а точнее, от состояния флага переноса CY; если он был установлен, то в 7-м бите появится единица, в противном случае - 0.

Существуют четыре команды для смещения битов в регистре A:

RLCA - циклический сдвиг влево. После выполнения этой команды старший бит переходит в младший и дублируется во флаге переноса CY (то есть он будет установлен, если 7-й бит перед выполнением команды был в 1 и сброшен, если 7-й бит имел нулевое значение).

RRCA - циклический сдвиг аккумулятора вправо. Эта команда в точности противоположна предыдущей: младший бит переходит в старший и повторяется во флаге CY.

RLA - это тоже циклический сдвиг, но не совсем обычный. В этой команде флаг переноса рассматривается как еще один дополнительный бит аккумулятора: 7-й бит перемещается в CY, а предыдущее значение флага переноса переносится в младший бит.

RRA - эта команда аналогична предыдущей с тем отличием, что движение битов происходит в обратном направлении, то есть слева направо.

На Рисунок 5.2 показаны схемы направлений перемещения битов во всех четырех перечисленных командах. После их выполнения все основные флаги (кроме CY, конечно) остаются без изменений, а во флагах N и H появляются нули.



Биты флагов



Рисунок 5.1. Биты флагов

Флаг нуля Z (zero) устанавливается в том случае, если в результате выполнения последней команды получился нулевой результат. Флаг переноса CY (carry) говорит о том, что в итоге арифметической операции или же после сдвига регистра произошел перенос. Например, при сложении чисел 254 и 2 должно получиться 256, но восьми разрядов регистров хватает только для чисел от 0 до 255, поэтому реально мы получим число 0, а старший девятый бит перейдет во флаг CY. Кроме того, после такой операции будет установлен и флаг нуля. Вообще же эти два флага наиболее важны и поэтому используются чаще других. В принципе, почти всегда можно обойтись только ими, лишь изредка и, скорее, для удобства прибегая к проверке остальных флагов.

Флаг четности/переполнения P/V (parity/overflow) - единственный, на который возложена индикация не одного, а сразу двух различных условий. Это возможно потому, что они относятся к двум принципиально различным типам команд и вместе никогда не встречаются. Флаг четности устанавливается, если в результате логической операции в байте оказалось установлено в 1 четное количество битов, а условие переполнения выполняется тогда, когда после арифметического действия изменился знак операнда.

Флаг S (sign) тоже достаточно важен и после флагов Z и CY используется наиболее часто. Он устанавливается, если при выполнении арифметической или логической операции получился отрицательный результат.

Остальные два флага - N (negative) и H (half-carry) используются довольно редко и преимущественно при работе с так называемыми двоично-десятичными числами.

Надо сказать, что далеко не все команды микропроцессора оказывают какое-то влияние на флаги, а некоторые команды изменяют лишь отдельные биты флагового регистра. Например, все описанные до сих пор типы команд, за исключением упомянутых INC и DEC, вообще на флаги не воздействуют.

По мере надобности мы еще будем возвращаться к вопросу о флагах, а сейчас продолжим прерванный разговор о циклах.



БЫСТРЫЙ ВЫВОД СПРАЙТОВ





БЫСТРЫЙ ВЫВОД СПРАЙТОВ

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



Цепочка окон



Рисунок 5.5. Цепочка окон

Программа состоит из двух основных частей. На первом этапе из блока данных считываются параметры окон и выполняются уже известные процедуры CLSV и SETV. В конце блока данных установлен байт со значением -1 (255), при считывании которого программа переходит ко второму этапу - формированию «бегущей строки». Такое решение позволяет легко изменять не только размеры, цвет и местоположение окон, но и добавлять другие или убирать лишние, что особенно важно при отладке программы. Текст «бегущей строки» дан в формате ASCIIZ, и это также дает возможность как угодно изменять его без коррекций в самой программе.

Несмотря на довольно большой размер программы, в ней не встретится никаких неизвестных команд, поэтому приводим ее текст лишь с краткими комментариями:

ORG 60000 ENT $ CALL SETSCR

; Вывод последовательности окон LD HL,COORD ;адрес блока данных параметров окон PW1 LD A,(HL) ;последовательное считывание параметров CP -1 ;проверка достижения конца блока данных JR Z,PW2 ;если да, переходим ко второму этапу INC HL LD (COL),A LD A,(HL) INC HL LD (ROW),A LD A,(HL) INC HL LD (LEN),A LD A,(HL) INC HL LD (HGT),A LD A,(HL) ;последний параметр - цвет окна INC HL RLCA ;сдвигаем на место атрибута PAPER RLCA RLCA OR 7 ;добавляем цвет INK 7 LD (ATTR),A PUSH HL PUSH BC XOR A OUT (254),A ;получаем «щелчок» LD BC,5 CALL 7997 ;PAUSE 5 LD A,16 OUT (254),A CALL CLSV ;вывод окна CALL SETV POP BC POP HL JR PW1 ;переход к следующему окну PW2 LD A,6 LD (23695),A ;устанавливаем временные атрибуты LD BC,#8780 ;B = 135, C = 128 LD HL,#3848 ;H = 56, L = 72 CALL BOX_0 ;прямоугольник вокруг последнего окна ; «Бегущая строка» в последнем окне LD HL,TEXT ;адрес текста «бегущей строки» PW3 LD A,22 ;AT 8,24 RST 16 LD A,8 RST 16 LD A,24 RST 16 LD A,16 ;INK 0 RST 16 XOR A RST 16 LD A,(HL) ;считываем очередной символ AND A ;дошли до конца? RET Z ;если да, то завершаем программу RST 16 ;выводим считанный символ на экран INC HL LD B,8 ;сдвигаем строку на 8 пикселей влево PW4 PUSH BC PUSH HL CALL SCROL ;скроллинг строки на 1 пиксель влево LD BC,1 CALL 7997 ;PAUSE 1 POP HL POP BC DJNZ PW4 JR PW3 ;переходим к выводу следующего символа SCROL LD HL,18448+8 ;заранее рассчитанный адрес экрана ; конца «бегущей строки» LD C,8 SCROL1 LD B,8 AND A PUSH HL SCROL2 RL (HL) DEC HL DJNZ SCROL2 POP HL INC H DEC C JR NZ,SCROL1 RET


COL DEFB 0 ROW DEFB 0 LEN DEFB 0 HGT DEFB 0 ATTR DEFB 0 ; Данные для всех окон. Параметры записаны в таком порядке: ; COL, ROW, LEN, HGT и последнее число - код цвета окна (PAPER) COORD DEFB 27,11,4,4,7 DEFB 26,12,4,4,4 DEFB 24,13,4,4,1 DEFB 23,15,4,4,2 DEFB 21,17,5,3,3 DEFB 19,18,5,3,5 DEFB 17,20,4,2,6 DEFB 14,21,4,2,2 DEFB 12,19,3,3,7 DEFB 10,18,3,3,1 DEFB 9,17,3,3,4 DEFB 7,15,4,4,3 DEFB 5,13,4,4,2 DEFB 4,12,4,4,5 DEFB 3,11,4,4,6 DEFB 1,8,5,5,1 DEFB 2,5,5,5,7 DEFB 3,4,5,5,2 DEFB 4,3,5,5,5 DEFB 6,2,5,5,3 DEFB 8,1,8,5,4 DEFB 11,0,6,5,6 DEFB 13,1,6,6,1 DEFB 15,3,7,6,3 DEFB 16,5,9,7,6 ; «Закрашивание» последнего окна DEFB 20,8,1,1,0 DEFB 19,7,3,3,0 DEFB 18,6,5,5,0 DEFB 17,6,7,5,0 DEFB 16,5,9,7,0 DEFB 255 ;------------------- TEXT DEFM "Sinclair Research Ltd. 1982" DEFM "······Program··W·I·N·D·O·W" DEFM "··*·Saint-Petersburg··1994··*" DEFM "·········" DEFB 0


ЧТО МОЖЕТ МИКРОПРОЦЕССОР Z80?



ЧТО МОЖЕТ МИКРОПРОЦЕССОР Z80?

Поскольку ассемблер непосредственно связан с машинными командами, то начинать его изучение будет резонно с вопроса «а что же может микропроцессор?» Так вот, если вы считаете, что он способен играть музыку, рисовать картинки или печатать текст, то глубоко заблуждаетесь. Ничего такого микропроцессор не умеет. Он может выполнять лишь самые элементарные действия вроде «2 + 2», а более сложным, таким как «2 ґ 2», его еще нужно научить. В этом и состоит задача программиста. Но у микропроцессора есть одно преимущество - за одну секунду он способен выполнить многие тысячи операций, поэтому в реальном времени он и кажется достаточно одаренным.

Вот краткий и не совсем полный список операций, доступных микропроцессору:

простейшие арифметические действия сложения и вычитания;

операции с памятью, такие как запись в определенную ячейку или считывание из памяти чисел (подобно POKE и PEEK в Бейсике);

связь с внешними устройствами через порты (то, чем занимаются в Бейсике OUT и IN);

обработка отдельных битов (разрядов двоичных чисел) (напомним, что в Бейсике можно задавать битовые константы с помощью ключевого слова BIN);

логические операции с двоичными числами;

различные вызовы других подпрограмм;

условные и безусловные переходы;

работа с прерываниями (это средство, совершенно недоступное Spectrum Бейсику, будет обсуждаться в отдельной главе).

Вам может показаться странным, что компьютер, располагая столь незначительными средствами, умудряется создавать на экране целые миры, исполнять сложные музыкальные произведения и даже управлять какими-то механизмами вроде принтера. Чтобы прояснить, как это получается, приведем маленький примерчик на Бейсике, построенный по тому же принципу, что и большинство программ в машинных кодах:

10 LET ADDR1=16384 20 LET ADDR2=15880 30 LET N=8 40 LET A=PEEK (ADDR2) 50 POKE (ADDR1),A 60 LET ADDR1=ADDR1+256 70 LET ADDR2=ADDR2+1 80 LET N=N-1 90 IF N<>0 THEN GO TO 40


Выполнив эту программку, вы увидите в верхнем левом углу экрана букву A. Наверное, нет большой необходимости подробно расписывать, как работает приведенный пример, тем не менее, кратко поясним, что же здесь происходит.

В переменную ADDR1 помещаем адрес (напоминаем, что адресом называется порядковый номер байта в памяти; в ZX Spectrum адреса имеют номера от 0 до 65535) начала экранной области памяти, а переменная ADDR2 указывает на начало данных, находящихся в ПЗУ и описывающих внешний вид символа A. В данном примере адрес ADDR2 рассчитан заранее, хотя обычно все вычисления возлагаются на программу. Далее в цикле последовательно считываются 8 байтов, составляющих символ, и переносятся на экран. При этом переменная ADDR1 изменяется с шагом 256, что обеспечивает заполнение одного знакоместа (чуть позже мы подробно остановимся на строении экрана и методах вычисления его адресов, а пока примите это как данность). Обратите внимание на способ организации цикла в этом примере. С точки зрения Бейсика вся эта программка выглядит довольно неказисто, но зато она довольно точно отражает последовательность действий микропроцессора при выполнении аналогичной задачи.

Вообще-то все на самом деле выглядит несколько сложнее, а здесь мы продемонстрировали лишь принцип работы одного из самых популярных операторов Бейсика - оператора PRINT. Но пусть вас это не пугает, ведь процедура вывода символов на экран уже имеется в компьютере, и совершенно не обязательно воспроизводить ее еще раз в своей собственной программе. Достаточно знать, как ее можно вызвать - и значительная часть проблем отойдет в сторону.

Однако, не будем забегать вперед, а прежде разберемся до конца с темой этой главы.


Действие процедуры BIGSYM



Рисунок 6.1. Действие процедуры BIGSYM

Приведем теперь текст программы BIGSYM, которая удваивает высоту одного символа, причем его код должен быть предварительно помещен в ячейку памяти по адресу 23296.

ORG 60000 ENT $ LD A,(23296) ;загружаем код печатаемого символа BIGSYM LD L,A LD H,0 ;переписываем этот код в HL ADD HL,HL ;умножаем код на 8 ADD HL,HL ADD HL,HL LD DE,(23606) ;в DE загружаем адрес начала ; текущего фонта ADD HL,DE EX DE,HL LD HL,(23684) ;в HL помещаем адрес в видеобуфере, ; по которому будет выводиться первый ; байт измененного символа LD B,4 PUSH HL ;делаем две копии HL, чтобы PUSH HL ; использовать их во втором цикле BIGS1 LD A,(DE) ;считываем байт из фонта LD (HL),A ;переписываем в видеобуфер INC H LD (HL),A ;еще раз - ниже INC H INC DE ;переходим к следующему байту DJNZ BIGS1 POP HL ;восстанавливаем HL LD BC,32 ;вычисляем адрес первого байта ADD HL,BC ; второго знакоместа LD B,4 BIGS2 LD A,(DE) ;аналогично циклу BIGS1 LD (HL),A INC H LD (HL),A INC H INC DE DJNZ BIGS2 POP HL ;восстанавливаем HL INC HL ;увеличиваем HL на 1 для подготовки ; печати следующего символа LD (23684),HL ;записываем в системную переменную ; адрес следующего знакоместа экрана RET

Обращаем ваше внимание на то, что процедура BIGSYM получилась не совсем универсальной. Сделано это с единственной целью упростить и сократить ее исходный текст. Применяя ее, нужно следить, чтобы символы при печати не выходили за пределы экрана ни по вертикали, ни по горизонтали. Кроме того, верхняя и нижняя половинки выводимых знаков не должны попадать в разные трети экрана, то есть не допускается позиционирование курсора на 7-ю и 15-ю строки экрана.

Чтобы посмотреть, как выглядит на экране целая строка удлиненных символов, введите небольшую программку на Бейсике. Само собой разумеется, что перед ее запуском ассемблерная программа должна быть оттранслирована.

10 PRINT AT 5,5; 20 LET a$="Starting program BIGSYM!" 30 FOR n=1 TO LEN a$ 40 POKE 23296,CODE a$(n) 50 RANDOMIZE USR 60000 60 NEXT n

Насладившись созерцанием новых букв, возможно, вы захотите воспользоваться процедурой BIGSYM в собственной программе. Впоследнем разделе этой главы, где приводится программа и описание многокадровой заставки, можно посмотреть, как это лучше сделать. Пока же можем сказать следующее: перед вызовом процедуры из ассемблера необходимо установить позицию печати в нужное место экрана, воспользовавшись командой RST 16, а в аккумулятор занести код выводимого символа. В этом случае надобность в команде LD A,(23296), предваряющей в приведенном примере процедуру BIGSYM, отпадает.



Действие процедуры DBLSYM



Рисунок 6.2. Действие процедуры DBLSYM

Поскольку работа этой процедуры не столь очевидна, как может показаться с первого взгляда, то для лучшего ее понимания мы приводим небольшую схемку (Рисунок 6.3), отражающую пути перемещений битов при многократном выполнении команд сдвигов. Напоминаем, что эти команды вместе с аналогичными схемами приведены в разделе пятой главы.



Динамическое окно


Продемонстрируем использование подпрограмм CLSV и SETV на примере довольно часто используемого способа вывода окна: оно появляется не внезапно, а как бы выплывает на поверхность экрана или приближается издалека. При этом изменяются не только размеры окна, но и его цвет.

Универсальная программа, создающая таким способом окно любого размера и в произвольном месте экрана, потребует достаточно сложных вычислений с привлечением не только операций сложения и умножения, но и деления. Поэтому, чтобы упростить задачу, напишем программу для конкретного окна. А чтобы вы могли лучше понять, как она работает, сначала сделаем ее в Laser Basic'е и только потом перепишем на ассемблере:

10 INK 5: PAPER 0: BORDER 0: CLS 20 FOR n=7 TO 1 STEP -1 30 LET m=7-n 40 .ROW=5+n:.COL=2+2*n 50 .HGT=2+2*m:.LEN=1+4*m 60 LET attr=8*n+64: POKE 23693,attr 70 .CLSV:.SETV 80 NEXT n

После этого можно обвести окно рамкой, а внутри что-нибудь написать, но это мы уже сделаем в ассемблерной программе.

Поскольку рамки вокруг окон рисуются довольно часто, прежде чем привести текст программы, напишем универсальную процедуру, выводящую в выбранном месте экрана прямоугольник произвольного размера. Этой процедурой мы еще воспользуемся впоследствии, поэтому ее также желательно иметь в отдельном библиотечном файле. Перед обращением к ней в регистрах B и C нужно задать координаты верхнего левого угла прямоугольника соответственно по вертикали и горизонтали, а в H и L - его высоту и ширину. Поскольку позже мы предложим более быстродействующую процедуру, то чтобы не возникло путаницы с именами, назовем ее BOX_0, а в имени другой процедуры изменим индекс.

BOX_0 PUSH HL CALL 8933 ;PLOT - верхний левый угол POP BC PUSH BC LD DE,#101 ;верхняя линия LD B,0 CALL 9402 ;DRAW POP BC PUSH BC LD D,-1 ;правая линия LD C,0 CALL 9402 POP BC PUSH BC LD E,-1 ;нижняя линия LD B,0 CALL 9402 POP BC LD DE,#101 ;левая линия LD C,0 CALL 9402 LD HL,10072 EXX RET

Если в приведенной процедуре вам что-нибудь не совсем ясно, вернитесь к четвертой главе и еще раз просмотрите раздел .


Используя подготовленные процедуры, можно написать программу «всплывающего» окна:

ORG 60000 ENT $ CALL SETSCR ;подготовка экрана DYNW LD B,7 DYNW1 LD A,7 ;вычисление промежуточной переменной ; для расчета размеров окна SUB B LD C,A LD A,B ;расчет переменной ROW ADD A,5 LD (ROW),A LD A,B ;расчет переменной COL ADD A,A ADD A,2 LD (COL),A LD A,C ;расчет переменной HGT ADD A,A ADD A,2 LD (HGT),A LD A,C ;расчет переменной LEN ADD A,A ADD A,A INC A LD (LEN),A LD A,B ;расчет байта атрибутов RLCA RLCA RLCA OR %01000000 ;64 - повышенная яркость LD (ATTR),A PUSH BC LD BC,3 CALL 7997 ;небольшая задержка перед выводом окна CALL CLSV ;очистка окна CALL SETV ;установка атрибутов окна POP BC DJNZ DYNW1 LD DE,D_ATTR ;установка атрибутов линий рамки LD BC,6 CALL 8252 LD BC,#7625 ;B = 118, C = 37 LD HL,#5EBE ;H = 94, L = 190 CALL BOX_0 ;рамка вокруг окна LD DE,TEXT LD BC,END-TEXT JP 8252 ;печать текста в окне

COL DEFB 0 ROW DEFB 0 LEN DEFB 0 HGT DEFB 0 ATTR DEFB 0 D_ATTR DEFB 16,0,17,1,19,1 TEXT DEFB 22,9,12,16,6,17,1,19,1 DEFM "Program" DEFB 22,11,8,16,4 DEFM "DYNAMIC··WINDOW" DEFB 22,15,8,16,5 DEFM "Saint-Petersburg" DEFB 22,17,14,16,3 DEFM "1994" END


ДИРЕКТИВЫ УСЛОВНОЙ ТРАНСЛЯЦИИ



ДИРЕКТИВЫ УСЛОВНОЙ ТРАНСЛЯЦИИ

Работая с GENS4, у вас есть возможность получать различные варианты исполняемого кода в зависимости от выполнения тех или иных условий. Достигается это включением в программу команд условной трансляции IF, ELSE и END, которые записываются в поле мнемоник (эти слова не относятся к зарезервированным и поэтому их можно использовать в качестве меток, но не макрокоманд). Общий вид текста программы при этом будет таким:

......... IF выражение команды_1 [ELSE команды_2] END .........

Команда ELSE и следующий за ней блок инструкций «команды_2» являются необязательной частью условной конструкции, поэтому в данном примере они заключены в квадратные скобки. Если значение выражения после команды IF истинно (то есть не равно нулю), то транслируется блок команд «команды_1» до ELSE или, если его нет, до END. В противном случае (если значение выражения равно нулю) ассемблируются «команды_2» после ELSE, конечно, если эта команда указана. После END трансляция текста протекает как обычно.

Часто эти команды используются для получения различных версий одной и той же программы, одна из которых, например, предназначена для работы на «обычном» Speccy, другая на ZX Spectrum 128 и т. п. Но, на наш взгляд, наиболее полезными они оказываются при написании макроопределений. В этом случае макрос можно составить таким образом, чтобы в зависимости от задаваемых в макрокоманде параметров получался максимально компактный код. Рассмотрим такой пример:

CHAN MAC IF =0 LD A,=0 ;если первый параметр не 0 ELSE XOR A ;если параметр равен 0 END CALL 5633 ENDM

Встретив в тексте макрокоманду CHAN, ассемблер обратится к одноименному макросу и в первую очередь проверит значение первого параметра =0. Если его величина отлична от 0 (условие истинно), то транслируется команда LD A,N, затем ассемблирование продолжается после команды END. В противном же случае, то есть если заданный параметр равен 0 (условие ложно), то обрабатываются команды после ELSE, в данном случае - XOR A и далее текст транслируется, как и в предыдущем варианте. Поэтому после трансляции макрокоманды


CHAN 2

получится последовательность инструкций

LD A,2 CALL 5633

а если задать

CHAN 0

то такая макрокоманда оттранслируется иначе:

XOR A CALL 5633

Приведем другой, более серьезный пример применения команд условной трансляции в макросах:

ORG 60000 UP EQU 1 DN EQU %10 RT EQU %100 LF EQU %1000 SCRL MAC PUSH BC LD HL,=1*256+=0 LD (COL),HL LD HL,=3*256+=2 LD (LEN),HL IF =4 & UP ;если 5-й параметр = UP CALL SCR_UP END IF =4 & DN ;если 5-й параметр = DN CALL SCR_DN END IF =4 & RT ;если 5-й параметр = RT CALL SCR_RT END IF =4 & LF ;если 5-й параметр = LF CALL SCR_LF END POP BC ENDM ; ------ LD B,16 SCRL1 SCRL 10,4,5,7,UP DJNZ SCRL1 LD B,16 SCRL2 SCRL 10,4,5,7,RT DJNZ SCRL2 LD B,16 SCRL3 SCRL 10,4,5,7,DN DJNZ SCRL3 LD B,16 SCRL4 SCRL 10,4,5,7,LF DJNZ SCRL4 RET

COL DEFB 0 ROW DEFB 0 LEN DEFB 0 HGT DEFB 0

В выражениях ассемблера GENS отсутствует знак равенства, но из этого затруднения можно выйти, если употребить поразрядную операцию «И» - AND, обозначаемую символом «амперсанд» (&), а в соответствующем параметре использовать отдельные биты, указывающие на различные действия. В приведенном макросе после определения графических переменных COL, ROW, LEN и HGT в зависимости от последнего параметра вызывается одна из четырех процедур скроллингов (напомним, что сами процедуры были описаны в 6-й главе). Как видите, благодаря командам условной трансляции стало возможно объединить их в одном макросе. В результате и текст программы заметно сократился и стал значительно более удобочитаемым. Правда, при этом несколько возрос размер исполняемого модуля, но этот недостаток также можно устранить, слегка доработав макрос. Например, можно добавить еще один условный блок в самом начале, в котором проверяется значение самого первого параметра и только если он не равен 0, транслируются команды определения переменных, а в противном случае они будут пропускаться.


«Длинные» циклы


Нередко могут понадобиться циклы с количеством повторений значительно больше256. На первый взгляд решение такой проблемы может показаться весьма тривиальным: достаточно использовать для размещения счетчика не отдельный регистр, а регистровую пару. Это так, но не совсем. Дело в том, что команды увеличения или уменьшения содержимого регистровых пар, оказывается, никак не влияют на флаги. Не получится использовать и операцию сравнения, разве только проверять регистры, составляющие пару, раздельно - сначала один, затем другой. Поэтому в таких случаях применяется совершенно иной подход.

Наиболее простой вариант состоит в проверке на обнуление регистровой пары: она будет содержать нулевое значение только в том случае, если оба ее регистра будут равны нулю. Иными словами, алгоритм такого цикла с использованием в качестве счетчика, для определенности, пары BC можно сформулировать так: «если регистр B не равен нулю ИЛИ регистр C не равен нулю, то перейти на начало цикла». Команда «ИЛИ» выглядит так же, как и в Бейсике - OR. Правда, в ассемблере она служит в основном совершенно для других целей, о чем мы обязательно расскажем. Но сначала приведем общий вид программы, иллюстрирующий «длинные» циклы:

LD BC,NN ;записываем в пару BC счетчик MET PUSH BC ;сохраняем ......... ;выполняем тело цикла POP BC ;восстанавливаем значение счетчика DEC BC ; и уменьшаем его на единицу LD A,B ;проверка условия завершения цикла OR C JR NZ,MET



Дуги


Как вы знаете, у оператора DRAW имеется возможность рисования не только отрезков прямых линий, но и фрагментов дуг, для чего кроме двух параметров относительного смещения нужно задать еще один- величину угла, образованного дугой. В ассемблере вы вполне можете воспроизвести и эту возможность, правда, описанная выше подпрограмма с такой задачей справиться не в состоянии. Для этого придется воспользоваться процедурой, «зашитой» по адресу 9108. Что же касается передачи параметров для нее, то здесь нужно поступить примерно так же, как и при печати чисел: последовательно положить три значения в стек калькулятора, а затем вызвать саму подпрограмму.

Как пример, приведем программку, соответствующую строке Бейсика

PLOT 100,80: DRAW 30,50,3

Для занесения чисел в стек калькулятора можно, конечно, воспользоваться уже известной подпрограммой 11563, но в данном случае нам не требуются числа, превышающие байтную величину (255), поэтому программа получится короче, если в стеке калькулятора размещать значения из аккумулятора, применив процедуру ПЗУ 11560, исходным данным для которой и является содержимое регистра A. Порядок действий будет совершенно таким же, как и при использовании подпрограммы 11563. Например, для занесения в стек калькулятора числа 123 можно написать такую последовательность инструкций:

LD A,123 CALL 11560

Зная это, можно написать такой фрагмент на ассемблере, соответствующий приведенной выше строке Бейсика:

ORG 60000 ENT $ LD BC,#5064 ;C = 100 (#64), B = 80 (#50) CALL 8933 ;PLOT 100,80 LD A,30 ;заносим в стек калькулятора CALL 11560 ; первый параметр LD A,50 ;второй параметр CALL 11560 LD A,3 ;третий параметр CALL 11560 CALL 9108 ;DRAW 30,50,3 LD HL,10072 ;восстанавливаем значение пары HL' EXX ; для нормального выхода в Бейсик RET



Фазы движения человечка



Рисунок 5.6. Фазы движения человечка

Прежде всего закодируем отдельные элементы двух изображений человечка, воспользовавшись, как и в предыдущей главе, определяемыми пользователем символами UDG. В результате кодирования получится блок данных, первому байту которого присвоим метку UDG:

UDG DEFB 0,0,3,4,102,68,35,62 ;A (144) DEFB 0,0,192,32,166,34,204,56 ;B (145) DEFB 31,15,7,7,3,7,6,6 ;C (146) DEFB 112,96,192,96,192,224,224,224 ;D (147) DEFB 6,6,6,4,8,0,0,0 ;E (148) DEFB 96,96,96,32,16,0,0,0 ;F (149) DEFB 3,4,6,4,195,236,127,55 ;G (150) DEFB 192,32,160,32,195,55,126,108 ;H (151) DEFB 7,7,7,14,28,12,6,14 ;I (152) DEFB 224,96,224,112,56,48,96,112 ;J (153) DEFB 8,16,108,254,190,158,78,60 ;Яблоко (154) DEFB 17,85,255,0,0,0,0,0 ;Трава (155)

Справа от каждой строки указаны коды, которые понадобятся нам для составления трех других блоков данных. Первый из них с меткой PEJZ мы используем для создания примитивного пейзажа, состоящего, как мы уже говорили, из зеленой травы и яблока (маленькую ветку проще получить программным путем).

PEJZ DEFB 22,13,13,16,4 DEFB 155,155,155,155,155,155 DEFB 22,8,15,16,2,17,0 DEFB 154

Можно заметить, что кроме кодов UDG в блок включены также и управляющие символы, устанавливающие позицию печати и изменяющие цвета спрайтов.

Второй и третий блоки - это как раз те два спрайта, которые должны попеременно появляться на вашем экране, создавая иллюзию движения:

SPR1 DEFB 6 DEFB 0,0,7,144,0,1,7,145 DEFB 1,0,7,146,1,1,7,147 DEFB 2,0,7,148,2,1,7,149 SPR2 DEFB 6 DEFB 0,0,0,32,0,1,0,32 DEFB 1,0,7,150,1,1,7,151 DEFB 2,0,7,152,2,1,7,153

Для вывода этих спрайтов на экран можно было бы, как и раньше, воспользоваться подпрограммой 8252, однако основная идея применения спрайтов заключается в том, чтобы их можно было легко перемещать по экрану. Но поскольку спрайт обычно состоит из нескольких знакомест, которые имеют строго фиксированное положение внутри него, то при использовании процедуры 8252 пришлось бы перекодировать координаты всех составляющих его фрагментов. Сами понимаете, что это не слишком удобно, особенно если спрайт состоит из десятка-другого знакомест. Это означает, что для вывода спрайтов на экран необходимо создать самостоятельную процедуру, которую мы назовем PUT.


Теперь следует сказать несколько слов о построении блоков данных SPR1 и SPR2, поскольку они тесно связаны с подпрограммой PUT и их формат, естественно, должен быть согласован с ее работой. Опишем их структуру. Первый байт соответствует количеству фрагментов, входящих в спрайт (в нашем примере как для первого, так и для второго спрайта - это число 6). Далее следуют параметры для каждого фрагмента, занимающие по 4 байта:

1-й байт - относительная вертикальная координата данного знакоместа в спрайте; 2-й байт - относительная горизонтальная координата данного знакоместа в спрайте; 3-й байт - байт суммарных атрибутов знакоместа; 4-й байт - код символа соответствующего фрагмента спрайта.

Поскольку подпрограмма PUT нам потребуется не только в данном примере, сохраните ее в виде отдельного библиотечного файла. Она имеет следующий вид:

PUT LD E,(HL) ;считываем количество фрагментов спрайта PUT1 INC HL LD A,22 RST 16 LD A,B ;регистр B содержит координату ROW ADD A,(HL) ;прибавляем к ней относительную ; координату внутри спрайта RST 16 INC HL LD A,C ;в C - горизонтальная координата COL ADD A,(HL) ;складываем ее с относительной ; координатой, взятой из блока данных RST 16 INC HL LD A,(HL) ;считываем байт атрибутов знакоместа LD (23695),A INC HL LD A,(HL) ;берем код выводимого символа RST 16 DEC E JR NZ,PUT1 ;переходим к выводу следующего ; фрагмента RET

Перед вызовом этой процедуры необходимо в регистровой паре HL указать начальный адрес блока данных, соответствующего выводимому спрайту, а в регистрах B и C задать координаты экрана по вертикали и горизонтали.

Наконец соберем все части программы в единое целое, добавив несколько уже знакомых процедур, и напишем небольшую часть, управляющую движением человечка:

ORG 60000 ENT $ CALL SETSCR LD HL,UDG LD (23675),HL CALL FON CYCLE LD HL,SPR1 ;вывод спрайта 1 LD BC,#A0F ;B = 10, C = 15 CALL PUT CALL PAUSE ;небольшая задержка LD HL,SPR2 ;вывод спрайта 2 LD BC,#A0F CALL PUT CALL PAUSE ;снова задержка ; Выход из цикла при нажатии клавиши Space



LD A,(23560) ;системная переменная LAST_K, в которой ; хранится код последней нажатой клавиши CP " " JR NZ,CYCLE ;повтор RET ; Задержка (PAUSE 12) PAUSE LD BC,12 JP 7997 ; Рисование пейзажа FON LD DE,PEJZ LD BC,19 CALL 8252 EXX PUSH HL LD A,6 LD (23695),A LD BC,#7078 ;B = 112, C = 120 CALL 8933 LD DE,#101 ;D = 1, E = 1 LD BC,10 CALL 9402 LD BC,#757D ;B = 117, C = 125 CALL 8933 LD E,#FF01 ;D = -1, E = 1 LD BC,#F0F ;B = 15, C = 15 CALL 9402 POP HL EXX RET

Эффект движения здесь создается с помощью простого цикла, начинающегося с метки CYCLE, в теле которого по очереди выводятся то первый спрайт, изображающий стоящего человечка, то второй, показывающий человечка в прыжке. Скорость смены фаз движения регулируется подпрограммой PAUSE.


ФОРМИРОВАНИЕ ИЗОБРАЖЕНИЯ В ПАМЯТИ



ФОРМИРОВАНИЕ ИЗОБРАЖЕНИЯ В ПАМЯТИ

В данном разделе мы собираемся рассказать, пожалуй, о самом совершенном способе вывода спрайтов и пейзажа на экран. Разобравшись с приведенной здесь программой, вы поймете, как формируется игровое пространство в наиболее солидных фирменных игрушках, где общая площадь лабиринта, города или иной местности во многие десятки (а то и сотни) раз превышает тот клочок земли, который мы наблюдаем на экране.

Скажем несколько слов о принципе работы этой программы, о том, как создается протяженный пейзаж и как формируется изображение. В отличие от приведенных выше примеров здесь картинка не выводится сразу на экран, а строится в памяти. И только после того как наложен последний штрих, весь кадр быстро перебрасывается в видеобуфер как один спрайт. Мало того, что такой метод позволяет избежать мелькания картинок, здесь не нужно заботиться о восстановлении фона и размышлять на тему, как «подсунуть» один спрайт под другой. Строится изображение так: в предварительно очищенную область памяти, используемую в качестве так называемого «теневого» или виртуального окна, выводятся спрайты, занимающие самое «дальнее» положение, затем, последовательно приближаясь к ближнему плану, строится и все остальное изображение. Кстати, именно таким способом получена «трехмерная» графика в играх вроде ALIEN 8, KNIGHT LORE и им подобных.

В «серьезных» игровых программах «теневое» окно обычно занимает лишь немногим большее пространство, чем окно на экране, отмечающее игровое поле. Но вы прекрасно знаете, что, имея дело с окнами, приходится идти на определенные трудовые затраты. Обычно процедуры, выполняющие вывод в виртуальное окно, довольно хитроумны и отличаются весьма солидными размерами, поэтому поместить их в книге оказалось просто нереально. Чтобы выйти из положения и все же продемонстрировать такой прием, мы решили отойти от принципа экономии памяти и вместо окна использовали целый экран. Однако для программ, после трансляции которых исполняемый файл занимает меньше 30-35 килобайт, такое «расточительство», в общем, допустимо. Зато при правильном выборе адреса «теневого» экрана работа с ним практически не отличается от вывода в физический видеобуфер, поэтому к нему применимы все приведенные ранее процедуры лишь с несущественной доработкой.


Для облегчения всех расчетов виртуальный экран должен располагаться по «ровному» шестнадцатеричному адресу: #6000, #7000, #8000 и т. д. Тогда все вычисления будут такими же, как при работе с физическим экраном, только к полученному адресу нужно добавлять смещение (достаточно только разницу старших байтов). В нашей программе адрес «теневого» экрана равен #8000, следовательно, разность старших байтов будет #80-#40=#40. Зададим это смещение константой V_OFFS, чтобы при желании несложно было перенести виртуальный экран в любое другое место.

На Рисунок  7.4 показана часть игрового пространства, состоящего из протяженного пейзажа и микроавтобуса, проезжающего мимо домов. На переднем плане мелькают дорожные фонари. Выехав за пределы города, автомобиль останавливается на пару секунд, а затем начинает свое движение с исходной точки трассы. Просмотрев этот мультфильм несколько раз, вы обнаружите, что дома появляются не каким-то случайным образом, а занимают строго определенное положение на пути следования автомобиля. То есть в программе создается вполне конкретный пейзаж и каждый момент времени на экране виден только небольшой его фрагмент.


Формирование огибающей



Рисунок 10.4. Формирование огибающей

Другая возможность касается «ударных инструментов». Их звучание также можно варьировать в некоторых пределах. Выберите в системном меню опцию SET PRESET NOISE VALUES, нажав клавишу 1. На экране вы увидите таблицу, показанную на Рисунок 10.5. В первой графе указан порядковый номер эффекта, а во второй - клавиша, за которой этот эффект закреплен в режиме редактирования мелодии. Нажав соответствующую цифровую клавишу, вы сможете изменить другие параметры, обозначенные в таблице. Сначала появится запрос об установке нового значения для графы FREQUENCY (частота), на который нужно ввести число от 0 до 31. Затем вводится номер огибающей (клавиши 1...8), помещаемый в графу ENVELOPE. Если вы хотите задать звук постоянной громкости, введите на этот запрос 0. В этом случае компьютер попросит указать уровень звука (графа VOLUME) вводом шестнадцатеричного числа от 0 до F.



Формирование окна


Прежде всего нам нужно разобраться, каким образом задаются окна. Для этого необходимо знать, какие параметры требуются для их описания.

В простейшем случае (и если вам доводилось работать с Laser Basic'ом, то это должно быть хорошо известно) достаточно определить четыре переменные: ROW - позиция по вертикали верхнего края окна, COL - горизонтальная координата левого края окна, LEN - ширина окна и HGT - его высота. Чаще все четыре параметра задаются в знакоместах, но иногда (как, например, в редакторе Art Studio) - в пикселях, правда, реализовать такие окна несравненно сложнее.

Если вы хотите иметь возможность заключать окна в рамки, причем с индивидуальным рисунком для каждого из них, то потребуется еще один параметр - тип рамки. Он может задаваться символом, содержащим рисунок рамки, просто ее порядковым номером или каким-то другим способом, а ноль, например, будет говорить об отсутствии окаймления.

Большее количество переменных может понадобиться для определения окон со сложной внутренней структурой. Например, окно может иметь собственное название, заголовок, выделяемый цветом и постоянно присутствующий в окне. Такие типы окон часто можно встретить в меню игровых (MICRONAUT ONE) или прикладных (Art Studio) программ (Рисунок  5.4). Кроме того, окно может быть как бы приподнятым над общим фоном экрана и отбрасывать на него «тень».



ФУНКЦИЯ ПОИСКА/ЗАМЕНЫ



ФУНКЦИЯ ПОИСКА/ЗАМЕНЫ

Пока вы имеете дело с небольшими обрывками программ, что-то подправить в них не представляет особого труда. Но попробуйте-ка перетряхнуть огромный текст, состоящий из многих сотен инструкций и найти в нем нужное место. Вряд ли это занятие доставит вам большое удовольствие, а когда вы наконец отыщите заветную строчку, охота заниматься с программой может и вовсе улетучиться. Чтобы уберечь своих пользователей от нервного истощения, вызванного такой работой, фирма HiSoft включила в редактор GENS команду, предназначенную для поиска нужной последовательности символов, да еще и с возможностью замены ее на другую. Эта команда записывается так:

F[номер начальной строки],[номер конечной строки], [текст для поиска][,текст для замены]

Как и раньше, квадратные скобки указывают на необязательность параметров. Если какой-то из них не задан, то он берется из последней введенной команды. Вообще же лучше на всякий случай перечислять все элементы команды, особенно при замене текста.

Предположим, вам нужно в тексте программы найти все места, где встречается метка LABEL. Введите в редакторе команду

F1,20000,LABEL

Как только функция найдет последовательность символов, совпадающую с заданной (LABEL), на экране появится строка текста, содержащая эту последовательность, и GENS перейдет в режим редактирования. При этом курсор для удобства автоматически устанавливается в самое начало найденного текста. После этого у вас есть два варианта дальнейших действий: либо закончить поиск, нажав клавишу Enter, либо продолжить его, для чего нет надобности набирать команду заново, а достаточно нажать клавишу F. Естественно, вам ничто не мешает сразу же внести в текст какие-то изменения, но если они должны быть везде однотипными, проще задать в команде F также и последний параметр. Например, чтобы заменить все имена LABEL на METKA введите в редакторе строку

F1,20000,LABEL,METKA

Внешне поведение функции при этом не изменится: при нахождении первого имени строка точно так же будет вызываться на редактирование, а курсор указывать на первый символ слова LABEL. Вы так же можете продолжать вносить изменения вручную и по желанию продолжать поиск без изменений текста или прервать выполнение команды. Но теперь у вас появилась и иная возможность. Если нажать клавишу S, то слово LABEL мгновенно заменится словом METKA, а на экране появится следующая найденная строка.



из которой вы узнаете, как



ГЛАВА ДЕВЯТАЯ,

из которой вы узнаете, как подсчитать число заработанных очков и вообще оценить состояние игры

В этой главе мы коснемся наиболее часто встречающихся типов оценки игровой ситуации, таких как подсчет очков (жизней, боеприпасов, сбитых самолетов) и контроля времени, а также рассмотрим некоторые приемы их применения на конкретных примерах игровых программ. Поскольку получение оценки немыслимо без различных математических действий, то здесь же приводятся процедуры умножения, деления, извлечения квадратного корня как для целых чисел с учетом знака, так и для дробных. Впоследнем случае уже не обойтись без обращения к калькулятору.
Сразу скажем, что оценка игровой ситуации не сводится к одним лишь только математическим расчетам. На самом деле не вся оценочная информация может выводиться на экран в виде чисел, а кое-что остается, так сказать, для «внутреннего пользования» самой программе, которая следит за развитием событий и соответствующим образом себя ведет. Например, в игре Tetris при завершении очередного ряда он должен автоматически удаляться, а все ряды выше «списанного» опускаться вниз. Это можно назвать уже не количественной, а качественной оценкой, используемой самой программой.

о некоторых дополнительных возможностях ассемблера



ГЛАВА ОДИННАДЦАТАЯ,

рассказывающая о некоторых дополнительных возможностях ассемблера GENS4

Во второй главе были перечислены лишь самые необходимые команды редактора GENS. До сих пор этого было вполне достаточно для ввода, редактирования и трансляции процедур и фрагментов программ, предложенных в книге. Но когда вы начнете писать свои собственные игры, они наверняка окажутся значительно больших размеров. Возможно даже, что исходные тексты в несколько раз превзойдут по объему всю имеющуюся в наличии память и их придется разбивать на части. Ивот тогда вы почувствуете, что описанных возможностей GENS явно маловато. Поэтому в данной главе мы приводим описание некоторых других весьма полезных команд редактора, а также способы сокращения исходного текста и придания ему большей наглядности.

в которой обсуждается структура игровой


в которой обсуждается структура игровой программы

Если вы удостоили вниманием первую книгу из серии «Как написать игру», то о структуре игровой программы знаете уже более чем достаточно и не много потеряете, пропустив эту главу. Но учитывая интересы тех, кто упомянутую книгу даже в руках не держал, мы все же повторимся немного и поясним некоторые термины, которыми будем в дальнейшем пользоваться.
Несмотря на несметное количество существующих компьютерных игр различного уровня сложности и жанра, все же есть в них что-то общее, поддающееся классификации. Если проанализировать любую из них от начала загрузки до появления на экране сообщения GAME OVER (игра окончена), то можно выделить несколько частей, которые, хотя и связаны между собой в единое целое какими-то общими переменными и данными, но тем не менее, выполняют в игре вполне самостоятельные функции. Это позволяет разрабатывать каждую из частей независимо от других (однако, не забывая о связях с остальными частями), что в значительной степени упрощает создание игровой программы в целом. Окаких же частях идет речь? Это:
заставка;
игровое пространство;
блок взаимодействия с играющим;
блок оценки игровой ситуации;
блок звукового сопровождения игры.
Одна из основных задач этой книги состоит именно в том, чтобы научить вас реализовывать в ассемблерных программах каждую из перечисленных выше частей, но вначале посмотрим, что же они собой представляют, какие функции в игровых программах выполняют и какими примерно они должны быть.

Как вы понимаете, никакая игра,



ГЛАВА ПЯТАЯ,

в которой изображения становятся подвижными

Как вы понимаете, никакая игра, даже с очень изысканной графикой, не станет достаточно интересна, если все изображения на экране будут неподвижны и персонажи не будут подавать признаков жизни. Для «оживления» картинок в программировании используется тот же принцип, что и в мультипликации: в одно и то же место экрана последовательно помещаются слегка отличающиеся друг от друга изображения, показывающие разные фазы движения одного объекта.
В этой главе мы объясним, как заставить двигаться сначала отдельные символы, а затем и целые спрайты. Ав завершение напишем полноценную динамическую заставку, которую в слегка измененном виде вполне можно использовать в какой-нибудь конкретной игре.
Как вы знаете, в программах на Бейсике для получения эффекта движения обычно используются циклы FOR...NEXT и операторы переходов GO TO и GO SUB, а также условный оператор IF...THEN, поэтому прежде всего нам необходимо выяснить, какие средства имеются в языке ассемблера для получения тех же результатов.

в которой вы научитесь создавать



ГЛАВА СЕДЬМАЯ,

в которой вы научитесь создавать все элементы игрового пространства

Как вы уже знаете, игровое пространство составляют перемещающиеся спрайты, которые появляются и исчезают на экране во время игры, и неподвижный или медленно перемещающийся пейзаж. Впредыдущих главах мы частично показали, каким образом можно создавать спрайты, используя привычные символы UDG и средства ассемблера. Однако такой способ пригоден лишь для небольших изображений, да и то, если их общая площадь не превышает двух десятков знакомест. Теперь пора нам подробно познакомиться с общим случаем, когда размеры спрайтов могут быть практически любыми, а их количество ограничено лишь сюжетом игры, вашей фантазией и трудолюбием. Есть и другая, не менее важная задача - создание таких процедур вывода спрайтов на экран и рисования пейзажей, которые бы требовали на это минимального времени и, желательно, были не слишком громоздкими.

в которой демонстрируются возможности ассемблера



ГЛАВА ШЕСТАЯ,

в которой демонстрируются возможности ассемблера на примере создания многокадровых заставок

Когда в первой главе мы рассматривали основные части игровой программы, то в общих чертах показали, как строится многокадровая заставка и какие функции должны выполнять отдельные ее элементы. Теперь настало время всерьез поговорить о том, как реализовать эти элементы в виде законченных подпрограмм, а затем собрать их в единое целое для получения готовой заставки. Конечно, трудно дать ответы на все вопросы, с которыми вы можете столкнуться при решении данной задачи, поэтому мы решили ограничиться только теми проблемами, которые ранее не рассматривались совсем. Среди них- создание простейших звуковых эффектов, преобразование символов стандартного набора таким образом, чтобы надписи не стыдно было поместить на самое видное место экрана, скроллинги окон с графическими изображениями и текстами во всех направлениях, а также включение в программу элемента случайности. После того, как все это будет подробно рассмотрено, станет возможно, наконец, непосредственно заняться многокадровой заставкой.

Существует несколько версий программы GENS,



ГЛАВА ТРЕТЬЯ,

обучающая вводу и редактированию игровых программ в ассемблере GENS4

Существует несколько версий программы GENS, но для определенности мы будем рассматривать GENS4. Тем не менее, почти все, о чем пойдет речь в нашей книге, справедливо и для других версий ассемблера фирмы HISOFT, поэтому мы не всегда станем уточнять версию, а чаще будем писать просто GENS.

о том, как управлять ходом



ГЛАВА ВОСЬМАЯ,

в которой рассказывается о том, как управлять ходом игры

Одним из главных достоинств компьютерных игр, как вы сами прекрасно знаете, является возможность играющего влиять на события, разворачивающиеся перед ним на экране. Это влияние можно реализовать по-разному, например, нажимая определенные клавиши клавиатуры или наклоняя в ту или иную сторону ручку джойстика и нажимая кнопку «огонь». При этом в большинстве игр предусматривается выбор любого вида управления, который обычно осуществляется через «Меню». Настало время и нам рассмотреть эти вопросы с разных точек зрения, а именно: как управлять спрайтами с помощью клавиатуры или джойстика и при этом учесть ограничения, обусловленные размерами экрана, как определить, подключен к компьютеру джойстик или нет, как организовать управление игрой, если играющих двое, и некоторые другие.

о том, что же такое



ГЛАВА ВТОРАЯ,

из которой вы узнаете о том, что же такое ассемблер и чем он отличается от Бейсика и машинных кодов, а также усвоите некоторые основные понятия

Прежде чем приступать к изучению ассемблера, нужно усвоить несколько новых терминов, чтобы понимать, о чем вообще идет речь. Эту главу можно считать введением в совершенно новый для вас язык программирования (если же вы знакомы хотя бы с азами ассемблера, то можете лишь бегло пролистать эту и следующую главы, ибо они предназначены, в первую очередь, для новичков, делающих первые шаги в изучении машинного языка).

как сделать надпись на экране



ГЛАВА ЧЕТВЕРТАЯ,

показывающая, как сделать надпись на экране и создать простейшие изображения

Получив из предыдущей главы некоторое представление о структуре ассемблерной строки, редакторе GENS и обо всем прочем, что совершенно необходимо для работы с ассемблером, можно, наконец, приступать к программированию. С чего же начать? Наверное, мы не сильно ошибемся, если предположим, что первейшее желание любого, начинающего изучать новый язык - это получить что-то на экране. Пусть это будет всего лишь какая-нибудь надпись, или даже просто одна-единственная буква.
Именно с таких простых действий мы и начнем наши опыты в программировании, постепенно усложняя задания и изучая все новые и новые команды ассемблера. На первых порах вы должны научиться выводить в определенное место экрана символы и числа, причем с заранее заданными атрибутами, уметь ставить точки, проводить линии, дуги и окружности. Освоив «джентльменский» набор команд и приемов программирования, можно попытаться создать на экране нечто полезное, например, текст, заключенный в рамку или один из кадров заставки с надписями как русскими, так и латинскими буквами.

в которой показано, как заставить



ГЛАВА ДЕСЯТАЯ,

в которой показано, как заставить компьютер звучать по вашим нотам

Как вы понимаете, никакая игра не сможет называться полноценной, если она будет протекать при гробовой тишине. Посему получение различных акустических эффектов и написание хотя бы простейшей компьютерной музыки является достаточно важным этапом создания игровой программы. Из данной главы вы узнаете, какими средствами достигаются эти цели, получите представление о способах вывода звука как в стандартный канал ZX Spectrum, так и о работе с трехканальным музыкальным сопроцессором AY-3-8912, которым снабжен компьютер Spectrum 128.

ИГРОВОЕ ПРОСТРАНСТВО



ИГРОВОЕ ПРОСТРАНСТВО

Игровое пространство мы ранее (см.[1]) определили как совокупность всех подвижных и неподвижных объектов, появляющихся на экране во время игры. Однако во многих программах игровое пространство имеет значительную протяженность и в каждый момент времени можно видеть лишь небольшую его часть, в то время как все остальное остается за кадром. Поэтому иногда мы будем пользоваться и другим термином - игровое поле. Это как раз и есть тот фрагмент игрового пространства, который можно наблюдать на экране.

Попытаемся теперь разобраться в вопросе о том, из каких частей состоит игровое пространство и, в самых общих чертах, как они создаются. Заметим, что более подробно эти вопросы будут рассмотрены в соответствующих главах.

В большинстве случаев игровое пространство может быть разбито на три существенно отличающиеся части. К первой отнесем все неподвижные элементы изображения, имитирующие небо, землю, воду, космическое пространство со звездами и планетами, а также «мелкие» объекты: здания, деревья, мосты, лабиринты и т. д. В дальнейшем все это для краткости будем называть пейзажем. Пример простейшего пейзажа - шахматная доска в игре CHESS. А в таких программах как SABOTEUR, DIZZY или ROBOCOP пейзажи представляют собой настоящие произведения искусства (Рисунок  1.3). Они состоят из большого количества различных предметов и выглядят весьма правдоподобно.



Вторую часть составляют подвижные объекты,



Рисунок 1.3. Игровое пространство программы DIZZY4

Вторую часть составляют подвижные объекты, которые перемещаются сами по себе и на которые вы не можете оказывать непосредственного воздействия. Как правило, к этой группе относятся изображения ваших противников, например, силуэты вертолетов, кораблей и самолетов в игре RIVER RAID или проносящиеся мимо вас мотоциклы в SPEED KING2.

Наконец, к третьей группе объектов игрового пространства относится одно (а реже - несколько, как в игре CHESS) изображение, действиями которого вы можете управлять с помощью клавиатуры или джойстика. Так, в игре LEGEND OF KAGE это неустрашимый герой, преодолевающий множество препятствий и ведущий бескомпромиссную борьбу с многочисленными врагами-ниндзя. Персонаж игр серии BOULDER DASH - маленький забавный человечек, бегающий по головоломным лабиринтам и собирающий алмазы, за которым охотятся страшные бабочки, живые «бомбы» и кровожадные челюсти. Наконец, в играх OPERATION WOLF, STAR RADIERS II и во многих других «милитаристских» играх управляемым элементом является всего лишь перекрестие прицела.

Таким образом, если обобщить сказанное, то становится понятно, что любое сложное изображение на экране можно представить в виде набора небольших по размерам и вполне законченных объектов. Некоторые из этих объектов могут двигаться, а основная их масса составляет фоновый рисунок, на котором разворачивается действие игры. Все перечисленные объекты в компьютерных играх носят название спрайты (от английского слова Sprite - эльф, светлячок). Более точно спрайт можно определить как перемещаемый графический объект с неизменным рисунком и размером (см. [4]). Трудно представить хорошую игру без таких объектов (хотя, в некоторых играх, например, в MICRONAUT ONE или DARK SIDE, изображение создается программными средствами), поэтому, чтобы научиться писать приличные игровые программы, прежде всего нужно освоить технику создания спрайтов.

О том, как делать маленькие и большие спрайты, а также сложные пейзажи, мы расскажем дальше, а здесь только перечислим способы их изготовления. Небольшие спрайты вполне могут быть изготовлены вручную. Для этого спрайт рисуется на клетчатой бумаге (каждая клетка соответствует одной точке на экране), а затем изображение переводится в последовательность чисел. Значительно интереснее, а главное, эффективнее, создать рисунок будущего объекта в каком-либо графическом редакторе (например, в Art Studio или The Artist II), после чего автоматически закодировать его с помощью специальной программы, называемой генератором спрайтов, и записать на ленту или диск в виде готового к использованию спрайт-файла.

Все эти способы были подробно расписаны в [1], поэтому в настоящей книге мы не станем подолгу останавливаться на них и рассмотрим лишь некоторые особенности, имеющие непосредственное отношение к языку ассемблера.


Изготовление маски спрайта



Рисунок 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


;Графические переменные 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


ИЗГОТОВЛЕНИЕ НОВЫХ НАБОРОВ СИМВОЛОВ (ФОНТОВ)



ИЗГОТОВЛЕНИЕ НОВЫХ НАБОРОВ СИМВОЛОВ (ФОНТОВ)

Поскольку ни одна компьютерная игра не обходится без различных надписей и текстов, очень важно уметь заменять стандартный символьный набор новыми шрифтами произвольного начертания. Вы, наверное, много раз обращали внимание, что даже в фирменных играх, дошедших к нам с родины Speccy, из Англии, далеко не всегда используется встроенный шрифт, а чаще заменяется на символы более привлекательной формы, хотя в этом и нет суровой необходимости. Для простых же российских граждан, как правило, не владеющих свободно английским языком, было бы великим облегчением увидеть на экране родные русские буквы.

Многие из вас наверняка знакомы с различными видами русификации ZX Spectrum - кто-то из книг, например, из [1] или [2], а кому-то повезло раздобыть готовые знакогенераторы. Тем не менее, в этом разделе мы хотим напомнить способы создания новых фонтов и привести два полных символьных набора: латинский и русский.

В некоторых программах для русификации используется область графических символов, определяемых пользователем (UDG). Но это, в основном, касается программ на Бейсике, а в ассемблере, как вы могли заметить из примера, приведенного в предыдущем разделе, прибегать к помощи символов UDG не слишком удобно. Кроме всего прочего в этом случае возможно получить не более 22 новых символов, что для полного набора букв явно маловато. Поэтому в ассемблерных программах область определяемых символов практически никогда не используется (тем не менее, некоторое время мы еще будем к ним прибегать, пока не научимся выводить символы и графику без использования команды RST 16).

Полный набор символов можно изготовить, воспользовавшись специальным редактором шрифта (Font Editor). Эту программу, как вы знаете, можно найти в качестве составной части в таких графических редакторах как Art Studio или The Artist II. Как работать с этими программами не раз объяснялось в литературе (см. [1] или [2]) и, наверное, нет нужды еще раз распространяться на сей счет. Вместо этого мы сразу приведем программки, содержащие коды символьных наборов, созданных именно таким образом. Набрав и выполнив их, вы получите в свое распоряжение готовые кодовые блоки, которые затем можете сразу использовать в своих программах или предварительно подкорректировать в упомянутых редакторах.



Элементы углов рамки



Рисунок 4.2. Элементы углов рамки

Для упрощения кодирования мы можем предложить простую сервисную программку на Бейсике, перед использованием которой следует создать полный набор символов, прибегнув к помощи одного из редакторов фонтов (например, из Art Studio). Но не обязательно все символы будут взяты из полученного шрифта, например, в нашей программе заставки их использовано только16. Вот текст кодировщика символов (в дальнейшем везде, где в программах потребуется ввести более одного пробела подряд, для наглядности будем обозначать их символом «·».):

10 BORDER 0: PAPER 0: INK 5: CLEAR 64255 30 INPUT "Font file name:"; LINE a$ 40 LOAD a$CODE 64256 50 INPUT "start symbol:"; LINE a$ 60 CLS : PRINT AT 15,0; INK 6;"'E' - to exit" 70 LET a$=a$(1) 80 LET start=64256+((CODE a$)-32)*8 90 LET s=CODE a$: LET c=0 100 GO TO 110 110 FOR m=start TO 64256+768 120 LET a=PEEK m: LET byte=a 130 LET c=c+1 140 DIM b(8) 150 FOR n=1 TO 8 160 LET a=a/2: LET b(n)=a<>INT a: LET a=INT a 170 NEXT n 180 PRINT AT 2,1; INK 2;"Symbol: "; INK 7; PAPER 1;CHR$ s; PAPER 0; INK 2;" Code: "; INK 7;s 190 FOR n=1 TO 8 200 IF b(n)=0 THEN PAPER 1 210 IF b(n)=1 THEN PAPER 6 220 PRINT AT c+3,9-n;" " 230 NEXT n 240 PAPER 0: PRINT AT c+3,10;byte;"··" 250 IF c>7 THEN LET c=0: LET s=s+1: GO TO 270 260 NEXT m 270 REM ----Если нажата клавиша E---- 280 LET k$=INKEY$ 290 IF k$="E" OR k$="e" THEN BEEP .01,0: GO TO 50 300 IF k$<>"" THEN BEEP .01,20: GO TO 260 310 GO TO 280

Использование этой программы не вызовет никаких затруднений - достаточно отвечать на вопросы, появляющиеся на экране. Добавим только, что при работе с дисководом следует включить в программу строку

20 RANDOMIZE USR 15619: REM : CAT

а строку 40 заменить на такую:

40 RANDOMIZE USR 15619: REM : LOAD a$CODE 64256

Мы пропустим утомительный этап кодирования символов, в результате чего получится довольно длинный ряд цифр, которые запишем после инструкции DEFB и присвоим первому байту этого блока данных метку UDG:


UDG DEFB 0,63,64,95,95,95,95,95 ;A (144) DEFB 0,252,30,250,250,250,242,242 ;B (145) DEFB 95,95,127,127,124,96,63,0 ;C (146) DEFB 226,194,130,2,2,2,252,0 ;D (147) DEFB 0,63,0,95,107,95,107,95 ;E (148) DEFB 0,244,0,208,234,208,234,208 ;F (149) DEFB 107,95,107,95,107,0,63,0 ;G (150) DEFB 234,208,234,208,234,0,244,0 ;H (151) DEFB 107,95,107,95,107,95,107,95 ;I (152) DEFB 234,208,234,208,234,208,234,208 ;J (153) DEFB 0,31,85,74,95,74,95,95 ;K (154) DEFB 0,255,85,170,255,170,255,255 ;L (155) DEFB 95,95,85,74,21,64,21,0 ;M (156) DEFB 255,255,85,170,85,0,85,0 ;N (157) DEFB 0,248,82,170,250,170,250,250 ;O (158) DEFB 250,250,82,170,80,2,80,0 ;P (159)

В комментариях справа от каждой строки указаны клавиши, соответствующие символам, вводимым в режиме курсора [G], и их десятичные коды. Буквы выписаны, скорее, как воспоминание о Бейсике, а вот коды нам понадобятся при составлении следующего блока, так как в редакторе GENS нет возможности вводить символы UDG непосредственно с клавиатуры.

Для получения блока данных, описывающего внешний вид рамки, за неимением какого-либо специализированного редактора лучше всего «включить» полученный набор UDG и составить программу сначала на Бейсике. При таком способе не нужно будет после каждого введенного символа транслировать программу заново и проверять правильность ввода, достаточно дать команду RUN. Кроме того, в Бейсике вы застрахованы от возникновения таких критических ошибок, после которых всю работу нужно начинать сначала. В конце останется только переписать полученный блок уже в редакторе GENS, не опасаясь, что где-то закралась ошибка.

«Включить» новые символы UDG в ассемблере до смешного просто. В Бейсике для этого требуется прежде всего определить адрес размещения символов в памяти, затем в цикле последовательно считывать коды из блока данных и переносить их по рассчитанному адресу. В ассемблере же ничего никуда перемещать не требуется, достаточно изменить адрес области UDG в системных переменных. А выполняется это всего двумя командами микропроцессора:



LD HL,UDG LD (23675),HL

Мы избавим вас от неблагодарной работы по составлению блока данных для печати рамки и приведем его в уже готовом виде:

RAMKA DEFB 22,4,0,16,5 DEFB 144,145,154,155,155,155,155,155 DEFB 155,155,158,154,155,155,155,155 DEFB 155,155,155,155,158,154,155,155 DEFB 155,155,155,155,155,158,144,145 DEFB 146,147,16,4,156,157,157,157,157,157 DEFB 157,157,159,156,157,157,157,157 DEFB 157,157,157,157,159,156,157,157 DEFB 157,157,157,157,157,159,16,5,146,147

DEFB 16,5,148,16,4,149,22,6,30,16,5,148,16,4,149 DEFB 16,5,152,16,4,153,22,7,30,16,5,152,16,4,153 DEFB 16,5,152,16,4,153,22,8,30,16,5,152,16,4,153 DEFB 16,5,150,16,4,151,22,9,30,16,5,150,16,4,151

DEFB 16,5,148,16,4,149,22,10,30,16,5,148,16,4,149 DEFB 16,5,152,16,4,153,22,11,30,16,5,152,16,4,153 DEFB 16,5,152,16,4,153,22,12,30,16,5,152,16,4,153 DEFB 16,5,150,16,4,151,22,13,30,16,5,150,16,4,151

DEFB 16,5 DEFB 144,145,154,155,155,155,155,155 DEFB 155,155,158,154,155,155,155,155 DEFB 155,155,155,155,158,154,155,155 DEFB 155,155,155,155,155,158,144,145 DEFB 146,147,16,4,156,157,157,157,157,157 DEFB 157,157,159,156,157,157,157,157 DEFB 157,157,157,157,159,156,157,157 DEFB 157,157,157,157,157,159,16,5,146,147

Вы можете заметить, что кроме кодов символов UDG в блок данных включены также и управляющие коды, изменяющие цвет и позицию печати.

Значительно проще составить блок данных для печати текста заставки. Он будет выглядеть примерно так:

TEXT DEFB 22,2,8,16,6 DEFM "*" DEFB 22,2,10,16,3 DEFM "F I G H T E R" DEFB 22,2,24,16,6 DEFM "*" DEFB 22,7,10,16,7 DEFM "Written by :" DEFB 22,9,7 DEFM "Kapultsevich···Igor" DEFB 22,12,5 DEFM "Saint-Petersburg··1994" DEFB 22,17,3,16,6 DEFM "Press any··key to continue"

Покончив с самой утомительной частью работы, нам остается вывести на экран подготовленные блоки данных. Вы уже знаете, как это можно сделать, но тем не менее позвольте дать небольшой совет. Все было очень просто, пока мы не сталкивались с блоками данных внушительных размеров. До сих пор мы ограничивались выводом десятка-другого символов. Но попробуйте-ка подсчитать, сколько байт занимает, например, блок под названием RAMKA. Думается, вас не очень вдохновит такая работа. А что если в текст заставки захочется внести какие-то изменения или дополнения? Снова пересчитывать его длину?



Ни в коем случае! Ведь ассемблер сам может выполнять некоторые несложные расчеты, оперируя как с числами, так и с метками. Расположим блоки данных в таком порядке: сначала блок TEXT, затем - RAMKA и самым последним запишем блок UDG. Таким образом, длина блока TEXT будет равна разности его конечного и начального адресов, а так как сразу за ним следует блок RAMKA, то его длина определяется выражением RAMKA-TEXT. (Обратите внимание на тот факт, что ассемблер может вычислять выражения с метками даже до того, как эти метки встретятся в программе. Именно для обеспечения таких «ссылок вперед» и необходим второй проход ассемблирования - Примеч. ред.) Аналогично длина блока RAMKA вычисляется выражением UDG-RAMKA.

Теперь можно написать программку, формирующую на экране статическую заставку:

ORG 60000 ENT $ ; Подготовка экрана LD A,5 LD (23693),A LD A,0 CALL 8859 CALL 3435 LD A,2 CALL 5633 ; «Включение» символов UDG LD HL,UDG LD (23675),HL ; Вывод заставки на экран LD DE,RAMKA LD BC,UDG-RAMKA CALL 8252 LD DE,TEXT LD BC,RAMKA-TEXT CALL 8252 RET ; Подготовленные заранее блоки данных ; --------------

; --------------

; --------------


КАК СДЕЛАТЬ НАЗВАНИЕ ИГРЫ



КАК СДЕЛАТЬ НАЗВАНИЕ ИГРЫ

Думается, вы согласитесь, какую важную роль играет в заставке изображение названия игры и все, что его окружает. Для доказательства достаточно загрузить какую-нибудь фирменную игру и убедиться, что так оно и есть. Если вы достаточно внимательно изучили предыдущие страницы, то уже должны быть в состоянии изменить стандартный набор символов и сделать вполне приличное название. Но все же хотелось бы научиться создавать что-то оригинальное и, кроме того, независимое от стандартного фонта. Можно, конечно, загрузить ArtStudio или The Artist II и попробовать нарисовать название там, но кто-то после первых же штрихов сделает простой вывод: пожалуй, это не для меня. Учитывая все сказанное, мы хотим предложить способ получения названий (и не только их), в основе которого лежит хорошо знакомый вам оператор DRAW.

Чтобы не утомлять вас долгими рассуждениями о том, каким образом формируется та или иная буква, лучше всего приведем в качестве своеобразного комментария программу на Бейсике с краткими пояснениями к ней, которые предлагаем внимательно изучить, после чего будет совсем нетрудно понять принцип работы соответствующей ей ассемблерной программы.

10 READ y: IF y=-1 THEN GO TO 100 20 READ x,hgt,len,ink,dx 30 LET y=113-y: LET x=x+40 40 INK ink 50 FOR i=1 TO hgt 60 PLOT x,y: DRAW len,0 70 PLOT x,y+1: DRAW len,0 80 LET y=y-4: LET x=x+dx: NEXT i 90 GO TO 10 100 STOP 1000 REM --- Буква «Н» --- 1010 DATA 0,0,12,6,2,0 1020 DATA 20,6,2,14,2,0 1030 DATA 0,20,12,10,2,0 1040 REM --- Буква «А» --- 1050 DATA 0,46,12,4,1,-1 1060 DATA 0,46,12,10,1,1 1070 DATA 24,42,2,16,1,0 1080 REM --- Буква «Р» --- 1090 DATA 0,72,12,10,3,0 1100 DATA 0,82,2,13,3,0 1110 DATA 24,82,2,13,3,0 1120 DATA 4,96,1,2,3,0 1130 DATA 24,96,1,2,3,0 1140 DATA 8,92,4,8,3,0 1150 REM --- Буква «Д» --- 1160 DATA 0,112,10,4,6,-1 1170 DATA 0,112,10,10,6,1 1180 DATA 40,100,2,34,6,0 1190 REM --- Буква «Ы» --- 1200 DATA 0,139,12,10,4,0 1210 DATA 16,150,2,13,4,0 1220 DATA 40,150,2,13,4,0 1230 DATA 20,163,1,2,4,0 1240 DATA 40,163,1,2,4,0 1250 DATA 24,160,4,8,4,0 1260 DATA 0,167,4,8,4,0 1270 DATA 16,169,1,6,4,0 1280 DATA 20,171,6,4,4,0 1290 DATA 44,169,1,6,4,0 1300 DATA -1

Теперь скажем несколько слов об этой программе, поскольку все основные идеи, заложенные в ней, затем будут реализованы в программе на ассемблере, но в начале покажем, что вы увидите на экране после ее исполнения (Рисунок  6.4).



Ключи ассемблирования


Первое число, следующее за командой A, называют ключами ассемблирования, потому что в зависимости от него трансляция протекает с теми или иными условиями. Ключи похожи на флаги, так как каждому из них соответствует определенный бит задаваемого значения. При объединении нескольких битов можно получить комбинацию различных условий. Перечислим значения всех используемых ключей при установке соответствующего бита в 1:

бит 0 (1) - заставляет ассемблер вывести таблицу адресов меток и значений констант в конце второго прохода трансляции;

бит 1 (2) - произвести проверку синтаксиса строк программы, не создавая при этом машинного кода;

бит 2 (4) - вывести на втором проходе листинг ассемблирования программы;

бит 3 (8) - на время трансляции назначить вывод вместо экрана на принтер;

бит 4 (16) - реально размещать машинный код сразу за таблицей меток (адрес, указанный в директиве ORG влияет только на создание ссылок в программе);

бит 5 (32) - не делать проверки расположения кода в памяти (обычно же максимальная верхняя граница задается системной переменной UDG и может быть изменена командой редактора U).

Поясним смысл некоторых наиболее важных ключей и покажем, как ими пользоваться на практике. Первый ключ бывает совершенно необходим, по крайней мере, в двух случаях: при отладке достаточно больших программ, чтобы знать, где искать ту или иную процедуру и для определения адресов подпрограмм и переменных при использовании машинного кода совместно с программами на других языках или для связи различных модулей, транслируемых раздельно (что поделаешь, память Speccy весьма ограничена и иногда поневоле приходится изощряться). В таких программах, как БИТВА С НЛО или ГЕНЕРАТОР СПРАЙТОВ мы предложили простой способ выбора необходимой процедуры из пакета, но он не всегда может оказаться пригодным. Более того, в ГЕНЕРАТОРЕ СПРАЙТОВ переменные располагались в буфере принтера, а это в некоторых случаях не допускается. Например, в компьютере ZX Spectrum 128, как мы уже говорили, в этой области содержится важная информация, разрушать которую никак нельзя. Вот здесь-то и поможет ключ 1. Разместите все переменные внутри программы и оттранслируйте ее командой A1. Когда ассемблирование закончится, на экран (или на принтер, если задать режим A9 - 1+8) будет выдана таблица меток, в которой найдите нужное имя, а рядом с ним - адрес. Для обращения к этому адресу из Бейсика вам останется только перевести шестнадцатеричное значение в десятичное.


Ключи 2 и 4 бывают особенно полезны на этапе отладки, а вот без ключа 16 не обойтись при трансляции больших программ или если машинный код должен располагаться в недоступных иными средствами областях памяти. Как уже говорилось, при использовании этого ключа исполняемый код располагается сразу за таблицей символов, создаваемой ассемблером на первом проходе трансляции (которая, в свою очередь, помещается следом за исходным текстом). Это позволяет отвести для исполняемого модуля всю свободную память. Однако после сохранения полученной программы на ленте или диске адрес загрузки ее кодов в заголовке файла окажется равным тому, который вы указали в директиве ORG. Все адреса переходов, естественно, также будут правильными.

Применять последний ключ 32 можно лишь в тех случаях, когда вы абсолютно уверены, что исполняемый код полностью уместится в оперативной памяти, иначе «хвост» программы залезет в начальные адреса ПЗУ, где толку от него не будет никакого. Этот ключ может принести пользу, пожалуй, лишь при создании операционных систем или перемещаемых модулей, но эта тема уже выходит за рамки нашей книги.


КОМАНДЫ АССЕМБЛЕРА



КОМАНДЫ АССЕМБЛЕРА

Это приложение содержит таблицу кодировки команд микропроцессора Z80. В первой графе указывается мнемоника, во второй - соответствующий ей машинный код, а в последней - время исполнения инструкции, измеряемое в тактах микропроцессора. Для условных и циклических команд (таких как LDIR) даются два значения времени, так как при соблюдении условия или завершении цикла на их выполнение требуется больше тактов. Для некоторых команд приведены два варианта машинных кодов, но время исполнения относится только к более короткому варианту. Следует помнить, что ассемблер при трансляции мнемоник также выбирает более короткий код. Вообще же для многих команд существуют дополнительные варианты кодировки, но они уже относятся к набору так называемых недокументированных команд, некоторые из них приведены в следующем приложении. Обозначения: ADDR - двухбайтовый адрес, N - однобайтовое число, NN - двухбайтовое число, PORT - младший байт адреса порта, XX - шестнадцатеричный байт-аргумент, Д - байт смещения (-128...+127).

КомандаКодыT
ADC A,(HL)8E7
ADC A,(IX+Д)DD8EXX19
ADC A,(IY+Д)FD8EXX19
ADC A,A8F4
ADC A,B884
ADC A,C894
ADC A,D8A4
ADC A,E8B4
ADC A,H8C4
ADC A,L8D4
ADC A,NCEXX7
ADC HL,BCED4A15
ADC HL,DEED5A15
ADC HL,HLED6A15
ADC HL,SPED7A15
ADD A,(HL)867
ADD A,(IX+Д)DD86XX19
ADD A,(IY+Д)FD86XX19
ADD A,A874
ADD A,B804
ADD A,C814
ADD A,D824
ADD A,E834
ADD A,H844
ADD A,L854
ADD A,NC6XX7
ADD HL,BC0911
ADD HL,DE1911
ADD HL,HL2911
ADD HL,SP3911
ADD IX,BCDD0915
ADD IX,DEDD1915
ADD IX,IXDD2915
ADD IX,SPDD3915
ADD IY,BCFD0915
ADD IY,DEFD1915
ADD IY,IYFD2915
ADD IY,SPFD3915
AND (HL)A67
AND (IX+Д)DDA6XX19
AND (IY+Д)FDA6XX19
AND AA74
AND BA04
AND CA14
AND DA24
AND EA34
AND HA44
AND LA54
AND NE6XX7
BIT 0,(HL)CB4612
BIT 0,(IX+Д)DDCBXX4620
BIT 0,(IY+Д)FDCBXX4620
BIT 0,ACB478
BIT 0,BCB408
BIT 0,CCB418
BIT 0,DCB428
BIT 0,ECB438
BIT 0,HCB448
BIT 0,LCB458
BIT 1,(HL)CB4E12
BIT 1,(IX+Д)DDCBXX4E20
BIT 1,(IY+Д)FDCBXX4E20
BIT 1,ACB4F8
BIT 1,BCB488
BIT 1,CCB498
BIT 1,DCB4A8
BIT 1,ECB4B8
BIT 1,HCB4C8
BIT 1,LCB4D8
BIT 2,(HL)CB5612
BIT 2,(IX+Д)DDCBXX5620
BIT 2,(IY+Д)FDCBXX5620
BIT 2,ACB578
BIT 2,BCB508
BIT 2,CCB518
BIT 2,DCB528
BIT 2,ECB538
BIT 2,HCB548
BIT 2,LCB558
BIT 3,(HL)CB5E12
BIT 3,(IX+Д)DDCBXX5E20
BIT 3,(IY+Д)FDCBXX5E8
BIT 3,ACB5F8
BIT 3,BCB588
BIT 3,CCB598
BIT 3,DCB5A8
BIT 3,ECB5B8
BIT 3,HCB5C8
BIT 3,LCB5D8
BIT 4,(HL)CB6612
BIT 4,(IX+Д)DDCBXX6620
BIT 4,(IY+Д)FDCBXX6620
BIT 4,ACB678
BIT 4,BCB608
BIT 4,CCB618
BIT 4,DCB628
BIT 4,ECB638
BIT 4,HCB648
BIT 4,LCB658
BIT 5,(HL)CB6E12
BIT 5,(IX+Д)DDCBXX6E20
BIT 5,(IY+Д)FDCBXX6E20
BIT 5,ACB6F8
BIT 5,BCB688
BIT 5,CCB698
BIT 5,DCB6A8
BIT 5,ECB6B8
BIT 5,HCB6C8
BIT 5,LCB6D8
BIT 6,(HL)CB7612
BIT 6,(IX+Д)DDCBXX7620
BIT 6,(IY+Д)FDCBXX7620
BIT 6,ACB778
BIT 6,BCB708
BIT 6,CCB718
BIT 6,DCB728
BIT 6,ECB738
BIT 6,HCB748
BIT 6,LCB758
BIT 7,(HL)CB7E12
BIT 7,(IX+Д)DDCBXX7E20
BIT 7,(IY+Д)FDCBXX7E20
BIT 7,ACB7F8
BIT 7,BCB788
BIT 7,CCB798
BIT 7,DCB7A8
BIT 7,ECB7B8
BIT 7,HCB7C8
BIT 7,LCB7D8
CALL ADDRCDXXXX17
CALL C,ADDRDCXXXX10/17
CALL M,ADDRFCXXXX10/17
CALL NC,ADDRD4XXXX10/17
CALL NZ,ADDRC4XXXX10/17
CALL P,ADDRF4XXXX10/17
CALL PE,ADDRECXXXX10/17
CALL PO,ADDRE4XXXX10/17
CALL Z,ADDRCCXXXX10/17
CCF3F4
CP (HL)BE7
CP (IX+Д)DDBEXX19
CP (IY+Д)FDBEXX19
CP ABF4
CP BB84
CP CB94
CP DBA4
CP EBB4
CP HBC4
CP LBD4
CP NFEXX7
CPDEDA916
CPDREDB921/16
CPIEDA116
CPIREDB121/16
CPL2F4
DAA274
DEC (HL)3511
DEC (IX+Д)DD35XX23
DEC (IY+Д)FD35XX23
DEC A3D4
DEC B054
DEC BC0B6
DEC C0D4
DEC D154
DEC DE1B6
DEC E1D4
DEC H254
DEC HL2B6
DEC IXDD2B10
DEC IYFD2B10
DEC L2D4
DEC SP3B6
DIF34
DJNZ Д10XX13/8
EIFB4
EX (SP),HLE319
EX (SP),IXDDE323
EX (SP),IYFDE323
EX AF,AF'084
EX DE,HLEB4
EXXD94
HALT764
IM 0ED468
IM 1ED568
IM 2ED5E8
IN (HL),(C)ED7012
IN A,(C)ED7812
IN A,(PORT)DBXX11
IN B,(C)ED4012
IN C,(C)ED4812
IN D,(C)ED5012
IN E,(C)ED5812
IN H,(C)ED6012
IN L,(C)ED6812
INC (HL)3411
INC (IX+Д)DD34XX23
INC (IY+Д)FD34XX23
INC A3C4
INC B044
INC BC036
INC C0C4
INC D144
INC DE136
INC E1C4
INC H244
INC HL236
INC IXDD2310
INC IYFD2310
INC L2C4
INC SP336
INDEDAA16
INDREDBA21/16
INIEDA216
INIREDB221/16
JP (HL)E94
JP (IX)DDE98
JP (IY)FDE98
JP ADDRC3XXXX10
JP C,ADDRDAXXXX10
JP M,ADDRFAXXXX10
JP NC,ADDRD2XXXX10
JP NZ,ADDRC2XXXX10
JP P,ADDRF2XXXX10
JP PE,ADDREAXXXX10
JP PO,ADDRE2XXXX10
JP Z,ADDRCAXXXX10
JR Д18XX12
JR C,Д38XX12/7
JR NC,Д30XX12/7
JR NZ,Д20XX12/7
JR Z,Д28XX12/7
LD (ADDR),A32XXXX13
LD (ADDR),BCED43XXXX20
LD (ADDR),DEED53XXXX20
LD (ADDR),HL22XXXX
ED63XXXX
16
LD (ADDR),IXDD22XXXX20
LD (ADDR),IYFD22XXXX20
LD (ADDR),SPED73XXXX20
LD (BC),A027
LD (DE),A127
LD (HL),A777
LD (HL),B707
LD (HL),C717
LD (HL),D727
LD (HL),E737
LD (HL),H747
LD (HL),L757
LD (HL),N36XX10
LD (IX+Д),ADD77XX19
LD (IX+Д),BDD70XX19
LD (IX+Д),CDD71XX19
LD (IX+Д),DDD72XX19
LD (IX+Д),EDD73XX19
LD (IX+Д),HDD74XX19
LD (IX+Д),LDD75XX19
LD (IX+Д),NDD36XXXX19
LD (IY+Д),AFD77XX19
LD (IY+Д),BFD70XX19
LD (IY+Д),CFD71XX19
LD (IY+Д),DFD72XX19
LD (IY+Д),EFD73XX19
LD (IY+Д),HFD74XX19
LD (IY+Д),LFD75XX19
LD (IY+Д),NFD36XXXX19
LD A,(ADDR)3AXXXX13
LD A,(BC)0A7
LD A,(DE)1A7
LD A,(HL)7E7
LD A,(IX+Д)DD7EXX19
LD A,(IY+Д)FD7EXX19
LD A,A7F4
LD A,B784
LD A,C794
LD A,D7A4
LD A,E7B4
LD A,H7C4
LD A,IED579
LD A,L7D4
LD A,RED5F9
LD A,N3EXX7
LD B,(HL)467
LD B,(IX+Д)DD46XX19
LD B,(IY+Д)FD46XX19
LD B,A474
LD B,B404
LD B,C414
LD B,D424
LD B,E434
LD B,H444
LD B,L454
LD B,N06XX7
LD BC,(ADDR)ED4BXXXX20
LD BC,NN01XXXX10
LD C,(HL)4E7
LD C,(IX+Д)DD4EXX19
LD C,(IY+Д)FD4EXX19
LD C,A4F4
LD C,B484
LD C,C494
LD C,D4A4
LD C,E4B4
LD C,H4C4
LD C,L4D4
LD C,N0EXX7
LD D,(HL)567
LD D,(IX+Д)DD56XX19
LD D,(IY+Д)FD56XX19
LD D,A574
LD D,B504
LD D,C514
LD D,D524
LD D,E534
LD D,H544
LD D,L554
LD D,N16XX7
LD DE,(ADDR)ED5BXXXX20
LD DE,NN11XXXX10
LD E,(HL)5E7
LD E,(IX+Д)DD5EXX19
LD E,(IY+Д)FD5EXX19
LD E,A5F4
LD E,B584
LD E,C594
LD E,D5A4
LD E,E5B4
LD E,H5C4
LD E,L5D4
LD E,N1EXX7
LD H,(HL)667
LD H,(IX+Д)DD66XX19
LD H,(IY+Д)FD66XX19
LD H,A674
LD H,B604
LD H,C614
LD H,D624
LD H,E634
LD H,H644
LD H,L654
LD H,N26XX7
LD HL,(ADDR)2AXXXX
ED6BXXXX
16
LD HL,NN21XXXX10
LD I,AED479
LD IX,(ADDR)DD2AXXXX20
LD IX,NNDD21XXXX14
LD IY,(ADDR)FD2AXXXX20
LD IY,NNFD21XXXX14
LD L,(HL)6E7
LD L,(IX+Д)DD6EXX19

КомандаКодыT LD L,(IY+Д)FD6EXX19 LD L,A6F4 LD L,B684 LD L,C694 LD L,D6A4 LD L,E6B4 LD L,H6C4 LD L,L6D4 LD L,N2EXX7 LD R,AED4F9 LD SP,(ADDR)ED7BXXXX20 LD SP,IXDDF910 LD SP,IYFDF910 LD SP,HLF96 LD SP,NN31XXXX10 LDDEDA816 LDDREDB821/16 LDIEDA016 LDIREDB021/16 NEGED448 NOP004 OR (HL)B67 OR (IX+Д)DDB6XX19 OR (IY+Д)FDB6XX19 OR AB74 OR BB04 OR CB14 OR DB24 OR EB34 OR HB44 OR LB54 OR NF6XX7 OTDREDBB21/15 OTIREDB321/15 OUT (C),AED7912 OUT (C),BED4112 OUT (C),CED4912 OUT (C),DED5112 OUT (C),EED5912 OUT (C),HED6112 OUT (C),LED6912 OUT (PORT),AD3XX12 OUTDEDAB16 OUTIEDA316 POP AFF110 POP BCC110 POP DED110 POP HLE110 POP IXDDE114 POP IYFDE114 PUSH AFF511 PUSH BCC511 PUSH DED511 PUSH HLE511 PUSH IXDDE515 PUSH IYFDE515 RES 0,(HL)CB8615 RES 0,(IX+Д)DDCBXX8623 RES 0,(IY+Д)FDCBXX8623 RES 0,ACB878 RES 0,BCB808 RES 0,CCB818 RES 0,DCB828 RES 0,ECB838 RES 0,HCB848 RES 0,LCB858 RES 1,(HL)CB8E15 RES 1,(IX+Д)DDCBXX8E23 RES 1,(IY+Д)FDCBXX8E23 RES 1,ACB8F8 RES 1,BCB888 RES 1,CCB898 RES 1,DCB8A8 RES 1,ECB8B8 RES 1,HCB8C8 RES 1,LCB8D8 RES 2,(HL)CB9615 RES 2,(IX+Д)DDCBXX9623 RES 2,(IY+Д)FDCBXX9623 RES 2,ACB978 RES 2,BCB908 RES 2,CCB918 RES 2,DCB928 RES 2,ECB938 RES 2,HCB948 RES 2,LCB958 RES 3,(HL)CB9E15 RES 3,(IX+Д)DDCBXX9E23 RES 3,(IY+Д)FDCBXX9E23 RES 3,ACB9F8 RES 3,BCB988 RES 3,CCB998 RES 3,DCB9A8 RES 3,ECB9B8 RES 3,HCB9C8 RES 3,LCB9D8 RES 4,(HL)CBA615 RES 4,(IX+Д)DDCBXXA623 RES 4,(IY+Д)FDCBXXA623 RES 4,ACBA78 RES 4,BCBA08 RES 4,CCBA18 RES 4,DCBA28 RES 4,ECBA38 RES 4,HCBA48 RES 4,LCBA58 RES 5,(HL)CBAE15 RES 5,(IX+Д)DDCBXXAE23 RES 5,(IY+Д)FDCBXXAE23 RES 5,ACBAF8 RES 5,BCBA88 RES 5,CCBA98 RES 5,DCBAA8 RES 5,ECBAB8 RES 5,HCBAC8 RES 5,LCBAD8 RES 6,(HL)CBB615
RES 6,(IX+Д)DDCBXXB623
RES 6,(IY+Д)FDCBXXB623
RES 6,ACBB78
RES 6,BCBB08
RES 6,CCBB18
RES 6,DCBB28
RES 6,ECBB38
RES 6,HCBB48
RES 6,LCBB58
RES 7,(HL)CBBE15
RES 7,(IX+Д)DDCBXXBE23
RES 7,(IY+Д)FDCBXXBE23
RES 7,ACBBF8
RES 7,BCBB88
RES 7,CCBB98
RES 7,DCBBA8
RES 7,ECBBB8
RES 7,HCBBC8
RES 7,LCBBD8
RETC910
RET CD811/5
RET MF811/5
RET NCD011/5
RET NZC011/5
RET PF011/5
RET PEE811/5
RET POE011/5
RET ZC811/5
RETIED4D14
RETNED4514
RL (HL)CB1615
RL (IX+Д)DDCBXX1623
RL (IY+Д)FDCBXX1623
RL ACB178
RL BCB108
RL CCB118
RL DCB128
RL ECB138
RL HCB148
RL LCB158
RLA174
RLC (HL)CB0615
RLC (IX+Д)DDCBXX0623
RLC (IY+Д)FDCBXX0623
RLC ACB078
RLC BCB008
RLC CCB018
RLC DCB028
RLC ECB038
RLC HCB048
RLC LCB058
RLCA074
RLDED6F18
RR (HL)CB1E15
RR (IX+Д)DDCBXX1E23
RR (IY+Д)FDCBXX1E23
RR ACB1F8
RR BCB188
RR CCB198
RR DCB1A8
RR ECB1B8
RR HCB1C8
RR LCB1D8
RRA1F4
RRC (HL)CB0E15
RRC (IX+Д)DDCBXX0E23
RRC (IY+Д)FDCBXX0E23
RRC ACB0F8
RRC BCB088
RRC CCB098
RRC DCB0A8
RRC ECB0B8
RRC HCB0C8
RRC LCB0D8
RRCA0F4
RRDED6718
RST 00C711
RST 08CF11
RST 10D711
RST 18DF11
RST 20E711
RST 28EF11
RST 30F711
RST 38FF11
SBC A,(HL)9E7
SBC A,(IX+Д)DD9EXX19
SBC A,(IY+Д)FD9EXX19
SBC A,A9F4
SBC A,B984
SBC A,C994
SBC A,D9A4
SBC A,E9B4
SBC A,H9C4
SBC A,L9D4
SBC A,NDEXX7
SBC HL,BCED4215
SBC HL,DEED5215
SBC HL,HLED6215
SBC HL,SPED7215
SCF374
SET 0,(HL)CBC615
SET 0,(IX+Д)DDCBXXC623
SET 0,(IY+Д)FDCBXXC623
SET 0,ACBC78
SET 0,BCBC08
SET 0,CCBC18
SET 0,DCBC28
SET 0,ECBC38
SET 0,HCBC48
SET 0,LCBC58
SET 1,(HL)CBCE15
SET 1,(IX+Д)DDCBXXCE23
SET 1,(IY+Д)FDCBXXCE23
SET 1,ACBCF8
SET 1,BCBC88
SET 1,CCBC98
SET 1,DCBCA8
SET 1,ECBCB8
SET 1,HCBCC8
SET 1,LCBCD8
SET 2,(HL)CBD615
SET 2,(IX+Д)DDCBXXD623
SET 2,(IY+Д)FDCBXXD623
SET 2,ACBD78
SET 2,BCBD08
SET 2,CCBD18
SET 2,DCBD28
SET 2,ECBD38
SET 2,HCBD48
SET 2,LCBD58
SET 3,(HL)CBDE15
SET 3,(IX+Д)DDCBXXDE23
SET 3,(IY+Д)FDCBXXDE23
SET 3,ACBDF8
SET 3,BCBD88
SET 3,CCBD98
SET 3,DCBDA8
SET 3,ECBDB8
SET 3,HCBDC8
SET 3,LCBDD8
SET 4,(HL)CBE615
SET 4,(IX+Д)DDCBXXE623
SET 4,(IY+Д)FDCBXXE623
SET 4,ACBE78
SET 4,BCBE08
SET 4,CCBE18
SET 4,DCBE28
SET 4,ECBE38
SET 4,HCBE48
SET 4,LCBE58
SET 5,(HL)CBEE15
SET 5,(IX+Д)DDCBXXEE23
SET 5,(IY+Д)FDCBXXEE23
SET 5,ACBEF8
SET 5,BCBE88
SET 5,CCBE98
SET 5,DCBEA8
SET 5,ECBEB8
SET 5,HCBEC8
SET 5,LCBED8
SET 6,(HL)CBF615
SET 6,(IX+Д)DDCBXXF623
SET 6,(IY+Д)FDCBXXF623
SET 6,ACBF78
SET 6,BCBF08
SET 6,CCBF18
SET 6,DCBF28
SET 6,ECBF38
SET 6,HCBF48
SET 6,LCBF58
SET 7,(HL)CBFE15
SET 7,(IX+Д)DDCBXXFE23
SET 7,(IY+Д)FDCBXXFE23
SET 7,ACBFF8
SET 7,BCBF88
SET 7,CCBF98
SET 7,DCBFA8
SET 7,ECBFB8
SET 7,HCBFC8
SET 7,LCBFD8
SLA (HL)CB2615
SLA (IX+Д)DDCBXX2623
SLA (IY+Д)FDCBXX2623
SLA ACB278
SLA BCB208
SLA CCB218
SLA DCB228
SLA ECB238
SLA HCB248
SLA LCB258
SRA (HL)CB2E15
SRA (IX+Д)DDCBXX2E23
SRA (IY+Д)FDCBXX2E23
SRA ACB2F8
SRA BCB288
SRA CCB298
SRA DCB2A8
SRA ECB2B8
SRA HCB2C8
SRA LCB2D8
SRL (HL)CB3E15
SRL (IX+Д)DDCBXX3E23
SRL (IY+Д)FDCBXX3E23
SRL ACB3F8
SRL BCB388
SRL CCB398
SRL DCB3A8
SRL ECB3B8
SRL HCB3C8
SRL LCB3D8
SUB (HL)967
SUB (IX+Д)DD96XX19
SUB (IY+Д)FD96XX19
SUB A974
SUB B904
SUB C914
SUB D924
SUB E934
SUB H944
SUB L954
SUB ND6XX7
XOR (HL)AE7
XOR (IX+Д)DDAEXX19
XOR (IY+Д)FDAEXX19
XOR AAF4
XOR BA84
XOR CA94
XOR DAA4
XOR EAB4
XOR HAC4
XOR LAD4
XOR NEEXX7



КОНТРОЛЬ ВРЕМЕНИ (РАБОТА СПРЕРЫВАНИЯМИ)



КОНТРОЛЬ ВРЕМЕНИ (РАБОТА С ПРЕРЫВАНИЯМИ)

Прерывания с полным правом можно отнести к наиболее мощным и интересным ресурсам компьютера. К сожалению, работа с ними из Бейсика абсолютно невозможна и именно поэтому данный вопрос для многих из вас может оказаться совершенно новым и неизведанным. К настоящему моменту вы уже, должно быть, достаточно освоились с ассемблером и, надеемся, неплохо представляете, как работает микропроцессор, что дает возможность приступить наконец к изучению этой серьезной темы.

Для начала выясним, что же собой представляют прерывания. Попробуем, не вдаваясь в конструкторские тонкости, объяснить принцип этого явления просто «на пальцах». Когда вы находитесь в редакторе Бейсика или GENS и размышляете над очередной строкой программы, компьютер не торопит вас и терпеливо ожидает нажатия той или иной клавиши. Может даже показаться, что микропроцессор в это время и вовсе не работает. Но, как вы уже знаете, это не так. Просто выполняется некоторая часть программы, аналогичная процедуре WAIT, описанной ранее: в цикле опрашивается системная переменная LAST_K и когда вы нажимаете какую-то клавишу, код ее появляется в ячейке 23560. Но, спрашивается, откуда он там берется? Программа ведь только читает ее значение, никак не модифицируя ее содержимое. А разрешается эта загадка довольно просто. Дело в том, что 50 раз в секунду микропроцессор отвлекается от основной программы и переключается на выполнение специальной процедуры обработки прерываний, расположенной по адресу 56, словно бы встретив команду RST 56 или CALL 56, только переход этот происходит не программным, а аппаратным путем. У процедуры 56 есть две основных задачи: опрос клавиатуры и изменение текущего значения таймера (системная переменная FRAMES - 23672/73/74). Результаты опроса клавиш также заносятся в область системных переменных, в частности, код нажатой клавиши помещается в LAST_K. После выхода из прерывания микропроцессор как ни в чем не бывало продолжает выполнять основную программу. В результате получается довольно интересный эффект: создается впечатление, будто бы параллельно работают два микропроцессора, каждый из которых выполняет свою независимую задачу.


Все это прекрасно, но какую пользу для себя мы можем из этого извлечь? Ведь в ПЗУ ничего не изменишь. Действительно, от прерываний программистам было бы не много проку, если бы невозможно было переопределять адрес процедуры для их обработки. Мы уже говорили о существовании регистра I, называемого регистром вектора прерываний, а сейчас расскажем, какую роль он выполняет в программах, использующих собственные прерывания.

Прежде всего вам нужно знать, что существует три различных режима прерываний. Они обозначаются цифрами от 0 до 2. Стандартный режим имеет номер 1, и о нем мы уже кое-что сказали. Нулевой режим нам не интересен, поскольку на практике он ничем не отличается от первого (именно, на практике, потому что на самом деле имеются существенные различия, но в ZX Spectrum они не реализованы). А вот о втором режиме нужно поговорить более основательно.

Сначала скажем несколько слов о том, как он работает и что при этом происходит в компьютере. С приходом сигнала прерываний микропроцессор определяет адрес указателя на процедуру обработки прерываний. Он составляется из байта, считанного с шины данных (младший), который, собственно, и называется вектором прерывания и содержимого регистра I (старший байт адреса). Затем на адресную шину переписывается значение полученного указателя, но предварительно прежнее состояние шины адреса заносится в стек. Таким образом, совершается действие, аналогичное выполнению команды микропроцессора CALL. Поскольку в ZX Spectrum вектор прерывания, как правило, равен 255, то на практике адрес указателя может быть определен только регистром I. Для этого его значение нужно умножить на 256 и прибавить 255.

Для установки нового обработчика прерываний нужно выполнить ряд действий. Перечислим их в том порядке, в котором они должны производиться:

Запретить прерывания, так как есть вероятность того, что сигнал прерываний придет во время установки, а это может привести к нежелательным последствиям. Достигается это выполнением команды микропроцессора DI.



Записать в память по рассчитанному заранее адресу указатель на процедуру обработки прерываний (то есть адрес этой процедуры).

Задать в регистре вектора прерываний I старший байт адреса указателя на обработчик.

Установить командой IM 2 второй режим прерываний.

Вновь разрешить прерывания командой EI.

Естественно, что к этому моменту сама процедура обработки прерываний должна иметься в памяти. Для возврата к стандартному режиму обработки прерываний нужно выполнить похожие действия:

Запретить прерывания.

Не помешает восстановить значение регистра I, записав в него число 63.

Назначить командой IM 1 первый режим прерываний.

Разрешить прерывания.

Несколько подробнее нужно остановиться на втором и третьем пунктах установки прерываний. Предположим, что процедура-обработчик находится по адресу 60000 (#EA60) и память, начиная с адреса 65000, никак в программе не используется. Значит указатель можно поместить именно в эту область. Для регистра I в этом случае можно выбрать одно из двух значений: 253 или 254. Тогда для размещения указателя можно использовать либо адреса 65023/65024 (253ґ256+255/256) либо 65279/65280 (254ґ256+255/256). Например, при I равном 254 запишем по адресу 65279 младший байт адреса обработчика - #60, а в 65280 поместим старший байт - #EA.

Однако нужно учитывать, что некоторые внешние устройства могут изменять значение вектора прерывания. Кроме того, если ваш Speccy сработан не слишком добросовестным производителем, то вектор прерывания иногда может скакать совершенно произвольным и непредсказуемым образом. Принимая это во внимание, даже во многих фирменных играх используется несколько иной подход. Вместо записи двух байтов по определенному адресу выстраивается целая таблица размером как минимум 257 байт с таким расчетом, чтобы при любом значении вектора прерываний считывался один и тот же адрес. Понятно, что для этого все байты таблицы должны быть одинаковыми. Это несколько осложняет установку прерывания и требует больше памяти, но зато значительно увеличивает надежность работы программы.



Наиболее удачным для такой таблицы представляется байт 255 (#FF). В этом случае обработчик прерываний должен находиться по адресу 65535 (#FFFF). На первый взгляд может показаться странным выбор такого адреса, ведь остается всего один байт! Но и этого единственного байта оказывается достаточным, если в него поместить код команды JR. Следующий байт, находящийся уже по адресу 0, укажет смещение относительного перехода. По нулевому адресу в ПЗУ записан код команды DI (#F3), поэтому полностью команда будет выглядеть как JR 65524. Далее в ячейке 65524 можно разместить уже более «длинную» команду JP address и заданный в ней адрес может быть совершенно произвольным.

Приведем пример такой подпрограммы установки прерываний:

IMON LD A,24 ;код команды JR LD (65535),A LD A,195 ;код команды JP LD (65524),A LD (65525),HL ;в HL - адрес обработчика прерываний LD HL,#FE00 ;построение таблицы для векторов прерываний LD DE,#FE01 LD BC,256 ;размер таблицы минус 1 LD (HL),#FF ;адрес перехода #FFFF (65535) LD A,H ;запоминаем старший байт адреса таблицы LDIR ;заполняем таблицу DI ;запрещаем прерывания на время ; установки второго режима LD I,A ;задаем в регистре I старший байт адреса ; таблицы для векторов прерываний IM 2 ;назначаем второй режим прерываний EI ;разрешаем прерывания RET

Перед обращением к ней в регистровой паре HL необходимо указать адрес соответствующей процедуры обработки прерываний. Учтите, что в области памяти, начиная с адреса 65024, менять что-либо не желательно. Если все же возникнет такая необходимость, убедитесь прежде, что своими действиями вы не затроните установленные процедурой байты.

Подпрограмма восстановления первого режима выглядит заметно проще и в комментариях уже не нуждается:

IMOFF DI LD A,63 LD I,A IM 1 EI RET

При составлении процедуры обработки прерываний нужно придерживаться определенных правил. Во-первых, написанная вами подпрограмма должна выполняться за достаточно короткий промежуток времени. Желательно, чтобы ее быстродействие было сопоставимо с «пульсом» прерываний, то есть чтобы ее продолжительность не превышала 1/50 секунды. Это правило не является обязательным, но в противном случае трудно будет получить эффект «параллельности» процессов. Во-вторых, и это уже совершенно необходимо, все регистры, которые могут изменить свое значение в вашей процедуре, должны быть сохранены на входе и восстановлены перед выходом. Это же относится и к любым переменным, используемым не только в прерывании, но и в основной программе. В связи с этим не рекомендуется обращаться к подпрограммам ПЗУ, по крайней мере, до тех пор, пока вы не знаете совершенно точно, какие в них используются регистры и какие системные переменные при этом могут быть изменены. Вызов подпрограмм ПЗУ не желателен еще и потому, что некоторые из них разрешают прерывания, что совершенно недопустимо во избежание рекурсии (т. е. самовызова) обработчика, который должен работать при запрещенных прерываниях. Однако использовать команду DI в самом начале процедуры не обязательно, так как это действие выполняется автоматически и вам нужно только позаботиться о разрешении прерываний перед выходом.



Если вы не хотите лишаться возможностей, предоставляемых стандартной процедурой обработки прерываний, можете завершать свою подпрограмму командой JP 56. А при использовании прерываний в бейсик-программах без этого просто не обойтись, иначе клавиатура окажется заблокирована. В общем случае обработчик прерываний может иметь такой вид:

INTERR PUSH AF PUSH BC PUSH DE PUSH HL ....... POP HL POP DE POP BC POP AF JP 56

В заключение этого раздела приведем процедуру, отсчитывающую секунды, остающиеся до окончания игры. Эта процедура может вызываться как из машинных кодов, так и из программы на Бейсике. В верхнем левом углу экрана постоянно будет находиться число, уменьшающееся на единицу по истечении каждой секунды. Для применения этой подпрограммы в реальной игре вам достаточно изменить адрес экранной области, куда будут выводиться числа и, возможно, начальное значение времени, отводимое на игру. Момент истечения времени определяется содержимым ячейки по смещению ORG+4. Если ее значение окажется не равным 0, значит игра закончилась.

ORG 60000 JR INITI JR IMOFF OUTTIM DEFB 0 INITI LD HL,D_TIM0 LD DE,D_TIME LD BC,3 LDIR XOR A LD (OUTTIM),A LD HL,TIM0 LD (HL),50 INC HL LD (HL),A INC HL LD (HL),A LD HL,TIMER ;установка прерывания

TIMER PUSH AF PUSH BC PUSH DE PUSH HL CALL CLOCK POP HL POP DE POP BC POP AF JP 56 TIM0 DEFB 50 ;количество прерываний в секунду TIM1 DEFB 0 ;время «проворота» третьего символа TIM2 DEFB 0 ;время «проворота» второго символа TIM3 DEFB 0 ;время «проворота» первого символа D_TIM0 DEFM "150" ;символы, выводимые на экран D_TIME DEFM "150" ;начальное значение времени ; Проверка необходимости изменения текущего времени CLOCK LD HL,TIM0 DEC (HL) JR NZ,CLOCK1 LD (HL),50 ; Уменьшение секунд LD A,8 ;символ «проворачивается» за 8 LD (TIM1),A ; тактов прерывания LD HL,D_TIME+2 LD A,(HL) DEC (HL) CP "0" JR NZ,CLOCK1 LD (HL),"9" ; Уменьшение десятков секунд LD A,8 LD (TIM2),A DEC HL LD A,(HL) DEC (HL) CP "0" JR NZ,CLOCK1 LD (HL),"9" ; Уменьшение сотен секунд LD A,8 LD (TIM3),A DEC HL LD A,(HL) DEC (HL) CP "0" JR Z,ENDTIM ;если время истекло CLOCK1 LD DE,#401D ;адрес экранной области LD A,(D_TIME) ;первый символ - сотни секунд LD HL,TIM3 CALL PRNT LD A,(D_TIME+1) ;второй символ - десятки секунд CALL PRNT LD A,(D_TIME+2) ;третий символ - секунды ; Печать символов с учетом их «проворота» PRNT PUSH HL LD L,A ;расчет адреса символа LD H,0 ; в стандартном наборе ADD HL,HL ADD HL,HL ADD HL,HL LD A,60 ADD A,H LD H,A EX (SP),HL LD A,(HL) LD C,A AND A JR Z,PRNT1 ;если символ «проворачивать» не нужно DEC (HL) EX (SP),HL NEG ;пересчет адреса символьного набора для ; создания иллюзии «проворота» цифры LD B,A LD A,L SUB B LD L,A JR PRNT2 PRNT1 EX (SP),HL PRNT2 LD B,8 PUSH DE PRNT3 LD A,(HL) LD (DE),A INC HL INC D LD A,C AND A JR Z,PRNT4 ; После цифры 9 при «провороте» должен появляться 0, а не двоеточие LD A,L CP 208 ;адрес символа : JR C,PRNT4 SUB 80 ;возвращаемся к адресу символа 0 LD L,A PRNT4 DJNZ PRNT3 POP DE POP HL INC DE DEC HL RET ; Истечение времени - выключение 2-го режима прерываний ENDTIM POP HL ;восстановление значения указателя стека ; после команды CALL CLOCK CALL IMOFF LD A,1 ;установка флага истечения времени LD (OUTTIM),A POP HL ;восстановление регистров POP DE POP BC POP AF RET


ЛИТЕРАТУРА



ЛИТЕРАТУРА

А. Капульцевич, И. Капульцевич, А. Евдокимов. Как написать игру для ZX Spectrum - книга первая, СПб.: Питер, 1994.

Ларченко А. А., Родионов Н. Ю. ZX Spectrum и TR-DOS для пользователей и программистов - 3-е изд., СПб.: Питер, 1994.

Системные программы для ZX Spectrum. Сборник описаний, 1-й выпуск (под редакцией Родионова Н. Ю.) - СПб.: Питер, 1993.

Диалекты Бейсика для ZX Spectrum (под редакцией Родионова Н. Ю., Ларченко А. А.) - СПб.: Питер, 1992.

Компьютерные миры ZX Spectrum. Сборник описаний игровых программ, 1-й выпуск - СПб.: Питер, 1994.



Логические операции


О команде OR стоит поговорить более подробно, так как она очень часто будет встречаться в дальнейшем. Наряду с двумя другими командами AND и XOR она относится к логическим операциям, присущим только машинному языку и не имеющим аналогии, по крайней мере, в стандартном «Спектрум-Бейсике». Эти три операции воздействуют на отдельные биты (разряды двоичного числа), изменяя их в соответствии с одним из трех возможных принципов поразрядного сложения. Сразу же скажем, что все три операции могут выполняться только на регистре A и изменяют флаги Z, P/V и S. Флаги CY и N после выполнения любой операции сбрасываются в 0 независимо от результата, а флаг H устанавливается после AND и сбрасывается после OR или XOR.

Принцип поразрядного объединяющего ИЛИ (OR) заключается в том, что если хотя бы в одном из двух двоичных чисел определенный разряд не равен нулю, то соответствующий разряд результата также будет ненулевым. Иными словами, если хотя бы в одном из объединяющихся байтов установлен бит, то и в результирующем байте данный бит будет установлен, а ноль получится лишь в том случае, если и там и там бит сброшен. Например, при сложении по принципу OR чисел 1001 и 1100 получится число 1101:

1 0 0 1 1 1 0 0 ---------- 1 1 0 1

Теперь, наверное, должно быть понятно, что после выполнения команд

LD A,B OR C

нулевой результат в аккумуляторе получится лишь тогда, когда оба регистра (B и C) будут содержать 0.

При сложении двух чисел по принципу поразрядного исключающего ИЛИ (XOR) нулевой бит может получиться в двух случаях: если соответствующий бит установлен в обоих слагаемых байтах либо если оба бита сброшены. Если же в одном из чисел бит установлен, а в другом сброшен, в результате получим установленный бит. Для тех же двух чисел результат сложения по XOR будет таким:

1 0 0 1 1 1 0 0 ---------- 0 1 0 1

этот принцип используется, например, при выводе текста и графики в режиме OVER 1.

Принцип поразрядного И (AND) состоит в том, что 1 в каком-то бите может получиться лишь тогда, когда данный бит установлен в обоих числах. В любом другом случае в результате получится нулевой бит:

1 0 0 1 1 1 0 0 ---------- 1 0 0 0

Команды, использующие принцип AND, применяются, в тех случаях, когда требуется в двоичном числе выделить несколько битов, оставив их неизменными, а остальные сбросить в 0. В дальнейшем вы увидите, что такие команды очень полезны, например, при различных графических построениях.



МАКРООПРЕДЕЛЕНИЯ



МАКРООПРЕДЕЛЕНИЯ

Как вы могли заметить, во многих программах повторяются совершенно однотипные фрагменты текста, отличающиеся только значениями отдельных регистров, а то и вовсе совпадающие. Оказывается, в таких случаях не обязательно каждый раз переписывать одну и ту же последовательность команд. Вы можете обозначить данную последовательность специальными директивами, о которых мы скажем ниже, и присвоить ей какое-нибудь имя. Ав дальнейшем, в тех местах текста, где она должна появиться, достаточно записывать только ее имя. Такие фрагменты текста называются макроопределениями или макросами, а имя макроса - макрокомандой.

Для определения макроса в поле меток записывается его имя, а в поле мнемоник - директива ассемблера MAC. Затем пишется тело макроопределения, состоящее из любых команд, и завершается запись директивой ENDM. В качестве примера можно предложить такой макрос:

PRAT MAC LD A,22 RST 16 LD A,B RST 16 LD A,C RST 16 ENDM

Всякий раз, когда в программу потребуется включить записанную между директивами MAC и ENDM последовательность инструкций, достаточно в поле мнемоник записать макрокоманду PRAT. Это позволит сократить исходный текст программы и сделать его несколько более наглядным, приблизив запись к языкам высокого уровня. Действительно, ведь макрокоманды очень похожи на операторы Бейсика. Например, вместо того чтобы писать

CALL 3435 LD A,2 CALL 5633

можно оформить этот фрагмент в виде макроса и присвоить ему имя CLS. Тогда для очистки экрана вы сможете на время забыть адреса соответствующих процедур ПЗУ и записывать в поле мнемоник этот старый знакомый оператор Бейсика.

При задании макросов имеется ряд ограничений, которые необходимо всегда учитывать. Во-первых, имена макроопределений в отличие от имен меток могут состоять не более чем из пяти символов. Причем лишние символы в этом случае не игнорируются, а приводят к появлению ошибки (вообще же лучше обходиться четырьмя символами, тогда вы избавите себя от лишних вопросов). Во-вторых, внутри макроопределения не должно быть строк с метками, так как многократное использование одного и того же макроса приведет к дублированию имен и в результате также появится сообщение об ошибке трансляции. Не допускаются и вложения макросов, то есть внутри макроопределения не могут встречаться ссылки на другие макросы.


Мы уже говорили, что в макросах можно определять не только строго совпадающие фрагменты исходного текста, но и слегка отличающиеся друг от друга. Это становится реальным благодаря возможности использования так называемых формальных параметров. Для каждого макроса допускается задавать до 16 таких параметров. Например, при рисовании точек на экране нужно указывать две координаты. Можно написать макрос, в котором регистры B и C будут загружаться требуемыми значениями и который вызывается командой

PLOT X,Y

где X и Y - любые допустимые в одноименном операторе Бейсика значения координат. Формальные параметры в макроопределении задаются знаком равенства (=) и символом, код которого соответствует порядковому номеру фактического параметра в макрокоманде. Для первого параметра этот символ может иметь коды 0, 16, 32, 48 и так далее, второй параметр будут описывать любые символы с кодами 1, 17, 33, 49... Чтобы не запутаться, рекомендуем использовать цифровые символы от 0 до 9 для определения первых десяти параметров, а остальные 6 можно задавать, например, буквами K, L, M, N, O и P. Тогда макрос PLOT будет записан следующим образом:

PLOT MAC LD C,=0 LD B,=1 CALL 8933 ENDM

После трансляции вышеприведенной макрокоманды PLOT с параметрами 100 для координаты X и 80 для Y получится следующая последовательность команд микропроцессора:

LD C,100 LD B,80 CALL 8933

то есть формальные параметры =0 и =1 заменятся фактическими 100 и 80 соответственно.

При написании макрокоманд нужно помнить, что если имя макроса состоит из пяти символов (напомним еще раз, что это максимальная длина имен макросов), то фактические параметры обязательно нужно заключать в круглые скобки, например:

PRINT (TEXT)

Прежде чем привести пример использования макросов в реальной программе, добавим, что в качестве параметров могут выступать только непосредственные числовые значения. Использование символьных строк (за исключением имен меток и констант) не разрешается.



Во время трансляции текст макроопределения не переводится сразу в машинные коды, а помещается в специальный буфер, из которого затем извлекается по мере необходимости. Поэтому перед вводом команды A необходимо указать размер этого буфера с помощью команды C. Помните, при вводе этой команды сначала запрашивается размер входного буфера Include buffer?, а затем появляется еще один запрос - Macro buffer? На него нужно ввести количество байт, достаточное для размещения текста всех макроопределений, заданных в программе. Если задать слишком маленькое число, то во время первого прохода ассемблирования появится сообщение No Macro Space. В этом случае нужно повторить ввод с большим числом. В приведенном ниже примере для размещения макросов достаточно 300 байт.

ORG 60000 ENT $ ; Печать ASCIIZ-строки в позиции экрана, задаваемой первыми двумя параметрами PRN MAC LD B,=0 LD C,=1 LD HL,=2 CALL PRNZ ENDM ; Позиционирование печати PRAT MAC LD A,22 RST 16 LD A,B RST 16 LD A,C RST 16 ENDM ; Установка цветов INK и PAPER, а также цвета бордюра COLOR MAC LD A,=1*8+=0 LD (23693),A LD A,=1 CALL 8859 ENDM ; Очистка экрана и назначение вывода на основной экран CLS MAC CALL 3435 LD A,2 CALL 5633 ENDM ; Установка PLOT-позиции без рисования точки PSET MAC LD L,=0 LD H,=1 LD (23677),HL ENDM ; Черчение линии из текущей PLOT-позиции DRAW MAC EXX PUSH HL LD DE,=0 LD C,=1 LD B,=2 CALL 9402 POP HL EXX ENDM ; Направления рисования линий UP_RT EQU #0101 ;вверх и вправо DN_RT EQU #FF01 ;вниз и вправо DN_LF EQU #FFFF ;вниз и влево UP_LF EQU #01FF ;вверх и влево ; ------ BEGIN COLOR (5,0) CLS PRN 5,8,TEXT1 PRN 7,7,TEXT2 PSET 48,144 DRAW UP_RT,131,0 DRAW DN_RT,0,39 DRAW UP_LF,131,0 DRAW UP_RT,0,39 PSET 50,142 DRAW UP_RT,127,0 DRAW DN_RT,0,35 DRAW UP_LF,127,0 DRAW UP_RT,0,35 RET ; Подпрограмма печати ASCIIZ-строки, вызываемая макросом PRN PRNZ PUSH HL PRAT PRNZ1 LD A,(HL) INC HL AND A JR Z,PRNZ2 RST 16 JR PRNZ1 PRNZ2 POP HL RET ; ------ TEXT1 DEFB 16,2,19,1 DEFM "*** DEMO ***" DEFB 0 TEXT2 DEFB 16,6,19,1 DEFM "### MACROS ###" DEFB 16,5,0

Имея возможность работать с дисководом, очень удобно собрать все макросы в одном или нескольких файлах (например, по родству выполняемых функций) и затем при необходимости включать их в исходный текст с помощью команды ассемблера *F. Так как макросы сразу не транслируются, то это никак не повлияет на размер исполняемого кода, даже если среди включаемых макросов есть такие, которые ни разу не используются в программе. Они, конечно, займут некоторый объем памяти, но так и останутся в буфере невостребованными.

В заключение хочется предостеречь вас от чрезмерного увлечения макроопределениями. Во всем нужно знать меру. Учтите, что макросы могут запросто свести все преимущества ассемблера на нет, снизив эффективность программы, в лучшем случае, до уровня компиляторов.


МАНИПУЛЯЦИИ С БУКВАМИ



МАНИПУЛЯЦИИ С БУКВАМИ

Иметь в своем распоряжении множество наборов символов различных начертаний, подобных тем, которые мы привели в четвертой главе, и использовать их для печати текстов заставки и других частей программы- это замечательно, но не всегда целесообразно. Любой фонт занимает, во-первых, какую-то часть памяти компьютера, а во-вторых - определенное место на ленте и требует дополнительного времени при загрузке игровой программы. Но ведь у вас под рукой всегда имеется стандартный набор символов, и возникает вопрос, а нельзя ли его как-то преобразовать программным путем, чтобы получить видоизмененные буквы и цифры. В предыдущей главе мы описали процедуру , применение которой приводит к утолщению символов, и этот прием довольно часто используется даже в фирменных играх. Рассмотрим теперь два способа преобразования символов, первый из которых увеличивает их высоту в два раза, а второй делает их в два раза шире.



МАШИННЫЕ КОДЫ И АССЕМБЛЕР



МАШИННЫЕ КОДЫ И АССЕМБЛЕР

Вы уже, вероятно, имеете некоторое представление о том, что такое машинные коды, а если нет, то мы сейчас постараемся объяснить это, хотя бы немного, что называется, «на пальцах», не вдаваясь в теорию проектирования компьютерной техники.

Нетрудно догадаться, что микропроцессор, будучи куском железа, пусть даже довольно интеллектуального, но все же железа, не способен понимать слова человеческого языка, а тем более- складывать отдельные слова в осмысленные фразы. Как и всякая порядочная электронная машина, он может воспринимать только электрические заряды. Но в отличие от пылесоса микропроцессор принимает заряды одновременно с восьми контактов (строго говоря, микропроцессор обрабатывает гораздо больше сигналов, но сейчас нас интересуют только восемь из них) и в зависимости от поступившего сигнала выполняет то или иное действие.

У каждого из этих восьми контактов может быть лишь два состояния - есть заряд или нет заряда. Поэтому его наличие можно представить как 1, а отсутствие - как 0. Последовательности из единиц и нулей дают числа в двоичном представлении, но их несложно перевести в привычный десятичный формат. Напомним, как это делается на примере числа 00111100. Самый младший разряд (то есть, крайнюю правую цифру) умножаем на 1, второй разряд - на 2, третий - на 4, следующий - на 8 и так далее. Иными словами, значение каждого разряда умножается на 2 в степени n, где n - номер разряда, который может изменяться от 0 до 7 (то есть, говоря научно, 2 - это основание системы счисления). Если вам не очень понятно такое определение, воспользуйтесь простой формулой для перевода нашего двоичного числа в десятичное:

00111100=0ґ128+0ґ64+1ґ32+1ґ16+1ґ8+1ґ4+0ґ2+0ґ1=60

Точно так же можно перевести в десятичное и любое другое число, представленное в двоичном формате. Попутно напомним, что разряды двоичных чисел в информатике принято называть битами, а последовательности из 8 битов составляют байты.


Теперь вы вправе спросить, а что же будет, если микропроцессору дать такую команду? В ответ мы напишем похожую строку на хорошо известном вам Бейсике:

LET A=A+1

то есть содержимое переменной A увеличивается на единицу. И если последнюю запись понять не так уж трудно, то на свете найдется не так уж много людей, способных не только воспринимать, но и писать достаточно сложные программы, оперируя голыми числами, да еще двоичными. Простой и логичный выход из создавшегося затруднения - заменить все коды машинного языка человеческими словами или, хотя бы сокращениями, поставив каждой команде микропроцессора в соответствие единственное обозначение. Именно такой язык и был назван ассемблером. Он стоит всего лишь на одну ступеньку выше машинных кодов, однако общаться с компьютером на таком языке несравненно проще, чем на языке цифр. Приведенная выше комбинация единиц и нулей 00111100 на ассемблере будет выглядеть так:

INC A

где INC - сокращение от английского слова increase (увеличиваться). Сразу же скажем, что сокращенные имена команд микропроцессора называют мнемониками. Запомните это слово хорошенько, так как оно не раз встретится в нашей книге.


Меню программы EAGLES NEST



Рисунок 1.2. Меню программы EAGLES NEST

Меню - это что-то вроде диспетчерского пункта, из которого можно вызвать любой кадр заставки. Обычно в меню выбирается способ управления игрой: один из типов джойстика (Kempston, Sinclair и т. д.) или клавиатура (Keyboard); начальный уровень (Level); запуск игры (Start Game). После выбора управления от клавиатуры часто появляется еще один кадр - Define Keys, в котором предлагается назначить удобные именно для вас управляющие клавиши. В ряде игр как отдельные кадры заставки выводится таблица рекордов (Hi Score), подсказка (Help), рассказ о сюжете (History) и прочая информация.



МНОГОКАДРОВАЯ ЗАСТАВКА



МНОГОКАДРОВАЯ ЗАСТАВКА

После того, как игра загрузится в память компьютера, скорее всего на экране появится основной кадр многокадровой заставки, который носит название «Меню» и где обычно перечисляются действия, связанные с настройкой игры. Поскольку в первой главе мы об этом уже говорили, то не будем повторяться и сразу перейдем к задачам, которые следует решать при создании такой заставки. На наш взгляд, таких задач будет три: формирование окон в заданных частях экрана, создание блоков данных с текстами для меню и всех кадров и, наконец, то, с чем мы раньше еще не встречались- составление программы управления всеми частями заставки.

Что касается окон, то в принципе этот вопрос нами уже решен и можно было бы к нему вновь не возвращаться. Однако использование подпрограмм, работающих с окнами в том виде, в котором мы их предложили ранее, не всегда удобно. Если все параметры окон фиксированы, то нелепо каждый раз переопределять переменные ROW, COL и иже с ними. Лучше составить блоки данных, содержащие сведения о размерах, местоположении и всех прочих характеристиках, а затем выполнять любые преобразования окон, передавая подпрограммам единственное значение - адрес таблицы параметров (то бишь блока данных).

В начале книги мы говорили о существовании такой группы регистров, как индексные. Группа эта немногочисленна и включает в себя лишь два регистра: IX и IY. Как мы уже сообщали, оба они состоят из 16 бит и разделить их на половинки «законными» методами невозможно, поэтому они обычно рассматриваются не как регистровые пары, а как отдельные регистры.

Но для каких целей они существуют, в каких ситуациях их удобно применять и в каких группах команд они могут встречаться? Обычно эти регистры используются при обработке массивов, блоков данных или таблиц, а употребляются они практически во всех типах команд, в которых может принимать участие регистровая пара HL. За более подробной информацией на этот счет можете обратиться к , где в алфавитном порядке приведены все команды микропроцессора.



Рисунок 6.9. Многокадровая заставка

MENU CALL 3435 LD A,2 CALL 5633 LD DE,TEXT5 LD BC,ENDTXT-TEXT5 CALL 8252 ;«Select options 0 to 4» LD IX,WIN0 ;окно для названия игры CALL CLSW CALL SETW CALL BOX

В сформированном окне напечатаем название игры FOOTBALL, для чего используем расширенные подпрограммой буквы.

LD DE,COORD ;позиционирование курсора LD BC,TEXT-COORD CALL 8252 LD HL,TEXT ;вывод названия игры LD B,8 MET LD A,(HL) PUSH HL PUSH BC CALL DBLSYM POP BC POP HL INC HL DJNZ MET

Сделаем для меню окно с черной тенью и поместим в него текст, в соответствии с которым вы можете обратиться к одному из четырех кадров, нажав клавиши 1-4, или начать игру, выбрав клавишу 0 (для упрощения программы нажатие 0 или 4 возвращает вас в редактор GENS4 или в Бейсик).

LD IX,WIN1 ;окно основного меню CALL WINDOW LD DE,TEXT1 ;текст меню LD BC,TEXT2-TEXT1 CALL 8252

Теперь можно написать блок управления, который, как ни странно, выглядит довольно простым. Программка «крутится» в цикле, пока не нажата одна из указанных в кавычках клавиш. При нажатии же на клавишу, команда сравнения CP изменит биты флагового регистра (в частности флаг Z установится в ноль), после чего следующая команда JR Z,KADR? осуществит переход на выбранный вами кадр.

XOR A LD (23560),A CYCLE LD A,(23560) CP "1" JR Z,KADR1 CP "2" JR Z,KADR2 CP "3" JR Z,KADR3 CP "4" JR Z,EXIT CP "0" JR NZ,CYCLE ; При нажатии на клавиши 0 или 4 восстанавливаются атрибуты экрана и стандартный ; набор символов, после чего происходит выход в редактор GENS или в Бейсик. EXIT LD A,7 CALL 8859 ;белый бордюр LD A,%00111000 ;стандартные атрибуты LD (23693),A CALL 3435 LD A,2 CALL 5633 LD HL,15360 LD (23606),HL RET

Части программы, формирующие окна кадров и печатающие в них тексты, исключительно похожи друг на друга. Тем не менее, имеются и некоторые отличия, которые определяются конкретными данными для окон и текстов. Каждый из кадров начинается со звукового сигнала SND, о котором мы говорили в начале этой главы и заканчивается процедурой WAIT, которая фиксирует кадр на экране, позволяя увидеть его содержание и выбрать дальнейшие действия (в этой программе чисто умозрительно, за исключением клавиши Е, которая действительно возвращает вас в меню).




Известно, что в Бейсике к любому элементу массива можно обратиться по индексу, например, оператор LET b=a(8) присвоит переменной b значение 8-го элемента массива a(). В ассемблере подобная запись может выглядеть так:

LD B,(IX+7)

Обратите внимание, что первый элемент массива имеет индекс 0, а не 1. В отличие от массивов Бейсика, максимальный индекс у регистров IX и IY не может превышать 127, но зато допускаются отрицательные значения номера элемента массива. Таким образом, общий размер адресуемой области составляет 256 байтовых элементов, а индексный регистр указывает на его «середину».

Реальным примером большой структуры данных могут служить системные переменные Бейсика. Обычно они адресуются регистром IY, который указывает на переменную ERR_NR, находящуюся по адресу 23610. Кстати, именно в связи с этим регистр IY лучше оставить в покое и никак не изменять его в своих программах, по крайней мере, до тех пор, пока вы так или иначе используете операционную систему компьютера. Что касается регистра IX, то им вы можете смело пользоваться при любых обстоятельствах.

Наверное, лучше всего объяснить идею применения индексных регистров на конкретном примере. В качестве такого примера приведем уже знакомые вам процедуры очистки окон и установки в них постоянных атрибутов (тем более, что они понадобятся при составлении программы многокадровой заставки), но графические переменные заменим единой структурой, первый элемент которой адресуем через IX. Саму же структуру оставим без изменения, то есть на первом месте (по смещению 0, задаваемом как IX+0 или просто IX) по-прежнему будет находиться параметр COL, на втором (задаваемом как IX+1) - ROW, дальше LEN, HGT и ATTR. А чтобы новые процедуры отличить от описанных выше, изменим их имена на SETW и CLSW:

SETW LD DE,#5800 LD B,(IX+3) ;HGT LD C,(IX+2) ;LEN LD A,(IX+1) ;ROW LD L,A LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,DE LD A,L ADD A,(IX) ;COL LD L,A LD A,(IX+4) ;ATTR SETW1 PUSH BC PUSH HL SETW2 LD (HL),A INC HL DEC C JR NZ,SETW2 POP HL POP BC LD DE,32 ADD HL,DE DJNZ SETW1 RET ; ----------------- CLSW LD B,(IX+3) ;HGT LD C,(IX+2) ;LEN LD A,(IX+1) ;ROW CLSW1 PUSH AF PUSH BC PUSH DE CALL 3742 POP DE LD A,L ADD A,(IX) ;COL LD L,A LD B,8 CLSW2 PUSH HL PUSH BC LD B,C CLSW3 LD (HL),0 INC HL DJNZ CLSW3 POP BC POP HL INC H DJNZ CLSW2 POP BC POP AF INC A DJNZ CLSW1 RET



KADR1 CALL SND ;звуковой сигнал LD IX,WIN2 CALL WINDOW ;вывод окна LD DE,TEXT2 ;текст в окне LD BC,TEXT3- TEXT2 CALL 8252 CALL WAIT ;ожидание нажатия клавиши E

JP MENU ;возврат в меню ; Формирование окна Кадра 2 и печать в нем текста KADR2 CALL SND LD IX,WIN3 CALL WINDOW LD DE,TEXT3 LD BC,TEXT4-TEXT3 CALL 8252 CALL WAIT JP MENU ; Формирование окна Кадра 3 и печать в нем текста KADR3 CALL SND LD IX,WIN4 CALL WINDOW LD DE,TEXT4 LD BC,TEXT5-TEXT4 CALL 8252 CALL WAIT JP MENU ; Подпрограмма ожидания нажатия клавиши E

WAIT XOR A LD (23560),A WAIT1 LD A,(23560) CP "E" RET Z CP "e" JR NZ,WAIT1 RET SND LD B,10 LD HL,350 LD DE,4

; Данные для формирования окна с названием игры

; Данные для формирования всех окон с тенями

; Данные для печати названия игры COORD DEFB 22,1,8,16,6,19,1 TEXT DEFM "FOOTBALL" ; Данные для печати текста Меню TEXT1 DEFB 22,7,13,16,7,17,2,19,1 DEFM "M E N U" DEFB 22,9,9 DEFM "0. START··GAME" DEFB 22,11,9 DEFM "1. KEYBOARD" DEFB 22,13,9 DEFM "2. JOYSTICK" DEFB 22,15,9 DEFM "3. HI··SCORE" DEFB 22,17,9 DEFM "4. DEMO MODE" ; Данные для печати текста Кадра 1 TEXT2 DEFB 22,6,14,17,3,16,1 DEFM "Define keys" DEFB 22,8,15 DEFM "LEFT....O" DEFB 22,10,15 DEFM "RIGHT...P" DEFB 22,12,15 DEFM "UP......Q" DEFB 22,14,15 DEFM "DOWN....A" DEFB 22,16,15 DEFM "FIRE....M" DEFB 22,18,16 DEFM "MENU--E" ; Данные для печати текста Кадра 2 TEXT3 DEFB 22,10,16,17,5,16,0 DEFM "Joystick" DEFB 22,12,14 DEFM "1. KEMPSTON" DEFB 22,14,14 DEFM "2. SINCLAIR" DEFB 22,16,14 DEFM "3. CURSOR" DEFB 22,18,16 DEFM "MENU--E" ; Данные для печати текста Кадра 3 TEXT4 DEFB 22,7,8,17,6,16,1 DEFM "Hi score" DEFB 22,8,7 DEFM "PETR...726" DEFB 22,10,7 DEFM "IGOR...694" DEFB 22,12,7 DEFM "ALEX...605" DEFB 22,14,7 DEFM "SERG...523" DEFB 22,16,7 DEFM "WLAD...419" DEFB 22,18,8 DEFM "MENU--E" ; Данные для текста под заставкой TEXT5 DEFB 22,21,6,16,7,17,4,19,0 DEFM "Select options 0 to 4" ENDTXT


к любому элементу массива можно


Известно, что в Бейсике к любому элементу массива можно обратиться по индексу, например, оператор LET b=a(8) присвоит переменной b значение 8-го элемента массива a(). В ассемблере подобная запись может выглядеть так:

LD B,(IX+7)

Обратите внимание, что первый элемент массива имеет индекс 0, а не 1. В отличие от массивов Бейсика, максимальный индекс у регистров IX и IY не может превышать 127, но зато допускаются отрицательные значения номера элемента массива. Таким образом, общий размер адресуемой области составляет 256 байтовых элементов, а индексный регистр указывает на его «середину».

Реальным примером большой структуры данных могут служить системные переменные Бейсика. Обычно они адресуются регистром IY, который указывает на переменную ERR_NR, находящуюся по адресу 23610. Кстати, именно в связи с этим регистр IY лучше оставить в покое и никак не изменять его в своих программах, по крайней мере, до тех пор, пока вы так или иначе используете операционную систему компьютера. Что касается регистра IX, то им вы можете смело пользоваться при любых обстоятельствах.

Наверное, лучше всего объяснить идею применения индексных регистров на конкретном примере. В качестве такого примера приведем уже знакомые вам процедуры очистки окон и установки в них постоянных атрибутов (тем более, что они понадобятся при составлении программы многокадровой заставки), но графические переменные заменим единой структурой, первый элемент которой адресуем через IX. Саму же структуру оставим без изменения, то есть на первом месте (по смещению 0, задаваемом как IX+0 или просто IX) по-прежнему будет находиться параметр COL, на втором (задаваемом как IX+1) - ROW, дальше LEN, HGT и ATTR. А чтобы новые процедуры отличить от описанных выше, изменим их имена на SETW и CLSW:

SETW LD DE,#5800 LD B,(IX+3) ;HGT LD C,(IX+2) ;LEN LD A,(IX+1) ;ROW LD L,A LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,DE LD A,L ADD A,(IX) ;COL LD L,A LD A,(IX+4) ;ATTR SETW1 PUSH BC PUSH HL SETW2 LD (HL),A INC HL DEC C JR NZ,SETW2 POP HL POP BC LD DE,32 ADD HL,DE DJNZ SETW1 RET ; ----------------- CLSW LD B,(IX+3) ;HGT LD C,(IX+2) ;LEN LD A,(IX+1) ;ROW CLSW1 PUSH AF PUSH BC PUSH DE CALL 3742 POP DE LD A,L ADD A,(IX) ;COL LD L,A LD B,8 CLSW2 PUSH HL PUSH BC LD B,C CLSW3 LD (HL),0 INC HL DJNZ CLSW3 POP BC POP HL INC H DJNZ CLSW2 POP BC POP AF INC A DJNZ CLSW1 RET


МУЛЬТИПЛИКАЦИЯ



МУЛЬТИПЛИКАЦИЯ

В большинстве компьютерных игр персонажи беспрерывно передвигаются по экрану, создавая неповторимые ситуации, чем собственно и привлекают к себе внимание многочисленной армии почитателей ZXSpectrum. В пятой главе мы уже слегка затронули проблему движения изображений, прояснив с помощью нескольких примеров те принципы, которые лежат в основе любого перемещения по экрану, будь то тексты или спрайты. Теперь настало время внести в этот вопрос полную ясность, показав способы, наиболее часто используемые в игровых программах.

У вас может возникнуть вопрос, а зачем, собственно, рассматривать несколько разных способов, не проще ли ограничиться одним, только очень хорошим, и применять его во всех случаях компьютерной «жизни». Все дело в том, что возможны совершенно отличные друг от друга ситуации, определяемые конкретным замыслом. Скажем, для простых сюжетов нет смысла использовать сложные способы передвижения спрайтов, а в насыщенных играх простые способы уже могут оказаться неэффективными.

Первый способ основан на скроллинге окон, и если вы припомните программу, которая раздвигает в разные стороны створки железной решетки, то получите о нем полное представление. Тем не менее, мы вновь к нему возвращаемся, поскольку в нашем распоряжении появилась удобная процедура для вывода спрайтов. Перечислим действия, характерные для этого способа:

поместите на экран спрайт, например, с помощью процедуры PTBL, установив режим SPRPUT;

задайте окно, по размерам покрывающее этот спрайт и выполните скроллинг при помощи одной из четырех рассмотренных выше процедур. Спрайт довольно резво устремится в нужную сторону, так что в программе необходимо предусмотреть небольшую задержку.



МУЛЬТИПЛИКАЦИОННАЯ ЗАСТАВКА



МУЛЬТИПЛИКАЦИОННАЯ ЗАСТАВКА

Разговор о подвижных изображениях хотелось бы завершить чем-то законченным, что можно прямо или с вашими дополнениями использовать в игровых программах. Но при этом текст будущей ассемблерной программы не должен быть уж слишком длинным. Учитывая все это, предлагаем один из вариантов динамической заставки, в которой создается настоящая мультипликационная картинка (Рисунок 5.7). По двум лестницам вверх и вниз передвигаются два маленьких человечка, поворачиваясь к вам то лицом, то спиной. Вся заставка окружена рамкой из шариков, переливающихся всеми цветами радуги. Когда смотришь на эту подвижную картинку, то не верится, что создана она довольно простыми средствами, уже рассмотренными нами в предыдущих разделах. Разберем теперь текст программы и подробно прокомментируем некоторые ее части, представляющие особый интерес.



Мультипликационная заставка



Рисунок 5.7. Мультипликационная заставка

Начнем, как и раньше, с составления блоков данных. Самое простое - это формирование блока, содержащего тексты, без которых не обходится ни одна заставка, будь она статическая или динамическая:

TEXT DEFB 22,2,9,16,6 DEFM "Welcome to the" DEFB 22,5,7,16,7 DEFM "L·I·T·T·L·E··M·A·N" DEFB 22,8,9,16,4 DEFM "0. START GAME" DEFB 22,10,9 DEFM "1. KEYBOARD" DEFB 22,12,9 DEFM "2. KEMPSTON" DEFB 22,14,9 DEFM "3. INSTRUCTIONS" DEFB 22,16,9 DEFM "4. DEFINE KEYS" DEFB 22,19,8,16,3 DEFM "Press key 0 to 4"

Поскольку с выводом текстов мы уже неоднократно сталкивались и подробно их комментировали, то не будем лишний раз заострять на этом внимание и сразу перейдем к следующему блоку. Для кодирования отдельных элементов подвижных изображений вновь воспользуемся символами UDG, определяющими отдельные фрагменты будущих спрайтов.

UDG DEFB 3,3,15,3,6,7,2,1 ; A (144) DEFB 224,224,248,224,176,240,32,19 ; B (145) DEFB 13,29,53,37,7,6,14,0 ; C (146) DEFB 208,216,200,208,240,112,48,56 ; D (147) DEFB 7,7,31,7,13,15,4,3 ; E (148) DEFB 192,192,240,192,96,224,64,128 ; F (149) DEFB 11,27,19,11,15,14,12,28 ; G (150) DEFB 176,184,172,164,224,96,112,0 ; H (151) DEFB 3,3,15,0,4,4,0,1 ; I (152) DEFB 224,224,248,0,16,16,0,192 ; J (153) DEFB 6,27,27,27,7,6,14,0 ; K (154) DEFB 176,120,248,240,240,112,48,56 ; L (155) DEFB 7,7,31,0,8,8,0,3 ; M (156) DEFB 192,192,240,0,32,32,0,128 ; N (157) DEFB 13,30,31,15,15,14,12,28 ; O (158) DEFB 96,216,216,216,224,96,112,0 ; P (159) DEFB 0,28,38,79,95,127,62,28 ; Q (160) DEFB 16,16,16,127,16,16,16,16 ; R (161) DEFB 8,8,8,254,8,8,8,8 ; S (162)

Следующий блок данных - это то, чем будет манипулировать программа, создавая на экране непрерывное движение маленьких симпатичных человечков:

SPR1 DEFB 4,0,0,7,144,0,1,7,145,1,0,4,146,1,1,4,147 SPR2 DEFB 4,0,0,7,148,0,1,7,149,1,0,4,150,1,1,4,151 SPR3 DEFB 4,0,0,7,152,0,1,7,153,1,0,4,154,1,1,4,155 SPR4 DEFB 4,0,0,7,156,0,1,7,157,1,0,4,158,1,1,4,159 SPR5 DEFB 2,0,0,6,161,0,1,6,162


Каждая из строк SPR1... SPR4 соответствует одной из фаз движения человечка, а SPR5 - фрагменту лестницы. Последний используется не только для рисования лестниц в статической картинке, но и для восстановления изображения позади бегущего человечка. Формат данных в этих блоках вам уже знаком. Он выбран точно таким же, как и в программе ПРЫГАЮЩИЙ ЧЕЛОВЕЧЕК, следовательно, для вывода всех спрайтов на экран можно воспользоваться подпрограммой PUT, которую мы уже описали. Кроме блоков данных нам понадобятся еще несколько переменных, необходимых для управления перемещением человечков по экрану:

ROW DEFB 0 ;позиция по вертикали левого человечка DIRECT DEFB 0 ;направление движения POSIT DEFB 0 ;фаза движения

Позже мы объясним смысл этих переменных более подробно.

После того, как мы разобрались с надписями и спрайтами, можно переходить к основной программе, которая, как и в большинстве игр, содержит две части: статическую и динамическую. Первая устанавливает атрибуты экрана и формирует на нем все неподвижные элементы заставки: текст, лестницы и рамки вокруг них (Рисунок  5.7). Во многих играх вместо загрузки дополнительных фонтов новый набор символов формируется программой. Например, можно предложить следующий способ получения шрифта Bold, буквы в котором имеют утолщенное начертание:

BOLD LD HL,15616 ;адрес стандартного набора символов LD DE,END ;метка конца программы; по этому ; адресу расположится новый фонт LD BC,#300 ;счетчики циклов: B = 3, C = 0 (256) PUSH DE BOLD1 LD A,(HL) ;байт из стандартного набора SRL A ; смещаем на 1 бит (пиксель) влево OR (HL) ; и объединяем с его прежним значением LD (DE),A ;помещаем в новый фонт INC HL ;переходим к следующему байту INC DE DEC C JR NZ,BOLD1 ;в общей сложности повторяем цикл DJNZ BOLD1 ; 768 раз POP HL DEC H ;уменьшаем адрес нового шрифта на 256 LD (23606),HL ; и заносим в системную ; переменную CHARS RET

После получения нового шрифта можно сформировать на экране все неподвижные части заставки:

SCREEN LD B,3 ; Рисование окон WIND LD C,3 CALL STAIRS LD C,27 CALL STAIRS INC B LD A,B CP 19 JR C,WIND ; Рисование рамок вокруг лестниц LD DE,ATRBOX LD BC,4 CALL 8252 LD BC,#1414 ;B = 20, C = 20 CALL 8933 CALL BOX LD BC,#14D4 ;B = 20, C = 212 CALL 8933 CALL BOX ; Печать текста LD DE,TEXT LD BC,UDG-TEXT JP 8252 ; Данные атрибутов рамок ATRBOX DEFB 16,7,17,1 ; Рисование фрагмента лестницы STAIRS LD A,17 RST 16 XOR A RST 16 LD HL,SPR5 JP PUT ; Рисование рамок BOX EXX PUSH HL LD BC,#8700 ;B = 135, C = 0 LD DE,#101 ;D = 1, E = 1 CALL 9402 LD BC,23 ;B = 0, C = 23 CALL 9402 LD BC,#8700 LD D,-1 CALL 9402 LD BC,23 LD E,-1 CALL 9402 POP HL EXX RET



Подпрограмма SCREEN не содержит ничего такого, что бы нами еще не рассматривалось, поэтому отдельно мы ее комментировать не будем, а ограничимся уже приведенными пояснениями и перейдем к динамической части заставки.

В первую очередь напишем подпрограмму, печатающую в заданной позиции экрана элемент, из которого составлена рамка вокруг всего изображения:

; Вывод на экран шарика цвета E в позицию BC PRINT LD A,22 RST 16 LD A,B DEC A RST 16 LD A,C DEC A RST 16 LD A,16 RST 16 LD A,E RST 16 LD A,160 RST 16 RET

Значительно сложнее заставить двигаться человечков, да чтобы они при этом еще переступали с ноги на ногу и поворачивались к вам то лицом, то спиной. Для этих целей введены переменные:

ROW - определяет вертикальную позицию левого человечка. Вертикальная координата правого легко вычисляется, так как они движутся в противофазе: когда один идет вверх, другой движется вниз и наоборот. Горизонтальные же координаты обоих человечков во время движения не изменяются.

DIRECT - задает направление движения также для левого человечка. Если она имеет значение 1, левый человечек идет вниз, а правый - вверх. При изменении направлений движения она меняет знак и становится равной минус 1 (255).

POSIT - сообщает программе, в какой из двух фаз выводить изображения человечков. При ее значении, равном 0, выводятся спрайты SPR2 и SPR4, а если она равна 1 - SPR1 и SPR3. Иными словами, эта переменная отвечает за то, на какую ногу в данный момент должен наступить каждый человечек.

Стоит сказать несколько слов о том, какими программными средствами изменяются эти переменные. Наиболее просто получается переключение переменной POSIT. Для этого достаточно использовать команду XOR 1, инвертирующую значение младшего бита. Чтобы изменить знак в переменной DIRECT, можно поступить следующим образом: инвертировать все ее биты, например, командой CPL, а затем увеличить ее на 1. Можно именно так и поступить, но лучше воспользоваться специальной командой, как раз предназначенной для изменения знака в аккумуляторе. Эта команда записывается мнемоникой NEG (Negative - отрицательный). Что касается ROW, то она изменяется простым сложением с переменной DIRECT.



Теперь приводим тексты соответствующих подпрограмм:

; Перемещение человечков MAN LD A,(ROW) LD B,A LD A,(DIRECT) LD D,A CP 1 ;проверка направления движения JR NZ,MAN1 CALL PRMAN1 LD E,17 ; нижняя граница координаты ROW JR MAN2 MAN1 CALL PRMAN2 LD E,3 ;верхняя граница координаты ROW MAN2 LD HL,POSIT ;изменение переменной POSIT LD A,(HL) XOR 1 LD (HL),A LD HL,ROW LD A,(HL) CP E ;проверка необходимости смены LD A,D ; направления движения JR NZ,MAN3 NEG ;изменение знака в аккумуляторе LD (DIRECT),A ;изменение переменной DIRECT MAN3 ADD A,(HL) ;изменение переменной ROW ; (адресуемой парой HL) LD (HL),A RET

; Левый человечек идет вниз, правый - вверх PRMAN1 DEC B ;координата ROW LD C,3 ;горизонтальная координата левого ; человечка LD HL,SPR5 ;восстановление фона CALL PUT INC B LD A,(POSIT) AND A LD HL,SPR1 JR NZ,PRM11 LD HL,SPR2 PRM11 CALL PUT LD A,22 ;вычисление вертикальной координаты SUB B ; правого человечка LD B,A LD C,27 ;горизонтальная координата правого ; человечка LD HL,SPR5 ;восстановление фона CALL PUT DEC B DEC B LD A,(POSIT) AND A LD HL,SPR4 JR Z,PRM12 LD HL,SPR3 PRM12 JP PUT

; Левый человечек идет вверх, правый - вниз PRMAN2 INC B INC B LD C,3 LD HL,SPR5 CALL PUT DEC B DEC B LD A,(POSIT) AND A LD HL,SPR3 JR NZ,PRM21 LD HL,SPR4 PRM21 CALL PUT LD A,19 SUB B LD B,A LD C,27 LD HL,SPR5 CALL PUT INC B LD A,(POSIT) AND A LD HL,SPR2 JR Z,PRM22 LD HL,SPR1 PRM22 JP PUT

Теперь объединим рассмотренные подпрограммы и напишем управляющую часть:

ORG 60000 ENT $ LD HL,UDG LD (23675),HL CALL BOLD LD A,15 LD (23693),A XOR A CALL 8859 CALL 3435 LD A,2 CALL 5633 ; Инициализация переменных LD A,4 LD (ROW),A LD A,1 LD (DIRECT),A LD (POSIT),A CALL SCREEN LD A,17 RST 16 XOR A RST 16 ; Начало динамической части заставки MAIN LD E,7 ;код цвета шариков, окаймляющих ; рабочий экран ; Вывод рамки, состоящей из разноцветных шариков MAIN1 LD B,22 ;в регистрах BC - координаты экрана MAIN2 LD C,1 CALL PRINT LD C,32 CALL PRINT DJNZ MAIN2 LD C,31 MAIN3 LD B,1 CALL PRINT LD B,22 CALL PRINT DEC C JR NZ,MAIN3 PUSH BC PUSH DE CALL MAN ;вывод очередной фазы движения ; человечков POP DE POP BC PUSH BC LD B,9 ;счетчик цикла задержки перемещения ; человечков MAIN4 PUSH BC LD BC,1 CALL 7997 POP BC ; Проверка нажатия клавиш от 0 до 4. Если нажаты - выход на EXIT LD A,(23560) CP "4"+1 JR NC,MAIN5 ;проверка >"4" (>="4"+1) CP "0" JR NC,EXIT MAIN5 DJNZ MAIN4 POP BC DEC E JR NZ,MAIN1 JR MAIN EXIT LD (KEY),A ;сохраняем код клавиши для других ; частей многокадровой заставки, ; принцип построения которой будет ; рассмотрен в следующей главе POP BC RET ; Подпрограммы

; Блоки данных

; Переменные ROW DEFB 0 DIRECT DEFB 0 POSIT DEFB 0 KEY DEFB 0 ; Адрес размещения нового фонта (обязательно в самом конце, ; чтобы коды набора не перекрыли коды программы) END


МУЗЫКАЛЬНЫЙ РЕДАКТОР WHAMFX



МУЗЫКАЛЬНЫЙ РЕДАКТОР WHAM FX

Кодирование вручную звуковых эффектов, а тем более - музыки, дело весьма утомительное. Хотя такой способ при наличии определенного опыта в конечном итоге и дает наилучшие результаты, но для начинающих он мало пригоден. А что говорить о простых пользователях, общающихся с компьютером только на уровне игровых или, в лучшем случае, прикладных программ. Поэтому стоит сказать хотя бы несколько слов о программе, специально предназначенной для создания компьютерной музыки - музыкальном редакторе.

В первой книге серии «Как написать игру для ZX Spectrum» мы рассказали о работе с редактором Wham. Этот же редактор был достаточно подробно описан и в книге [2]. Поэтому здесь мы не станем еще раз повторяться, а объясним, как работать с другой версией этой программы, специально рассчитанной на возможности музыкального сопроцессора.

Редактор Wham FX был создан тем же автором и внешне не слишком отличается от своего предшественника. Не особенно сильно изменен и принцип управления, а также способ ввода мелодии. Но, конечно, имеются и некоторые существенные отличия, на которых в основном мы и хотим заострить ваше внимание.

После загрузки программы вы услышите мелодию, демонстрирующую возможности редактора. Нажав любую клавишу, можно прервать прослушивание и попасть в главное меню, многие пункты которого повторяют функции первой версии Wham. Среди них уже знакомые LOAD TUNE и SAVE TUNE (загрузка и сохранение пьесы), SET TEMPO (изменение темпа), HELP PAGE (подсказка) и EDIT MODE (режим ввода и редактирования). Остальные две опции - SYST MENU (системное меню) и ENVELOPES (формирование огибающих звука) - мы рассмотрим ниже, а сейчас сразу перейдем к редактированию мелодии, нажав клавишу 6.

Большая часть экрана в режиме редактирования (Рисунок  10.2) занята нотными линейками, на которых отображается вводимая вами мелодия. Нижнюю часть экрана занимает изображение клавиатуры фортепиано, которое помогает установить соответствие между клавишами компьютера и вводимыми звуками. В средней части экрана расположено несколько окон: три небольших окна слева символизируют характер звучания в каждом из трех каналов; в четвертом окне указываются номера редактируемого канала (CHN) и текущей октавы (OKT); в следующем, самом большом окне выводится информация об используемых в произведении эффектах; последнее окно, изображающее прибор со стрелкой, носит скорее декоративный характер - при проигрывании мелодии стрелка постоянно прыгает в такт звукам, отражая уровень громкости.



МУЗЫКАЛЬНЫЙ СОПРОЦЕССОР



МУЗЫКАЛЬНЫЙ СОПРОЦЕССОР

Несоизмеримо большими возможностями для создания звукового оформления игровых программ обладают компьютеры ZXSpectrum 128 и Scorpion ZS 256, благодаря встроенному в них трехканальному музыкальному сопроцессору.

Мы уже показали, как можно в одном стандартном звуковом канале совместить два голоса, однако из-за наложения частот звук при этом получается «грязным», чересчур насыщенным гармониками, напоминая по тембру широкораспространенный в свое время гитарный эффект FUZZ. Музыкальный же сопроцессор позволяет получить одновременно до трех чистых тонов. Но его достоинства не ограничиваются только этим.

Как известно, музыкальный звук характеризуется тремя основными параметрами: частотой, которая определяет высоту тона, тембром (окраской), зависящим от количества и состава гармоник и амплитудой, определяющей громкость звучания. Используя только стандартный звуковой канал, легко можно управлять высотой звука, несколько сложнее получить различную окраску тона и практически невозможно влиять на громкость, а вот музыкальный сопроцессор позволяет манипулировать и этим третьим параметром, варьируя громкость звука от Forte до полного его исчезновения. Правда, что касается тембра, то и здесь, к сожалению, нет простых решений и приходится прибегать примерно к тем же методам, что и при управлении стандартным выводом. Зато вы можете без труда получить эффект «белого» шума, причем его среднюю высоту также можно регулировать.

Нельзя не упомянуть и еще об одной особенности музыкального сопроцессора. Он работает совершенно независимо, без опеки центрального процессора, поэтому последний может быть занят каким-нибудь полезным делом (например, опрашивать клавиатуру либо выводить на экран текст или графику), в то время как музыкальный сопроцессор самостоятельно извлекает звук. CPU лишь изредка нужно отвлекаться от своей работы, чтобы дать своему «коллеге» указание перейти к следующей ноте, а затем он вновь может вернуться к решению более насущных проблем.


Вы знаете, что для управления работой музыкального сопроцессора из Бейсика-128 имеется дополнительный (по отношению к стандартному Spectrum-Бейсику) оператор PLAY, но он на самом деле не реализует и десятой доли всех немыслимых возможностей, которые могут быть осуществлены только из ассемблера. Об этом можно судить хотя бы по тем играм, которые написаны специально для Spectrum 128.

Звук извлекается программированием собственных регистров сопроцессора, которые так же, как и регистры CPU имеют по 8 разрядов. Всего их насчитывается 16 (обозначаются от R0 до R15), но нас будут интересовать только 14 из них, так как остальные два служат для несколько иных целей, о чем сказано, например, в [2]. Сначала мы рассмотрим функции этих регистров, а затем расскажем о том, как с ними обращаться.

Первые шесть регистров (R0...R5) образуют три пары и задают высоту звука для каждого из трех каналов в отдельности (сами каналы обозначаются буквами A, B и C). То есть регистровая пара R0/R1 определяет частоту тона в канале A, пара R2/R3 делает то же самое для канала B и R4/R5 - для C. Хотя каждая пара состоит из 16 бит, используются только 12 младших разрядов: все 8 бит младшего регистра (R0, R2 и R4) и 4 младших бита старшего регистра (R1, R3 и R5). Таким образом, числа, определяющие высоту звука, находятся в пределах от 0 до 4095 включительно. В табл. 10.1 приводится соответствие звуков из диапазона неполных девяти октав и чисел, определяющих эти ноты.

Таблица 10.1. Значения для регистров R0...R5

 СККБМ12345
До 338916958474242121065326
До диез 319916008004002001005025
Ре 30201510755377189944724
Ре диез 28501425712356178894522
Ми 26901345673336168844221
Фа 25391270635317159794020
Фа диез 23971198599300150753719
Соль 22621131566283141713518
Соль диез 21351068534267133673317
Ля403120151008504252126633116
Си бемоль38041902951476238119593015
Си35911795898449224112562814
<


Следующий регистр - R6 - определяет среднюю частоту выводимого шума. Поскольку получение шумовых эффектов одновременно в разных голосах не имеет практического применения (их все равно невозможно будет различить на слух), то этот регистр является общим для всех трех каналов. Для него можно задавать значения от 0 до 31, то есть значащими являются только пять младших битов.

Регистр R7 служит для управления звуковыми каналами. Он подобен флаговому регистру центрального процессора и значение имеет каждый отдельный бит. Младшие три бита используются для управления выводом чистого тона в каждый из трех каналов. Если бит установлен, вывод запрещен, а при сброшенном бите вывод звука разрешается. Бит 0 связан с каналом A, 1-й бит относится к каналу B и 2-й - к C. Биты 3, 4 и 5 заведуют выводом в каналы A, B и C соответственно частоты «белого» шума. При установке бита вывод также запрещается, а при сбросе его в 0 - разрешается. 6-й и 7-й биты для извлечения звука значения не имеют.

Регистры R8, R9 и R10 определяют громкость звука, выводимого соответственно в каналы A, B и C. С их помощью можно получить 16 уровней громкости, посылая в них значения от 0 до 15. То есть значение для получаемой амплитуды в этих регистрах имеют 4 младших бита. Однако следует знать еще об одной интересной особенности этих трех регистров. Если в каком-нибудь из них установить 4-й бит (например, послав в него число 16), то получится звук не с постоянной, а с изменяющейся во времени громкостью. В этом случае необходимо указать дополнительную информацию в регистрах R11, R12 и R13.

Спаренные регистры R11 и R12 задают скорость изменения громкости звука: чем больше число, тем более плавной будет огибающая. В них можно записывать значения от 0 до 65535. Надо сказать, что изменение младшего регистра почти не ощущается, поэтому чаще достаточно определять лишь регистр R12.

Регистр R13 формирует огибающую выходного сигнала. Установкой одного или нескольких битов из младшей четверки можно получить несколько разнообразных эффектов. При установке нулевого бита звук получается затухающим, если установить 2-й бит, громкость будет наоборот увеличиваться, а установив одновременно 1-й и 3-й биты, вы получите звук, постоянно изменяющийся по громкости.



Последними тремя регистрами действительно иногда бывает удобно пользоваться, хотя гораздо чаще огибающая формируется программным путем, что позволяет получить значительно большее разнообразие оттенков звучания.

Перейдем теперь к вопросу, как программируются регистры музыкального сопроцессора. Связь с ними осуществляется через порты с адресами 49149 (#BFFD) и 65533 (#FFFD). Чтобы записать какое-либо значение в любой из регистров, его необходимо прежде всего выбрать (или назначить), выполнив команду OUT в порт 65533. Например, регистр R8 выбирается следующими командами:

LD BC,65533 ;в паре BC - адрес порта ; для выбора регистра LD A,8 ;в аккумуляторе - номер регистра OUT (C),A ;выбор

После этого в выбранный регистр можно записывать данные либо читать его содержимое. Для записи используется порт 49149, а для чтения - опять же 65533. Приведем фрагмент программы, в котором читается значение установленного ранее регистра и если оно не равно 0, содержимое регистра уменьшается на единицу:

LD BC,65533 ;адрес порта для чтения IN A,(C) ;читаем значение текущего регистра JR Z,ZERO ;если 0, обходим DEC A ;уменьшаем на 1 ; Выбираем порт 49149 для записи ; (значение регистра C остается прежним - #FD) LD B,#BF OUT (C),A ;записываем значение в выбранный регистр ZERO ......... ;продолжение программы

Добавим к сказанному, что выбор регистров музыкального сопроцессора и все манипуляции с ними лучше производить при запрещенных прерываниях, хотя в приведенных примерах это и не отражено.

Можно написать универсальную подпрограмму, которая считывает из блока данных значения всех регистров и тем самым задает параметры звуков одновременно для всех трех каналов:

OUTREG DI ;запрещаем прерывания ; Данные будем считывать из массива DATREG ; в обратном порядке, начиная с последнего элемента LD HL,DATREG+13 LD D,13 ;начальный номер загружаемого регистра LD C,#FD ;младший байт адреса порта сопроцессора OUTR1 LD B,#FF ;адрес для выбора регистра OUT (C),D ;выбираем регистр LD B,#BF ;адрес для записи в регистр ; Записываем в порт байт из ячейки, адресуемой парой HL, ; и уменьшаем HL на 1 OUTD DEC D ;переходим к следующему регистру ; (с меньшим номером) JP P,OUTR1 ;повторяем, если записаны еще не все ; регистры (D >= 0) EI ;разрешаем прерывания RET DATREG DEFS 14 ;массив данных для регистров сопроцессора



Эта процедура вполне может быть использована как для получения отдельных звуковых эффектов, так и для создания музыкальных произведений. Основная сложность заключается в написании программы, которая бы изменяла соответствующим образом элементы массива DATREG и тем самым управляла работой сопроцессора. Чтобы получить действительно первоклассное звучание, потребуется достаточно серьезная программа, которую объем книги, к сожалению, не позволяет здесь привести (надеемся, ее все же удастся включить в одну из последующих книг серии «Как написать игру»). Поэтому мы предлагаем более простую процедуру, извлекающую отдельные звуки, характер которых, тем не менее, вы сможете изменять практически в неограниченных пределах. (Упомянутая музыкальная программа строится, в общем, по такому же принципу, что и приводимая здесь процедура. Поэтому после ее досконального изучения вы можете попытаться самостоятельно написать программу, пригодную для исполнения музыкальных произведений.)

Эту программу также лучше составить в виде прерывания, чтобы можно было получать звуки любой продолжительности и не отвлекаться на их формирование в основной программе. Начнем с выяснения, какие переменные нам здесь потребуются. Помимо уже известных по предыдущему примеру значений базового и текущего адресов в блоке данных, описывающем характер звучания и флага разрешения вывода звука понадобятся переменные, задающие частоту тона и шума, длительность звучания, количество повторений эффекта, флаг разрешения вывода в канал тона или шума. Кроме этого, необходимо каким-то образом задавать огибающую звука и характер изменения высоты тона или шума. Эти две последние характеристики должны изменяться независимо друг от друга, поэтому порядок их изменения лучше всего определять в двух дополнительных блоках данных. То есть к перечисленным переменным добавятся еще четыре: базовые и текущие адреса для каждого из этих двух блоков.

В связи с обилием переменных, а также и с тем, что все они должны иметься в трех экземплярах - для каждого канала по полному набору - лучше всего свести их в таблицу, начало которой передавать в регистре IX. Построим такую таблицу с указанием смещений от начала и значений каждого смещения:



0/1 - базовый адрес блока данных эффекта 2/3 - текущий адрес в блоке данных 4/5 - частота тона 6 - частота шума 7 - флаг разрешения вывода в канал 8 - длительность звучания эффекта 9 - количество повторений эффекта 10 - вывод тона (1), шума (8) или их комбинации (9) 11/12 - базовый адрес данных для формирования частоты 13/14 - текущий адрес данных для формирования частоты 15/16 - базовый адрес данных для формирования огибающей 17/18 - текущий адрес данных для формирования огибающей

Поскольку задавать все значения переменных в основной программе немыслимо, они должны быть определены в блоках данных, а чтобы иметь возможность устанавливать их выборочно (ведь, например, при выводе чистого тона частота шума не имеет значения), используем принцип управляющих кодов. В основном блоке данных будем определять адреса двух дополнительных блоков и заказывать выводимый звук - чистый тон, шум или их комбинацию, а также длительность звучания эффекта:

0 - конец данных (возврат на повторение эффекта) 1 - адрес данных для изменения тона (+2 байта адреса) 2 - адрес данных для формирования огибающей (+2 байта адреса) 3 - управление выводом тона/шума (+2 байта: 1-й определяет вывод тона, шума или комбинированный вывод, 2-й - длительность звучания)

Когда программа встретит код 0, вывод звука либо прекратится, либо весь эффект повторится еще раз - в зависимости от заданного изначально количества повторений. Следующие два байта после кодов 1 или 2 интерпретируются как адреса дополнительных блоков данных, определяющих характер изменения частоты звука и огибающей соответственно. С этих двух кодов должен начинаться любой блок данных, иначе программа не будет «знать», каким образом изменять звук. Последний код (3) служит собственно для извлечения звука. После него необходимо указать еще два однобайтовых параметра: первый задает вывод тона (1), шума (8) либо одновременно и тона и шума (9), второй определяет продолжительность заданного звука в 50-х долях секунды.



В дополнительных блоках данных также используем некоторые управляющие коды. После байта со значением 128 будет задаваться двухбайтовая величина частоты тона (см. ), а за кодом 129 последует байт средней частоты шума, который может иметь значения от 0 (самый высокий звук) до 31 (самый низкий). Определив частоты, можно поставить метку начала их изменения, поставив код 130. Далее должны следовать значения приращения частоты, которые лежат в диапазоне от -124 до +127. За один такт прерываний (1/50 секунды) будет выполнен один из этих кодов. Завершаться этот блок данных должен кодом 131, после которого все составляющие его числа будут проинтерпретированы с начала или от кода 130, если таковой был использован. Соберем все коды данных для изменения частоты воедино:

128 - задание частоты тона (+2 байта частоты тона) 129 - задание частоты шума (+1 байт частоты шума) 130 - метка повторения эффекта 131 - возврат на начало или метку -124...+127 - приращение частоты

В блоке данных для формирования огибающей будет использован только один управляющий код 128, отмечающий конец интерпретации записанных значений и переход на повторение эффекта. Остальные коды, задающие громкость звука, могут передаваться числами от 0 до 15. Каждое из этих чисел также обрабатывается за одно прерывание.

128 - возврат на начало данных огибающей 0...15 - значение громкости

Прежде чем продолжить, хочется обратить ваше внимание на то, что при построении собственных блоков данных необходимо следить за порядком следования управляющих кодов, иначе результат будет очень далек от ожидаемого. Кроме того, ни один из блоков не может состоять только из управляющих кодов, поскольку в этом случае компьютер просто «зависнет».

Выяснив, что мы хотим в итоге получить, напишем процедуру для интерпретации описанных блоков данных. Она имеет довольно внушительные размеры, но все же попытайтесь с ней разобраться. На входе перед обращением к ней в аккумуляторе задается номер канала (0 для A, 1 для B и 2 для C), а индексный регистр IX, как мы уже говорили, адресует таблицу переменных соответствующего канала:



GETSND LD (N_CHAN),A ;запоминаем номер канала LD A,(IX+7) AND A RET Z ;выход, если вывод в канал не разрешен DEC (IX+8) ;уменьшаем счетчик длительности звука JR NZ,GETS5 LD L,(IX+2) ;текущий адрес основного блока данных LD H,(IX+3) GETS1 LD A,(HL) INC HL AND 3 JR NZ,GETS2 ; Код 0 - возврат на повторение эффекта LD A,(IX+11) ;установка текущего адреса LD (IX+13),A ; данных изменения частоты LD A,(IX+12) ; на начало LD (IX+14),A ; блока LD L,(IX) ;начальный адрес основного LD H,(IX+1) ; блока данных DEC (IX+9) ;уменьшение количества повторений JR NZ,GETS1 XOR A ;завершение звучания в канале LD (IX+7),A ;запрет вывода звука в канал LD (IX+10),A LD A,(N_CHAN) ADD A,8 LD E,A XOR A JP SETREG ;выключение громкости GETS2 DEC A JR NZ,GETS3 ; Код 1 - адрес данных для изменения тона LD A,(HL) ;младший байт адреса LD (IX+11),A INC HL LD A,(HL) ;старший байт адреса LD (IX+12),A INC HL JR GETS1 GETS3 DEC A JR NZ,GETS4 ; Код 2 - адрес данных для формирования огибающей LD A,(HL) ;младший байт адреса LD (IX+15),A LD (IX+17),A INC HL LD A,(HL) ;старший байт адреса LD (IX+16),A LD (IX+18),A INC HL JR GETS1 ; Код 3 - управление выводом тона/шума GETS4 LD A,(HL) ;1 - тон, 8 - шум, 0 - пауза, ; 9 - тон и шум одновременно INC HL AND 9 LD (IX+10),A LD A,(HL) ;продолжительность вывода INC HL LD (IX+8),A LD (IX+2),L LD (IX+3),H ; Восстановление текущего адреса данных для изменения тона LD A,(IX+11) LD (IX+13),A LD A,(IX+12) LD (IX+14),A GETS5 LD L,(IX+13) LD H,(IX+14) GETS6 LD A,(HL) INC HL CP 128 ;задание частоты тона JR NZ,GETS7 LD A,(HL) LD (IX+4),A INC HL LD A,(HL) AND 15 LD (IX+5),A INC HL JR GETS6 GETS7 CP 129 ;задание частоты шума JR NZ,GETS8 LD A,(HL) AND 31 LD (IX+6),A JR GETS6 GETS8 CP 130 ;метка нового начала JR NZ,GETS9 LD (IX+11),L LD (IX+12),H JR GETS6 GETS9 CP 131 ;возврат к началу JR NZ,GETS10 LD L,(IX+11) LD H,(IX+12) JR GETS6 ; Изменение частоты звука или шума GETS10 LD (IX+13),L LD (IX+14),H LD D,0 BIT 7,A JR Z,GETS11 LD D,255 GETS11 LD E,A LD L,(IX+4) LD H,(IX+5) ADD HL,DE LD (IX+4),L LD (IX+5),H ADD A,(IX+6) LD (IX+6),A ; Определение элементов массива DATREG, задающих частоту LD (DATREG+6),A ;частота шума LD A,(N_CHAN) ADD A,A LD E,A LD A,L PUSH HL CALL SETREG ;младший байт частоты тона POP HL INC E LD A,H CALL SETREG ;старший байт частоты тона ; Формирование огибающей LD L,(IX+17) LD H,(IX+18) GETS12 LD A,(HL) INC HL CP 128 ;повторение с начала JR NZ,GETS13 LD L,(IX+15) LD H,(IX+14) JR GETS12 GETS13 LD (IX+17),L LD (IX+18),H AND 15 PUSH AF LD A,(N_CHAN) ADD A,8 LD E,A POP AF JP SETREG ;задание громкости звука



В процедуре обработки прерываний приведенная подпрограмма будет вызываться трижды для определения характера звучания в каждом из трех каналов независимо друг от друга. Это дает возможность использовать в программе одновременно три самостоятельных источника звука, закрепив за каждым игровым объектом свой звуковой канал. Правда, здесь есть одно небольшое ограничение: поскольку средняя частота «белого» шума общая для всех трех каналов, то его лучше выводить только в какой-то один, а другие два использовать для вывода изменяющегося тона.

Вот описываемая процедура обработки прерываний:

SND128 PUSH AF PUSH BC PUSH DE PUSH HL PUSH IX CALL NXTSND POP IX POP HL POP DE POP BC POP AF JP 56 NXTSND LD IX,CHAN_A XOR A CALL GETSND ;задание переменных для канала A LD IX,CHAN_B LD A,1 CALL GETSND ;задание переменных для канала B LD IX,CHAN_C LD A,2 CALL GETSND ;задание переменных для канала C ; Вычисление значения регистра R7, ; управляющего выводом в каналы тона и шума LD A,(CHAN_C+10) AND 9 ;выделяем биты 0 и 3 RLCA ;сдвигаем влево LD B,A ;результат сохраняем в регистре B LD A,(CHAN_B+10) AND 9 ;то же самое для других двух каналов OR B RLCA LD B,A LD A,(CHAN_A+10) AND 9 OR B CPL ;инвертируем биты LD E,7 ;устанавливаем данные регистра R7 в DATREG CALL SETREG ; Извлечение звука OUTREG LD HL,DATREG+13 LD D,13 LD C,#FD OUTR1 LD B,#FF OUT (C),D LD B,#BF OUTD DEC D RET M ;выход, если D < 0 JR OUTR1 DATREG DEFS 14 ; Задание элемента E массива DATREG значением из аккумулятора SETREG LD HL,DATREG LD D,0 ADD HL,DE LD (HL),A RET N_CHAN DEFB 0 ;номер текущего канала ; Таблицы переменных для каждого канала CHAN_A DEFS 19 CHAN_B DEFS 19 CHAN_C DEFS 19

Наше прерывание остается дополнить процедурами включения и выключения 2-го режима. Попутно желательно выполнить и некоторые другие действия, а именно, при установке 2-го режима нужно очистить все три таблицы переменных, инициализировав их нулевым байтом, а при возврате первого режима прерываний необходимо еще убедиться, что звук выключен:



INITI LD HL,CHAN_A ;инициализация таблиц переменных LD DE,CHAN_A+1 LD BC,19*3-1 LD (HL),0 LDIR LD HL,SND128 ;установка прерывания

STOPI CALL IMOFF ;возврат к 1-му режиму LD A,#FF ;выключение звука LD E,7 CALL SETREG ;запись в регистр сопроцессора R7 ; значения #FF JP OUTREG

Порядок действий при использовании описанной программы должен быть следующим. В начале работы нужно включить 2-й режим прерываний, вызвав процедуру INITI. Извлечение очередного звука необходимо начинать с определения некоторых переменных в таблицах CHAN_A, CHAN_B или CHAN_C. Для этого нужно записать в первые два байта таблицы, соответствующей выбранному каналу, адрес начала основного блока данных и то же значение продублировать в следующих двух байтах таблицы. Затем указать количество повторений звука по смещению +9, а элементы таблицы +8 и +7 инициализировать байтом 1 (переменную по смещению +7 нужно задавать обязательно в последнюю очередь, так как именно она «запускает» звук). По окончании работы (или если в программе предусмотрены обращения к дисководу) следует восстановить стандартный режим обработки прерываний и выключить звук, обратившись к подпрограмме STOPI.

Продемонстрируем применение описанной процедуры извлечения звуков на примере небольшой игры, которую назовем БИТВА С НЛО. По земле катается грузовик с зенитной лазерной установкой, который методично расстреливает маячащую в небе «летающую тарелку» (Рисунок  10.1). НЛО также не остается внакладе и отвечает хоть и малоприцельным, но зато плотным веерным огнем.


Название для игры НАРДЫ



Рисунок 6.4. Название для игры НАРДЫ

Вот такая симпатичная надпись, в которой каждая буква составлена из отрезков параллельных двойных линий, нарисованных DRAW'ами. В высоту укладывается двенадцать таких линий плюс промежутки между ними. Точно такие любой из вас сможет легко начертить по линейке на листе бумаги, измерить их длины и координаты, после чего ввести в программу свои вычисления в виде блока данных. Посмотрим, как это делается. Прежде всего разберемся со структурой строки блока данных, поскольку с подобным построением раньше мы еще не встречались, а именно оно все и определяет. Строка списка DATA содержит шесть элементов, каждому из которых в программе соответствует определенная переменная, а именно:

y - вертикальная координата начала линии (в пикселях);

x - горизонтальная координата начала линии (опять же в пикселях);

hgt - высота буквы (количество двойных линий плюс промежутки между ними);

len - длина проводимой горизонтальной линии (снова в пикселях);

ink - цвет линии (0...7);

dx - приращение линии (еще раз в пикселях).

Работу программы лучше всего показать на примере какой-нибудь буквы, скажем, «Д» (Рисунок  6.5, а), при построении которой участвуют все эти переменные (правда, некоторые величины в списках DATA равны нулю).



НЕКОТОРЫЕ НЕДОКУМЕНТИРОВАННЫЕ КОМАНДЫ



НЕКОТОРЫЕ НЕДОКУМЕНТИРОВАННЫЕ КОМАНДЫ

К недокументированным относятся те команды, которые не были описаны фирмой-разработчиком микропроцессора Z80A CPU. Вполне возможно, они даже не были запланированы, а получились, если так можно выразиться, как издержки производства. В связи с этим каждый программист «открывает» их для себя, пользуясь методом «научного тыка». Вы также можете поэкспериментировать, используя некоторые правила построения системы команд, о которой мы и хотим здесь рассказать. Внимательно изучив коды, приведенные в предыдущем приложении, вы сможете заметить определенную закономерность. Сравните, например, команды LD HL,NN, LD IX,NN и LD IY,NN. Не правда ли, кодировки очень похожи друг на друга? Команды, использующие индексные регистры, состоят из тех же кодов, что и LD HL,NN, но предваряются специальным префиксом #DD для IX или #FD для IY. Среди «стандартных» мнемоник отсутствуют команды обработки половинок индексных регистров, но воспользовавшись правилами кодировки, не трудно получить такие команды. Они будут соответствовать кодам обработки регистров H и L, перед которыми стоит один из указанных выше префиксов. Например, для загрузки младшей половинки регистра IX числом 32 можно написать следующую последовательность:

DEFB #DD LD L,32

В мнемоническом обозначении такая команда обычно записывается как LD IXL,32. С префиксами #DD и #FD аналогичным образом могут быть получены следующие команды:

ADD A,s ADC A,s AND s CP s DEC s INC s LD r,s LD s,n LD s,r OR s SBC A,s SUB s XOR s

где s - IXH, IXL, IYH или IYL; r - A, B, C, D или E; n - однобайтовое числовое значение. Существует еще целый ряд интересных команд, получаемых с префиксами #DD и #FD, которые могут выполнять сразу два действия - устанавливать или сбрасывать биты одновременно в регистре и в памяти, адресуемой индексными регистрами. Например, команда

SET 1,A(IX+3)

сначала установит бит 1 в ячейке, адресуемой IX со смещением в 3 байта, а затем поместит полученное значение в регистр A. То есть выполняются как бы две команды:


SET 1,(IX+3) LD A,(IX+3)

Приведем способы кодировки некоторых команд этой группы, а остальные предлагаем вам составить самостоятельно по аналогии с приведенными. Описанная выше команда получается из стандартной

SET 1,A

Эта команда кодируется двумя байтами #CB и #CF (см. ). Для получения новой команды необходимо вначале поставить код префикса, а между байтами исходной команды вставить байт величины смещения. Таким образом, приведенная выше мнемоника должна кодироваться последовательностью #DD, #CB, #03, #CF. Команда

RES 5,H(IY-5)

получается, исходя из кодировки команды

RES 5,H

В результате у вас должна получиться последовательность кодов #FD, #CB, #FB, #AC. Конечно, все предложенные мнемоники не поддерживаются ассемблером GENS, поэтому использовать их можно одним лишь способом - записывая коды непосредственно в директиве DEFB. То есть последняя команда в ассемблерном тексте будет выглядеть так:

DEFB #FD,#CB,#FB,#AC

Несколько сложнее дела обстоят с другими префиксами: #CB и #ED. С ними также можно получить ряд новых команд, хотя часто они лишь повторяют «стандартные» инструкции (особенно это относится к префиксу #ED) и практической пользы поэтому от них мало. Такие команды чаще используются для защиты коммерческих программ, чтобы текст невозможно было прочитать (ни один из известных дизассемблеров недокументированные команды не распознает). Для поисков лучше всего использовать отладчик MONS, так как он в режиме трассировки тупо выполняет машинные инструкции, совершенно не вникая в их смысл. Но в данном случае вам это как раз и нужно. Любой код, который вы ему подсунете, будет выполнен абсолютно так же, как и в программе (заметим, что другой трассировщик - Mon2 - перед выполнением очередной команды справляется о ней в таблице, поэтому, если вы решите использовать для экспериментов именно этот дизассемблер, стоит оформлять искомые коды в виде подпрограммы и трассировать их, нажимая клавишу S). Поскольку «пропущенных» команд, получаемых с использованием префикса #CB немного, приведем их полный список (кстати, дизассемблер Mon2, в отличие от MONS, прекрасно «справляется» с этой группой команд).



SLL (HL) ; CB36 SLL (IX+Д) ; DDCBXX36 SLL (IY+Д) ; FDCBXX36 SLL A ; CB37 SLL B ; CB30 SLL C ; CB31 SLL D ; CB32 SLL E ; CB33 SLL H ; CB34 SLL L ; CB35

Эти команды дополняют перечень команд сдвига. При их выполнении содержимое регистра или ячейки памяти сдвигается влево на один разряд. Старший бит переходит во флаг CY, а младший в любом случае заполняется единицей. Например, после сдвига числа 00101110 командой SLL (в такой мнемонике данные команды показываются дизассемблером Mon2, однако в литературе часто предлагается другое обозначение - SLI; тем не менее, это те же самые команды) получится значение 01011101. Команда воздействует на флаги CY, Z, P/V и S. Флаги H и N сбрасываются в 0. Если вы решите использовать в своей программе приведенные выше инструкции, то помните, что ассемблер GENS понятия не имеет о существовании мнемоники SLL. Поэтому все команды придется кодировать исключительно с помощью директивы DEFB.


Ноты пьесы



Рисунок 10.3. Ноты пьесы

Таблица 10.2. Ввод пьесы

ГОЛОС 1ГОЛОС 2ГОЛОС 3
Okt. 3Ent&nbspEntOkt. 1VE5
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspVE7Ent&nbspEnt&nbsp
&nbspM&nbspSS / O&nbspEnt&nbsp
Okt. 4A&nbspEnt&nbspEnt&nbsp
&nbspZ&nbspEnt&nbspEnt&nbsp
&nbspC&nbspEnt&nbspH&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspVVAEnt&nbspEnt&nbsp
&nbspVV5SS / E&nbspEnt&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspHE5Ent&nbspB&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspV&nbspEnt&nbspEnt&nbsp
&nbspEnt&nbspSS / O&nbspEnt&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspMVAEntOkt. 2Z&nbsp
&nbspMV5Ent&nbspEnt&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspEnt&nbspSS / QOkt. 1M&nbsp
&nbspEnt&nbspSS / O&nbspH&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspEnt&nbspSS / Q&nbspB&nbsp
Okt. 3Ent&nbspEnt&nbspV&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspVE7Ent&nbspEnt&nbsp
&nbspM&nbspSS / O&nbspEnt&nbsp
Okt. 4A&nbspEnt&nbspEnt&nbsp
&nbspZ&nbspEnt&nbspEnt&nbsp
&nbspC&nbspEnt&nbspH&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspVVAEnt&nbspEnt&nbsp
&nbspVV5SS / E&nbspEnt&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspEnt&nbspEnt&nbspB&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspME5Ent&nbspEnt&nbsp
&nbspH&nbspSS / O&nbspEnt&nbsp
&nbspEnt&nbspEnt&nbspEnt&nbsp
&nbspV&nbspEntOkt. 2Z&nbsp
&nbspC&nbspSS / O&nbspEnt&nbsp
&nbspF&nbspEnt&nbspEnt&nbsp
&nbspV&nbspSS / W&nbspZ&nbsp
&nbspZVFSS / ROkt. 1F&nbsp
&nbspZV7Ent&nbspEnt&nbsp
&nbspCE7SS / Q&nbspEnt&nbsp



После того как мелодия введена в память, желательно сохранить ее на ленте или диске. Для этого выйдите в главное меню, нажав клавишу 6, и прежде чем воспользоваться пунктом 2 (SAVE TUNE) выберите устройство, на котором пьеса будет сохранена. Это делается так. Войдите в системное меню, нажав клавишу 3. Перед вами предстанет новый список, в котором нас сейчас интересует третий пункт. В нем белым цветом будет выделена надпись BETA DISK или CASS TAPE. Если надпись не соответствует желаемой, нажмите 3, а затем выберите нужное устройство, иначе нажмите клавишу 0 для возврата в главное меню. Теперь остается сохранить пьесу, предварительно указав имя файла (при работе с диском возможен также вывод на экран каталога).

Когда работа над пьесой завершена, ее нужно скомпилировать, чтобы получить файл, исполняемый независимо от редактора. Вернитесь в режим редактирования, прокрутите мелодию до конца и расставьте метки цикла во всех голосах: нажмите клавишу W и на запрос SET LOOP HERE? (Y/N) ответьте клавишей Y, затем повторите то же самое для других двух голосов. Не забудьте поставить метку также и в канале эффектов FX, нажав Extend Mode и выбрав в дополнительном меню пункт LOOP.

Расставив метки, переходите в главное меню, а затем в системное, откуда вызывается опция COMPILE & SAVE (компиляция и сохранение). Это пятый пункт системного меню. Если все метки расставлены и компиляция прошла успешно, на экран выводится ряд сообщений, из которых можно понять, что для проигрывания мелодии нужно ввести из Бейсика команду

RANDOMIZE USR 64000

или аналогичную ей, а из ассемблера полученная подпрограмма вызывается соответственно командой

CALL 64000

Тут же сообщается, что для регулирования темпа исполнения нужно записать некоторое число по адресу 64062 (при исполнении нашей пьесы это число будет равно примерно 230). Изучив выданную информацию, наберите имя файла для сохранения скомпилированной мелодии и нажмите Enter.



На этом можно было бы поставить точку в данном кратком описании, но все же хочется сказать пару слов и о других возможностях, предоставляемых редактором Wham FX. Если вас не слишком устраивают предлагаемые программой формы огибающих звуков, то вы можете с помощью пункта 4 главного меню исправить положение и создать свои собственные уникальные звуки.

Нажав клавишу 4, вы попадаете во встроенный редактор для формирования огибающих. В верху экрана выстроятся 8 диаграмм, показывающих существующие формы звуков: первые 7 можно заказывать в дополнительном меню режима редактирования, а восьмой, помеченный двумя звездочками, относится к шумовым эффектам. Нажмите цифровую клавишу, соответствующую звуку, который вы хотите изменить. На экране появится выбранная диаграмма в увеличенном масштабе (Рисунок  10.4), а под ней - курсор в виде стрелки. Управляя клавишами Left (Caps Shift/5) и Right (Caps Shift/8) переместите стрелку в нужную точку огибающей и отрегулируйте громкость с помощью клавиш Down (Caps Shift/6) и Up (Caps Shift/7). Получив таким образом нужную форму огибающей закончите редактирование, нажав Enter.


ОЦЕНКА ИГРОВОЙ СИТУАЦИИ



ОЦЕНКА ИГРОВОЙ СИТУАЦИИ

Во время игры на экране появляются не только предметы и персонажи, определенные сюжетом, но еще и выводится различная информация, помогающая играющему разобраться в текущем положении. Например, в игре BOULDER DASH ведется постоянный подсчет собранных алмазов, а на экране показывается, сколько их еще нужно собрать до перехода на следующий уровень. Это очень важно, так как вам объявлена настоящая вендетта по-корсикански, а времени, чтобы уйти от погони, у вас в обрез (оставшиеся в вашем распоряжении секунды также высвечиваются на экране, чтобы безжалостная надпись «Out of time» не показалась слишком неожиданной). Поэтому, только забрав последний алмаз, вы можете вздохнуть с некоторым облегчением (правда, ненадолго).

В более сложных играх, таких как INTO THE EAGLES NEST, VIDEO POOL или OPERATION WOLF количество выводимой информации также становится более значительным. Кроме чисел на экране можно увидеть и тексты, поясняющие результаты ваших действий или предупреждающие о возникшей опасности (например, в SILENT SERVICE). Чаще всего такая информация выводится в виде «бегущей строки» или в специальном окне, предназначенном именно для этих целей (Рисунок  1.4). А во многих стратегических играх оценочная информация настолько обильна, что в них для вывода различных сообщений отводятся уже целые кадры, занимающие полный экран (скажем, как в играх SIM CITY или DICTATOR).



Оценка игровой ситуации в игре GUN FRIGHT



Рисунок 1.4. Оценка игровой ситуации в игре GUN FRIGHT

Несмотря на невероятное разнообразие реализованных в игровых программах сюжетов, все же есть нечто, что объединяет выводимую на экран дополнительную информацию: она представляет собой оценку игровой ситуации. Следовательно, в программе должна присутствовать часть, которая оценивает действия играющего и его противников на всех стадиях игры и выводит на экран результаты этой оценки.

В большинстве игр ведется подсчет очков по тому или иному критерию, что позволяет легко выяснить, как со временем растет мастерство играющего или сравнить силу нескольких игроков. Очень часто выводится время, оставшееся до конца игры, число попаданий в противника, а также количество оставшихся у вас ресурсов (бомб, торпед, патронов). Все эти характеристики могут использоваться для оценки игровой ситуации и должны быть взяты вами «на вооружение» при создании собственных программ.



ОКНА



ОКНА

Пожалуй, найдется не так уж много игровых программ, в которых в той или иной форме не использовались бы окна. Вы, наверное, уже хорошо знакомы с этим термином, но тем не менее поясним, что именно мы будем под ним подразумевать. Окно- это некоторая прямоугольная часть рабочего экрана, внутри которой можно производить различные преобразования независимо от внешней области. Например, можно изменить цвет окна, вывести в него какой-нибудь текст, переместить внутри него изображения и многое другое. Некоторые игры буквально напичканы окнами: в одном происходит сражение, в другом сообщается, сколько времени вам осталось «жить», в третьем... Да что перечислять, вы и сами это знаете не хуже нас. Поговорим лучше о том, как создаются окна и как получаются всевозможные эффекты, связанные с ними.



Окна в игре MICRONAUT ONE



Рисунок 5.4. Окна в игре MICRONAUT ONE

Как видите, можно придумать множество типов окон, но давайте сначала посмотрим, как получить наиболее простые из них и напишем подпрограммы, выполняющие наиболее распространенные преобразования в окнах. Первую из них, которая выполняет очистку заданной области экрана, назовем в соответствии с аналогичной процедурой LaserBasic'а CLSV. Перед обращением к ней необходимо заполнить 4 переменные, под которые нужно зарезервировать память инструкцией ассемблера DEFB:

COL DEFB 0 ROW DEFB 0 LEN DEFB 0 HGT DEFB 0

Обратите внимание, что переменные должны располагаться именно в том порядке, в котором они указаны; менять их местами или разбрасывать по тексту программы недопустимо. Это условие введено для упрощения всех последующих процедур, работающих с окнами, о чем мы еще скажем при детальном разборе подпрограмм. Другое упрощение касается допустимых значений переменных: во-первых, окно не может иметь нулевые размеры ни по ширине, ни по высоте, а во-вторых, оно не должно выходить за пределы экрана.

Такие допущения сделаны не только для упрощения подпрограмм и сокращения их размеров. Это позволяет также и несколько увеличить их быстродействие, так как все необходимые проверки возлагаются на программиста. Кстати, в большинстве игровых программ операции с окнами (да и многие другие) производятся как раз безо всяких проверок корректности задаваемых параметров. Ведь подобные действия нужны, в основном, лишь на стадии отладки программы, а когда работа закончена, они становятся «мертвым грузом», только замедляющим выполнение процедур.

Итак, приводим подпрограмму очистки окна экрана:

CLSV LD BC,(LEN) ;чтение сразу двух переменных: ; C = LEN, B = HGT LD A,(ROW) CLSV1 PUSH AF PUSH BC CALL 3742 ;адрес начала строки экрана LD A,(COL) ;прибавляем смещение ADD A,L ; COL по горизонтали LD L,A LD B,8 ;в каждой строке 8 рядов пикселей CLSV2 PUSH HL LD E,C ;счетчик циклов в E, равный ширине окна XOR A ;в аккумуляторе 0 CLSV3 LD (HL),A ;обнуляем очередной байт видеобуфера INC HL ;переходим к следующему DEC E ;пока не дойдем до правого края окна JR NZ,CLSV3 POP HL INC H ;переходим к следующему ряду пикселей DJNZ CLSV2 POP BC POP AF INC A ;переходим к следующей строке экрана DJNZ CLSV1 ;повторяем, пока не дойдем ; до нижнего края окна RET


Эта подпрограмма только очищает окно, но никак не влияет на его цвет. Для изменения атрибутов нужна другая процедура, которую назовем SETV. Здесь нам потребуется дополнительная переменная для хранения байта атрибутов, которую нужно определить в программе строкой

ATTR DEFB 0

и перед обращением к подпрограмме SETV занести в нее необходимое значение.

Прежде чем привести текст подпрограммы, скажем несколько слов о способе расчета требуемого адреса в области атрибутов экрана. Эта задача намного проще определения адреса в области данных, так как атрибуты имеют линейную организацию. Поэтому достаточно номер строки экрана умножить на 32, то есть на длину полной строки экрана, добавить величину смещения от левого края (горизонтальную позицию интересующего знакоместа) и полученную величину сложить с адресом начала области атрибутов. Наиболее сложным моментом расчетов может показаться операция умножения и это было бы действительно так, не будь один из сомножителей числом, равным степени 2, что позволяет свести задачу к простому сложению.

Вот текст подпрограммы, выполняющей установку атрибутов в окне экрана:

SETV LD DE,#5800 ;адрес начала области атрибутов экрана LD BC,(LEN) ;C = LEN, B = HGT LD A,(ROW) LD L,A ;расчет адреса левого верхнего угла окна LD H,0 ; в области атрибутов экрана ADD HL,HL ;умножаем на 32 (2 в 5-ой степени) ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,DE ;полученное смещение складываем ; с началом области атрибутов LD A,(COL) ;добавляем горизонтальное смещение окна ADD A,L LD L,A LD A,(ATTR) ;в аккумуляторе байт атрибутов SETV1 PUSH BC PUSH HL SETV2 LD (HL),A ;помещаем в видеобуфер INC HL DEC C ;до правого края окна JR NZ,SETV2 POP HL POP BC LD DE,32 ;переходим к следующей строке ADD HL,DE ; (длина строки 32 знакоместа) DJNZ SETV1 ;повторяем, пока не дойдем до нижнего ; края окна RET

Еще одним довольно распространенным видом преобразований, выполняемых с окнами, является инвертирование изображения. Подпрограмма, реализующая это действие очень похожа на процедуру очистки окна, только в этом случае байт данных из видеобуфера инвертируется в аккумуляторе и затем помещается обратно на свое место. Как вы знаете, инверсия - это изменение состояния битов на противоположное: если бит установлен, он сбрасывается и наоборот. Такую операцию можно выполнить, например, с помощью команды



XOR 255

однако микропроцессор имеет для этих целей специальную инструкцию CPL, которая и выполняется быстрее и занимает в памяти всего один байт. Ею мы и воспользуемся в подпрограмме INVV:

INVV LD BC,(LEN) LD A,(ROW) INVV1 PUSH AF PUSH BC CALL 3742 LD A,(COL) ADD A,L LD L,A LD B,8 INVV2 PUSH HL LD E,C INVV3 LD A,(HL) ;читаем байт из видеобуфера CPL ;инвертируем байт в аккумуляторе LD (HL),A ;возвращаем обратно в видеобуфер INC HL DEC E JR NZ,INVV3 POP HL INC H DJNZ INVV2 POP BC POP AF INC A DJNZ INVV1 RET

Поскольку здесь говорится об окнах, задаваемых с точностью до знакоместа, есть возможность несколько ускорить процедуру инвертирования изображения, что может оказаться существенным при работе с большой площадью экрана. Ведь вместо того, чтобы инвертировать каждый байт данных, можно просто поменять местами цвета INK и PAPER в области атрибутов и вместо восьми байт для каждого знакоместа обрабатывать только один. Для успешного решения этой задачи еще раз напомним значения битов в байте атрибутов: биты 0, 1 и 2 отвечают за цвет «чернил» INK, биты 3, 4 и 5 определяют цвет «бумаги» PAPER, 6-й бит задает уровень яркости BRIGHT, а старший 7-й бит включает или выключает мерцание FLASH.

Вот эта подпрограмма:

INVA LD DE,#5800 ;начало как в процедуре SETV LD BC,(LEN) LD A,(ROW) LD L,A LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,DE LD A,(COL) ADD A,L LD L,A INVA1 PUSH BC PUSH HL INVA2 LD A,%11000000 ;маскируем 2 старших бита атрибутов, ; которые не будут изменяться AND (HL) ;выделяем их из суммарных атрибутов LD B,A ;запоминаем их в регистре B LD A,%00111000 ;маскируем биты для цвета PAPER AND (HL) ;выделяем их RRCA ;сдвигаем на место битов цвета INK RRCA RRCA OR B ;объединяем с выделенными ранее битами LD B,A ;снова запоминаем LD A,%00000111 ;маскируем биты для цвета INK AND (HL) ;выделяем их RLCA ;сдвигаем на место битов цвета PAPER RLCA RLCA OR B ;объединяем все атрибуты LD (HL),A ;возвращаем их в видеобуфер INC HL DEC C JR NZ,INVA2 POP HL POP BC LD DE,32 ADD HL,DE DJNZ INVA1 RET



Довольно часто в игровых программах применяется зеркальное отображение окон. Подобная процедура имеется и в Laser Basic'е. Ее выполняет оператор .MIRV, поэтому и нашу подпрограмму мы назовем так же. Перед обращением к ней, как и во всех предшествующих процедурах необходимо определить переменные ROW, COL, HGT и LEN с теми же ограничениями, о которых мы уже говорили.

MIRV LD BC,(LEN) LD A,(ROW) MIRV1 PUSH AF PUSH BC CALL 3742 ;в HL - адрес экрана LD A,(COL) OR L LD L,A ;начальный адрес левого края окна ; В DE получаем соответствующий адрес противоположного края окна LD D,H LD A,C ADD A,L DEC A LD E,A LD B,8 ;8 рядов пикселей MIRV2 PUSH DE PUSH HL PUSH BC SRL C ;делим ширину окна на 2 JR NC,MIRV3 ;продолжаем, если разделилось без остатка INC C ;иначе увеличиваем счетчик на 1 MIRV3 LD A,(HL) ;получаем байт с левой стороны CALL MIRV0 ;зеркально отображаем его PUSH BC ;запоминаем его LD A,(DE) ;берем байт с правой стороны CALL MIRV0 ;отображаем POP AF ;восстанавливаем предыдущий байт ; в аккумуляторе LD (HL),B ;записываем «правый» байт ; на левую сторону окна LD (DE),A ; и наоборот INC HL ;приближаемся с двух сторон DEC DE ; к середине окна DEC C JR NZ,MIRV3 ;повторяем, если еще не дошли до середины POP BC POP HL POP DE INC H ;переходим к следующему ряду пикселей INC D DJNZ MIRV2 POP BC POP AF INC A ;переходим к следующей строке экрана DJNZ MIRV1 RET ; Подпрограмма зеркального отображения байта в аккумуляторе MIRV0 RLA RR B ;отображенный байт получится в B RLA RR B RLA RR B RLA RR B RLA RR B RLA RR B RLA RR B RLA RR B RET

В этой процедуре применены уже достаточно сложные средства, о которых стоит поговорить особо. Ядром ее является внутренняя подпрограмма MIRV0, которая переворачивает байт: 7-й бит переходит в 0-й, 6-й - в 1-й и т. д. Исходный байт на входе в нее помещается в аккумулятор, а «перевернутый» получаем на выходе в регистре B. Поясним, как это происходит. После выполнения команды RLA биты аккумулятора сдвигаются влево и вытесняемый бит переходит во флаг переноса, который используется в качестве пересылочного буфера. При выполнении же команды RR B бит из флага переноса попадает в младший бит регистра B и на следующих этапах постепенно перемещается к левому краю, то есть в сторону старшего бита. После выполнения восьми пар команд сдвигов все биты из аккумулятора перейдут в регистр B, но окажутся записанными в обратном, «зеркальном» порядке. Добавим, что вместо команд



RLA RR B

с тем же результатом можно использовать команды

RRA RL B

На первый взгляд кажется, что подпрограмма MIRV0 написана не слишком экономично. Раньше мы призывали вас использовать все доступные методы оптимизации программы, а теперь вдруг размахнулись вместо того, чтобы заключить одни и те же повторяющиеся команды в цикл. Однако, мы должны заметить, что существует оптимизация не только размеров программы; иногда бывает гораздо важнее оптимизировать ее по времени исполнения. И в данном случае важность достижения наивысшего быстродействия процедуры зеркального отображения окна значительно «перевешивает» стремление к сокращению ее объема. Попробуйте переписать подпрограмму MIRV0, использовав один из возможных способов организации циклов, и результат будет заметен даже при отображении сравнительно небольших окон.

Желанием ускорить работу программы обусловлен и выбор команды сдвига аккумулятора. Ведь с тем же успехом можно было написать любую другую инструкцию, например, RL A или SLA A, но если вы загляните в , то заметите, что команда RLA выполняется в два раза быстрее других команд сдвига, всего за 4 такта (столько же времени требует и команда RLCA, которую также можно использовать в данной подпрограмме).

Другим интересным моментом подпрограммы является сохранение в стеке с последующим восстановлением отображенного байта внутри цикла MIRV3. Как мы уже сказали, требуемый байт получается в регистре B и применение команды PUSH BC поэтому должно быть понятно. Но затем почему-то использована инструкция POP AF. Это не ошибка, так и должно быть. Дело в том, что после второго вызова подпрограммы MIRV0 регистр B оказывается занят значением другого «перевернутого» байта, пары HL и DE также содержат нужную информацию. Свободными остаются только регистры C и A, но C связан в пару с занятым B, к тому же это младший регистр, а нам нужно восстановить из стека значение старшего. По счастью, аккумулятор в регистровой паре AF как раз занимает «старшее» место, а состояние флагов в данном случае нас не интересует. Именно это и делает возможным применение единственной инструкции POP AF вместо ряда пересылок между регистрами. Как видите, не всегда обязательно восстанавливать из стека ту же регистровую пару, которая была до этого сохранена командой PUSH.



Зеркальное отображение окон не будет полноценным, если «переворачивать» только пиксели, нужно также отображать и атрибуты. Процедура, выполняющая такую операцию, в общем должна быть похожа на только что описанную, она будет даже несколько проще благодаря тому, что сами байты в этом случае отображать уже не требуется. Если вам понятен принцип работы предыдущей подпрограммы, то с этой вы разберетесь без труда, посему приводим ее исходный текст без лишних предисловий и комментариев:

MARV LD BC,(LEN) 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 DE,#5800 ADD HL,DE LD A,(COL) ADD A,L LD L,A LD D,H LD A,C ADD A,L DEC A LD E,A MARV1 PUSH BC PUSH DE PUSH HL SRL C JR NC,MARV2 INC C MARV2 LD A,(HL) EX AF,AF' LD A,(DE) LD (HL),A EX AF,AF' LD (DE),A INC HL DEC DE DEC C JR NZ,MARV2 POP DE POP HL LD BC,32 ADD HL,BC EX DE,HL ADD HL,BC POP BC DJNZ MARV1 RET


Окружности


В Бейсике имеется еще один графический оператор - CIRCLE, предназначенный для рисования окружностей. Значит, можно без особых хлопот, прибегнув к помощи ПЗУ, реализовать в программах на ассемблере и его. Сразу же сообщим, что соответствующая процедура находится по адресу 9005, а параметры ей передаются так же, как и при рисовании дуг, через стек калькулятора. Перед вызовом CALL 9005 нужно последовательно занести в стек три числа: координаты центра окружности по горизонтали и вертикали, а также ее радиус в пикселях.

Для примера напишем программку, выполняющую то же самое, что и оператор

CIRCLE 120,80,60

Внешне она очень напоминает программку, рисующую на экране фрагменты дуг:

ORG 60000 ENT $ CALL SETSCR ;Установка атрибутов экрана LD A,120 ;Заносим в стек калькулятора CALL 11560 ; X-координату центра окружности LD A,80 ;Y-координата центра окружности CALL 11560 LD A,60 ;Радиус CALL 11560 CALL 9005 ;CIRCLE 120,80,60 LD HL,10072 EXX RET

Процедура 9005 также «портит» регистровую пару HL', то есть при необходимости возврата в Бейсик ее обязательно нужно восстановить.

Еще раз повторим, что при желании вы можете рисовать все графические примитивы, описанные в этом разделе, заданным цветом, для чего нужно воспользоваться одним из способов, перечисленных в разделе .



Операция сравнения


Программируя на Бейсике, вы привыкли, что в циклах можно задавать любые граничные значения управляющей переменной. Оказывается, в ассемблере это также возможно, хотя здесь и есть ряд ограничений, касающихся выбора регистра и, естественно, диапазона границ его изменения. В этом случае в качестве счетчика удобнее всего использовать аккумулятор, что избавит от необходимости применения дополнительных команд пересылок между регистрами. Перед началом цикла нужно занести в аккумулятор стартовое значение счетчика, а в конце сравнивать его с числом, до которого он должен измениться. Команда сравнения величины регистра A (и только его!) с числовым значением или с содержимым другого регистра записывается как CP (compare - сравнить), а в поле операндов помещается число или имя регистра, например:

CP 5 ;сравнить значение в аккумуляторе с числом 5

или

CP D ;сравнить содержимое аккумулятора с регистром D

Операция сравнения исключительно важна и ее применение, конечно, далеко не ограничивается только циклами, поэтому мы сочли необходимым привести возможные результаты сравнения регистра A с операндом X и используемые при этом мнемоники условий для переходов в табл. 5.1.

Таблица 5.1. Результаты операции сравнения

Результат сравненияСостояние флаговМнемоника условия
перехода
A = XZ = 1Z
A <> XZ = 0NZ
Беззнаковое сравнение (числа от 0 до 255)
A < XCY = 0C
A >= XCY = 0NC
Сравнение с учетом знака (числа от -128 до +127)
A < XS = 1P
A >= XS = 0M

Используя операцию сравнения, можно написать цикл, в котором регистр A изменяется, например, от 12 до 24:

LD A,12 ;задаем начальное значение в аккумуляторе CYCLE PUSH AF ;сохраняем в стеке ......... ;тело цикла POP AF ;восстановление аккумулятора INC A ;увеличение счетчика CP 25 ;сравниваем содержимое регистра A ; с числом 25 JR C,CYCLE ;переход на начало, если меньше 25 ; (меньше или равно 24)

В данном примере ничего принципиально не изменится, если команду JR C,CYCLE заменить на JR NZ,CYCLE - результат будет тем же, но если шаг цикла окажется отличным от единицы, то второй вариант может не сработать, поэтому предпочтительнее все же применять проверку флага переноса, а не нуля.


Приведенный вариант организации циклов уже почти повторяет такие строки Бейсика:

FOR N=12 TO 24 .............. NEXT N

Основное отличие, пожалуй, состоит лишь в одном: ассемблерный цикл в любом случае выполнит хотя бы один проход, независимо от граничных условий, ведь проверка здесь находится в конце цикла. Поэтому, чтобы исключить такой нежелательный эффект, в тех случаях, когда заранее неизвестны границы цикла, необходимо проверку производить в самом начале. Это лишь немногим усложнит программу, и она примет следующий вид:

;Предположим, что значение аккумулятора до начала цикла неизвестно CYCLE CP 25 ;проверяем на достижение конечного ; значения, то есть выполнение условия ; A > 24 (A >= 25) JR NC,AROUND ;если да, обходим цикл PUSH AF ;сохраняем счетчик в стеке ......... ;тело цикла POP AF ;восстановление значения счетчика INC A ; и увеличение его на 1 JR CYCLE ;безусловный переход на начало AROUND ......... ;продолжение программы

Прежде чем продолжить разговор о циклах, скажем еще несколько слов об особенностях использования в ассемблере чисел со знаком. Вы, конечно, понимаете, что ни в какую ячейку памяти невозможно записать отрицательное значение. Попробуйте в редакторе GENS ввести, а затем оттранслировать команду

LD A,-1

после чего просмотрите память, например, функцией Бейсика PEEK. На первом месте вы увидите байт 62, это код самой команды, а следом за ним вместо -1 обнаружите число 255. Казалось бы, что с отрицательными числами в машинных кодах иметь дело совершенно невозможно, но тем не менее, можно считать, что старший бит в байте или двухбайтовом значении иногда будет играть роль знака: если он установлен, то число отрицательное, а если сброшен - положительное. Таким образом, применение чисел со знаком в ассемблере в достаточной степени условно, но все же возможно. А следить за тем, какой тип чисел применяется в каждом конкретном случае должен сам программист.


ОРГАНИЗАЦИЯ ЦИКЛОВ В АССЕМБЛЕРЕ



ОРГАНИЗАЦИЯ ЦИКЛОВ В АССЕМБЛЕРЕ

Способы организации циклов в ассемблере существенно отличаются от бейсиковского оператора FOR...NEXT. Здесь нет столь простых решений, зато вы можете получить множество разновидностей циклов, отвечающих любым требованиям.



ОРГАНИЗАЦИЯ ПАМЯТИ



ОРГАНИЗАЦИЯ ПАМЯТИ

При создании программ на ассемблере вы в той или иной степени лишаетесь опеки операционной системы и вынуждены самостоятельно следить за размещением в памяти кодов программы, переменных, массивов и различных рабочих областей, ежели таковые потребуются. Отчасти подобные проблемы уже могли вставать перед вами, если в своих программах на Бейсике вы использовали дополнительные шрифты или процедуры из пакетов Supercode и NewSupercode. Но в Бейсике задача по размещению кодов решается довольно просто - нужно только опустить RAMTOP чуть ниже адреса загрузки кодового блока, выполнив оператор CLEAR. Когда же вы начнете программировать на ассемблере, то во многих случаях этого окажется недостаточно. Поэтому необходимо четко представлять, как распределяется память между различными областями, а также какие области памяти вообще существуют и для чего они предназначены. Не обойтись и без знания строения некоторых из них. Например, для успешной обработки изображений (скажем, вывода спрайтов, скроллинга окон и т. п.) нужно уметь по координатам экрана быстро определять адрес соответствующего байта в видеобуфере. Именно этим вопросам и будет посвящен данный раздел. Не вдаваясь в подробности сразу скажем, что описываемое здесь распределение памяти будет одинаково и для стандартной конфигурации Speccy с 48 килобайтами оперативной памяти, и для ZX Spectrum 128 со 128К, и даже для таких монстров, у которых ОЗУ занимает 256 или 512К.



Ответ на задачу


А сейчас, как мы и обещали, даем ответ на задачу, поставленную в параграфе «Бегущая строка» и приводим текст программы на ассемблере, соответствующий предложенному фрагменту на Бейсике. Не стоит очень расстраиваться, если вы обнаружите в чем-то расхождения, ведь любую, даже очень небольшую программу, можно написать тысячью и одним способом. Поэтому, если ваша программа работает, можете считать, что с поставленной задачей вы справились.

ORG 60000 ENT $ LD A,6 ;подготовка экрана LD (23693),A XOR A CALL 8859 CALL 3435 LD A,2 CALL 5633 LD HL,TEXT ;адрес текстовой строки MAIN1 LD DE,PR_AT ;позиционирование курсора, черный LD BC,5 ; (совпадающий с фоном) цвет символов CALL 8252 LD A,(HL) ;чтение очередного символа строки AND A JR Z,MAIN3 ;если 0, закончить вывод RST 16 INC HL PUSH HL ; Восьмикратное (по ширине символов в пикселях) скроллирование строки влево LD B,8 MAIN2 PUSH BC LD A,21 CALL SCRLIN CALL PAUSE ;задержка для получения более ; плавного смещения строки POP BC DJNZ MAIN2 POP HL JR MAIN1 ; Скроллинг, пока вся строка не исчезнет за левым краем экрана (0 = 265 раз) MAIN3 LD B,0 MAIN4 PUSH BC LD A,21 CALL SCRLIN CALL PAUSE POP BC DJNZ MAIN4 RET PAUSE LD BC,1 JP 7997

PR_AT DEFB 22,21,31,16,0 TEXT DEFM "Examine yourself how you know the assembler!" DEFB 0



ПЕЧАТЬ ЦЕЛЫХ ЧИСЕЛ



ПЕЧАТЬ ЦЕЛЫХ ЧИСЕЛ

Если печать символов не вызывает особых затруднений, то вот с выводом чисел дела обстоят несколько сложнее. Объясняется это тем, что любое число прежде всего необходимо представить в виде последовательности цифровых символов, а затем уже выводить их на экран (или на принтер). Задача осложняется еще тем, что числовые значения могут храниться в памяти в различных форматах, занимая один, два или больше байт. Так, например, Бейсик пользуется пятибайтовым представлением чисел, а обрабатывает их большая и сложная программа ПЗУ, называемая калькулятором.

Используя калькулятор, можно выполнять множество математических расчетов с любыми целыми и дробными числами, доступными Бейсику, но пока мы не будем объяснять, как это делается, поскольку тема эта носит самостоятельный характер, а также требует определенной предварительной подготовки. (Работе с калькулятором будет посвящен отдельный раздел девятой главы.) Сейчас же мы сосредоточимся на выводе только целых чисел из диапазона 0...65535, для чего используем особую рабочую область памяти, именуемую стеком калькулятора. Мы уже говорили во второй главе, что эта область не имеет определенного строго фиксированного адреса, да в большинстве случаев нам и не обязательно знать это, поскольку операционная система сама следит за ее местоположением и размером. Но при желании вы можете выяснить и то и другое. Адрес дна стека калькулятора можно прочитать из системной переменной STKBOT (23651/23652), а адрес его вершины определяется другой переменной- STKEND (23653/23654).

Для печати чисел можно вызвать подпрограмму, находящуюся по адресу 11747. Она напечатает число, расположенное в данный момент на вершине стека калькулятора, значит, требуемое число прежде нужно туда поместить. В этом вам поможет другая подпрограмма - 11563, размещающая на стеке калькулятора значение из регистровой пары BC. Следовательно, программа, которая выводит на экран, например, число 12345, может иметь следующий вид:

ORG 60000 ENT $ CALL SETSCR ;установка экрана LD BC,12345 ;требуемое число 12345 загружаем ; в регистровую пару BC CALL 11563 ;подпрограмма ПЗУ заносит в стек ; калькулятора число из BC CALL 11747 ;подпрограмма ПЗУ печатает число, ; находящееся на вершине ; стека калькулятора RET ;здесь должна располагаться подпрограмма, ; описанная в предыдущем разделе


В примере мы использовали еще одну полезную директиву ассемблера - ENT. Она дает возможность запускать оттранслированную программу непосредственно из редактора GENS, без выхода в Бейсик. Это бывает особенно удобно при отладке небольших фрагментов, в которых требуется подобрать тот или иной параметр методом «научного тыка», не прибегая к расчетам.

Знак доллара ($) при трансляции заменяется адресом начала строки, в которой он встретился, и в нашем случае он примет значение 60000. После трансляции программы, кроме обычной информации, вы увидите на экране строку

Execute 60000

говорящую о том, что программа может быть выполнена с адреса 60000. Теперь для запуска полученного машинного кода достаточно ввести команду редактора R, завершив ввод, как всегда, нажатием клавиши Enter.

Описанный способ позволяет выводить любые числа от 0 до 65535. Для печати же больших значений требуется совершенно иной подход, и здесь нет столь простых решений, поэтому мы оставим этот вопрос «на потом». А сейчас представим еще одну подпрограмму из ПЗУ компьютера, предназначенную для вывода целых чисел.

В тех случаях, когда можно обойтись гораздо меньшими числовыми величинами, не превышающими 9999, лучше пользоваться подпрограммой, находящейся по адресу 6683. Она печатает число, находящееся в регистровой паре BC без привлечения стека калькулятора, что в некоторой степени облегчает обращение к ней. В качестве примера приведем такой фрагмент:

ORG 60000 ENT $ CALL SETSCR LD BC,867 CALL 6683 ;печать 4-значного десятичного числа ; из регистровой пары BC RET

Для любознательных можем добавить относительно подпрограммы 6683, что она используется операционной системой компьютера для вывода номеров строк бейсик-программ. Этим, в общем-то и объясняется то, что она может работать только с четырехзначными числами. Если же попытаться напечатать с ее помощью число, превышающее 9999, то ничего страшного не произойдет, просто вы получите довольно бессмысленную последовательность символов, имеющую мало общего с заданным значением.



При описании обоих методов печати чисел мы пользовались постоянными атрибутами, но это ни в коем случае не означает, что здесь недопустима установка временных цветов. Все сказанное в разделе в полной мере относится и к числам. Вы можете устанавливать любые допустимые режимы печати, указывать координаты экрана, использовать табуляцию и так далее. Словом, с числовыми значениями вы можете обращаться в точности так, как и с отдельными символами. И чтобы подтвердить это, приведем пример программы, печатающей число 3692 в 5-й строке и 14-м столбце экрана с применением инверсии:

ORG 60000 ENT $ CALL SETSCR LD DE,DATA1 LD BC,5 CALL 8252 LD BC,3692 CALL 6683 RET DATA1 DEFB 22,5,14 ;управляющие коды для AT 5,14 DEFB 20,1 ;управляющие коды для INVERSE 1

Если в данном примере заменить команду CALL 6683 на CALL 11563 и CALL 11747, результат окажется тем же самым.


Печать символов шириной в два знакоместа


По аналогии с предыдущим параграфом, начнем с двух небольших рисунков, из которых можно легко понять, как осуществляется требуемое преобразование. Слева (Рисунок  6.2, а) приведен исходный символ, а справа (Рисунок  6.2, б) - то, что получится после работы программы DBLSYM. Сравнивая два этих рисунка, можно заметить, что правый символ получается в результате повторения каждого вертикального ряда битов, взятого из левого символа (напомним, что действие программы BIGSYM приводит к повтору горизонтальных рядов битов исходного символа).



Печать символов высотой в два знакоместа


Чтобы лучше понять, как работает ассемблерная программа, удваивающая высоту символов, приведем сначала два рисунка. На первом из них (Рисунок  6.1, а) показан исходный символ стандартного набора, а на другом (Рисунок  6.1, б) - то, что должно получиться после преобразования. Нетрудно заметить, что все байты (начиная с верхнего) левого символа, повторяются дважды на правом рисунке. Таким образом, заставив программу проделать эту операцию с каждым из восьми байтов, мы получим увеличение высоты символа ровно в два раза. Если вам очень захочется, то можно таким способом «вырастить» буквы в три, четыре и более раз, но над программой тогда придется немного поработать. Заметим, что символы высотой, скажем, в шесть знакомест выглядят на экране весьма эффектно.



Печатающий квадрат


Чтобы создать программу этого эффектного вывода символов на экран, напоминающего работу пишущей машинки или телетайпа, достаточно воспользоваться простым циклом, содержащим команду DJNZ. Вначале два слова о том, что мы должны увидеть на экране, а затем рассмотрим особенности программы и новые команды микропроцессора, которые нам понадобятся для осуществления этого замысла.

После запуска программы слева направо начнет передвигаться синий квадратик, следом за которым будут постепенно появляться буквы заданного текста. Вывод желательно сопроводить звуком, имитирующим стук клавиш.

Поскольку такой способ печати текстов может встречаться в игровой программе неоднократно, необходимо создать универсальную процедуру, которая сможет выводить любой заданный текст. В этом случае перед ее вызовом достаточно будет лишь указать нужные параметры в некоторых регистрах. Какие это должны быть параметры? Во-первых, конечно, сам текст, а точнее, адрес строки символов. Другим параметром может быть длина выводимой строки, но от этого значения можно избавиться несколькими способами (вообще же везде, где это только возможно, нужно стараться избегать лишних параметров). Например, можно вставить байт длины строки непосредственно перед ее началом, но это не слишком удобно. Более распространенный способ - ограничить строку, вставив в ее конец какой-нибудь конкретный байт, служащий маркером конца текста. Обычно для этих целей используется байт 0 и такой формат строки называют ASCIIZ-форматом. Существуют и другие способы задания строк, но о них мы поговорим чуть позже.

Используя при выводе строк способом «печатающего квадрата» ASCIIZ-строки, перед вызовом процедуры будем заносить их адреса в регистровую пару HL. Алгоритм самой подпрограммы может быть таким: считываем из строки очередной символ и если он равен нулю, заканчиваем вывод; печатаем символ; следом выводим пробел с синим фоном и возвращаем позицию печати на один символ назад; получаем короткий звук, имитирующий стук пишущей машинки и делаем небольшую задержку; возвращаемся на начало цикла. Кроме этого, перед завершением программы нужно стереть с экрана синий квадрат, для чего достаточно просто вывести пробел.


Программа, выполняющая все эти действия будет выглядеть примерно таким образом:

T_TAPE LD A,(HL) ;читаем из строки очередной символ AND A ;проверяем на 0 JR NZ,TTAPE1 ;если нет, продолжаем LD A," " ;стираем изображение RST 16 ; «печатающего квадрата» RET ;выход TTAPE1 RST 16 ;печатаем считанный символ INC HL ;перемещаем указатель текущего ; символа на следующий PUSH HL ;сохраняем в стеке значение указателя LD DE,TXTCUR ;формируем изображение LD BC,4 ; «печатающего квадрата» CALL 8252 CALL 3405 ;сбрасываем временные атрибуты XOR A ;в аккумуляторе 0 OUT (254),A ;«выключаем» динамик ; ------------------ ; Задержка (PAUSE 5) LD BC,5 ;пауза 5/50 секунды CALL 7997 ;вызов подпрограммы PAUSE ; ------------------ LD A,%00010000 ;устанавливаем 4 бит аккумулятора OUT (254),A ;«включаем» динамик POP HL ;восстанавливаем значение указателя JR T_TAPE ;переход на начало цикла TXTCUR DEFB 17,1," ",8

Как вы видите, в этой небольшой процедуре появилось несколько новых команд, поэтому помимо кратких комментариев дадим еще и более основательные пояснения.

Команда LD A,(HL) в принципе очень похожа на известную вам LD A,(Address), но отличается от нее тем, что в аккумулятор загружается значение не из какой-то конкретной ячейки памяти, а из той, на которую указывает регистровая пара HL. Например, если в HL записать число 16384, то в регистр A загрузится содержимое ячейки, находящейся по адресу 16384, то есть выполнится команда LD A,(16384). Эту инструкцию очень удобно применять в тех случаях, когда значение адреса для чтения или записи заранее неизвестно и может быть любой переменной величиной. Попутно скажем, что существует и обратная команда, то есть LD (HL),A, которая записывает содержимое аккумулятора по адресу, указанному в HL.

Существенное преимущество этого типа команд состоит еще и в том, что кроме аккумулятора таким способом можно пересылать и содержимое любого другого регистра данных, в том числе и регистров H и L, например, LD L,(HL) или LD (HL),H. И даже более того, по адресу, указанному в HL, можно записывать не только содержимое регистров, но и непосредственные числовые значения, конечно, не превышающие 255, скажем, LD (HL),153. Что же касается аккумулятора, то для него имеется возможность адресации не только с помощью пары HL, но также и BC либо DE, и эти команды еще не раз появятся в нашей книге.



В приведенной процедуре встретились две логические команды AND A и XOR A, хотя в данном примере они применены и не совсем по назначению, а скорее для сокращения машинного кода. Первая из них выполняет то же действие, что и команда CP 0, а вторая заменяет инструкцию LD A,0 и такой способ записи среди программистов считается хорошим стилем. В самом деле, при объединении аккумулятора по принципу AND с самим собой, ноль может получиться лишь тогда, когда он имеет нулевое значение (кстати, с тем же успехом можно пользоваться инструкцией OR A). Весьма ценно и то, что при этом содержимое аккумулятора не изменяется, а команда воздействует только на флаги, и в частности, на флаг Z, который нам и нужно проверить. Другая команда, XOR A, обнулит содержимое аккумулятора при любом его изначальном значении. Ведь, как мы уже говорили, при объединении двух чисел по принципу XOR любой бит будет сброшен в 0, если соответствующие биты одинаковы в обоих числах. То есть при объединении по XOR двух одинаковых величин результат всегда будет нулевым. Запомните это обстоятельство, поскольку в дальнейшем мы всегда будем пользоваться описанными приемами. Единственный случай, когда команда LD A,0 незаменима, это если нужно сохранить значение флагового регистра для последующей проверки. Помните, что все логические команды воздействуют на флаги, а инструкции загрузки регистров - нет.

Вы, наверное, знаете, что для получения звука в Бейсике, кроме оператора BEEP, можно воспользоваться командой OUT, которая записывает число в указанный порт. Динамик в Spectrum'е подключен к порту 254, а за его «включение» или «выключение» ответственен 4-й бит посылаемого байта. Остальные биты несут другую нагрузку, а здесь нас интересуют еще биты 0, 1 и 2, которые, как уже было сказано в разделе главы 4, определяют цвет бордюра. При заданных в процедуре значениях аккумулятора 0 и 16 получится черный бордюр, но его легко изменить на любой другой цвет, просто прибавив к коду нужного цвета значение «бита для динамика» (точнее, код цвета объединяется со значением 4-го бита аккумулятора по принципу OR, для чего, кстати, логические команды в основном и используются). Быстрое чередование значения этого бита приводит к появлению звука. Более подробно о получении различных звуковых эффектов мы расскажем в .



В процедуре «печатающий квадрат» показано применение еще одной полезной подпрограммы ПЗУ, которая вызывается при интерпретации оператора PAUSE. Перед обращением к ней необходимо в пару BC поместить величину задержки, измеряемую в 50-х долях секунды - так же, как и в Бейсике. Если в BC поместить 0, то получится бесконечная пауза, прерываемая только при нажатии на любую клавишу (опять же, как в Бейсике).

Теперь приведем пример использования данной процедуры и напечатаем на экране таким способом какой-нибудь конкретный текст. Например:

ORG 60000 ENT $ CALL SETSCR ;подготавливаем экран LD A,22 ;помещаем текущую позицию печати в ; начало 5-й строки экрана (AT 5,0) RST 16 LD A,5 RST 16 XOR A RST 16 LD HL,TEXT ;указываем адрес ASCIIZ-строки с текстом CALL T_TAPE ;выводим методом телетайпа RET TEXT DEFM "COMPUTER Sinclair ZX-Spectrum" DEFB 0 ;подпрограмма из раздела «Подготовка ; экрана к работе из ассемблера» главы 4


Перемещение спрайта скроллингом окна



Рисунок 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 ; Подпрограмма скроллинга окна вправо

; Подпрограмма вывода спрайта

; Переменные к процедуре скроллинга 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 ; Подпрограмма вывода спрайта

; Заголовок данных для «динозавра» 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



; Заголовок данных первой фазы спрайта «солдат» 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



; измененная процедура 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, в. Так вот, то, что получилось на последнем рисунке, и есть маска для исходного спрайта «корабль».


Перемещения битов



Рисунок 6.3. Перемещения битов

ORG 60000 ENT $ LD A,(23296) ;начало - как в процедуре BIGSYM DBLSYM LD L,A LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL LD DE,(23606) ADD HL,DE EX DE,HL LD HL,(23684) LD B,8 ;количество повторений внешнего цикла PUSH HL ; Во внешнем цикле по очереди берутся 8 байтов исходного символа ; для преобразования во внутреннем цикле DBLS1 PUSH BC ;сохраняем BC для внешнего цикла PUSH DE ;сохраняем текущий адрес ; в символьном наборе LD A,(DE) LD DE,0 ;подготавливаем пару DE для последующего ; формирования в ней расширенного ; байта символа LD B,8 ;количество повторений внутреннего цикла ; (равное числу бит в одном байте) ; Во внутреннем цикле каждый бит исходного символа дважды копируется ; в регистровую пару DE, где в конце концов получаем расширенный ; в два раза байт (Рисунок 6.3) DBLS2 RLCA PUSH AF RL E RL D POP AF RL E RL D DJNZ DBLS2 LD (HL),D ;вывод преобразованных байтов из INC HL ; пары DE на экран LD (HL),E DEC HL POP DE ;восстанавливаем значение DE INC H INC DE ;переходим к обработке следующего ; байта из набора символов POP BC ;восстанавливаем BC для внешнего цикла DJNZ DBLS1 POP HL INC HL ;переходим к следующему INC HL ; знакоместу экрана LD (23684),HL ;записываем в системную переменную ; адрес следующего символа RET

Чтобы напечатать на экране строку с каким-то текстом, можно воспользоваться той же программкой на Бейсике, что и для демонстрации процедуры BIGSYM.

Теперь в вашем распоряжении имеются три программы, в той или иной мере изменяющие форму стандартных символов и, чтобы лучше понять их работу, можно порекомендовать слегка «поиздеваться» над символами, создавая комбинированные программы. Например, часть символа увеличить по высоте, а оставшуюся часть - по ширине, или BIGSYM и DBLSYM соединить с программой BOLD. Надо сказать, что последний вариант дает совсем недурные результаты, в чем вы сможете убедиться, дойдя до последнего раздела этой главы.



ПЕРЕМЕЩЕНИЯ СИМВОЛОВ



ПЕРЕМЕЩЕНИЯ СИМВОЛОВ

В очень многих фирменных игровых программах используются те или иные способы движения отдельных букв, строк, или даже целых текстовых экранов. Так, например, в OCEANCONQUER осуществляется побуквенный вывод текста (что-то вроде «печатающего квадрата», рассмотренного в [1]), в F-16 COMBAT PILOT можно увидеть бегущую строку, а в играх OVERLORD, ZULU WAR и многих других - скроллинги (перемещения) экранов с текстами. И в этом разделе мы рассмотрим некоторые из подобных эффектов, делающих игровые программы более интересными.



ПОДГОТОВКА ЭКРАНА К РАБОТЕ ИЗ АССЕМБЛЕРА



ПОДГОТОВКА ЭКРАНА К РАБОТЕ ИЗ АССЕМБЛЕРА

Разобравшись с выводом символов, неплохо было бы научиться очищать экран, устанавливать постоянные атрибуты и цвет бордюра также из программы в машинных кодах, а не производить эту предварительную подготовку в Бейсике. Тем более, что выполняются все эти операции достаточно просто.

Прежде всего необходимо задать постоянные атрибуты. Сделать это можно по-разному, но проще всего рассчитать байт атрибутов и поместить его в системную переменную ATTR_P по адресу 23693. Напомним, что в байте атрибутов биты 0..2 определяют цвет «чернил» INK, биты 3..5 отвечают за цвет «бумаги» PAPER, а 6-й и 7-й биты устанавливают или сбрасывают соответственно атрибуты яркости BRIGHT и мерцания FLASH. Поэтому требуемое значение цвета можно подсчитать по формуле

INK+PAPERґ8+BRIGHTґ64+FLASHґ128

Так для

INK 6: PAPER 0: BRIGHT 1: FLASH 0

искомый байт будет равен

6+0ґ8+1ґ64+0ґ128=70

А если вам лень считать, можете поступить проще: очистите экран и введите с клавиатуры последовательно две строки

PRINT INK 6; PAPER 0; BRIGHT 1; FLASH 0; " " PRINT ATTR (0,0)

В верхнем левом углу экрана появится черный квадратик, а под ним - искомое число 70.

Теперь остается полученное число поместить в ячейку с адресом 23693, то есть выполнить инструкцию, аналогичную оператору Бейсика POKE 23693,70. Но вот беда - микропроцессор Z80 не располагает командами пересылки в память или из памяти непосредственных значений. Поэтому такую простую операцию приходится выполнять в два захода: сначала число нужно поместить в аккумулятор (и только в аккумулятор - никакой другой регистр для этого не подходит!), а затем значение из него переписать в ячейку. Команда записи в память очень напоминает загрузку регистров, только адрес или метка в этом случае заключается в круглые скобки. То есть предложение «загрузить ячейку с адресом 23693 значением из аккумулятора» записывается как LD (23693),A. Обратите внимание, что данный тип команд может выполняться только с регистром A!


Раз уж мы заговорили о способах пересылки значений между регистрами и памятью, приведем и другие инструкции, относящиеся к этой группе. Действие, обратное LD (Address),A и аналогичное функции Бейсика PEEK Address, выполняется командой LD A,(Address). Все прочие регистры могут обмениваться числовыми значениями с памятью только в парах. Выглядят такие команды следующим образом:

LD (Address),rp LD rp,(Address)

где rp - одна из регистровых пар BC, DE или HL. (Забегая вперед, добавим, что в указанных командах могут участвовать также регистры IX, IY и SP.) Первая из них загружает две смежные ячейки памяти значением из регистровой пары, а вторая, наоборот, пересылает из памяти двухбайтовое число в обозначенные регистры. Заметим, что всегда предпочтительнее в данных командах применять пару HL, так как с ее участием эти инструкции занимают на байт меньше памяти и выполняются быстрее.

Таким образом, для установки постоянных атрибутов можно написать две строки вроде:

LD A,70 ;байт атрибутов LD (23693),A ;помещаем в системную переменную ATTR_P

Хотя таким способом можно пользоваться в большинстве случаев, он оказывается не всегда удобен. Например, если нужно установить какой-то один из атрибутов, то придется изменять не весь байт, а только некоторые его биты. А если требуется указать режимы OVER или INVERSE, либо для INK и PAPER задать значения 8 или 9, то описанный метод и вовсе непригоден.

В этих случаях можно поступить так. Первым делом необходимо установить текущий поток, связанный с основным экраном так же, как мы это делали раньше. Затем вызвать уже известную вам подпрограмму 3405 для «сброса» временных атрибутов. Следующим этапом с помощью команды RST 16 или процедуры 8252 установить новые временные атрибуты. И, наконец, временные атрибуты перевести в постоянные, для чего лучше всего вызвать соответствующую подпрограмму ПЗУ, находящуюся по адресу 7341.

Для иллюстрации этого способа напишем фрагмент, устанавливающий режимы OVER 1 и PAPER 8:



LD A,2 CALL 5633 ; определяем вывод на основной экран CALL 3405 ;«сбрасываем» временные атрибуты LD DE,ATTR1 LD BC,4 CALL 8252 ;выводим управляющие коды для новых ; временных атрибутов CALL 7341 ;переводим временные атрибуты ; в постоянные RET ATTR1 DEFB 21,1,17,8 ;последовательность управляющих кодов ; для OVER 1 и PAPER 8

Стоит ли говорить, что подобное действие может выполняться в реальной игровой программе неоднократно и при этом наверняка потребуется каждый раз изменять различные атрибуты. Посему было бы очень полезно иметь универсальную процедуру, которая работала бы по-разному в зависимости от входных параметров.

Сложность здесь заключается лишь в том, как до подпрограммы 8252 «донести» содержимое регистровых пар BC и DE - ведь перед ней должны выполниться две процедуры (CALL 5633 и CALL 3405), которые обязательно изменят значения нужных регистров. Значит, до поры до времени их нужно как-то сохранить.

Решение может показаться простым и очевидным: нужно запомнить значения регистров где-то в памяти и тем самым освободить их для каких-либо нужд, а затем восстановить их первоначальный вид, прочитав из памяти записанные ранее числа. Да, действительно, иногда так и делают. Так же поступает и большинство компиляторов, но как вы знаете, они не отличаются сообразительностью и используют ресурсы компьютера не самым оптимальным образом. Ведь известно, что команды пересылок между регистрами и памятью выполняются заметно дольше, чем обмен данными непосредственно между регистрами. Кроме того, дополнительные временные переменные лишь попусту транжирят память. И ведь еще необходимо помнить, где что лежит!

Применение такой методики чревато и еще одной неприятностью. Когда вы начнете писать на ассемблере достаточно большие программы (а мы надеемся, что это время не за горами), то очень скоро обнаружите, что шести символов для меток маловато. В результате этого очень легко можно ошибиться и поставить метку с уже существующим именем. Поэтому старайтесь везде, где только можно, обходиться без лишних меток.



Всего перечисленного можно избежать, если пойти другим путем, используя гораздо более удобное и эффективное средство - машинный стек. Во второй главе мы уже объясняли, что это такое, но не рассказывали, как с ним работать. Вообще-то, к помощи стека мы уже прибегали много раз, даже не подозревая об этом. Дело в том, что все команды вызова подпрограмм, будь то CALL или RST, прежде всего заносят в стек адрес возврата, то есть адрес следующей за вызовом команды. После выполнения подпрограммы завершающая команда RET снимает со стека этот адрес и тем самым возвращает управление основной программе.

Кроме такого косвенного взаимодействия со стеком имеется возможность непосредственного обмена с ним числовыми значениями из регистровых пар. Для этих целей служат две команды: PUSH (втолкнуть), которая помещает в стек значение из регистровой пары и POP (вытолкнуть, выскочить), забирающая с вершины стека двухбайтовое число в регистровую пару. Например, предложение «Запомнить в стеке значение регистровой пары BC» запишется так:

PUSH BC

а команда «Взять в регистровую пару HL значение с вершины стека» будет выглядеть следующим образом:

POP HL

Как мы уже говорили, существует определенный порядок работы со стеком, и числа, занесенные на его вершину последними, должны быть сняты в первую очередь. При этом нужно очень внимательно следить не только за очередностью обмена со стеком, но и за тем, чтобы его состояние при выходе из подпрограммы было таким же, как и при входе. Иными словами, необходимо, чтобы количество команд PUSH и POP в каждой подпрограмме было одинаковым (хотя, заметим, что вовсе не обязательно забирать числа со стека в те же регистровые пары, из которых производилась запись). Несоблюдение этих правил может привести к совершенно непредсказуемым результатам. Кстати, стековые ошибки относятся к наиболее распространенным, поэтому, если при отладке программы вы обнаружите, что в какой-то момент компьютер «зависает», «сбрасывается» или ведет себя как-то странно, то первым делом следует проверить те строки, где встречаются команды обмена со стеком.



Все только что сказанное верно, однако, изучая фирменные игрушки, можно заметить, что опытные программисты порой обращаются со стеком весьма своевольно, совершенно не придерживаясь общепринятых правил. В будущем мы поговорим о некоторых оригинальных приемах программирования, позволяющих иногда заметно сократить машинный код, а главное, увеличить быстродействие программы. Но пока вы не прочувствуете хорошенько идею стека, слишком увлекаться экспериментами в этой области мы вам все же не советуем.

Теперь, зная кое-что о машинном стеке, можно переписать предыдущий пример, оформив его в виде самостоятельной процедуры:

ATTRIB PUSH BC ;сохраняем в стеке значения регистровых PUSH DE ; пар BC и DE LD A,2 CALL 5633 CALL 3405 ; Снимаем с вершины стека сохраненные ранее значения в обратном порядке: POP DE ;сначала в DE, POP BC ; а затем в BC CALL 8252 CALL 7341 RET

Для вызова этой процедуры необходимо задать строку DEFB с перечислением управляющих кодов, занести адрес этой строки в DE и в регистровой паре BC указать ее длину:

LD DE,ATTR2 LD BC,6 CALL ATTRIB RET ATTR2 DEFB 16,5,17,1,19,1

Если установка постоянных атрибутов иногда может оказаться довольно сложным процессом, то остальные настройки экрана не должны вызвать никаких трудностей. Окрасить бордюр в любой нужный цвет можно двумя способами. Первый из них почти в точности повторяет оператор Бейсика

OUT 254,color

При записи в порт, так же, как и при записи в память, в ассемблере нельзя использовать непосредственные значения, поэтому код цвета прежде необходимо поместить в регистр A. Адрес порта обязательно нужно заключать в круглые скобки. Например, для установки красного бордюра можно воспользоваться такими командами:

LD A,2 OUT (254),A

Как вы знаете из Бейсика, установленный подобным образом цвет бордюра обычно надолго не задерживается. Для более долговечного его изменения используется оператор BORDER, а выполняет эту процедуру подпрограмма ПЗУ по адресу 8859. Перед обращением к ней в аккумуляторе должен содержаться код цвета. Скажем, для установки голубого бордюра следует написать:



LD A,5 CALL 8859

Для полноты информации напомним также, что байт атрибутов для бордюра обычно сохраняется в области системных переменных по адресу 23624.

Что же касается очистки экрана, то это самая простая операция. Соответствующая подпрограмма находится по адресу 3435 и не требует никаких входных параметров. Единственное, что нужно помнить - после выполнения команды CALL 3435 текущим устанавливается поток, связанный с выводом в служебное окно экрана. Таким образом, после вызова этой процедуры необходимо вновь назначить требуемый поток. Например:

CALL 3435 LD A,2 CALL 5633

В завершение этого раздела предлагаем вам законченную процедуру, окрашивающую экран и бордюр в черный цвет, устанавливающую голубой цвет для выводимых символов и тем самым подготавливающую основной экран к приему текстовой и графической информации:

SETSCR LD A,5 LD (23693),A LD A,0 CALL 8859 CALL 3435 LD A,2 CALL 5633 RET

Мы присвоили этой процедуре собственное имя (метку) SETSCR не случайно. В последующих разделах и главах эта подпрограмма будет использоваться неоднократно, и чтобы не переписывать ее каждый раз заново, мы будем просто ссылаться на нее, вставляя в текст команду CALL SETSCR. Вам же мы посоветуем записать ее (вместе с процедурой ATTRIB) на ленту или дискету в виде отдельного файла, тогда в будущем вам достаточно будет только подгрузить ее к основному тексту с помощью команды редактора G. Точно так же рекомендуем вам поступить и с другими подпрограммами, которые встретятся в книге, и в конце концов в вашем распоряжении появится библиотека наиболее важных и часто используемых в игровых программах процедур.