из которой вы узнаете, как подсчитать число заработанных очков и вообще оценить состояние игры
В этой главе мы коснемся наиболее часто встречающихся типов оценки игровой ситуации, таких как подсчет очков (жизней, боеприпасов, сбитых самолетов) и контроля времени, а также рассмотрим некоторые приемы их применения на конкретных примерах игровых программ. Поскольку получение оценки немыслимо без различных математических действий, то здесь же приводятся процедуры умножения, деления, извлечения квадратного корня как для целых чисел с учетом знака, так и для дробных. В последнем случае уже не обойтись без обращения к калькулятору.
Сразу скажем, что оценка игровой ситуации не сводится к одним лишь только математическим расчетам. На самом деле не вся оценочная информация может выводиться на экран в виде чисел, а кое-что остается, так сказать, для «внутреннего пользования» самой программе, которая следит за развитием событий и соответствующим образом себя ведет. Например, в игре Tetris при завершении очередного ряда он должен автоматически удаляться, а все ряды выше «списанного» опускаться вниз. Это можно назвать уже не количественной, а качественной оценкой, используемой самой программой.
Покажем в начале, каким образом реализовать в игровой программе наиболее простые виды оценок, например, подсчет количества очков, контроль числа оставшихся жизней, количество попаданий в противника и так далее, в основе которых лежит, в общем-то, простая операция сложения чисел. До начала подсчета числа очков, следует обнулить переменную SUM, которая будет выполнять функции счетчика:
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
Как вы уже могли заметить, в игровых программах в большинстве случаев вполне можно обойтись только целыми числами. Но иногда все же приходится привлекать к расчетам и вещественные величины, особенно в блоке оценки игровой ситуации (это вы могли заметить в программе МИШЕНЬ: при расчете среднего арифметического явно требуются дробные числа). В свое время мы говорили, что для подобных вычислений можно обращаться к программе ПЗУ, выполняющей различные математические операции именно с такими числами и именуемой калькулятором. Эта программа расположена по адресу 40, что позволяет вызывать ее командой RST 40.
Работать с этой программой непросто, что объясняется, во-первых, большим количеством допустимых операций, а во-вторых, необходимостью следить за порядком обмена данными со стеком калькулятора. Поэтому мы расскажем лишь о самых необходимых в игровых программах функциях.
Необходимо знать, что параметры калькулятору передаются через его собственный стек, о котором вы уже знаете достаточно, а выполняемое действие определяется последовательностью байтов-литералов, записываемых непосредственно за командой 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.
Таблица 9.1. Значение некоторых кодов калькулятора
Литерал | Операция | Состояние стека после операции |
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.
Для установки нового обработчика прерываний нужно выполнить ряд действий. Перечислим их в том порядке, в котором они должны производиться:
- Запретить прерывания, так как есть вероятность того, что сигнал прерываний придет во время установки, а это может привести к нежелательным последствиям. Достигается это выполнением команды микропроцессора 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 ;установка прерывания
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