В этой главе мы коснемся наиболее часто встречающихся типов оценки игровой ситуации, таких как подсчет очков (жизней, боеприпасов, сбитых самолетов) и контроля времени, а также рассмотрим некоторые приемы их применения на конкретных примерах игровых программ. Поскольку получение оценки немыслимо без различных математических действий, то здесь же приводятся процедуры умножения, деления, извлечения квадратного корня как для целых чисел с учетом знака, так и для дробных. В последнем случае уже не обойтись без обращения к калькулятору.
Сразу скажем, что оценка игровой ситуации не сводится к одним лишь только математическим расчетам. На самом деле не вся оценочная информация может выводиться на экран в виде чисел, а кое-что остается, так сказать, для «внутреннего пользования» самой программе, которая следит за развитием событий и соответствующим образом себя ведет. Например, в игре Tetris при завершении очередного ряда он должен автоматически удаляться, а все ряды выше «списанного» опускаться вниз. Это можно назвать уже не количественной, а качественной оценкой, используемой самой программой.
XOR A LD (SUM),A ..............Если же контролируется количество «жизней», то следует наоборот занести в SUM какое-то начальное значение:
LD A,10 LD (SUM),A ..............Затем эта переменная будет изменяться в блоке оценки игровой ситуации, увеличиваясь или уменьшаясь в зависимости от ее типа:
............ LD A,(SUM) INC A ;или DEC A LD (SUM),A CALL PRINT ;вывод числовой оценки на экран ............ SUM DEFB 0 ;переменная для накопления суммыОднако все так просто лишь до тех пор, пока подсчеты не требуют разного рода дополнительных проверок. Более сложным и интересным является случай, когда одновременно с подсчетами, выполняется еще и анализ игровой ситуации, а результаты этого анализа оказывают влияние на сами оценки.
Для иллюстрации рассмотрим программу МИШЕНЬ, которая уже использовалась нами в пятой главе для демонстрации случайных чисел. Дополним теперь эту программу некоторыми оценками, например, после каждого выстрела будем суммировать набранные очки, выводить общее количество произведенных выстрелов и, наконец, подсчитаем средний балл, полученный за один выстрел.
Но прежде приведем несколько процедур для выполнения арифметических действий с целыми числами. В них реализованы интересные только для математиков алгоритмы вычислений и больше ничего, поэтому здесь мы обойдемся без пояснений и предлагаем вам эти процедуры в качестве стандартных библиотечных функций.
Мы уже упоминали подпрограмму ПЗУ, расположенную по адресу 12457, выполняющую умножение двух целых чисел, находящихся в регистровых парах HL и DE. Ниже показана аналогичная процедура, отличающаяся только тем, что при умножении учитываются знаки сомножителей, заданных также в HL и DE. То есть числа, участвующие в операции могут находиться в пределах от -32768 до +32767. Произведение возвращается в регистровой паре HL.
MULT LD B,8 LD A,D AND A JR Z,MULT1 RLC B MULT1 LD C,D LD A,E EX DE,HL LD HL,0 MULT2 SRL C RRA JR NC,MULT3 ADD HL,DE MULT3 EX DE,HL ADD HL,HL EX DE,HL DJNZ MULT2 RETСледующая подпрограмма предназначена для деления знаковых величин. Делимое перед обращением к ней должно находиться в паре HL, а делитель - в DE. Напоминаем, что деление на 0 невозможно, поэтому в программе имеет смысл выполнять подобную проверку. Если такая ошибка все же произойдет, будет выдано сообщение Бейсика Number too big. После выполнения процедуры частное окажется в паре HL, а остаток от деления будет отброшен.
DIVIS LD A,D OR E JR NZ,DIVIS1 RST 8 DEFB 5 ;Number too big DIVIS1 CALL DIVIS5 PUSH BC LD C,E LD B,D LD DE,0 PUSH DE EX DE,HL INC HL DIVIS2 ADD HL,HL EX DE,HL ADD HL,HL LD A,C SUB L LD A,B SBC A,H EX DE,HL JR NC,DIVIS2 EX DE,HL DIVIS3 EX DE,HL XOR A LD A,H RRA LD H,A LD A,L RRA LD L,A OR H JR Z,DIVIS4 EX DE,HL XOR A RR H RR L LD A,C SUB L LD A,B SBC A,H JP M,DIVIS3 LD A,C SUB L LD C,A LD A,B SBC A,H LD B,A EX (SP),HL ADD HL,DE EX (SP),HL JR DIVIS3 DIVIS4 POP HL POP BC BIT 7,B JR NZ,MINUS RET DIVIS5 LD B,H LD A,H RLA CALL C,MINUS EX DE,HL LD A,H XOR B LD B,A BIT 7,H RET Z MINUS LD A,H CPL LD H,A LD A,L CPL LD L,A INC HL RETКак вы помните, извлечение квадратного корня из отрицательного числа невозможно, результат также не может быть меньше нуля, поэтому и соответствующая процедура работает с величинами из диапазона 0...65535. Перед обращением к ней число, из которого нужно извлечь корень, поместите в пару HL. Результат, как и в предыдущих подпрограммах, будет возвращен в HL. Дробная часть при этом, к сожалению, также теряется.
SQR LD A,L LD L,H LD H,0 LD DE,64 LD B,8 SQR1 SBC HL,DE JR NC,SQR2 ADD HL,DE SQR2 CCF RL D ADD A,A ADC HL,HL ADD A,A ADC HL,HL DJNZ SQR1 LD L,D LD H,A RETА теперь приводим модифицированный текст программы МИШЕНЬ, в которой присутствуют некоторые типы оценок, а также демонстрируется применение только что предложенных математических процедур:
ORG 60000 ENT $ LD A,7 LD (23693),A XOR A CALL 8859 CALL 3435 LD A,2 CALL 5633 ;Основная часть программы МИШЕНЬ LD HL,UDG LD (23675),HL LD HL,0 LD (SCORE),HL ;обнуление счетчика количества очков, ; заработанных при стрельбе по «мишени» XOR A LD (KOL_W),A ;обнуление счетчика числа выстрелов, ; произведенных по «мишени» CALL MISH ;рисование «мишени» MAIN CALL WAIT ;ожидание нажатия любой клавиши LD A,22 RST 16 LD E,19 ;диапазон изменения координаты Y ; для пулевого отверстия CALL RND ;задаем координату Y LD (ROW),A ;заносим ее в переменную ROW RST 16 LD E,30 ;диапазон изменения координаты X ; для пулевого отверстия CALL RND ;задаем координату X LD (COL),A ;заносим ее в переменную COL RST 16 LD A,16 RST 16 LD A,6 ;задаем цвет пулевых отверстий RST 16 LD E,3 ;количество видов пулевых отверстий CALL RND ADD A,144 RST 16 CALL SND ;звуковой сигнал, имитирующий полет пули CALL OCENKA ;вычисление оценок результата стрельбы ; и вывод их на экран LD A,(23560) ;выход из программы CP " " JR NZ,MAIN RET ; Подпрограмма оценки результата стрельбы OCENKA LD BC,(COL) ;заносим в BC координаты выстрела LD A,10 ;вертикальная координата центра «мишени» CP B JR C,BOT_Y SUB B ;пулевое отверстие находится в верхней ; половине «мишени» JR CONT1 BOT_Y LD A,B SUB 10 ;пулевое отверстие находится в нижней ; половине «мишени» CONT1 LD B,A ;в регистре B длина катета по Y LD A,15 ;горизонтальная координата центра «мишени» CP C JR C,BIG_X SUB C JR CONT2 BIG_X LD A,C SUB 15 CONT2 LD C,A ;в регистре C длина катета по X ; Определяем длину гипотенузы прямоугольного треугольника LD H,0 LD L,B LD D,H LD E,L PUSH BC CALL MULT ;вычисляем квадрат величины Y LD B,H LD C,L POP HL LD H,0 LD D,H LD E,L PUSH BC CALL MULT ;вычисляем квадрат величины X POP BC ADD HL,BC CALL SQR ;определяем длину гипотенузы, ; величину которой помещаем в пару HL ; По длине гипотенузы находим количество заработанных очков ; в результате одного выстрела LD C,0 LD A,L CP 11 JR NC,SUM LD DE,D_SUM ADD HL,DE LD C,(HL) ; Вычисление трех оценочных характеристик стрельбы и их вывод на экран SUM PUSH BC LD DE,TXT1 LD BC,11 CALL 8252 POP BC LD HL,(SCORE) LD B,0 ADD HL,BC LD (SCORE),HL LD B,H LD C,L CALL 11563 CALL 11747 ;печать общего количества заработанных ; в результате стрельбы очков LD DE,TXT2 LD BC,11 CALL 8252 LD HL,KOL_W INC (HL) LD C,(HL) LD B,0 CALL 11563 CALL 11747 ;печать количества произведенных ; по «мишени» выстрелов LD DE,TXT3 LD BC,10 CALL 8252 LD HL,(SCORE) LD D,H LD E,L LD A,(KOL_W) LD L,A LD H,0 CALL DIVIS LD B,H LD C,L CALL 11563 JP 11747 ;печать среднего числа очков за один выстрел MULT ......... DIVIS ......... SQR ......... ; Подпрограммы MISH, CIRC и другие, а также блоки TEXT и UDG, ; на которые имеются ссылки в основной программе, описывались нами ; в первом варианте программы МИШЕНЬ MISH ......... CIRC ......... RND ......... SND ......... WAIT ......... ; Данные для мишени TEXT ......... LENTXT EQU $-TEXT ; Данные для пулевых отверстий UDG ......... ; Данные оценок D_SUM DEFB 10,10,8,8,8,6,6,6,4,4,4 TXT1 DEFB 22,21,0,16,4 DEFM "SCORE:" TXT2 DEFB 22,21,12,16,1 DEFM "SHOTS:" TXT3 DEFB 22,21,22,16,2 DEFM "MEAN:" ; Переменные для оценок SCORE DEFW 0 KOL_W DEFB 0 COL DEFB 0 ROW DEFB 0
Работать с этой программой непросто, что объясняется, во-первых, большим количеством допустимых операций, а во-вторых, необходимостью следить за порядком обмена данными со стеком калькулятора. Поэтому мы расскажем лишь о самых необходимых в игровых программах функциях.
Необходимо знать, что параметры калькулятору передаются через его собственный стек, о котором вы уже знаете достаточно, а выполняемое действие определяется последовательностью байтов-литералов, записываемых непосредственно за командой RST 40. Поскольку все математические операции калькулятор выполняет на своем стеке, то прежде всего необходимо научиться записывать туда числа и затем снимать со стека результат. По крайней мере с двумя процедурами записи в стек значений из аккумулятора и пары BC мы вас уже познакомили, но существуют и другие подпрограммы, о которых также не мешает знать.
По адресу 10934 в ПЗУ имеется процедура, записывающая в стек калькулятора вещественное число в пятибайтовом представлении. Эти пять байт числа перед обращением к процедуре нужно последовательно разместить на регистрах A, E, D, C и B. Основная сложность здесь заключена в разбивке числа с плавающей запятой на 5 компонентов, так как при этом применяются довольно хитрые расчеты. Однако если вам требуется записать заранее предопределенную константу, то можно воспользоваться очень простым способом, заставив операционную систему саму выполнить все необходимые действия. Идея сводится к тому, что при вводе строки в редакторе Бейсика все числа, прежде чем попадут в программу, переводятся интерпретатором из символьного в пятибайтовое представление. Делается это для того, чтобы во время выполнения программы уже не заниматься такими расчетами и тем самым сэкономить время. Следом за символами каждого числа записывается байт 14 и затем рассчитанные 5 байт. Например, число 12803.52 в памяти будет выглядеть таким образом:
1 2 8 0 3 . 5 2 Префикс Число 49 50 56 48 51 46 53 50 14 142 72 14 20 123Код 14 и пять байт числа при выводе листинга бейсик-программы на экран пропускаются, но в памяти они всегда присутствуют. Просмотрев дамп программы, нетрудно найти нужные байты. Можно воспользоваться и небольшой программкой, которая будет печатать нужные числа на экране, так что останется только записать их, а затем использовать в своей программе на ассемблере. Вот примерный текст такой программки:
10 PRINT 12803.52 20 LET addr=PEEK 23635+256*PEEK 23636+5 30 LET addr=addr+1: IF PEEK (addr-1)<>14 THEN GO TO 30 40 FOR n=addr TO addr+4: PRINT PEEK n: NEXT nДадим некоторые пояснения относительно этой программки. В строке 10 после оператора PRINT записывается любое вещественное число, пятибайтовое представление которого вы хотите узнать. Эта строка может иметь другой номер, но обязательно должна располагаться в самом начале программы. Учтите, что перед ней не должно быть даже комментариев.
Далее, в 20-й строке вычисляется адрес начала бейсик-программы (берется из системной переменной PROG) и пропускается 5 байт, включающих номер, длину строки и код оператора PRINT.
Операторы строки 30 отыскивают байт с кодом 14, за которым в памяти располагаются нужные нам байты числа. А в следующей строке эти 5 байт последовательно считываются в цикле и выводятся на экран.
Узнав таким образом значения составляющих числа в пятибайтовом представлении, можно загрузить регистры и вызвать процедуру 10934 для записи его на вершину стека калькулятора:
LD A,142 ;размещаем 5 байт числа на регистрах A, LD E,72 ; E LD D,14 ; D LD C,20 ; C LD B,123 ; и B CALL 10934 ;заносим число в стек калькулятораМожно предложить еще один способ укладки десятичного числа в стек калькулятора с применением процедуры 11448. Именно этой процедурой пользуется интерпретатор, работая с числовыми величинами в символьном представлении. Выполняя программу, Бейсик сохраняет адрес текущего интерпретируемого кода в системной переменной CH_ADD (23645/23646) и в данном случае нам достаточно записать в нее адрес символьной строки, содержащей требуемое число, чтобы заставить интерпретатор разбить его на 5 байт и уложить в стек калькулятора. Не помешает предварительно сохранить, а затем восстановить прежнее значение переменной CH_ADD, иначе нормальный выход в операционную систему, а тем более, продолжение выполнения бейсик-программы окажется невозможным. Не забывайте, пользуясь этим методом, в конце строки, представляющей десятичное число, ставить код 13 (в принципе, это может быть практически любой символ, кроме цифр, точки, плюса и минуса, а также букв E и e).
LD HL,(23645) ;запоминаем в машинном стеке PUSH HL ; значение переменной CH_ADD LD HL,NUMBER ;адрес строки с десятичным числом LD (23645),HL ; записываем в переменную CH_ADD LD A,(HL) ;берем в аккумулятор первый символ ; (обязательно!) CALL 11448 ;помещаем число из текстовой строки ; NUMBER в стек калькулятора POP HL ;восстанавливаем прежнее значение LD (23645),HL ; системной переменной CH_ADD ......... ;продолжаем программу ; Символьное представление десятичного числа NUMBER DEFM "12803.52" DEFB 13 ;байт-ограничитель символьной строкиНадо добавить, что хотя этот способ и кажется наиболее удобным, но у него есть один серьезный недостаток - работает он несравненно дольше всех предыдущих. Самое смешное, что он требует даже больше времени, чем в Бейсике, так как эта операция выполняется при вводе строки и во время исполнения программы интерпретатор уже располагает пятибайтовым представлением каждого числа.
После занесения в стек калькулятора тем или иным способом числовых значений, с ними нужно что-то сделать, для чего и предназначена команда RST 40. Как вы помните, раньше мы использовали стек калькулятора для вывода чисел на экран, а также для рисования линий и окружностей. Теперь посмотрим, как над числами в стеке производить различные математические операции.
Как мы уже сказали, для этого нужно записать специальные управляющие последовательности байтов непосредственно за командой RST 40. В табл. 9.1 перечислены наиболее употребительные команды калькулятора, выполняемые ими функции и состояние стека после выполнения операции, считая, что изначально в стеке были записаны два числа: X - на вершине (был записан последним) и Y - под ним. Например, для сложения этих двух вещественных чисел применяется литерал 15, а для деления - 5. В одной команде можно перечислить произвольное количество действий, а для завершения расчетов в конце последовательности литералов всегда обязательно указывать байт 56, который возвращает управление на следующую за ним ячейку памяти. Понятно, что последовательность литералов в программу на ассемблере может быть вставлена с помощью директивы DEFB.
Литерал | Операция | Состояние стека после операции | ||
1 | Замена элементов | X | Y | |
3 | Вычитание | Y - X | ||
4 | Умножение | Y ґ X | ||
5 | Деление | Y / X | ||
6 | Возведение в степень | YX | ||
15 | Сложение | Y + X | ||
27 | Изменение знака | Y | -X | |
39 | Целая часть числа | Y | INT X | |
40 | Квадратный корень | Y | SQR X | |
41 | Знак числа | Y | SGN X | |
42 | Абсолютная величина | Y | ABS X | |
49 | Копирование стека | Y | X | X |
56 | Конец расчетов | Y | X | |
88 | Округление числа | Y | INT(X+.5) | |
160 | Дописать 0 | Y | X | 0 |
161 | Дописать 1 | Y | X | 1 |
162 | Дописать 0.5 | Y | X | .5 |
163 | Дописать PI/2 | Y | X | PI / 2 |
164 | Дописать 10 | Y | X | 10 |
В качестве иллюстрации приведем программку, вычисляющую выражение 823ґ5503/(32-17) и выводящую результат на экран. При выполнении расчетов необходимо внимательно следить за очередностью выполнения операций, поэтому прежде нужно продумать порядок занесения чисел в стек (помните, что калькулятор имеет доступ только к величинам, находящимся на вершине стека). Поскольку в данном случае первым должно выполняться действие в скобках, а умножение и деление имеют одинаковый приоритет, то укладывать числа в стек будем в той же последовательности, в которой они встречаются в выражении, чтобы калькулятор мог выбирать их в обратном порядке:
ORG 60000 ENT $ CALL 3435 ;очищаем экран LD A,2 ; и подготавливаем его для печати CALL 5633 LD BC,823 ;заносим в стек все части выражения CALL 11563 LD BC,5503 CALL 11563 LD A,32 CALL 11560 LD A,17 CALL 11560 RST 40 ;вызываем калькулятор DEFB 3 ; X = 32 - 17 DEFB 5 ; X = 5503 / X DEFB 4 ; X = 823 ґ X DEFB 56 ; конец расчетов CALL 11747 ;выводим результат на экран RETПосле запуска этой подпрограммы вы увидите на экране число 301931.27. Тот же результат получается и при выполнении оператора PRINT 823*5503/(32-17).
Обязательным условием при работе с калькулятором является не только соблюдение порядка выполнения расчетов. При ошибке вы в худшем случае получите неверный результат. Гораздо важнее следить за состоянием стека калькулятора, так как если после завершения программы он окажется не в том же виде, как и в начале, последствия могут даже оказаться фатальными. Для безопасности перед выходом в Бейсик можно вызвать процедуру по адресу 5829, которая очистит стек калькулятора, хотя нужно сказать, что и это лекарство в тяжелых случаях может не помочь. Поэтому при особо сложных вычислениях (а по началу и в самых простых случаях) желательно проследить за стеком на каждом шаге расчетов. Для приведенной выше программки можно сделать примерно такую схемку:
Последовательно заносим числа в стек:
823 823 5503 823 5503 32 823 5503 32 17Вызываем калькулятор (RST 40):
823 5503 15 ; 32 - 17 823 366.867 ; 5503 / 15 301931.27 ; 823 ґ 366.867Из этой схемы сразу видно, что в конце расчетов на вершине стека калькулятора осталось единственное число - результат. Перед выходом в Бейсик необходимо удалить также и его. Для этого мы вызвали процедуру 11747, которая сняла полученное значение с вершины стека и напечатала его на экране. Таким образом, состояние стека осталось тем же, что до начала работы нашей программки.
Если вы не собираетесь сразу после вычислений печатать результат на экране или использовать его для вывода графики, нужно каким-то образом снять полученное значение со стека и сохранить его для будущего применения. Для этого нужно обратиться к одной из перечисленных ниже процедур, выбрав из них наиболее подходящую для каждого конкретного случая.
Подпрограмма, расположенная по адресу 11682, снимает число с вершины стека, округляет его до ближайшего целого и помещает в регистровую пару BC. Если число было положительным или нулем, то устанавливается флаг Z, в противном случае он будет сброшен. Может оказаться, что значение в стеке по абсолютной величине превышает максимально допустимое для регистровых пар (как это произошло в предыдущем примере). В этом случае на ошибку укажет флаг CY, который будет установлен в 1. Поэтому если вы не уверены в том, что результат не превысит 65535, лучше всегда проверять условие C и при его выполнении производить в программе те или иные коррекции, либо выводить на экран соответствующее сообщение.
Процедура 8980 похожа на предыдущую, но округленное значение из стека калькулятора помещается в аккумулятор. Здесь знак числа возвращается в регистр C: 1 для положительных чисел и нуля и -1 для отрицательных. Если число в стеке превысит величину байта и выйдет из диапазона -255...+255, то будет выдано сообщение Бейсика Integer out of range. Естественно, что ни о каком продолжении программы в этом случае речи быть не может, поэтому не применяйте ее, если не уверены, что результат не окажется слишком велик.
Округление чисел не всегда может оказаться удовлетворительным решением. Иногда требуется сохранить число в первозданном виде и для этого можно применить вызов процедуры ПЗУ, находящейся по адресу 11249. Она выполняет действие, обратное подпрограмме 10934 и извлекает из стека калькулятора все 5 байт числа, а затем последовательно размещает их на регистрах A, E, D, C и B. Выделив в программе на ассемблере область в 5 байт с помощью директивы DEFS 5, можно сохранить там полученный результат, чтобы впоследствии вновь им воспользоваться при расчетах.
Однако приведенные процедуры мало пригодны при работе с большим количеством пятибайтовых переменных. В этом случае лучше не обращаться за помощью к ПЗУ, а написать собственные процедуры для обмена данными между переменными и стеком калькулятора.
В процедуре укладки в стек пятибайтовой переменной не повредит предварительная проверка на предмет наличия свободной памяти. Для этого вызовем подпрограмму 13225, которая проверит, можно ли разместить на стеке 5 байт, и в случае нехватки памяти выдаст сообщение об ошибке Out of memory. Затем перенесем 5 байт переменной на вершину стека калькулятора и увеличим системную переменную STKEND, выполняющую ту же роль, что и регистр SP для машинного стека. Перед обращением к процедуре в паре HL нужно указать адрес пятибайтовой переменной.
PUTNUM CALL 13225 ;проверка наличия свободной памяти LD BC,5 ;переносим 5 байт LD DE,(23653) ;адрес вершины стека калькулятора LDIR ;переносим LD (23653),DE ;новый адрес вершины стека RETПроцедура GETNUM будет выполнять противоположное действие: перемещение пяти байт числа с вершины стека калькулятора и уменьшение указателя STKEND. Адрес переменной также будем указывать в HL. Заодно можно выполнить проверку перебора стека, так как именно эта ошибка наиболее опасна.
GETNUM PUSH HL LD DE,(23653) ;проверка достижения «дна» стека LD HL,(23651) ;системная переменная STKBOT, адресующая ; основание стека калькулятора AND A SBC HL,DE ;сравниваем значения STKEND и STKBOT JR NC,OUTDAT ;переход на сообщение, если стек ; полностью выбран POP HL LD BC,5 ADD HL,BC ;указываем на последний байт переменной DEC HL DEC DE EX DE,HL LDDR ;переносим 5 байт из стека в переменную INC HL LD (23653),HL ;обновляем указатель на вершину ; стека калькулятора RET OUTDAT RST 8 ;сообщение об ошибке DEFB 13 ; Out of DATA
Для начала выясним, что же собой представляют прерывания. Попробуем, не вдаваясь в конструкторские тонкости, объяснить принцип этого явления просто «на пальцах». Когда вы находитесь в редакторе Бейсика или 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.
Для установки нового обработчика прерываний нужно выполнить ряд действий. Перечислим их в том порядке, в котором они должны производиться:
Однако нужно учитывать, что некоторые внешние устройства могут изменять значение вектора прерывания. Кроме того, если ваш 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 ;установка прерывания IMON ......... IMOFF ......... 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
Глава 8 | Глава 10