ПОДСЧЕТ КОЛИЧЕСТВЕННЫХ ВЕЛИЧИН
ПОДСЧЕТ КОЛИЧЕСТВЕННЫХ ВЕЛИЧИН
Покажем в начале, каким образом реализовать в игровой программе наиболее простые виды оценок, например, подсчет количества очков, контроль числа оставшихся жизней, количество попаданий в противника и так далее, в основе которых лежит, в общем-то, простая операция сложения чисел. До начала подсчета числа очков, следует обнулить переменную 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 ;печать среднего числа очков за один выстрел
; Подпрограммы MISH, CIRC и другие, а также блоки TEXT и UDG, ; на которые имеются ссылки в основной программе, описывались нами ; в первом варианте программы МИШЕНЬ
; Данные для мишени
LENTXT EQU $-TEXT ; Данные для пулевых отверстий
; Данные оценок 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
Полный набор латинских символов
Рисунок 4.3. Полный набор латинских символов
Первая программа создает полный набор, состоящий из латинских букв, цифр, знаков препинания и специальных символов (Рисунок 4.3). Наберитесь терпения и введите ее строки в компьютер. Львиную долю программы составляют блоки данных, содержащие коды всех символов. Если вы нечаянно ошибетесь при вводе хотя бы одного числа, программа в конце работы сообщит об этом и текстом и звуком. А чтобы ошибку легче было обнаружить, все символы, один за другим, выводятся на экран по мере выполнения программы. Если текст программы введен без ошибок, то через некоторое время на экране появится стандартное сообщение Start tape then press any key. Включите магнитофон на запись и нажмите любую клавишу. Коды нового фонта запишутся на ленту. Если вы работаете с дисководом, слегка измените строку 90:
90 RANDOMIZE USR 15619: REM: SAVE "latfont"CODE 64000,768
В этом случае нужно вставить дискету в карман дисковода еще до запуска программы, а полученные коды запишутся сразу по окончании обработки блока данных.
10 PAPER 5: BORDER 5: CLEAR 63999: LET s=0: LET ad=64000 20 PRINT INK 1;"Please wait"''': PLOT 0,160: DRAW INK 6;255,0: PLOT 0,116: DRAW INK 6;255,0 25 RANDOMIZE ad-256: POKE 23606,PEEK 23670: POKE 23607,PEEK 23671 30 FOR i=CODE " " TO CODE "": FOR j=1 TO 8 40 READ a: LET s=s+a: POKE ad,a: LET ad=ad+1 50 NEXT j 60 PRINT CHR$ i;: NEXT i 70 POKE 23606,0: POKE 23607,60 80 IF s<>37874 THEN PRINT AT 10,8; INK 2; FLASH 1; "Error in DATA!!!": BEEP .5,-20: STOP 90 SAVE "latfont"CODE 64000,768 1000 DATA 0,0,0,0,0,0,0,0: REM Space 1010 DATA 0,48,48,48,48,0,48,0: REM ! 1020 DATA 0,108,108,0,0,0,0,0: REM " 1030 DATA 0,54,127,54,54,127,54,0: REM # 1040 DATA 0,8,62,104,60,22,124,16: REM $ 1050 DATA 0,99,102,12,24,51,99,0: REM % 1060 DATA 0,24,44,24,58,108,58,0: REM & 1070 DATA 48,48,96,0,0,0,0,0: REM ' 1080 DATA 0,12,24,24,24,24,12,0: REM ( 1090 DATA 0,48,24,24,24,24,48,0: REM ) 1100 DATA 0,0,54,28,127,28,54,0: REM * 1110 DATA 0,0,24,24,126,24,24,0: REM + 1120 DATA 0,0,0,0,0,48,48,96: REM , 1130 DATA 0,0,0,0,126,0,0,0: REM - 1140 DATA 0,0,0,0,0,48,48,0: REM . 1150 DATA 0,3,6,12,24,48,96,0: REM / 1160 DATA 0,60,102,110,118,102,60,0: REM 0 1170 DATA 0,24,56,24,24,24,60,0: REM 1 1180 DATA 0,60,102,6,60,96,126,0: REM 2 1190 DATA 0,60,102,12,6,102,60,0: REM 3 1200 DATA 0,12,28,44,76,126,12,0: REM 4 1210 DATA 0,124,96,124,6,70,60,0: REM 5 1220 DATA 0,60,96,124,102,102,60,0: REM 6 1230 DATA 0,126,6,12,24,48,48,0: REM 7 1240 DATA 0,60,102,60,102,102,60,0: REM 8 1250 DATA 0,60,102,102,62,6,60,0: REM 9 1260 DATA 0,0,48,48,0,48,48,0: REM : 1270 DATA 0,0,48,48,0,48,48,96: REM ; 1280 DATA 0,0,12,24,48,24,12,0: REM < 1290 DATA 0,0,0,126,0,126,0,0: REM = 1300 DATA 0,0,48,24,12,24,48,0: REM > 1310 DATA 0,56,108,12,24,0,24,0: REM ? 1320 DATA 0,60,110,110,110,96,62,0: REM @ 1330 DATA 0,60,102,102,126,102,102,0: REM A 1340 DATA 0,124,102,124,102,102,124,0: REM B 1350 DATA 0,60,102,96,96,102,60,0: REM C 1360 DATA 0,124,102,102,102,102,124,0: REM D 1370 DATA 0,126,96,124,96,96,126,0: REM E 1380 DATA 0,126,96,124,96,96,96,0: REM F 1390 DATA 0,60,102,96,110,102,60,0: REM G 1400 DATA 0,102,102,126,102,102,102,0: REM H 1410 DATA 0,60,24,24,24,24,60,0: REM I 1420 DATA 0,28,12,12,12,76,56,0: REM J 1430 DATA 0,100,104,120,104,100,102,0: REM K 1440 DATA 0,96,96,96,96,98,126,0: REM L 1450 DATA 0,99,119,107,107,99,99,0: REM M 1460 DATA 0,102,102,118,110,102,102,0: REM N 1470 DATA 0,60,102,102,102,102,60,0: REM O 1480 DATA 0,124,102,102,124,96,96,0: REM P 1490 DATA 0,60,102,102,102,124,58,0: REM Q 1500 DATA 0,124,102,102,124,108,102,0: REM R 1510 DATA 0,60,96,60,6,102,60,0: REM S 1520 DATA 0,126,24,24,24,24,24,0: REM T 1530 DATA 0,102,102,102,102,102,60,0: REM U 1540 DATA 0,102,102,102,102,60,24,0: REM V 1550 DATA 0,99,99,99,107,127,34,0: REM W 1560 DATA 0,76,76,56,56,100,100,0: REM X 1570 DATA 0,102,102,60,24,24,24,0: REM Y 1580 DATA 0,126,14,28,56,112,126,0: REM Z 1590 DATA 0,28,24,24,24,24,28,0: REM [ 1600 DATA 0,96,48,24,12,6,3,0: REM \ 1610 DATA 0,56,24,24,24,24,56,0: REM ] 1620 DATA 24,60,126,24,24,24,24,0: REM
1630 DATA 0,0,0,0,0,0,0,255: REM _ 1640 DATA 0,28,50,120,48,48,126,0: REM Ј
1650 DATA 0,0,60,6,62,102,62,0: REM a 1660 DATA 0,96,96,124,102,102,124,0: REM b 1670 DATA 0,0,60,102,96,102,60,0: REM c 1680 DATA 0,6,6,62,102,102,62,0: REM d 1690 DATA 0,0,60,102,124,96,60,0: REM e 1700 DATA 0,28,48,56,48,48,48,0: REM f 1710 DATA 0,0,62,102,102,62,6,60: REM g 1720 DATA 96,96,108,118,102,102,102,0: REM h 1730 DATA 24,0,56,24,24,24,60,0: REM i 1740 DATA 12,0,14,12,12,108,44,24: REM j 1750 DATA 96,96,102,108,120,108,102,0: REM k 1760 DATA 56,24,24,24,24,24,60,0: REM l 1770 DATA 0,0,118,127,107,107,99,0: REM m 1780 DATA 0,0,108,118,102,102,102,0: REM n 1790 DATA 0,0,60,102,102,102,60,0: REM o 1800 DATA 0,0,124,102,102,124,96,96: REM p 1810 DATA 0,0,60,76,76,60,12,14: REM q 1820 DATA 0,0,92,102,96,96,96,0: REM r 1830 DATA 0,0,60,96,60,6,124,0: REM s 1840 DATA 48,48,120,48,48,54,28,0: REM t 1850 DATA 0,0,102,102,102,110,54,0: REM u 1860 DATA 0,0,102,102,102,60,24,0: REM v 1870 DATA 0,0,99,107,107,127,54,0: REM w 1880 DATA 0,0,70,44,24,52,98,0: REM x 1890 DATA 0,0,102,102,102,62,6,60: REM y 1900 DATA 0,0,126,28,56,112,126,0: REM z 1910 DATA 0,12,24,24,48,24,24,12: REM { 1920 DATA 0,24,24,24,24,24,24,0: REM | 1930 DATA 0,48,24,24,12,24,24,48: REM } 1940 DATA 108,108,36,72,0,0,0,0: REM ~ 1950 DATA 60,98,221,217,217,221,98,60: REM ©
Получить новый набор, включающий в себя русские буквы (Рисунок 4.4.), поможет следующая программа. Она отличается от предыдущей фактически только блоком данных, поэтому все вышесказанное в полной мере относится и к ней.
Полный набор русских символов
Рисунок 4.4. Полный набор русских символов
10 PAPER 5: BORDER 5: CLEAR 64767: LET s=0: LET ad=64768 20 PRINT INK 1;"Please wait"''': PLOT 0,160: DRAW INK 6;255,0: PLOT 0,116: DRAW INK 6;255,0 25 RANDOMIZE ad-256: POKE 23606,PEEK 23670: POKE 23607,PEEK 23671 30 FOR i=CODE " " TO CODE "": FOR j=1 TO 8 40 READ a: LET s=s+a: POKE ad,a: LET ad=ad+1 50 NEXT j 60 PRINT CHR$ i;: NEXT i 70 POKE 23606,0: POKE 23607,60 80 IF s<>43996 THEN PRINT AT 10,8; INK 2; FLASH 1; "Error in DATA!!!": BEEP .5,-20: STOP 90 SAVE "rusfont"CODE 64768,768 1000 DATA 0,0,0,0,0,0,0,0: REM Space 1010 DATA 0,48,48,48,48,0,48,0: REM ! 1020 DATA 0,102,102,34,68,0,0,0: REM " 1030 DATA 0,32,96,255,255,96,32,0: REM # 1040 DATA 0,4,6,255,255,6,4,0: REM $ 1050 DATA 24,60,126,24,24,24,24,24: REM % 1060 DATA 24,24,24,24,24,126,60,24: REM & 1070 DATA 0,224,96,124,102,102,124,0: REM ' 1080 DATA 0,6,12,12,12,12,6,0: REM ( 1090 DATA 0,96,48,48,48,48,96,0: REM ) 1100 DATA 0,0,54,28,127,28,54,0: REM * 1110 DATA 0,0,24,24,126,24,24,0: REM + 1120 DATA 0,0,0,0,0,48,48,96: REM , 1130 DATA 0,0,0,0,124,0,0,0: REM - 1140 DATA 0,0,0,0,0,48,48,0: REM . 1150 DATA 0,4,12,24,48,96,64,0: REM / 1160 DATA 0,60,102,110,118,102,60,0: REM 0 1170 DATA 0,24,56,24,24,24,60,0: REM 1 1180 DATA 0,60,70,6,60,96,126,0: REM 2 1190 DATA 0,60,102,12,6,102,60,0: REM 3 1200 DATA 0,12,28,44,76,126,12,0: REM 4 1210 DATA 0,124,96,124,6,70,60,0: REM 5 1220 DATA 0,60,96,124,102,102,60,0: REM 6 1230 DATA 0,126,6,12,24,48,48,0: REM 7 1240 DATA 0,60,102,60,102,102,60,0: REM 8 1250 DATA 0,60,102,102,62,6,60,0: REM 9 1260 DATA 0,0,48,48,0,48,48,0: REM : 1270 DATA 0,0,48,48,0,48,48,96: REM ; 1280 DATA 0,0,12,24,48,24,12,0: REM < 1290 DATA 0,0,0,126,0,126,0,0: REM = 1300 DATA 0,0,48,24,12,24,48,0: REM > 1310 DATA 0,56,76,12,24,0,24,0: REM ? 1320 DATA 0,102,107,123,123,107,102,0: REM @ 1330 DATA 0,60,102,102,126,102,102,0: REM A 1340 DATA 0,124,96,124,102,102,124,0: REM B 1350 DATA 0,100,100,100,100,100,126,2: REM C 1360 DATA 0,30,38,38,38,38,127,0: REM D 1370 DATA 0,126,96,124,96,96,126,0: REM E 1380 DATA 0,126,219,219,219,126,24,0: REM F 1390 DATA 0,126,98,96,96,96,96,0: REM G 1400 DATA 0,70,46,28,56,116,98,0: REM H 1410 DATA 0,102,102,110,126,118,102,0: REM I 1420 DATA 24,90,102,110,126,118,102,0: REM J 1430 DATA 0,100,104,112,120,108,102,0: REM K 1440 DATA 0,30,38,38,38,38,102,0: REM L 1450 DATA 0,99,119,107,107,99,99,0: REM M 1460 DATA 0,102,102,126,102,102,102,0: REM N 1470 DATA 0,60,102,102,102,102,60,0: REM O 1480 DATA 0,126,102,102,102,102,102,0: REM P 1490 DATA 0,62,102,102,62,102,102,0: REM Q 1500 DATA 0,124,102,102,124,96,96,0: REM R 1510 DATA 0,60,102,96,96,102,60,0: REM S 1520 DATA 0,126,24,24,24,24,24,0: REM T 1530 DATA 0,102,102,102,62,6,60,0: REM U 1540 DATA 0,153,90,126,90,153,153,0: REM V 1550 DATA 0,124,102,124,102,102,124,0: REM W 1560 DATA 0,96,96,124,102,102,124,0: REM X 1570 DATA 0,99,99,121,109,109,121,0: REM Y 1580 DATA 0,60,102,12,6,102,60,0: REM Z 1590 DATA 0,98,98,106,106,106,126,0: REM [ 1600 DATA 0,60,102,14,6,102,60,0: REM \ 1610 DATA 0,98,98,106,106,106,127,1: REM ] 1620 DATA 0,102,102,102,62,6,6,0: REM
1630 DATA 0,0,224,96,124,102,124,0: REM _ 1640 DATA 0,0,102,107,123,107,102,0: REM Ј
1650 DATA 0,0,60,6,62,102,62,0: REM a 1660 DATA 0,0,124,96,124,102,124,0: REM b 1670 DATA 0,0,100,100,100,100,126,2: REM c 1680 DATA 0,0,30,38,38,38,127,0: REM d 1690 DATA 0,0,60,102,124,96,60,0: REM e 1700 DATA 0,0,60,90,90,60,24,24: REM f 1710 DATA 0,0,124,100,96,96,96,0: REM g 1720 DATA 0,0,76,108,56,108,100,0: REM h 1730 DATA 0,0,102,102,110,118,102,0: REM i 1740 DATA 24,24,102,102,110,118,102,0: REM j 1750 DATA 0,0,102,108,120,108,102,0: REM k 1760 DATA 0,0,30,38,38,38,102,0: REM l 1770 DATA 0,0,99,119,107,107,99,0: REM m 1780 DATA 0,0,102,102,126,102,102,0: REM n 1790 DATA 0,0,60,102,102,102,60,0: REM o 1800 DATA 0,0,126,102,102,102,102,0: REM p 1810 DATA 0,0,62,102,62,102,102,0: REM q 1820 DATA 0,0,124,102,102,124,96,96: REM r 1830 DATA 0,0,60,102,96,102,60,0: REM s 1840 DATA 0,0,126,24,24,24,24,0: REM t 1850 DATA 0,0,102,102,102,62,6,60: REM u 1860 DATA 0,0,219,90,60,90,219,0: REM v 1870 DATA 0,0,124,102,124,102,124,0: REM w 1880 DATA 0,0,96,96,124,102,124,0: REM x 1890 DATA 0,0,99,99,121,109,121,0: REM y 1900 DATA 0,0,60,102,12,102,60,0: REM z 1910 DATA 0,0,98,106,106,106,126,0: REM { 1920 DATA 0,0,60,102,14,102,60,0: REM | 1930 DATA 0,0,98,106,106,106,127,1: REM } 1940 DATA 0,0,102,102,62,6,6,0: REM ~ 1950 DATA 60,98,221,217,217,221,98,60: REM ©
Теперь попробуем, используя вновь приобретенные фонты, сотворить что-нибудь полезное и продемонстрируем на практике применение новых наборов символов. Например, сделаем один из кадров заставки - страничку «Правила игры» (Рисунок 4.5).
ПОЛУЧЕНИЕ ЧИСТОГО ТОНА
ПОЛУЧЕНИЕ ЧИСТОГО ТОНА
Наиболее простой способ получения звука определенной длительности и высоты- обратиться к подпрограмме ПЗУ, ответственной за выполнение оператора Бейсика BEEP. Мы уже упоминали о ней в шестой главе, но тем не менее напомним, что располагается она по адресу 949 и требует определения регистровых пар HL и DE. Например:
LD DE,440 LD HL,964 CALL 949 RET
Таким способом можно получить звук практически любой высоты и продолжительности - ограничения Бейсика здесь отсутствуют. Однако при этом отсутствуют и удобства, предоставляемые интерпретатором. Чтобы написать даже очень коротенькую музыкальную фразу, придется немало попотеть, рассчитывая значения задаваемых параметров. А рассчитываются они так. В регистровую пару DE заносится число, определяемое как fґt, где f - частота, измеряемая в герцах, а t - время в секундах (при извлечении звука ЛЯ первой октавы, который имеет частоту 440 Гц, длительностью в 1 секунду получится 440ґ1=440). Пара HL на входе должна содержать число, равное 437500/f-30.125 (если выполнить указанные вычисления, то получим величину 964). Таким образом, приведенная выше программа делает то же самое, что и оператор Бейсика
BEEP 1,9
Чтобы упростить задачу, можно предложить небольшую программку на Бейсике, которая, конечно, не может претендовать на роль музыкального редактора, но, по крайней мере, автоматизирует расчеты требуемых значений. После ее запуска на экране появится некоторое подобие меню. Нажав цифровую клавишу 1, вы сможете прослушать свое произведение. При нажатии клавиши 2 на экран выводится два столбика чисел: значения из левого столбика предназначены для загрузки пары DE, а для HL числа берутся из правого столбика. Клавиша 0 позволяет выйти в редактор Бейсика. После нажатия клавиш 1 или 2 нужно ввести желаемый темп исполнения по метроному (количество четвертных нот, исполняемых в минуту).
Ноты записываются в операторе DATA, начиная со строки 1000, парами чисел, первое из которых определяет высоту и задается так же, как и в операторе BEEP (0 - нота ДО первой октавы), а второе представляет собой относительную длительность звуков, то есть четверти, например, записываются дробью 1/4, восьмушки - 1/8 и так далее. Для обозначения конца блока данных в самом его конце нужно написать два нуля (строка 8990).
Ниже приводится текст программы, в котором для примера уже включены несколько строк данных короткой музыкальной фразы (строки 1010...1030). Наберите и сохраните программу без этих строк, а затем, дописав их, можете проверить ее работу.
10 DIM f(12): RESTORE 9000 20 FOR n=1 TO 12: READ f(n): NEXT n 50 CLS : PRINT "1. Listen"'"2. Code"'"0. Stop" 60 PAUSE 0: IF INKEY$="1" THEN CLS : GO TO 100 70 IF INKEY$="2" THEN CLS : GO TO 200 80 IF INKEY$="0" THEN STOP 90 GO TO 60 100 REM Прослушивание 110 RESTORE 1000: INPUT "TEMPO: (M.M.) = ";temp 120 READ n,d: IF d THEN BEEP d/temp*240,n: GO TO 120 130 GO TO 50 200 REM Расчет значений для DE и HL. 210 RESTORE 1000: INPUT "TEMPO: (M.M.) = ";temp 220 READ n,d: IF NOT d THEN GO TO 300 230 LET d=d/temp*240 240 LET f1=INT (n/12): LET f2=n-f1*12 250 LET f=f(f2+1)/2(4-f1) 260 PRINT "LD DE,";INT (f*d+.5),"LD HL,";INT ((437500/f-30.125)+.5) 270 GO TO 220 300 PRINT #0;"Press any key": PAUSE 0: GO TO 50 1000 REM Данные мелодии. 1010 DATA 7,1/16,5,1/16,4,1/16,2,1/16 1020 DATA 4,1/8,7,1/8 1030 DATA -5,1/8,-1,1/8,0,1/4 8990 DATA 0,0 9000 REM Частота в герцах для звуков одной октавы 9010 DATA 4186.01,4434.92,4698.64,4978.03 9020 DATA 5274.04,5587.65,5919.91,6271.93 9030 DATA 6644.87,7040,7458.62,7902.13
Получив тем или иным способом ряд чисел для загрузки регистров, нужно каким-то образом обратить их в мелодию. Для этих целей напишем процедуру, последовательно считывающую из блока данных, адресованного парой HL, двухбайтовые значения регистровых пар и извлекающую соответствующие звуки. Подпрограмма завершит свою работу либо при достижении конца блока данных (два байта 0) либо при нажатии на любую клавишу:
MELODY XOR A ;опрос клавиатуры IN A,(254) CPL AND #1F JR NZ,MELODY ;пока все клавиши не отпущены MELOD1 XOR A ;опрос клавиатуры IN A,(254) CPL AND #1F RET NZ ;выход, если нажата любая клавиша LD E,(HL) ;считывание в пару DE INC HL LD D,(HL) LD A,D OR E RET Z ;выход, если конец блока данных (DE=0) INC HL LD A,(HL) ;считывание данных для пары HL INC HL PUSH HL LD H,(HL) ;загрузка пары HL LD L,A CALL 949 ;вывод звука POP HL INC HL JR MELODY ;следующая нота
Прежде чем обратиться к данной процедуре выпишем числа, полученные с нашим импровизированным «редактором» при заданном темпе, равном 150, в виде блока двухбайтовых данных, который завершим числом 0 (тоже двухбайтовым):
DATMEL DEFW 39,1086,35,1223,33,1297 DEFW 29,1460,66,1297,78,1086 DEFW 39,2202,49,1742,105,1642 DEFW 0 ;маркер конца блока данных
Теперь можно вызвать процедуру MELODY, например, таким образом:
ORG 60000 ENT $ LD HL,DATMEL CALL MELODY RET
Существует и другой способ извлечения звуков, при котором все необходимые расчеты выполняются в самой программе. Хотя он и несколько сложнее только что описанного, но может показаться вам более привлекательным. Во всяком случае он более привычен, так как исходными данными при этом являются те же числа, что и параметры оператора BEEP. Подпрограмма, расположенная по адресу 1016, сама выполняет приведенные выше вычисления и обращается к процедуре вывода звука 949, а значения длительности и высоты звука передаются ей через стек калькулятора:
LD A,1 ;загружаем в аккумулятор значение ; длительности звучания (1 секунда). CALL 11560 ;содержимое аккумулятора заносим ; в стек калькулятора. LD A,12 ;нота ДО второй октавы CALL 11560 ;посылаем в стек калькулятора CALL 1016 ;вызываем процедуру извлечения звука RET
Этот метод хорош всем, за исключением одной «мелочи» - числа, записываемые в аккумулятор могут быть только целыми и не отрицательными. Чтобы исправить этот недостаток, можно условиться, что длительности будут задаваться не в секундах, а в сотых долях секунды, а значение высоты звука будем интерпретировать как число со знаком. Такой подход позволит получать продолжительность звучания нот примерно до двух с половиной секунд, что в большинстве случаев вполне достаточно; диапазон звуков останется таким же, как и в Бейсике: от -60 до +68. Параметры для этой подпрограммы будем задавать в регистрах B (длительность в сотых долях секунды) и C (высота в полутонах):
BEPER PUSH BC LD A,B ;берем в аккумулятор первый параметр CALL 11560 ;заносим его в стек калькулятора LD A,100 CALL 11560 ;помещаем в стек число 100 RST 40 DEFB 5,56 ;выполняем деление POP BC LD A,C ;берем второй параметр AND A JP M,BEPER1 ;если отрицательный, переходим на BEPER1 CALL 11560 ; иначе помещаем в стек без изменений JP 1016 ; и извлекаем звук BEPER1 NEG ;получаем абсолютное значение CALL 11560 ;отправляем в стек RST 40 DEFB 27,56 ;меняем знак JP 1016 ; и извлекаем звук
Взяв за основу эту подпрограмму, можно написать процедуру, аналогичную MELODY, которая бы проигрывала произвольный музыкальный фрагмент, считывая параметры из заранее подготовленного блока данных. Приведем такой пример:
ORG 60000 ENT $ LD HL,D_MEL1 CALL MELBEP RET MELBEP LD B,(HL) ;в B - длительность INC B ;один из способов проверки DEC B ; содержимого регистра на 0 RET Z INC HL LD C,(HL) ;в C - высота звука INC HL PUSH HL CALL BEPER ;извлечение звука POP HL JR MELBEP
D_MEL1 DEFB 10,-5,10,0,10,1,10,2 DEFB 20,5,10,2,20,5,10,7 DEFB 0
Во всех приведенных выше фрагментах в конечном итоге использовалась подпрограмма ПЗУ 949. В этом случае, пока звучит очередная нота, компьютер оказывается полностью выключен из работы. Поэтому иногда бывает очень важно уметь получать чистый тон на низком уровне, непосредственно программируя порт динамика. Принцип получения звука таким способом предельно прост: достаточно с определенной частотой попеременно то включать, то выключать динамик. С таким способом в некоторой степени вы также уже знакомы, поэтому отметим лишь некоторые важные моменты, без знания которых невозможно получить качественный звук. Во-первых, напомним, что динамик управляется четвертым битом 254-го порта. При установке или сбросе этого бита слышны короткие щелчки, которые при быстром чередовании сливаются в сплошной звук. Но кроме динамика этот же порт отвечает и за цвет бордюра, поэтому кроме четвертого бита нужно правильно устанавливать и три младших разряда выводимого байта. Не менее важно при извлечении звука помнить о необходимости запрета прерываний. Если этого не сделать, то невозможно будет получить чистый тон. Это связано с тем, что 50 раз в секунду микропроцессор будет отвлекаться на выполнение подпрограммы обработки прерываний, что неминуемо скажется на частоте создаваемого звука.
Теперь перейдем к делу и создадим программку, извлекающую различные звуки при нажатии цифровых клавиш. При этом звучание будет продолжаться до тех пор, пока клавиша не будет отпущена.
ORG 60000 ENT $ DI LD A,(23624) ;получаем в аккумуляторе цвет бордюра AND #38 ;выделяем биты 3, 4 и 5 RRA ;сдвигаем на место битов 0, 1 и 2 RRA RRA LD E,A ;запоминаем в регистре E ORGAN1 CALL 8020 ;проверка нажатия клавиши BREAK JR NC,EXIT ;если нажата, выход из программы PUSH DE LD DE,#100 ;счетчики для определения ; нажатых клавиш LD A,#F7 ;опрос полуряда 1...5 CALL KEYS LD A,#EF ;опрос полуряда 6...0 CALL KEYS LD D,0 ;выбор высоты звука из таблицы LD HL,DATNOT ADD HL,DE POP DE LD B,(HL) INC B DEC B JR Z,ORGAN1 ;если в B ноль, клавиши не нажаты, звука нет LD A,E OUT (254),A ;извлечение звука XOR 16 LD E,A ORGAN2 LD C,20 ;цикл задержки для получения звука ; определенной высоты ORGAN3 DEC C JR NZ,ORGAN3 DJNZ ORGAN2 JR ORGAN1 EXIT EI ;выход из программы RET KEYS IN A,(254) ;опрос выбранного полуряда LD B,5 ;5 клавиш в полуряду KEYS1 RRCA ;сдвигаем биты вправо JR C,KEYS2 ;если младший бит установлен, ; клавиша отпущена LD E,D ;иначе запоминаем номер нажатой клавиши KEYS2 INC D ;увеличиваем номер определяемой клавиши DJNZ KEYS1 ;переходим к следующей RET ; Данные для получения необходимой задержки для каждого звука DATNOT DEFB 0,55,49,44,41,36 DEFB 21,24,27,29,32
Построение буквы «Д»
Рисунок 6.5. Построение буквы «Д»
10, 20 - в этих строках порция данных переписывается из строки 1160 в переменные, после чего они принимают значения: y=0, x=112, hgt=10, len=4, ink=6 и dx=-1.
30 - центровка названия.
40 - устанавливается цвет линий (желтый).
50 - начало цикла рисования фрагмента буквы.
60, 70 - рисуются подряд две горизонтальные линии (длиной по len=4пикселя).
80 - координаты y и x изменяются, после чего в этом же цикле выводится следующая пара линий, и так - 10 раз (hgt), в результате получится левый элемент буквы (Рисунок 6.5, б).
90 - после того, как hgt пар линий будет напечатано, переход на начало.
На следующем этапе построения буквы «Д» прочитывается вторая порция данных из строки 1170: y=0, x=112, hgt=10, len=10, ink=6, dx=1 и рисуется правая часть буквы (Рисунок 6.5, в), каждая линия которой составляет 10 пикселей. Наконец, на основании третьей порции (строка 1180): y=40, x=100, hgt=2, len=34, ink=6 и dx=0 ставятся две последние линии по 34 пикселя (Рисунок 6.5, г), завершающие построение буквы «Д».
После того как мы выяснили способ формирования каждой буквы средствами Бейсика, перейдем непосредственно к ассемблерной программе строка за строкой. Но прежде чем приступить к описанию текста, следует заметить, что к этому моменту мы наговорили вам довольно много всякого, поэтому небольшие по размерам программы нет смысла разбивать на отдельные части и подробно распространяться о каждой из них, как мы это делали в четвертой и пятой главах. Думается, что сейчас будет уже вполне достаточно прокомментировать отдельные команды и только в случаях, представляющих особый интерес, рассмотреть более детально некоторые группы команд.
ORG 60000 LD HL,DATA ; Считывание параметров из блока данных CICL1 LD B,(HL) ;Y-координата начала линии INC B RET Z ;выход, если это конец блока данных LD A,112 ;отсчитывать будем не снизу, а сверху. ; Кроме того, сразу добавляем смещение ; от верхнего края в 64 пикселя и ; увеличиваем на 1, поскольку выполнялась ; команда INC B (175-64+1=112) SUB B LD B,A INC HL LD A,(HL) ;X-координата начала линии ADD A,40 ;центрируем надпись LD C,A INC HL LD D,(HL) ;высота рисуемой части буквы INC HL LD E,(HL) ; и ее ширина INC HL LD A,16 ;управляющий код для INK RST 16 LD A,(HL) ;код цвета RST 16 INC HL LD A,(HL) ;наклон рисуемой части INC HL PUSH HL ;сохраняем текущий адрес в блоке данных ; Цикл рисования части буквы, описанной очередной порцией данных CICL2 PUSH AF ;сохраняем в стеке аккумулятор CALL DRAW ;чертим первую линию DEC B ;вторая линия будет на 1 пиксель ниже CALL DRAW ;чертим вторую линию DEC B ;переходим ниже DEC B ;пропускаем еще два ряда пикселей DEC B POP AF ;восстанавливаем в аккумуляторе ; параметр наклона PUSH AF ADD A,C ;смещаем X-координату для LD C,A ; получения наклона POP AF DEC D ;повторяем заданное параметром JR NZ,CICL2 ; «высота» количество раз POP HL ;восстанавливаем адрес в блоке данных JR CICL1 ;переходим на начало ; Подпрограмма рисования одной горизонтальной линии DRAW PUSH BC ;сохраняем нужные регистры PUSH DE ; в машинном стеке CALL 8933 ;ставим точку с координатами ; из регистров B и C POP DE ;регистр E нам понадобится, ; поэтому восстанавливаем PUSH DE ; и снова сохраняем EXX PUSH HL ;сохраняем регистровую пару HL' EXX LD C,E ;длина линии (горизонтальное смещение) LD B,0 ;вертикальное смещение - 0 (линия ; идет горизонтально) LD DE,#101 ;положительное значение ; для обоих смещений CALL 9402 ;рисуем линию EXX POP HL ;восстанавливаем регистры из стека EXX POP DE POP BC RET
; Блоки данных для рисования букв
; буква «Н» DATA DEFB 0,0,12,6,2,0 DEFB 20,6,2,14,2,0 DEFB 0,20,12,10,2,0 ; буква «А» DEFB 0,46,12,4,1,-1 DEFB 0,46,12,10,1,1 DEFB 24,42,2,16,1,0 ; буква «Р» DEFB 0,72,12,10,3,0 DEFB 0,82,2,13,3,0 DEFB 24,82,2,13,3,0 DEFB 4,96,1,2,3,0 DEFB 24,96,1,2,3,0 DEFB 8,92,4,8,3,0 ; буква «Д» DEFB 0,112,10,4,6,-1 DEFB 0,112,10,10,6,1 DEFB 40,100,2,34,6,0 ; буква «Ы» DEFB 0,139,12,10,4,0 DEFB 16,150,2,13,4,0 DEFB 40,150,2,13,4,0 DEFB 20,163,1,2,4,0 DEFB 40,163,1,2,4,0 DEFB 24,160,4,8,4,0 DEFB 0,167,4,8,4,0 DEFB 16,169,1,6,4,0 DEFB 20,171,6,4,4,0 DEFB 44,169,1,6,4,0 DEFB -1 ;указатель конца блока данных
Не решите нечаянно, что таким методом допускается рисовать исключительно буквы. В заставке и, безусловно, в других частях программы ему найдется немало областей применения, например, для создания всевозможных орнаментов, статических рисунков, а если ее слегка дополнить, то возможно даже создавать элементы игрового пространства, скажем, лабиринты.
ПОСТРОЕНИЕ ПЕЙЗАЖЕЙ
ПОСТРОЕНИЕ ПЕЙЗАЖЕЙ
Деление изображения на пейзаж и спрайты, с точки зрения его формирования на экране, вообще-то является достаточно условным, поскольку все процедуры, используемые для рисования спрайтов, применимы и при создании пейзажей. Однако если рассматривать этот вопрос с игровой точки зрения, то здесь уже можно увидеть существенные различия, которые позволяют выделить создание пейзажа как самостоятельную задачу. Прежде всего, он в большинстве игр занимает всю площадь экрана, а спрайты, хотя и бывают довольно большими, но все же ограничены в размерах. Другое важное отличие- пейзаж, как правило, неподвижен или, если и изменяется, то незначительно.
Рассмотрим несколько приемов формирования пейзажа и проиллюстрируем их небольшими программами и рисунками. Начать нужно, по-видимому, с самого простого - с голубого неба (синей глади воды, сплошного ковра зеленой травы, желтого песка пустыни и т. д.). С этой целью достаточно в самом начале программы задать:
LD A,40 ;INK 0, PAPER 5 LD (23693),A CALL 3435
и требуемый фон готов. Несколько сложнее обстоят дела с космическим пространством. То есть, задать черный цвет «бумаги» никакой проблемы не составит, но вот со звездами не все так просто. Можно, конечно, нарисовать и закодировать маленькие спрайты и даже заставить их случайным образом вспыхивать (особенно хорошо выглядит задание повышенной яркости в фазе максимума). Но ведь каждый спрайт-звездочка - это целое знакоместо, поэтому много ли их можно разместить на экране. Значительно лучше смотрятся звездочки размером в пиксель, как, например, в игре ELITE, но тут уже требуется небольшая программка, которую мы предлагаем вам написать самостоятельно.
Простые пейзажи можно сформировать из отдельных спрайтов, либо фрагментов, составленных из повторяющихся спрайтов. Здесь «трава» содержит восемь спрайтов, каждый из которых занимает четыре знакоместа, «куст» - из трех спрайтов, а «человечек» - это отдельный спрайт из шести знакомест. Ниже приводится соответствующая этому пейзажу программа:
ORG 60000 ENT $ LD A,5 LD (23693),A XOR A CALL 8859 CALL 3435 ; Вывод восьми спрайтов «трава» LD B,8 ;задаем количество выводимых ; спрайтов «трава» LD HL,DATA1 ;читаем адрес блока координат LD DE,TRAW CALL POSIT ; Вывод трех спрайтов «куст» LD B,3 LD HL,DATA2 LD DE,KUST CALL POSIT ; Вывод спрайта «человечек» LD B,1 LD HL,DATA3 LD DE,MAN ; Подпрограмма начальной установки регистров для процедуры PTBL POSIT PUSH BC LD B,(HL) ;устанавливаем Y INC HL LD C,(HL) ;устанавливаем X INC HL PUSH HL PUSH DE EX DE,HL LD A,SPRPUT ;вывод с наложением CALL PTBL POP DE POP HL POP BC DJNZ POSIT RET
; Задание пар координат (Y,X) для вывода восьми спрайтов «трава» DATA1 DEFB 17,0,17,4,17,8,17,12,17,16 DEFB 17,20,17,24,17,28 ; Заголовок спрайта «трава» TRAW DEFB 4, 0,0,4, 0,1,4, 0,2,4, 0,3,4 ; Данные для спрайта «трава» DEFB 193,106,191,253,255,235,181,36 DEFB 18,170,247,255,255,94,107,41 DEFB 101,200,255,219,255,255,87,10 DEFB 18,170,247,255,255,95,107,41 ; Задание пар координат для печати трех спрайтов «куст» DATA2 DEFB 11,14,13,14,15,14 ; Заголовок спрайта «куст» KUST DEFB 4, 0,0,6, 0,1,4, 1,0,6, 1,1,4 ; Данные для спрайта «куст» DEFB 33,12,102,242,185,13,53,121 DEFB 56,100,192,218,160,134,157,56 DEFB 29,204,110,28,54,182,87,91 DEFB 50,112,119,238,236,234,90,84 ; Пара координат для вывода «человечка» DATA3 DEFB 14,23 ; Заголовок спрайта «человечек» MAN DEFB 6, 0,0,5, 0,1,5, 1,0,5 DEFB 1,1,5, 2,0,5, 2,1,5 ; Данные для спрайта «человечек» DEFB 3,15,17,36,48,16,26,31 DEFB 192,240,248,56,132,60,120,240 DEFB 25,14,7,24,31,59,55,55 DEFB 240,192,184,124,244,246,250,194 DEFB 56,27,3,0,6,6,7,0 DEFB 26,104,160,144,48,36,184,0
Задание в виде отдельных блоков данных (DATA1...DATA3) пар координат, первая из которых соответствует вертикальной позиции спрайта, а вторая - горизонтальной, позволяет легко «разбрасывать» спрайты по всему экрану, создавая любые их комбинации. Вывод спрайтов осуществляется процедурой PTBL.
Пейзажи в игровых программах довольно часто формируются из многократно повторяющихся фрагментов, причем последние могут занимать как одно знакоместо, так и состоять из нескольких. Взять, к примеру, всевозможные лабиринты, разрезы зданий и других сооружений, карты боевых действий и так далее, всего просто не перечесть. Составим программу, которая формирует картинки именно такого типа, причем для простоты будем считать, что все различающиеся фрагменты имеют размеры одного знакоместа. В этом случае для их быстрого вывода на экран можно воспользоваться способом, аналогичным тому, который использовался в рассмотренной раньше процедуре PRSYM.
Предположим, что мы хотим получить изображение, которое представляет собой внешнюю часть лабиринта к создаваемой игре. Соответствующая этой картинке программа выглядит так:
ORG 60000 LD IX,LAB_0 ;перед вызовом в IX заносится адрес ; данных лабиринта LABS1 LD C,(IX+1) ;позиция начального элемента по горизонтали LD B,(IX+2) ;позиция начального элемента по вертикали LD A,(IX+3) ;количество повторений и направление AND 31 JR Z,LABS5 ;если выводится одиночный элемент LD E,A LABS2 LD A,(IX) ;код символа (0...5) CALL PRINT ;вывод символа на экран BIT 7,(IX+3) ;проверка направления вывода JR NZ,LABS3 INC C ;слева направо JR LABS4 LABS3 INC B ;сверху вниз LABS4 DEC E ;следующий элемент JR NZ,LABS2 JR LABS6 LABS5 LD A,(IX) ;вывод одиночного элемента CALL PRINT LABS6 LD DE,4 ;увеличиваем адрес в блоке данных ADD IX,DE ; на 4 байта LD A,(IX) ;проверка на достижение конца блока данных INC A ;если -1 (255) JR NZ,LABS1 RET PRINT PUSH BC PUSH DE PUSH HL LD L,A ;по коду определяем адрес символа LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL LD DE,D_SYMB ADD HL,DE PUSH HL LD A,B ;вычисляем адрес видеобуфера CALL 3742 LD A,L ADD A,C LD L,A POP DE LD B,8 PUSH HL PRINT1 LD A,(DE) ;переносим на экран 8 байт LD (HL),A INC DE INC H DJNZ PRINT1 POP HL ;восстанавливаем начальный адрес экрана LD A,H ;рассчитываем адрес атрибутов ; соответствующего знакоместа AND #18 RRCA RRCA RRCA ADD A,#58 LD H,A LD A,(23695) ;записываем байт атрибутов в видеобуфер LD (HL),A POP HL POP DE POP BC RET ; Данные для построения лабиринта (рамки) ; IX+0 - номер символа в таблице D_SPR (0..5) ; IX+1 - начальная позиция экрана по горизонтали ; IX+2 - начальная позиция экрана по вертикали ; IX+3 - младшие 5 битов - количество повторений вывода символа, ; 7-й бит определяет направление вывода: ; установлен - сверху вниз (задается символами @#80), ; сброшен - слева направо LAB_0 DEFB 0,2,8,0, 1,3,8,2 DEFB 2,5,8,0, 3,5,9,0 DEFB 5,5,10,0, 1,6,10,20 DEFB 3,2,9,2@#80, 5,2,11,0 DEFB 1,3,11,0, 2,4,11,0 DEFB 3,4,12,8@#80, 4,4,20,0 DEFB 1,3,20,0, 0,2,20,0 DEFB 3,2,21,2@#80, 5,2,23,0 DEFB 1,3,23,2, 4,5,23,0 DEFB 3,5,22,0, 0,5,21,0 DEFB 1,6,21,20, 4,26,10,0 DEFB 3,26,9,0, 0,26,8,0 DEFB 1,27,8,2, 2,29,8,0 DEFB 3,29,9,2@#80, 4,29,11,0 DEFB 1,28,11,0, 0,27,11,0 DEFB 2,26,21,0, 3,26,22,0 DEFB 5,26,23,0, 1,27,23,2 DEFB 4,29,23,0, 3,29,21,2@#80 DEFB 2,29,20,0, 1,28,20,0 DEFB 5,27,20,0, 3,27,12,8@#80 DEFB 0,3,9,0, 2,4,9,0 DEFB 4,4,10,0, 5,3,10,0 DEFB 0,27,9,0, 2,28,9,0 DEFB 4,28,10,0, 5,27,10,0 DEFB 0,27,21,0, 2,28,21,0 DEFB 4,28,22,0, 5,27,22,0 DEFB 0,3,21,0, 2,4,21,0 DEFB 4,4,22,0, 5,3,22,0 DEFB -1 ;конец данных ; Данные символов (элементов лабиринта) D_SYMB DEFB 127,213,159,191,253,182,248,181 DEFB 255,85,255,255,85,170,0,255 DEFB 254,83,249,245,121,181,121,181 DEFB 249,181,249,181,249,181,249,181 DEFB 249,117,249,245,81,169,3,254 DEFB 249,189,255,191,213,170,192,127
Используя эту программу без всяких переделок и изменив только ее блоки данных, можно с успехом рисовать различные рамки для оформления кадров меню или окон, предназначенных для вывода всевозможной оценочной информации, правил игры и т. д.
Прежде чем перейти к следующему разделу, поясним встретившуюся в блоке данных LAB_0 не очень понятную запись @#80. В комментарии было сказано, что это означает установку 7-го бита в числе. Дело в том, что ассемблер GENS помимо простых арифметических операций сложения, вычитания, умножения и деления предоставляет возможность использования в выражениях поразрядных операций OR, XOR и AND. Обозначаются они соответственно символами @, ! и &. Поэтому, например, выражение 2@#80 (2 OR #80) примет значение #82 или 130, а запись %101!%110 (%101 XOR %110) после вычислений заменится числом %011 или 3.
Правила игры
Рисунок 4.5. Правила игры
После ввода исходного текста программы следует выйти в Бейсик и подгрузить в память только что полученные кодовые файлы:
LOAD "latfont"CODE 64000 LOAD "rusfont"CODE 64768
Затем оттранслируйте набранный текст и введите команду редактора R для исполнения машинного кода.
Вот текст программы «Правила игры»:
ORG 60000 ENT $ LATF EQU 64000-256 RUSF EQU LATF+768 LD A,5 LD (23693),A LD A,0 CALL 8859 CALL 3435 LD A,2 CALL 5633 ; Печать «отрывной части блокнота» LD DE,TXT LD BC,TXT1-TXT CALL 8252 ; Печать названия игры «MOON SHIP» LD HL,LATF LD (23606),HL LD DE,TXT1 LD BC,TXT2-TXT1 CALL 8252 ; Печать русского текста LD HL,RUSF LD (23606),HL LD DE,TXT2 LD BC,TXT3-TXT2 CALL 8252 ; Печать слова «Enter» LD HL,LATF LD (23606),HL LD DE,TXT3 LD BC,TXT4-TXT3 CALL 8252 ; Печать русского текста LD HL,RUSF LD (23606),HL LD DE,TXT4 LD BC,TXT5-TXT4 CALL 8252 ; Печать слова «Space» LD HL,LATF LD (23606),HL LD DE,TXT5 LD BC,TXT6-TXT5 CALL 8252 ; Печать русского текста LD HL,RUSF LD (23606),HL LD DE,TXT6 LD BC,END-TXT6 CALL 8252 ; Рисование «листка блокнота» EXX ;сохранение HL' PUSH HL LD A,5 LD (23695),A ; Левая вертикальная линия LD DE,#101 ;DRAW 169,0 LD BC,#A900 CALL 9402 ; Верхняя горизонтальная линия LD DE,#101 ;DRAW 0,255 LD BC,255 CALL 9402 ; Правая вертикальная линия LD DE,#FF01 ;DRAW -169,0 LD BC,#A900 CALL 9402 ; Нижняя горизонтальная линия LD DE,#1FF ;DRAW 0,-255 LD BC,255 CALL 9402 POP HL ;восстановление HL' EXX ; Восстановление стандартного шрифта LD HL,15360 LD (23606),HL RET
; Данные для печати «отрывной части блокнота» TXT DEFB 22,1,0,16,5 DEFB 32,131,32,131,32,131,32,131 DEFB 32,131,32,131,32,131,32,131 DEFB 32,131,32,131,32,131,32,131 DEFB 32,131,32,131,32,131,32,131
; Данные текста правил игры TXT1 DEFB 22,2,8,16,4 DEFM "M O O N··S H I P" DEFB 22,3,7,16,6 DEFM "------------------" TXT2 DEFB 22,4,10,16,7 DEFM "Prawila igry" DEFB 22,6,2,16,2,"1",".",16,5 DEFM " Dlq··uprawleniq···korablem" DEFB 22,7,1 DEFM "ispolxzujte··klawi{i " DEFB 16,7 DEFM "# $ % " DEFB 16,5,"i" DEFB 16,7 DEFM " &" DEFB 22,9,2,16,2,"2",".",16,5 DEFM " Dlq ustanowki dozy gorЈ~e-" DEFB 22,10,1 DEFM "go naberite ~islo ot 10 do 200" DEFB 22,11,1 DEFM "ili 0 i··navmite klawi{u " TXT3 DEFB 16,3 DEFM "Enter" TXT4 DEFB 22,13,2,16,2,"3",".",16,5 DEFM " Wremennaq ostanowka -" TXT5 DEFB 16,3 DEFM "Space" TXT6 DEFB 22,15,2,16,2,"4",".",16,5 DEFM " Blestq}aq posadka dostiga-" DEFB 22,16,1 DEFM "etsq, esli··na··wysote, rawnoj" DEFB 22,17,1 DEFM "nulЈ, skorostx··lunnogo modulq" DEFB 22,18,1 DEFM "takve budet rawna nulЈ" DEFB 22,20,7,16,1 DEFM "Prodolvatx ? (" DEFB 16,2,18,1,"D" DEFB 16,1,18,0,"/" DEFB 16,2,20,1,18,1,"N" DEFB 16,1,20,0,18,0,")" END
Правила игры
Рисунок 6.6. Правила игры
Перед запуском ассемблерной программы следует с адреса 64768 загрузить набор русских букв, однако при желании вы можете использовать и аналогичные латинские, выполнив некоторые изменения в программе. Отом, как это сделать, вполне достаточно сказано в четвертой главе.
ORG 60000 ENT $ ; Задание адресов новых фонтов и постоянных атрибутов экрана LATF EQU 64000-256 RUSF EQU LATF+768 LD A,6 LD (23693),A LD A,0 CALL 8859 CALL 3435 LD A,2 CALL 5633 ; Ввод символа UDG для рисования фактуры LD HL,UDG LD (23675),HL ; Заполнение всего экрана фактурой CALL DESK ; Начало основной программы LD HL,RUSF LD (23606),HL NEW LD HL,TEXT CYCLE1 CALL PRINT LD B,10 CYCLE2 PUSH HL PUSH BC CALL SCR_UP CALL PAUSE POP BC POP HL JR Z,EXIT DJNZ CYCLE2 LD A,(HL) AND A JR NZ,CYCLE1 LD B,80 ;расстояние между текстами CYCLE3 PUSH BC CALL SCR_UP CALL PAUSE POP BC JR Z,EXIT DJNZ CYCLE3 JR NEW ; Восстановление стандартного набора символов и выход EXIT LD HL,15360 LD (23606),HL RET ; Для того, чтобы строки выводимого текста можно было успеть прочитать, ; в цикл вставлена подпрограмма задержки, а нажав клавишу Space, ; можно в любой момент завершить работу программы. PAUSE LD BC,6 CALL 7997 LD A,(23560) CP " " RET ; Подпрограмма печати строки текста PRINT PUSH BC LD DE,PAR LD BC,5 CALL 8252 LD B,24 ;число символов в строке PR_CI LD A,(HL) RST 16 INC HL DJNZ PR_CI POP BC RET PAR DEFB 22,15,4,16,0 ; Подпрограмма заполнения экрана фактурой DESK LD BC,704 LD A,#14 RST 16 LD A,1 RST 16 DESK1 LD A,144 RST 16 DEC BC LD A,B OR C JR NZ,DESK1 LD A,#14 RST 16 LD A,0 RST 16 ; Создание в фактуре окна для вывода текста CALL CLSV CALL SETV RET ; Подпрограммы
; Данные для окна с текстом COL1 DEFB 4 ROW1 DEFB 4 LEN1 DEFB 24 HGT1 DEFB 12 ATTR DEFB %01000010 ; Данные для окна в фактуре COL DEFB 3 ROW DEFB 3 LEN DEFB 26 HGT DEFB 13 ; Данные для выводимого в окно текста TEXT DEFM "····P·I·R·A·M·I·D·A·····" DEFM "························" DEFM "Celx igry sostoit w tom," DEFM "~toby··perwym··postawitx" DEFM "swoi··fi{ki··na··wer{inu" DEFM "piramidy. Dlq |togo nuv-" DEFM "no ispolxzowatx gorizon-" DEFM "talxnye··i··wertikalxnye" DEFM "peredwiveniq fi{ek,krome" DEFM "togo,velatelxno wospolx-" DEFM "zowatxsq··dopolnitelxny-" DEFM "mi prodwiveniqmi,···sutx" DEFM "kotoryh··sostoit··w tom," DEFM "~toby sostawitx fi{ki··w" DEFM "wide treugolxnika, posle" DEFM "~ego sdelatx hod werhnej" ; При желании текст можно продолжить DEFB 0 ; Данные для фактуры вокруг окна UDG DEFB 248,116,34,71,143,7,34,113
Два вида горизонтального скроллинга продемонстрируем на примере еще одной программы, где эти процедуры действуют одновременно. Сразу после запуска программа создает на экране фрагмент средневекового замка из красного кирпича с зубчатыми стенами и бойницами, а в средней его части - ажурные ворота. До тех пор, пока никакая клавиша не нажата, изображение неподвижно. После нажатия любой из них створки ворот начинают медленно расходиться в разные стороны (Рисунок 6.7), создавая совершенно бесподобный эффект.
Несколько слов о самой программе. Как вы заметили, здесь впервые встретилась директива ассемблера EQU (Equal - равный), которая служит для определения констант. Эта директива обязательно должна располагаться следом за какой-нибудь меткой, и именно этой метке в результате будет присвоено значение выражения после EQU. Таким образом, метка LATF в нашей программе становится численно равной адресу размещения латинского шрифта, уменьшенного на 256, то есть 63744, а метка RUSF принимает значение 64512.
Директиву EQU особенно удобно использовать в тех случаях, когда какая-либо постоянная величина многократно встречается в тексте программы. Если во время отладки ее потребуется изменить, то не нужно будет редактировать множество строк, достаточно будет лишь скорректировать выражение после EQU.
Есть одна особенность применения констант. Если вы используете в выражении ссылки на какие-нибудь другие имена, например
ENDTXT EQU TEXT+LENTXT
то все они (в данном случае TEXT, и LENTXT) должны быть определены в программе до строки EQU, иначе ассемблер не сможет вычислить значение выражения и выдаст сообщение об ошибке.
Использование констант LATF и RUSF в программе «Правила игры» позволяет легко изменить при желании адреса загрузки шрифтов, а «включение» того или иного набора в тексте программы становится более наглядным. Эти «переключения» выполняют строки
LD HL,LATF LD (23606),HL
или
LD HL,RUSF LD (23606),HL
Здесь число 23606 - адрес системной переменной CHARS, которая указывает местоположение в памяти текущего символьного набора. Напомним, что для «включения» нового фонта необходимо прежде уменьшить его адрес на 256, а затем записать два байта полученного числа в ячейки 23606 и 23607 (младший байт, как всегда, на первом месте).
Возможно, вам не совсем понятно, зачем нужно уменьшать адрес загрузки шрифта на 256 перед занесением его в системную переменную CHARS, поэтому поясним, отчего так происходит. Назвав новые наборы символов полными, мы слегка погрешили против истины, так как на самом деле полный набор должен включать все коды от 0 до 255. Мы же пользуемся только «печатными» символами, имеющими коды от 32 до 127 включительно, то есть первые 32 символа оказываются как бы «выброшенными». Каждый символ занимает в памяти 8 байт, поэтому «реальный» адрес размещения фонта и будет равен адресу загрузки нового набора минус 32ґ8=256 байт. (Вообще говоря, в недрах подпрограммы печати символов из ПЗУ скрывается операция, вновь увеличивающая значение адреса на 256, но этот «тонкий» ход остается на совести разработчиков интерпретатора Бейсика для Speccy Примеч. ред.)
ПРЕИМУЩЕСТВА И НЕДОСТАТКИ ЯЗЫКА АССЕМБЛЕРА
ПРЕИМУЩЕСТВА И НЕДОСТАТКИ ЯЗЫКА АССЕМБЛЕРА
Бейсик, Паскаль, Си и подобные им языки относятся к так называемым языкам высокого уровня. Это означает, что операторы, составляющие их словарный запас, по сути своей являются целыми программами, написанными, как правило, на языке низкого уровня- на изучаемом нами ассемблере, а по сути, в машинных кодах, которыми «говорит» компьютер. Что касается ассемблера, то он дает возможность писать программы на среднем или низком уровне (точнее, почти на самом низком, как мы уже говорили). Разница между средним и низким уровнем достаточно условная и заключается в том, что первый в полную силу использует возможности операционной системы, обращаясь к готовым подпрограммам, «зашитым» в ПЗУ, а второй - нет. Выбор уровня зависит от целей программиста, у каждого из них есть свои преимущества и недостатки, а весь этот разговор мы затеяли лишь затем, чтобы помочь вам сориентироваться в таком выборе.
Итак, что же мы теряем и что приобретаем с переходом к более низким уровням программирования?
Преимущества:
значительное увеличение скорости выполнения программ;
большая гибкость (отсутствуют рамки Бейсика, независимость от операционной системы, более оптимально используются возможности компьютера);
полученные программы занимают меньше памяти.
Недостатки:
программы требуют больше времени и внимательности при написании;
сложность отладки (отсутствуют привычные сообщения об ошибках, текст трудно читать);
трудно выполнять арифметические действия (микропроцессор не может обрабатывать дробные числа, да и применение целых чисел имеет ряд ограничений).
Чтобы понять, почему программы, написанные на ассемблере, обычно работают во много раз быстрее, давайте посмотрим, какими методами пользуются интерпретаторы (например, тот же Бейсик) и компиляторы.
Метод, используемый интерпретаторами можно сравнить с переводом со словарем. Микропроцессор последовательно считывает текст программы слово за словом, оператор за оператором, затем лезет в специальную таблицу, содержащую имена команд и адреса подпрограмм, выполняющих заданное действие. И только после того, как весь оператор прочитан до конца, начинается его исполнение. Не увеличивает скорость перевода еще и то, что у компьютера весьма «короткая память», и надо за каждым словом вновь и вновь лезть в словарь, даже если это слово только что встречалось.
Несколько быстрее работают компиляторы. Полученные с их помощью программы можно сравнить с подстрочником, составленным довольно неумелым переводчиком, поэтому микропроцессору над каждой фразой приходится еще поломать голову, что же хотел сказать этим автор. (Если быть более точным, компилятор каждую фразу исходного языка заменяет кусочком машинного кода, а то, как эффективно он это делает, зависит от авторов данного компилятора - Примеч. ред.) Кроме того, большинство компиляторов имеет дурную привычку «навешивать» на программу воз и маленькую тележку совершенно никому не нужного хлама, что при 48K максимальной свободной памяти кажется, мягко говоря, несколько расточительным.
Что же касается машинных кодов, то это родной язык компьютера, и совершенно естественно, что программу на таком языке микропроцессор может выполнить в самые кратчайшие сроки - ведь в этом случае не приходится прибегать к услугам переводчиков. Безусловно, и тут при желании можно «загнуть» такую заумную фразу, которая надолго оставит компьютер в недоумении, но это уже будет на совести автора программы.
Конечно, умение писать на ассемблере не означает полный отказ от Бейсика и других языков высокого уровня, особенно на первых порах. Поэтому мы ставим цель прежде всего научить вас создавать коротенькие фрагменты, позволяющие значительно обогатить игры и придать им динамичность. Большинство предлагаемых в этой книге подпрограмм построено по принципу широкоизвестного набора процедур в машинных кодах под названием Supercode. Причем некоторые из предлагаемых примеров будут работать в «тандеме» с программами на Бейсике.
Пример статической заставки
Рисунок 4.1. Пример статической заставки
Предположим, что первая заставка выглядит, как показано на Рисунок 4.1. Как видно, на экране должны появляться не только тексты, но и достаточно сложные графические изображения, составляющие рамку. В Бейсике для получения такого рисунка мы воспользовались бы определяемыми пользователем символами UDG, закодировав в них отдельные элементы целого изображения. Но и в ассемблере можно поступить так же. На Рисунок 4.2 показаны четыре элемента, из которых состоят углы рамки, а справа от каждого квадратика расставлены десятичные значения восьми байтов изображенного символа. Таким же образом следует нарисовать и все остальные элементы, а затем рассчитать коды каждого байта в каждом из них.
Прямые линии
Продвинемся еще на один шаг вперед и научимся проводить прямые линии. Для этого потребуется сперва указать начальные координаты, поставив точку, от которой протянется линия, а затем задать величины смещения по горизонтали и вертикали до конечной точки линии. В Бейсике это должно выглядеть примерно так:
PLOT 120,80: DRAW 35,-60
Оператор DRAW реализует подпрограмма ПЗУ, находящаяся по адресу 9402. Перед обращением к ней в регистры C и B необходимо последовательно занести значения параметров, взятые по абсолютной величине, то есть в C в нашем примере помещается число 35, а в B нужно загрузить не -60, а 60. Но чтобы не потерять знаки, их следует разместить на регистрах E и D. Это значит, что в регистр E заносится единица, а в D - минус единица (или, что то же самое, 255). Таким образом, приведенная выше строка Бейсика на ассемблере запишется так:
LD BC,#5078 ;C = 120 (#78), B = 80 (#50) CALL 8933 LD BC,#3C23 ;C = 35 (#23), B = 60 (#3C) LD DE,#FF01 ;E = 1 (#01), D = -1 = 255 (#FF) CALL 9402 RET
Вроде бы все просто, однако здесь вы можете столкнуться с одной серьезной проблемой. Если запустить эту программку не из ассемблера (использовав директиву ENT и команду редактора R), а из Бейсика с помощью функции USR, то вы заметите, что компьютер ведет себя довольно странно. В лучшем случае появится какое-нибудь сообщение об ошибке, а в худшем - компьютер «зависнет» или «сбросится». А происходит это оттого, что при выполнении подпрограммы 9402 теряется некоторая информация, необходимая для нормального завершения функции USR. Значит, нам нужно выяснить, что это за информация и где она находится, чтобы можно было сохранить ее на входе и восстановить на выходе из нашей программы.
До сих пор мы говорили только о семи регистрах общего назначения, но на самом деле микропроцессор Z80 имеет их гораздо больше. Постепенно мы изучим их все, а сейчас скажем еще несколько слов об уже известных вам регистрах. Дело в том, что имеется не один, а два набора регистров данных, но активным в каждый момент времени может быть только какой-то один из них, то есть одновременно вы все равно можете работать только с семью регистрами общего назначения.
Регистры второго, или, как говорят, альтернативного набора абсолютно ничем не отличаются от регистров первого набора. Они имеют те же имена (для отличия альтернативных регистров от активных в данный момент времени после имени пары, включающей в себя этот регистр, ставят символ апострофа, например, DE') и выполняют те же функции, поэтому нет никакой возможности определить, какой из двух наборов активен в данный момент - об этом должен позаботиться программист.
В любой момент вы можете переключиться на альтернативные регистры, использовав команду EXX. Выполнив эту же команду повторно, вы вернете прежние значения регистров, ничего не потеряв. Потому данная команда, как правило, в программах встречается парами, подобно PUSH и POP.
Применяя команду EXX, нужно также помнить, что она переключает на альтернативный набор не все семь регистров, а только 6: BC, DE и HL. Для переключения аккумулятора существует другая команда. Не вдаваясь пока в смысл символики, скажем, что записывается она так:
EX AF,AF'
Добавим еще к сказанному, что мнемоника EX и EXX происходит от английского слова exchange - обменивать.
Вернемся снова к функции USR. В простых и небольших по объему программах семи регистров общего назначения, как правило, вполне хватает (во всяком случае, мы вам советуем не слишком злоупотреблять командой EXX, лучше при необходимости для временного хранения информации пользоваться стеком). Но в таких больших и сложных программах, как операционная система ZX Spectrum, иногда возникает необходимость привлекать и альтернативный набор регистров. Так функция USR перед вызовом программы в машинных кодах заносит важную информацию в регистровую пару HL', поэтому для нормального выхода в Бейсик ее необходимо сохранять всегда, когда она может измениться. В частности, при использовании подпрограммы рисования линий 9402.
Перепишем предыдущий пример таким образом, чтобы его можно было вызвать из Бейсика:
ORG 60000 EXX ;в начале программы меняем ; на альтернативный набор PUSH HL ;сохраняем регистровую пару HL LD BC,#5078 CALL 8933 LD BC,#3C23 LD DE,#FF01 CALL 9402 POP HL ;восстанавливаем значение HL EXX ;делаем его альтернативным RET
Поскольку перед вызовом подпрограммы в машинных кодах функция USR загружает регистр HL' всегда одним и тем же значением, а именно, числом 10072, то можно не сохранять его в стеке, а просто записать перед выходом в Бейсик:
LD HL,10072 EXX RET
При желании вы можете проверить содержимое пары HL', оттранслировав такую программку:
ORG 60000 EXX ;меняем на альтернативный набор PUSH HL ;запоминаем в стеке значение HL' EXX ;возвращаем «стандартные» регистры POP BC ;забираем число из стека в пару BC ; для передачи в Бейсик RET ;возврат в Бейсик
и затем выполнив ее строкой
PRINT USR 60000
в результате чего на экране должно появиться число 10072.
Программа БИТВА С НЛО
Рисунок 10.1. Программа БИТВА С НЛО
Вначале создадим акустическое сопровождение программы. Нам понадобятся такие звуки: выстрелы с обеих сторон, попадания в НЛО и в лазерную установку, а также соударение НЛО со «стенкой», ограничивающей игровое поле. Учитывая характер звуков, а также и то, что шум можно выводить только в один из каналов, составим блоки данных по правилам, описанным выше.
Начнем с выстрелов управляемой игроком лазерной установки. Для получения этого звука используем «белый» шум. Этот и следующий звуки закрепим за каналом музыкального сопроцессора A.
D_SND1 DEFB 1 DEFW FREQ1 DEFB 2 DEFW ENV1 DEFB 3,8,2,3,8,1,0 FREQ1 DEFB 129,0,130,5,131 ;изменение частоты ENV1 DEFB 15,14,12,128 ;изменение громкости
Для создания эффекта попадания в автомобиль с лазерной установкой также будем выводить шум, но для большей убедительности в самом начале звука дадим короткий сигнал низкого тона:
D_SND2 DEFB 1 DEFW FREQ2 DEFB 2 DEFW ENV2 DEFB 3,1,2,1 DEFW FREQ3 DEFB 3,8,20,0 FREQ2 DEFB 128 DEFW 1000 DEFB 130,10,131 FREQ3 DEFB 129,3,130,1,131 ENV2 DEFB 15,14,15,12,15,14,12,10,14,12 DEFB 9,8,7,6,5,4,3,2,11,0,128
Звуки, сопровождающие выстрелы «летающей тарелки» и попадания в нее будем выводить в канал B. Попробуем сымитировать эти звучания изменением чистого тона. Блок данных для формирования частоты и огибающей «выстрела» может выглядеть примерно так:
D_SND3 DEFB 1 DEFW FREQ4 DEFB 2 DEFW ENV3 DEFB 3,1,15,0 FREQ4 DEFB 128 DEFW 200 DEFB 130,20,131 ENV3 DEFB 15,14,14,13,12,12,11,128
Звук, подражающий попаданию в НЛО, должен быть более протяжным, поэтому и блок данных, описывающий огибающую окажется несколько длиннее:
D_SND4 DEFB 1 DEFW FREQ5 DEFB 2 DEFW ENV4 DEFB 3,1,24,0 FREQ5 DEFB 128 DEFW 700 DEFB 130,100,131 ENV4 DEFB 15,14,14,15,15,12,10,11,8,7 DEFB 7,5,6,7,10,12,14,15,10,8,128
Все предыдущие звуки были достаточно глухими, поэтому удар НЛО о «стенку» сделаем напоминающим хрустальный звон. Если вам это покажется уж слишком нелогичным, попытайтесь создать более подходящее звучание самостоятельно. Этот эффект будет выводиться в канал C:
D_SND5 DEFB 1 DEFW FREQ6 DEFB 2 DEFW ENV5 DEFB 3,1,9, 0 FREQ6 DEFB 128 DEFW 20 DEFB 130,10,-15,40,-30,-5,131 ENV5 DEFB 15,9,12,10,15,13,128
В игре нам понадобится создать изображения грузовика и НЛО. Общую часть программы мы собираемся написать на Бейсике, поэтому проще всего для вывода графических изображений вновь обратиться к символам UDG. Можно было бы закодировать их и в бейсик-программе, но, во-первых, как вы знаете, циклы в интерпретаторе - самое больное место и выполняются они в десятки, если не в сотни раз медленнее, чем в машинных кодах. Но самое главное даже не в этом, а в том, что область памяти, в которой располагаются коды определяемых символов, занята командами перехода на процедуру обработки прерываний (адреса 65524...65526 и 65535, соответствующие положению последних двух символов - T и U). Поэтому, чтобы застраховаться от неожиданностей, коды символов лучше расположить в ассемблерной части программы, что мы и делаем:
SETUDG LD HL,UDG LD (23675),HL RET ; НЛО (A, B, C и D) UDG DEFB 1,65,32,23,13,27,59,55 DEFB 128,130,4,232,240,248,252,252 DEFB 127,125,0,173,173,0,7,1 DEFB 254,190,0,181,181,0,224,128 ; Лазерная установка (E, F, G, H, I и J) DEFB 173,97,191,127,191,127,192,158 DEFB 199,207,201,201,207,207,207,79 DEFB 252,254,35,33,225,225,255,255 DEFB 63,97,204,146,173,37,18,12 DEFB 1,126,255,126,126,0,0,0 DEFB 254,134,51,73,181,148,72,48
Теперь остается собрать все подпрограммы в один блок так, чтобы их удобно было вызывать из Бейсика. Кроме того, допишем недостающие процедуры инициализации звуков:
ORG 60000 ; 60000 - включение 2-го режима прерываний и инициализация массива DATREG JP INITI ; 60003 - выключение 2-го режима прерываний и звука JP STOPI ; 60006 - выстрел лазерной установки JP SND1 ; 60009 - попадание в лазерную установку JP SND2 ; 60012 - выстрел НЛО JP SND3 ; 60015 - попадание в НЛО JP SND4 ; 60018 - соударение НЛО со «стенкой» JP SND5 ; 60021 - символы UDG
SND5 LD HL,D_SND5 LD (CHAN_C),HL LD (CHAN_C+2),HL LD A,1 LD (CHAN_C+9),A LD (CHAN_C+8),A LD (CHAN_C+7),A RET SND4 LD HL,D_SND4 JR SND3_1 SND3 LD HL,D_SND3 SND3_1 LD (CHAN_B),HL LD (CHAN_B+2),HL LD A,1 LD (CHAN_B+9),A LD (CHAN_B+8),A LD (CHAN_B+7),A RET SND2 LD HL,D_SND2 LD A,1 JR SND1_1 SND1 LD HL,D_SND1 LD A,3 SND1_1 LD (CHAN_A),HL LD (CHAN_A+2),HL LD (CHAN_A+9),A LD A,1 LD (CHAN_A+8),A LD (CHAN_A+7),A RET ; Блоки данных, описывающие каждый из пяти используемых в программе звуков
; Далее следуют уже описанные процедуры прерывания
А вот бейсик-программа самой игры:
10 REM *** НЛО *** 20 BORDER 0: PAPER 0: INK 7: CLEAR 59999 30 RANDOMIZE USR 15619 : REM : LOAD "snd_ufo"CODE 40 LET cd=USR 60021: REM *** UDG *** 50 LET cd=USR 60000: REM *** Прерывания *** 60 REM -------------- 70 CLS 80 FOR n=19 TO 20: PRINT AT n,0; PAPER 4; TAB 31;" ": NEXT n 90 FOR n=0 TO 18: PRINT AT n,0; PAPER 4;" ";AT n,31;" ": NEXT n 100 LET kl=0: LET ik1=7: LET ik2=ik1: LET pow=0: LET pow1=pow: LET x1=4: LET y1=1: LET w=0: LET xp=0: LET yp=0: LET s1=10: LET s=10: LET x=4: LET y=1 110 LET a$=INKEY$ 120 IF a$="p" AND s<28 THEN LET s=s+1: GO TO 180 130 IF a$="o" AND s>1 THEN LET s=s-1: GO TO 190 140 IF a$=" " THEN LET cd=USR 60006: GO TO 230 150 IF a$="e" THEN LET cd=USR 60003: STOP 160 REM -------------- 170 GO TO 200 180 PRINT AT 17,s1;" ";AT 18,s1;" ": GO TO 200 190 PRINT AT 17,s1+2;" ";AT 18,s1+2;" " 200 INK 6: PRINT AT 17,s;"EFG";AT 18,s;"HIJ" 210 LET s1=s 220 LET ik2=7: GO TO 280 230 IF INT x=s OR INT x+1=s THEN LET yy=INT y+1: LET pow1=pow1+1: LET cd=USR 60015: LET ik2=2: GO TO 250 240 LET yy=0 250 GO SUB 270: OVER 1: GO SUB 270: OVER 0 260 GO TO 280 270 INK 7: PLOT s*8+5,41: DRAW 0,126-yy*8: RETURN 280 REM ----- нло ---- 290 IF w=0 THEN LET xp= (RND*1.5+.5): LET yp= (RND*1.5): LET w=INT (RND*25+5): LET xp=xp*SGN (RND*4-2): LET yp=yp*SGN (RND*4-2) 300 IF x+xp>=1 AND x+xp<30 AND y+yp>=0 AND y+yp<=12 AND w>0 THEN LET x=x+xp: LET y=y+yp: LET w=w-1: LET kl=0: GO TO 330 310 IF ((INT (x+xp)<1 AND INT x=1) OR (INT x=29 AND INT (x+xp)>29)) AND kl=0 THEN LET kl=1: LET cd=USR 60018 320 LET w=0: GO TO 290 330 PRINT AT INT y1,INT x1;"··";AT INT y1+1,INT x1;"··" 340 INK 5: PRINT AT INT y,INT x;"AB";AT INT y+1,INT x;"CD" 350 LET x1=x: LET y1=y 360 IF RND*10<3 THEN LET cd=USR 60012: LET xf=RND*255: GO SUB 410: OVER 1: GO SUB 410: OVER 0: LET xf1=INT (xf/8): IF xf1=s OR xf1=s+1 OR xf1=s+2 THEN LET pow=pow+1: LET cd=USR 60009: LET ik1=2: GO TO 380 370 LET ik1=7 380 PRINT AT 21,1; INK ik2;"Score car:";INT pow1;" " 390 PRINT AT 21,19; INK ik1;"Score ufo:";INT pow;" " 400 GO TO 110 410 INK 2: PLOT INT (x)*8+7,159-INT (y)*8: DRAW xf-(INT (x)*8+7),25-(159-INT (y)*8): RETURN
Прокомментируем немного строки этой программы и поясним значение некоторых переменных.
40 - «включение» символов UDG;
50 - включение 2-го режима прерываний;
80...90 - рисование грунта;
100 - задание начальных значений переменных:
kl - управление звуком при подлете НЛО к стенке (kl=0 - звук разрешен, kl=1 - запрещен);
ik1, ik2 - цвет надписей;
pow - количество попаданий НЛО;
pow1 - количество попаданий автомобиля;
x1, y1 - координаты для удаления предыдущего изображения НЛО;
w - количество перемещений НЛО до изменения направления движения;
xp, yp - приращения координат НЛО;
s1 - координата для удаления предыдущего изображения автомобиля;
s - текущая координата автомобиля;
x, y - текущие координаты НЛО.
110...150 - опрос клавиатуры;
180...200 - восстановление фона позади движущегося автомобиля и перемещение его в новое положение;
230 - проверка попадания луча лазера в НЛО;
250 - рисование луча лазера;
290...400 - управление полетом НЛО;
410 - рисование следа от выстрела НЛО.
Программа МИШЕНЬ
Рисунок 6.8. Программа МИШЕНЬ
ORG 60000 ENT $ ; Задание постоянных атрибутов экрана LD A,7 LD (23693),A XOR A CALL 8859 CALL 3435 LD A,2 CALL 5633 ; Ввод символов UDG - три «пулевые отверстия» LD HL,UDG LD (23675),HL
; Основная часть программы CALL MISH ;рисование мишени MAIN CALL WAIT ;ожидание нажатия любой клавиши CP " " RET Z LD A,22 RST 16 LD E,20 ;задание диапазона для координаты Y CALL RND RST 16 LD E,30 ;задание диапазона для координаты X CALL RND RST 16 LD A,16 RST 16 LD A,6 RST 16 LD E,3 ;задание номера «пулевого отверстия» CALL RND ADD A,144 ;вычисление кода спрайта RST 16 CALL SND ;звуковой сигнал JR MAIN
; Подпрограмма вывода на экран мишени MISH LD C,20 CALL CIRC LD C,40 CALL CIRC LD C,60 CALL CIRC LD C,80 CALL CIRC LD DE,TEXT LD BC,LENTXT JP 8252
; Подпрограмма рисования окружностей CIRC EXX PUSH HL EXX PUSH BC LD A,120 CALL 11560 LD A,90 CALL 11560 POP BC LD B,0 CALL 11563 CALL 9005 EXX POP HL EXX RET
; Подпрограмма остановки счета WAIT XOR A LD (23560),A WAIT1 LD A,(23560) AND A JR Z,WAIT1 RET ; Подпрограммы
SND LD B,80 LD HL,150 LD DE,1
; Данные для мишени TEXT DEFB 22,10,14 DEFM "10" DEFB 22,10,18 DEFM "8" DEFB 22,10,21 DEFM "6" DEFB 22,10,23 DEFM "4" DEFB 22,10,11 DEFM "8" DEFB 22,10,8 DEFM "6" DEFB 22,10,6 DEFM "4" LENTXT EQU $-TEXT ; Данные для «пулевых отверстий» UDG DEFB 4,20,62,60,127,60,40,8 DEFB 9,95,252,63,126,44,8,8 DEFB 16,48,244,63,28,56,28,8
ПРОСМОТР ТЕКСТА
ПРОСМОТР ТЕКСТА
После ввода достаточно больших программ часто возникает необходимость просмотра полученного текста. Команда L позволяет вывести на экран листинг программы с любой строки по любую. Если команда введена без параметров, листинг будет выводиться с самой первой строки и до конца порциями по 15 строк. После вывода очередной порции будет ожидаться нажатие любой клавиши кроме Edit, которая прервет вывод и вернет управление редактору. Общий вид команды такой:
L[начальная строка[,конечная строка]]
Квадратные скобки указывают на необязательность параметров.
Если хотите, можете воспользоваться командой L и еще раз взглянуть на только что набранный текст.
ПРОСТЕЙШАЯ ПРОГРАММА В МАШИННЫХ КОДАХ
ПРОСТЕЙШАЯ ПРОГРАММА В МАШИННЫХ КОДАХ
Как мы уже говорили, ассемблер невысоко поднялся над машинным языком, поэтому, прежде чем бросаться с головой в программирование, будет полезно составить хотя бы несколько коротеньких программок чисто в машинных кодах. Это позволит гораздо лучше прочувствовать идеологию ассемблера и понять, как работает микропроцессор.
Большинство программ на машинном языке, имеющих возврат в Бейсик, заканчивается кодом 201 (в мнемоническом обозначении - RET), который аналогичен оператору RETURN. Поэтому простейшая программа может состоять всего из одного байта. Давайте сейчас создадим такую программу, а потом и выполним ее. Введите с клавиатуры оператор
POKE 40000,201
а затем, чтобы проверить действие полученной «программы», запустите ее с помощью функции USR, например, так:
RANDOMIZE USR 40000
Вроде бы ничего особенного не произошло - никаких видимых или слышимых эффектов. Но, по крайней мере, ваш компьютер выдержал подобное испытание и при этом не «завис» и не «сбросился» (если он исправен, конечно, на что мы надеемся). Программа нормально завершила свою работу и благополучно вышла в Бейсик с сообщением 0 OK.
Теперь попробуем запустить ее несколько иным способом. Заменим оператор RANDOMIZE на PRINT:
PRINT USR 40000
То, что вы увидели на экране - весьма существенно и может очень пригодиться в будущем. Компьютер напечатал то же самое число, которое мы использовали в качестве аргумента функции USR. Можете проверить, что это не случайное совпадение, введя строку
POKE 40001,201: PRINT USR 40001
Можете попробовать проделать то же самое и с другими адресами, только не слишком увлекайтесь, чтобы не залезть в «запрещенные» области памяти. Для подобных экспериментов лучше не выходить из диапазона адресов от 30000 до 60000, да и то лишь в том случае, если память компьютера свободна от каких-либо других программ.
Для того, чтобы каким-то образом использовать полученный результат, необходимо понять причину такого странного поведения компьютера. Ответ на эту загадку заключается в том, что USR - это функция, а любая функция, по своему определению, должна получать что-то на входе и возвращать нечто на выходе. Поэтому остается лишь выяснить сущность этих «что-то» и «нечто» - и вопрос можно считать решенным.
Как вы увидите позже, большинство процедур в машинных кодах, если это необходимо, получают входные параметры и возвращают значения на регистрах. Как правило, это оказывается наиболее удобно. Поскольку функция USR предназначена для вызова машинных процедур, то и она может обмениваться числовыми данными с программой на Бейсике также через регистры, а именно - через регистровую пару BC. В качестве входного параметра используется аргумент функции, а на выходе значение пары BC передается бейсик-программе. А так как в приведенных выше примерах никакие регистры не изменялись, то и на экране появлялось то же самое число, которое использовалось в качестве аргумента.
Теперь модернизируем нашу программку так, чтобы содержимое регистровой пары BC изменялось. Можно, например, просто записать в нее какое-нибудь число. Обычно запись в регистры числовых значений называют загрузкой, поэтому в мнемоническом обозначении такие команды начинаются с LD (сокращение от известного вам по Бейсику слова LOAD - загрузить). А выражение «загрузить регистровую пару BC значением 1000» записывается как
LD BC,1000
Эта команда всегда состоит из трех байт: первый равен 1, а второй и третий соответствуют двухбайтовому представлению числа в памяти. Таким образом, программа из двух команд
LD BC,1000 RET
в памяти будет представлена последовательностью кодов
1, 232, 3 и 201
Введите их последовательно, начиная, например, с адреса 60000 и выполните закодированную программку оператором
PRINT USR 60000
Если вы ничего не напутали, то на экране должно появиться число 1000.
Надо думать, на этих примерах вы уже почувствовали «прелесть» программирования в машинных кодах и догадались, что подобным методом может пользоваться только сумасшедший или неукротимый фанатик. Однако и фанатик в конце концов понимает, что лучше все же воспользоваться ассемблером, благо фирма HISOFT подарила синклеристам весьма недурную реализацию этого языка, по многим параметрам могущую считаться вполне профессиональной. (Лучшей реализацией языка ассемблера для компьютеров семейства ZX Spectrum считается транслятор фирмы OCEAN Software из пакета Laser Genius, однако ассемблер фирмы HISOFT остается непревзойденным по минимальному объему занимаемой самим транслятором памяти и, соответственно, максимальному размеру области, отводимой для создаваемого им кода программы. Как и Бейсик 48, этот ассемблер использует несколько усеченный строчный текстовый редактор. Это, конечно, немного хуже, чем экранный редактор (как, например, в Бейсике 128), но за все приходится чем-то расплачиваться - Примеч. ред.)
Простые циклы
Рассмотрим сначала наиболее простой случай- получить заданное количество повторений некоторого фрагмента программы. Для этих целей в наборе команд микропроцессора имеется специальная инструкция DJNZ, которую можно расшифровать как «уменьшить и перейти, если не ноль». В этой команде уменьшается и проверяется на равенство нулю регистр B, который можно рассматривать в качестве счетчика количества циклов, а переход осуществляется по адресу, определенному меткой, указанной в команде DJNZ.
Проиллюстрируем цикл с использованием команды DJNZ на примере программки, печатающей на экране ряд из 32-х звездочек:
ORG 60000 ENT $ CALL 3435 ;Подготовка экрана к печати LD A,2 CALL 5633 LD B,32 ;В регистре B - количество повторений LOOP LD A,"*" ;Печать символа «*» RST 16 DJNZ LOOP ;Уменьшение регистра B на 1 и если ; B не равно 0, переход на метку LOOP RET
Такой способ организации циклов наиболее прост и удобен, но у него есть два существенных ограничения. Во-первых, как вы понимаете, количество повторений здесь не может превышать максимального значения для регистров, то есть 256 (если изначально в B записан 0), а во-вторых, между адресом начала цикла и командой DJNZ может быть расстояние не более 126 байт. Со вторым ограничением можно справиться, например, если тело цикла оформить как подпрограмму, тогда между меткой начала цикла и командой DJNZ будет находиться единственная инструкция CALL.
Еще раз напоминаем вам о необходимости сохранять регистры, которые могут быть изменены. В данном случае нужно позаботиться о сохранении регистра B, иначе цикл может никогда не закончиться, то есть произойдет «зависание» компьютера. В приведенном выше примере мы не сделали этого только потому, что команда RST 16 все регистры данных, за исключением аккумулятора, оставляет без изменений, но это не всегда справедливо при использовании других процедур ПЗУ. Если вы не совсем уверены в том, что какая-то подпрограмма сохраняет нужные регистры, лучше на всякий случай перестраховаться. Таким образом, в общем виде цикл может выглядеть так:
LD B,N ;Указываем количество повторений МЕТКА PUSH BC ;Сохраняем счетчик цикла ......... ;Тело цикла, которое может быть выделено ; в отдельную подпрограмму, и тогда ; здесь будет располагаться единственная ; инструкция CALL POP BC ;Восстановление счетчика DJNZ МЕТКА ;Уменьшение счетчика и если не конец, ; переход на начало цикла
Проверка условий. Флаги
Иногда бывает удобнее задавать счетчик цикла не в регистреB, а в каком-то другом. Это немногим сложнее описанного способа, но тут нам понадобятся, по меньшей мере, еще две новые команды: одна для уменьшения значения выбранного регистра и другая, похожая на бейсиковский оператор IF...THEN, для проверки условия достижения конца цикла.
Для изменения содержимого регистров на единицу имеется два типа команд: INC (Increase) - увеличение и DEC (Decrease) - уменьшение. Попутно заметим, что эти же команды применимы не только к отдельным регистрам, но и к регистровым парам. Например, для уменьшения на 1 значения аккумулятора нужно написать команду
DEC A
а для других регистров или регистровых пар, как вы догадываетесь, достаточно вместо A написать другое имя. Команды увеличения INC пишутся аналогичным образом.
Несколько сложнее дела обстоят с проверкой условий. Это еще один момент, имеющий со средствами Бейсика лишь очень отдаленное сходство. Чтобы объяснить, как микропроцессор реагирует на то или иное условие, прежде всего нужно ввести еще одно новое понятие - флаги.
Строго говоря, с принципом флагов вы уже встречались и в Бейсике. Вспомните, например, команду POKE 23658,8, которая включает режим ввода прописных букв. Флаги применяются в тех случаях, когда нужно передать какое-то сообщение из одной части программы в другую. Установив бит 3 (число 8) системной переменной FLAGS2, мы посылаем драйверу клавиатуры сообщение о том, что все вводимые буквы нужно переводить в верхний регистр.
Вернемся теперь к флагам микропроцессора. Здесь они имеют то же назначение и передают программе сообщения о выполнении или невыполнении различных условий. Как вы знаете, компьютер - машина бескомпромиссная и приемлет лишь два ответа: либо да, либо нет, а все остальное от лукавого. Поэтому для сообщения о принятии того или иного решения достаточно одного-единственного бита: если бит установлен в 1, то решение положительное, если он сброшен в 0, то это означает, что условие не выполнено. Другое дело, что самих проверяемых условий может быть несколько, и для каждого из них зарезервирован свой бит специального регистра, который так и называется: флаговый, или регистр F. Кстати, это и есть тот самый загадочный регистр, который образует пару совместно с аккумулятором.
Сначала перечислим все возможные условия, проверяемые микропроцессором, а затем кратко поясним каждое из них в отдельности. Вот они:
ноль (флаг Z);
перенос (флаг CY);
четность/переполнение (флаг P/V);
отрицательный результат, знак (флаг S);
вычитание (флаг N);
вспомогательный перенос (флаг H).
Всего, как видите, можно проверить 6 условий, и в регистре флагов F задействовано, соответственно, 6 битов, а два остались без применения. На Рисунок 5.1 показано, какой из битов за какое условие ответственен. Крестиками обозначены неиспользуемые биты.
ПЗУ и ОЗУ
Вся память компьютера делится на две основные области: постоянное запоминающее устройство (ПЗУ) и оперативная память (ОЗУ). ПЗУ начинается с адреса 0 и содержит коды операционной системы Бейсик. Вэтой области памяти ничего нельзя изменить, все, что там записано, сохраняется и при выключенном питании компьютера. Тем не менее, в ПЗУ имеется множество полезных подпрограмм, которыми мы в дальнейшем будем пользоваться с большим успехом, кроме того, мы часто будем обращаться к кодам знакогенератора, расположенным в самых последних ячейках ПЗУ, начиная с адреса 15616, и представляющим собой полный набор символов, печатаемых на экране. Простирается постоянная память вплоть до адреса 16384, с которого начинается область ОЗУ (Рисунок 2.1).
65535 | ||
ОЗУ | Определяемые пользователем символы | |
UDG (23675) | ||
Вершина машинного стека | ||
RAMTOP (23730) | ||
Машинный стек | ||
Свободная память | ||
STKEND (23653) | ||
Рабочие области Бейсика | ||
STKBOT (23651) | ||
Стек калькулятора | ||
WORKSP (23649) | ||
Область редактирования строк бейсик-программ | ||
ELINE (23641) | ||
Переменные Бейсика | ||
VARS (23627) | ||
Текст бейсик-программы | ||
PROG (23635) | ||
Канальная информация | ||
CHANS (23631) | ||
Системные переменные | ||
23552 | ||
Буфер принтера | ||
23296 | ||
Видеобуфер | ||
16384 | ||
ПЗУ | Знакогенератор | |
15616 | ||
Операционная система Бейсик | ||
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 |
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
Раскрытие ворот замка скроллингом окон
Рисунок 6.7. Раскрытие ворот замка скроллингом окон
А теперь перейдем непосредственно к программе.
ORG 60000 ENT $ ; Подготовка экрана LD A,7 LD (23693),A XOR A CALL 8859 CALL 3435 LD A,2 CALL 5633 ; Назначение нового адреса символов UDG LD HL,UDG LD (23675),HL ; Вывод на экран изображения крепостной стены с воротами CALL SCRN LD BC,0 CALL 7997 ; Основная часть программы, в которой створки ворот ; со стуком разъезжаются в разные стороны LD B,64 MAIN PUSH BC CALL SCR_LF LD A,0 OUT (254),A CALL SCR_RT LD A,16 OUT (254),A LD BC,6 CALL 7997 POP BC DJNZ MAIN RET ; Формирование изображения крепостной стены с воротами: ; Изображение стены SCRN LD DE,D_WALL LD BC,7 CALL 8252 LD BC,384 SCRN1 LD A,147 RST 16 DEC BC LD A,B OR C JR NZ,SCRN1 ; Зеленая трава LD DE,D_GRAS LD BC,5 CALL 8252 LD B,32 SCRN2 LD A," " RST 16 DJNZ SCRN2 ; Зубцы на стене LD BC,#400 ;AT 4,0 CALL PR_AT LD B,16 SCRN3 LD DE,D_BATT PUSH BC LD BC,10 CALL 8252 POP BC DJNZ SCRN3 ; Ворота LD BC,#908 ;AT 9,8 CALL PR_AT LD B,16 SCRN4 LD A," " RST 16 DJNZ SCRN4 ; Бойницы LD H,4 SCRN5 LD A,H ADD A,7 LD B,A LD C,4 CALL PR_AT LD A," " RST 16 LD C,27 CALL PR_AT LD A," " RST 16 DEC H JR NZ,SCRN5 ; Штыри решетки LD A,16 RST 16 LD A,5 RST 16 LD B,10 ;Y LD H,8 SCRN6 LD L,16 LD C,8 ;X SCRN7 CALL PR_AT LD A,145 RST 16 INC C DEC L JR NZ,SCRN7 INC B DEC H JR NZ,SCRN6 ; Пики решетки LD BC,#A08 ;AT 10,8 CALL PR_AT LD B,16 SCRN8 LD A,144 RST 16 DJNZ SCRN8 ; Узор решетки LD L,2 LD B,13 SCRN9 LD C,8 CALL PR_AT LD B,16 SCRN10 LD A,146 RST 16 DJNZ SCRN10 LD B,16 DEC L JR NZ,SCRN9 RET ; Подпрограмма позиционирования вывода спрайтов PR_AT LD A,22 RST 16 LD A,B RST 16 LD A,C RST 16 RET ; Подпрограммы скроллинга окон
; Данные для левого окна COL DEFB 8 ROW DEFB 10 HGT DEFB 8 LEN DEFB 8 ; Данные для правого окна COL1 DEFB 16 ROW1 DEFB 10 LEN1 DEFB 8 HGT1 DEFB 8 ; Данные для рисования крепостной стены и решетки UDG DEFB 34,34,34,119,34,34,34,34 ;пики (144) DEFB 34,34,34,34,34,34,34,34 ;штыри (145) DEFB 54,42,170,255,170,42,54,34 ;узор (146) DEFB 255,2,2,2,255,32,32,32 ;кирпич (147) ; Данные позиционирования печати D_BATT DEFB 17,2,16,7,147,147,17,0,32,32 D_WALL DEFB 22,6,0,17,2,16,7 D_GRAS DEFB 22,18,0,17,4
Распределение областей памяти
Рисунок 2.1. Распределение областей памяти
Если в ПЗУ все уже предопределено, то оперативная память служит для временного хранения и обработки информации. Это могут быть различные программы на Бейсике или в машинных кодах, текстовые файлы, блоки данных и т. п. Все программы, которые приведены в книге, должны размещаться именно в оперативной памяти.
Для успешного программирования в машинных кодах и на ассемблере нужно четко представлять, какие области оперативной памяти для каких целей служат. Наиболее важной из них является видеобуфер, так как никакими программными средствами невозможно изменить его местоположение, размер или строение. Экранная память начинается с адреса 16384 и занимает 6912 байт. Вся остальная память, с адреса 23296 и до 65535 включительно, находится в вашем безраздельном распоряжении. Правда, это только в том случае, если вы создаете программу, полностью независимую от операционной системы компьютера. Но пока программа не заблокирует систему, вы не можете нарушать содержимого и структуры некоторых областей, о назначении и расположении которых вы должны быть хорошо осведомлены, начиная программировать на ассемблере.
«Растворение» символов
Продолжим изучение новых команд и приемов программирования, а заодно рассмотрим еще один интересный эффект, который можно назвать «растворение» символов. Он используется, например, в таких играх, как LODERUNNER, THE DAM BUSTER и других. Возможно, он заинтересует и вас. Суть его состоит в том, что при переходе от одной картинки, формируемой программой, к другой происходит не мгновенная очистка экрана, как оператором CLS, а изображение постепенно как бы растворяется. Это очень напоминает таяние снега. Подпрограмма, создающая такой эффект удивительно проста и коротка:
ORG 60000 ENT $ THAW LD B,8 ;экран очищается за 8 циклов LD DE,0 ;адрес начала кодов ПЗУ THAW1 LD HL,#4000 ;адрес начала экранной области PUSH DE THAW2 LD A,(DE) ;берем «случайный» байт из ПЗУ AND (HL) ;объединяем с байтом из видеобуфера LD (HL),A ;помещаем обратно в видеобуфер INC HL ;переходим к следующим адресам INC DE LD A,H ;проверяем, нужно ли повторять цикл CP #58 ;если прошли еще не весь видеобуфер ; (#5800 - адрес начала области атрибутов) JR NZ,THAW2 ; то повторяем PUSH BC LD BC,1 CALL 7997 ;PAUSE 1 POP BC POP DE LD HL,100 ADD HL,DE ;увеличиваем адрес в ПЗУ на 100 EX DE,HL ;меняем HL на DE DJNZ THAW1 ;повторяем цикл JP 3435 ;окончательно очищаем экран
Сначала объясним смысл вновь встретившихся команд, а затем более подробно расскажем о принципе работы программы.
Как мы говорили в самом начале книги, микропроцессор способен выполнять элементарные арифметические операции над числами. Сложению соответствует мнемоника ADD (Addition - сложение) а вычитанию - SUB (Subtraction). В этих операциях может участвовать регистр A (арифметические операции над однобайтовыми числами) или пара HL (при сложении или вычитании двухбайтовых чисел). К содержимому аккумулятора можно прибавлять (или вычитать) значение другого регистра или непосредственную числовую величину, а к паре HL можно только прибавлять и только содержимое другой регистровой пары (кроме AF, конечно). Результат арифметического действия получается в аккумуляторе или в регистровой паре HL соответственно.
Помимо обычных операций сложения и вычитания существуют их разновидности - сложение и вычитание с переносом (или, как еще говорят, с заемом). Они отличаются тем, что в операции принимает участие еще и флаг переноса: при сложении он прибавляется к результату, а при вычитании - отнимается. Записываются такие команды с мнемоникой ADC или SBC. В отличие от обычного вычитания в операции вычитания с переносом регистровая пара HL вполне может участвовать.
Есть небольшая особенность в записи этих команд: операция вычитания с участием аккумулятора выглядит не SUB A,S или SBC A,S, как это можно было ожидать, а просто SUB S или SBC S. То есть имя регистра A не пишется. Во всех остальных командах нужно указывать и имя. Вот некоторые примеры:
ADD A,7 ;прибавить к содержимому аккумулятора 7 ADC A,C ;прибавить к аккумулятору регистр C и флаг CY SUB B ;вычесть из аккумулятора значение регистра B SBC 127 ;вычесть из аккумулятора число 127 и флаг CY ADD HL,HL ;удвоить значение регистровой пары HL ADC HL,DE ;сложить содержимое пар HL и DE и добавить ; значение флага CY SBC HL,BC ;вычесть с учетом флага переноса BC из HL
Все перечисленные команды изменяют основные флаги за исключением команды ADD HL,SS, которая влияет лишь на флаг переноса.
Другая новая команда, встретившаяся в подпрограмме «растворения» экрана - это команда EX DE,HL. Она выполняет самое элементарное действие: обменивает содержимым регистровые пары HL и DE. То, что раньше было в HL, переходит в DE и наоборот. Правда, в данном случае она использована просто для пересылки полученного в HL результата в пару DE. Такой, вроде бы, нелепый ход объясняется отсутствием в системе команд микропроцессора Z80 инструкций для пересылки значений между регистровыми парами (типа LD DE,HL), поэтому команда EX DE,HL иногда может заменять последовательность
LD D,H LD E,L
что не только сокращает запись, но и несколько ускоряет работу программы.
Теперь вернемся к нашему примеру и посмотрим, как он работает. Основную роль здесь выполняет команда AND (HL), объединяющая байт из ПЗУ с байтом из экранной области памяти. В данном случае коды ПЗУ можно рассматривать как некоторую последовательность «случайных» чисел, поэтому в результате операции AND мы получим в аккумуляторе байт из видеобуфера, но некоторые биты в нем окажутся «выключенными», а какие именно - предсказать довольно трудно. Каждый следующий байт экрана объединяется с другим байтом из ПЗУ, отчего уже после первого прохождения цикла часть изображения пропадет. После второго прохода на экране останется еще меньше «включенных» пикселей. Но для этого нужно изменить последовательность «случайных» чисел, чего легче всего добиться изменением начального адреса в ПЗУ. В нашем примере он просто увеличивается на 100 байт. Поскольку нет гарантии, что после установленных восьми циклов все изображение окончательно исчезнет, в самом конце программа переходит на процедуру полной очистки экрана. Кстати, обратите внимание, что вместо последовательности
CALL 3435 RET
стоит единственная команда
JP 3435
Это позволительно делать практически всегда, когда за командой безусловного вызова следует безусловный же выход из подпрограммы. Исключение составляют лишь некоторые особые случаи, о которых мы поговорим, когда в этом возникнет необходимость.
В подпрограмме «растворения» экрана есть еще один довольно интересный момент - это способ организации внутреннего цикла. Зная длину области данных видеобуфера, которая равна 6144 байт, можно было бы задать количество повторений в явном виде, но в данном случае, поскольку адрес экранной области начинается с ровного шестнадцатеричного значения (то есть младший байт адреса равен нулю) и размер обрабатываемого блока также кратен 256, достаточно проверять только старший байт адреса на достижение определенного значения, а именно, #58, так как с адреса #5800 начинается область хранения атрибутов для каждого знакоместа экрана.
«Размножающиеся» окна
Рассмотрим программу, которая последовательно выводит на экран цепочку разноцветных окон различных размеров и которую при желании можно использовать как основу для создания оригинальной заставки. Самое последнее окно, появляющееся на экране, постепенно закрашивается черным цветом, оставляя только желтый контур, а затем в нем выводится заданный текст в виде «бегущей строки» (Рисунок 5.5).
Редактирование шумовых эффектов
Рисунок 10.5. Редактирование шумовых эффектов
Аналогично можно отредактировать любой из девяти эффектов, а для возврата в главное меню нужно нажать клавишу 0.
В системном меню есть еще одна интересная функция - SET CHANNEL LOOP PARAMETERS, служащая для установки начальных меток циклов в каждом канале. К услугам этого пункта полезно прибегать в тех пьесах, где мелодия начинается не с сильной доли (то есть не на счет «раз»), а с затакта. Нажмите клавишу 2 и укажите, в каком канале и с какого смещения мелодия в выбранном голосе будет начинаться при повторении. Начальные метки задаются для каждого канала в отдельности (в том числе и для канала эффектов FX), а смещения могут иметь как положительные, так и отрицательные значения. Добавим, что ошибиться в расстановке этих меток не страшно, ибо в любой момент их можно переместить как вперед, так и назад (если ввести отрицательное число).
в текст строки двумя способами:
Рисунок 3.1. Редактирование строки в GENS4
Вы можете вносить изменения в текст строки двумя способами: в режиме вставки, в котором вновь введенные символы «раздвигают» строку и не затирают имеющийся текст, и в режиме замены, в котором новые символы ложатся поверх прежних. В любом случае сначала необходимо подвести курсор к тому месту строки, где требуются какие-либо изменения. Вправо курсор можно перемещать по одной позиции, нажимая пробел, или разом перескакивать к следующему полю при нажатии Caps Shift/8. Перемещая курсор, вы увидите появляющийся под ним текст строки. Возврат на одну позицию влево происходит при нажатии клавиши Delete (Caps Shift/0)(обратите внимание на тот факт, что пока еще ничего не удаляется - вы просто перемещаете курсор, а верхняя строка нужна для подсказки).
Подведите курсор к числу 1000 так, чтобы он находился точно под единицей. Затем нажмите клавишу C для перехода в режим замены символов (в режим вставки можно перейти, нажав клавишу I, но это - на будущее). Обратите внимание, что курсор при этом изменяет свой вид. Теперь он выглядит как инвертированный символ + (в режиме вставки курсор имеет вид символа *, и это тоже - на будущее). Замените цифру 1 на 5, а следующий ноль, скажем, на 3. Затем выйдите из режима замены, нажав Enter и, наконец, введите строку в программу, еще раз нажав Enter. Редактирование строки закончено, и сейчас она должна иметь вид
20 LD BC,5300
Можете ассемблировать новый текст и, выйдя в Бейсик, выполнить получившуюся программку оператором
PRINT USR 60000
Прежде чем показать вам, как можно сохранить исходный текст и готовый машинный код, приведем и остальные команды редактирования строки. Вот они:
L (List) - показать на экране текущий вид строки; K (Kill) - удалить символ в позиции курсора; Z (Zap) - удалить весь текст от курсора до конца строки; X (eXpand) - переместить курсор в конец строки и войти в режим вставки; Q (Quit) - отменить все сделанные правки и закончить редактирование строки; R (Reload) - отменить все исправления и начать редактирование заново.
Все эти команды выполняются до включения режимов вставки или замены символов (команды I или C). Если же один из этих режимов уже включен, следует прежде выйти из него, нажав Enter.
РЕГИСТРЫ И РЕГИСТРОВЫЕ ПАРЫ
РЕГИСТРЫ И РЕГИСТРОВЫЕ ПАРЫ
Для общения с компьютером на уровне машинных кодов необходимо усвоить еще одно новое понятие кроме таких, как память, адрес, байт, бит, с которыми, надеемся, вы уже достаточно неплохо разобрались. Речь идет о регистрах микропроцессора. Регистры можно представить как совершенно особые внутренние ячейки памяти, являющиеся неотъемлемой частью центрального процессора. Роль их настолько важна, что практически ни одна операция не обходится без участия регистров, а различные арифметические и логические действия без них и вовсе невозможны.
Мы назвали регистры особыми ячейками, но в чем же их особенность и чем они отличаются от ячеек обычной оперативной памяти? Впервую очередь их особенность проявляется в том, что регистры не равноценны, то есть действия, допустимые с использованием одного регистра невозможны с другими и наоборот. Кроме того, если значения одних регистров можно изменять непосредственно, записывая в них те или иные числа, то другие изменяются автоматически, и узнать их содержимое возможно только лишь косвенными методами.
Другая особенность регистров состоит в том, что для обращения к ним используются не адреса, а собственные имена, состоящие из одной или двух букв латинского алфавита (конечно же, имена присутствуют только в языке ассемблера, а не в машинных кодах команд).
Есть и еще одно свойство, отличающее регистры от ячеек памяти - это способность их объединяться определенным образом, составляя регистровые пары. Во всем же остальном они очень схожи с отдельными ячейками памяти компьютера. Они также имеют размер байта (8 бит), в них можно записывать числа и читать их значение (за исключением системных регистров), информация в них может сохраняться, как и в памяти, до тех пор, пока не будет изменена программой.
Все регистры могут быть подразделены на несколько групп, учитывая характер функций, которые они выполняют. Начнем с самой многочисленной и наиболее важной группы - с так называемых регистров общего назначения или регистров данных. Их насчитывается семь: A, B, C, D, E, H и L. Как уже говорилось, каждый регистр может использоваться лишь в строго определенных операциях и каждый из них в этом смысле уникален. Например, регистр A (часто называемый аккумулятором) участвует во всех арифметических и логических операциях, результат которых мы получаем в том же регистре A. Использование регистра B наиболее удобно при организации циклов. При этом он выполняет роль, схожую с обязанностями управляющих переменных циклов FOR...NEXT в Бейсике. Другие регистры проявляют свою индивидуальность, преимущественно, объединившись в пары. Возможны следующие регистровые пары: BC, DE и HL. И вам следует запомнить, что никаких других вариантов соединения регистров не существует.
Из сказанного может создаться впечатление, что аккумулятор остался в одиночестве, не найдя своей половинки. Однако это не совсем так. На самом деле существует пара и для него. Просто пока мы сознательно умалчиваем об этом, так как регистр, дополняющий аккумулятор до пары, имеет совершенно особый статус и заслуживает отдельного разговора, который мы поведем в разделе главы 5.
Каждая из регистровых пар так же, как и любой из отдельных регистров, выполняет вполне конкретные, возложенные именно на нее функции. Так пара BC часто используется, подобно регистру B в качестве счетчика в циклах. HL несет наибольшую нагрузку, играя примерно ту же роль, что и аккумулятор: только с этой парой можно выполнять арифметические действия. Пара DE зачастую адресует пункт назначения при перемещениях данных из одной области памяти в другую.
Работая с регистровыми парами, приходится иметь дело с двухбайтовыми величинами. Поэтому необходимо четко представлять, как такие числа хранятся в памяти и каким образом они размещаются на регистрах. В Бейсике вам, вероятно, уже доводилось сталкиваться с подобной задачей. Если вы пользовались оператором POKE и функцией PEEK, например, для изменения или чтения системных переменных, то вам уже должно быть известно, что двухбайтовые значения хранятся в памяти, как правило, в обратном порядке - сначала младший байт, затем старший. Это можно продемонстрировать на таких примерах: число 1 запишется в памяти в виде последовательности байтов 1 и 0; у числа 255 старшая часть также равна нулю, поэтому оно будет представлено как 255 и 0; следующее число 256, расположившись в двух ячейках, будет выглядеть как 0 и 1. На всякий случай напомним вам способ, позволяющий разложить любое число из диапазона 0...65535 на два байта и определить значения старшей и младшей половинки:
LET high= INT(N/256): REM Старшая часть LET low=N-256*high: REM Младшая часть
Вам также часто придется сталкиваться с необходимостью изменять только старший или только младший регистр в регистровых парах. Поэтому следует хорошенько запомнить правило, которому подчиняются регистры при объединении. Оказывается, порядок здесь прямо противоположный по сравнению с числами в памяти - первым записывается старший регистр, а за ним младший. То есть в паре BC старшим окажется регистр B, в DE - D, а в HL - H. Чтобы лучше запомнить это, можете представить имя регистровой пары HL как сокращения английских слов HIGH (высокий, старший) и LOW (низкий, младший), а то, что порядок следования старшей и младшей половинок в остальных парах аналогичен, это уже само собой разумеется.
К следующей группе относятся два индексных регистра, имена которых начинаются с буквы I (Index) - IX и IY. В отличие от регистров данных, индексные регистры состоят из 16 разрядов, то есть являются как бы неделимыми регистровыми парами. (На самом деле существуют методы разделения индексных регистров на 8-разрядные половинки, что уже относится к программистским изощрениям. Об этих методах вы можете узнать из .) В основном они применяются при обработке блоков данных, массивов или разного рода таблиц, но также вполне могут использоваться и как обычные регистры общего назначения. Удобство употребления этих регистров заключается в том, что они позволяют обратиться к любому элементу массива или таблицы без изменения содержимого самого регистра, а лишь указанием величины смещения для данного элемента (иначе, его номера или индекса, например, IX+5). Заметим, что регистр IY обычно адресует область системных переменных Бейсика и поэтому отчасти и только в компьютерах ZX Spectrum может быть отнесен к следующей группе - системным регистрам.
К системным или иначе - аппаратным регистрам относятся: указатель вершины стека SP (Stack Point), вектор прерываний I (Interrupt) (точнее, этот регистр содержит старший байт адреса векторов прерываний; позднее мы подробно расшифруем это понятие) и регистр регенерации R. Первый из них, так же, как и индексные регистры, имеет 16 разрядов, разделить которые на 8-битовые половинки нет никакой возможности. Но это и не нужно, ведь регистр SP служит для вполне определенных целей - указывает адрес вершины области машинного стека, как это и следует из его названия. Хотя с ним и можно обращаться, как с обычным регистром данных (записывать или читать из него информацию), но делать это нужно, совершенно точно представляя, что при этом происходит. Обычно же за регистром SP следит микропроцессор и изменяет его так, как надо при выполнении некоторых команд. Например, без этого регистра оказались бы совершенно невозможны вызовы подпрограмм с нормальным возвратом из них в основную программу.
Регистры I и R, в противоположность всем прочим, никогда не объединяются в пары и существуют только по отдельности. Содержимое вектора прерываний I также может быть изменено программным путем, однако делать этого не стоит до тех пор, пока вы не разберетесь с таким достаточно сложным вопросом, как прерывания. Что же касается регистра R, то читать из него можно, а вот записывать в него информацию в большинстве случаев бесполезно, так как он изменяется аппаратно. Правда, используется для аппаратных нужд только 7 младших разрядов, так что, если вам для чего-то окажется достаточно одного бита, можете хранить его в старшем разряде регистра регенерации.
В свое время мы подробно расскажем о применении всех существующих регистров, а сейчас закончим этот краткий обзор и займемся другими вопросами.
Режим редактирования
Рисунок 10.2. Режим редактирования
Прежде чем начинать вводить новую мелодию, нужно убрать из памяти старую, оставшуюся после загрузки программы или от предыдущих упражнений. Нажмите клавишу 7 и на запрос ERASE CURRENT TUNE (Y/N)?- удалить текущую мелодию (да/нет)? - ответьте утвердительно нажатием клавиши Y. Теперь можно записывать ноты.
Ввод мелодии в Wham FX в принципе ничем не отличается от записи музыки в первой версии программы. Для получения звуков здесь также используются клавиши двух нижних рядов и Enter - для ввода пауз, октавы переключаются клавишами 1...4, а переход к редактированию другого голоса происходит при нажатии клавиши T. Правда, данная версия рассчитана на компьютер Spectrum 128, а следовательно, на расширенную клавиатуру, поэтому нота ДО извлекается нажатием клавиши Caps Lock (на обычной клавиатуре - Caps Shift/2), а также задействованы кнопки с символами запятой и точки. Клавиши Caps Shift и Symbol Shift служат здесь для других целей, поэтому при вводе звуков не используются.
При записи музыки вы можете по мере надобности сдабривать ее различными шумовыми эффектами, имитирующими ударные инструменты. Таких инструментов в одной пьесе можно иметь до девяти, а вставляются подобные звуки при одновременном нажатии клавиши Symbol Shift и одной из клавиш второго ряда сверху от Q до O.
Если вы ошиблись при вводе очередного звука, вернуться на одну позицию назад можно с помощью клавиши Delete (Caps Shift/0), а для быстрой прокрутки назад на несколько тактов воспользуйтесь клавишей True Video (Caps Shift/3). Для продвижения вперед нажимайте клавиши O (быстро) или P (медленно). Чтобы прослушать полученную музыку, нужно вернуться в самое начало пьесы, нажав R, а затем включить проигрывание клавишей Q.
Наиболее интересной особенностью редактора Wham FX является возможность изменения характера звучания каждого голоса в отдельности. Для этого нужно подвести курсор к тому месту в пьесе, начиная с которого вы хотите получить иной звук (первая нота фрагмента должна появиться у правого края экрана) и нажать клавишу Extend Mode (Caps Shift/Symbol Shift). Внизу экрана появится дополнительное меню, состоящее из пяти пунктов:
ENVELOPE VOLUME BLANK SLIDE LOOP
Выбор интересующей функции осуществляется нажатием клавиши, соответствующей первой букве слова. Поясним назначение каждого из этих пунктов.
После выбора ENVELOPE компьютер даст два дополнительных запроса: какой формы должна быть огибающая (нужно ввести число от 1 до 7) и на какой голос данный эффект будет распространяться (нажмите клавишу от 1 до 3 или 0, если хотите иметь одинаковое звучание во всех трех голосах).
В отличие от предыдущего пункта, VOLUME задает постоянный уровень громкости. При выборе этой опции нужно сначала ввести шестнадцатеричное число от 1 до F, соответствующее желаемой громкости, а затем опять же указать голос, к которому данное изменение относится.
Опция SLIDE имитирует такой распространенный в эстрадной музыке прием исполнения, как BEND. При этом звук плавно изменяется по высоте в пределах нескольких полутонов, повышаясь или же наоборот, понижаясь. Компьютер попросит сначала ввести интервал (от 0 до 7 полутонов), на который звук «поедет», затем направление (UP или DOWN - вверх или вниз) и, как и для предыдущих пунктов, номер канала.
BLANK используется в том случае, если вы по ошибке поставили эффект не в том месте, где хотели. Никаких дополнительных запросов в этой опции не предусмотрено.
Последний пункт LOOP служит для установки метки цикла в канале эффектов FX и обязательно должен использоваться перед началом компиляции пьесы. Однако до этих пор прибегать к нему не следует, так как если вы решите продолжить ввод мелодии, снять поставленную метку не удастся.
Немного потренировавшись во вводе нот и изменении их звучания, можно приступать к программированию настоящей музыки. Чтобы помочь вам в этом нелегком предприятии, предлагаем сначала ввести небольшой фрагмент, приведенный на Рисунок 10.3 и в табл. 10.2. Просим музыкальных критиков не придираться к правописанию нот, так как рисунок отражает то, что вы увидите на экране монитора, а не то, как должны быть записаны ноты для исполнения пьесы грамотными музыкантами. В таблице выписана последовательность нажатия клавиш при вводе звуков каждого голоса. Сокращение Okt. обозначает октаву, Ent - клавишу Enter, SS - Symbol Shift (запись SS/E, например, обозначает одновременное нажатие Symbol Shift и E), а буквы и цифры справа от обозначения клавиш указывают на установку того или иного эффекта (V - VOLUME, E - ENVELOPE).
РИСОВАНИЕ ГРАФИЧЕСКИХ ПРИМИТИВОВ
РИСОВАНИЕ ГРАФИЧЕСКИХ ПРИМИТИВОВ
Как вы знаете, ни одна игровая программа, за исключением лишь некоторых игр жанра Adventure, не обходится без более или менее сложной графики. И даже в упомянутых текстовых играх развитие сюжета часто сопровождается различными изображениями на экране (как, например, в программе The Hobbit). В дальнейшем мы уделим достаточно внимания созданию и выводу графики, а в этом разделе только приступим к данному вопросу и начнем с построения графических примитивов - точек, прямых линий, дуг и окружностей.
Системные области
Сразу за видеобуфером следует небольшая область памяти, называемая буфером принтера. Она используется только при работе с различными печатающими устройствами, поэтому если вы не предполагаете в своей программе делать какие-то распечатки на бумаге, смело можете занимать память в диапазоне адресов от23296 до 23551 включительно (то есть 256 байт) под любые нужды. Во всяком случае, буфер принтера может использоваться для временного хранения информации или как рабочий массив.
Однако нужно помнить, что все сказанное о буфере принтера справедливо лишь для стандартной конфигурации компьютера, то есть для ZX Spectrum 48. Если вы пишете программы, которые должны работать на модели Spectrum 128 или Scorpion ZS 256, то столь произвольно обращаться с этой областью памяти нельзя, потому как в данных адресах указанные модели содержат жизненно важную информацию, при разрушении которой компьютер не сможет нормально продолжать работу (хотя, разумеется, можно выполнить программу и в режиме «эмуляции» обычного Speccy - Примеч. ред.).
С адреса 23552 начинается наиболее важная из системных областей. Вы уже частично (а может быть, и полностью) знакомы с системными переменными Бейсика. В различных ячейках этой области хранится различная информация о текущем состоянии всех без исключения параметров операционной системы, в том числе и информация о расположении всех прочих областей памяти, которые не имеют жесткой привязки к конкретным адресам (описание всех системных переменных Spectrum-Бейсика, а также ZX Spectrum 128 и TR-DOS можно найти в [2]).
Системная переменная CHANS, находящаяся в ячейках 23631 и 23632, адресует область канальной информации, содержащей необходимые сведения о расположении процедур ввода/вывода (напоминаем, что на первом месте стоит младший байт адреса и для перевода двухбайтового значения в число требуется содержимое старшего байта умножить на 256 и прибавить к нему число из младшего байта; например, для определения значения переменной CHANS нужно выполнить команду PRINT PEEK 23631+256*PEEK 23632). Далее следует область, хранящая текст бейсик-программы. Ее начальный адрес содержит переменная PROG (23635/23636). Сразу за бейсик-программой располагаются переменные Бейсика. Их начало можно определить, прочитав значение системной переменной VARS по адресу 23627/23628. После переменных Бейсика расположена область, предназначенная для ввода и редактирования строк бейсик-программ. Ее адрес записан в системной переменной E_LINE (23461/23642). За областью редактирования строк находится рабочая область Бейсика WORKSP (23649/23650), предназначенная для самых разных нужд. Сюда, например, считываются заголовки файлов при загрузке программ с ленты, там же размещаются строки загружаемой оператором MERGE программы до объединения их со строками программы в памяти и т. д.
Следом за областью WORKSP расположена весьма важная область, называемая стеком калькулятора. Название говорит само за себя: сюда записываются числовые значения, над которыми производятся различные математические операции, здесь же остается до востребования и результат расчетов. В дальнейшем мы не раз будем прибегать к помощи этой области, так как многие процедуры операционной системы, которыми мы будем пользоваться, берут параметры именно отсюда. Системная переменная STKBOT (23651/23652) указывает на начало стека калькулятора, а STKEND (23653/23654) - на его вершину. Иногда бывает важно учитывать, что каждое значение, заносимое на вершину стека калькулятора, имеет длину 5 байт.
Системная переменная RAMTOP (23730/23731) указывает на местоположение в памяти еще одной важной области - машинного стека (не путайте со стеком калькулятора!). Но надо помнить, что в ассемблерных программах стек вполне может потерять всякую связь с RAMTOP, ибо он не является неотъемлемой частью бейсик-системы, а скорее уж, находится в «собственности» микропроцессора. Вообще же стек - это удивительно удобная штука для временного хранения различной информации, потому как при его использовании не приходится запоминать, где, по какому адресу или в какой переменной находится то или иное число. Важно лишь соблюсти очередность обмена данными, а чтобы не нарушить установленный порядок, следует знать, по какому принципу работает стек. Этот принцип часто называют «Last In, First Out» (LIFO), что значит «Последним вошел, первым вышел». Совсем как в автобусе в час пик - чтобы выпустить какого-нибудь пассажира, прежде должны выйти все вошедшие за ним. Поэтому данные, которые понадобятся в первую очередь нужно заносить в стек последними (это же, кстати, в полной мере относится и к порядку обмена данными со стеком калькулятора).
Говоря о машинном стеке, нужно отметить один довольно интересный факт. В отличие от способа организации других областей памяти (а также и от стека калькулятора) он растет «головой вниз», то есть каждое следующее значение, отправленное в стек, будет располагаться по адресу на 2 байта ниже предыдущего (машинный стек работает только с двухбайтовыми величинами). Поэтому вас не должны вводить в заблуждение такие выражения как «Положить значение на вершину стека» или «Снять значение с вершины стека» - эта самая «вершина» всегда будет не выше основания.
Существуют и другие области памяти, как то: UDG, системные переменные TR-DOS или карта микродрайва. Область определяемых пользователем символов UDG мы рассмотрим в следующих главах, а о других разделах памяти (в том числе и об архитектуре Spectrum 128) вы можете получить дополнительные сведения, например, в книге [2].
СКРОЛЛИНГИ ОКОН
СКРОЛЛИНГИ ОКОН
Плавное перемещение изображений по экрану в разных направлениях можно достаточно часто увидеть в компьютерных играх. Пожалуй, легче сказать, где оно не используется, чем наоборот, поэтому решение такой задачи представляется нам достаточно важным. Мы уже показали, как формировать самые разные окна, а теперь попробуем написать несколько процедур для их плавного перемещения (скроллинга) во всех четырех направлениях. Затем на примере двух программ покажем, как применять такие процедуры для решения конкретных задач. Учтите, что каждая из приведенных ниже программ скроллинга сдвигает изображение в окне только на один пиксель. Следовательно, если вам потребуется переместить изображение в какую-то сторону, скажем, на 50 пикселей, то соответствующую процедуру следует выполнить несколько раз подряд в цикле, например:
LD B,50 ;количество сдвигов LOOP PUSH BC ;сохраняем содержимое регистра B .......... ;процедура скроллинга POP BC ;восстанавливаем регистр B DJNZ LOOP RET
До того, как мы приступим к детальному описанию процедуры скроллинга окна вверх, желательно рассмотреть все команды и подпрограммы ПЗУ, которые встречаются здесь впервые. Таких наберется всего две: чрезвычайно полезная команда LDIR, перемещающая блок памяти с инкрементом (т. е. с увеличением содержимого регистров, в которых записаны адреса пересылок) и подпрограмма ПЗУ, расположенная по адресу 8880. Рассмотрим их в том порядке, как они встречаются в программе.
Процедура 8880 вычисляет адрес байта в видеобуфере по координатам точки, заданным в пикселях. Началом отсчета считается левый верхний угол экрана. Таким образом, входными данными к подпрограмме являются:
вертикальная координата, помещаемая в аккумулятор;
горизонтальная координата, помещаемая в регистр C,
а выходными:
в регистровой паре HL возвращается вычисленный адрес байта видеобуфера;
в регистр A помещается значение от 0 до 7, численно равное величине смещения заданной точки в пикселях от левого края того знакоместа, для которого рассчитывается адрес.
Более подробно рассмотрим действие команды LDIR. С ее помощью группа байтов, расположенных в сторону увеличения адресов от ячейки, на которую указывает HL, пересылается в область памяти, адресуемую регистровой парой DE. Количество передаваемых байтов определяется регистровой парой BC. Чтобы почувствовать всю прелесть этой команды и оценить ее по достоинству, приведем текст цикла, выполняющего то же, что и LDIR.
MET LD A,(HL) LD (DE),A INC HL INC DE DEC BC LD A,B OR C JR NZ,MET RET
Глядя на этот фрагмент, можно заметить очевидные достоинства команды LDIR: в цикле изменяется регистр A, а в LDIR он не затрагивается, кроме того, программа занимает 8 строк текста вместо одной и работает примерно в два с половиной раза медленнее.
Если в программу ввести исходные данные, то может получиться эффект, который мы наблюдаем в некоторых играх, например, в SOKOBAN'е. Суть его состоит в том, что из ПЗУ с адреса 0 переписываются 6144 байта в область экранной памяти, начиная с адреса 16384. Если это действие повторять в цикле, то создастся впечатление бегущей по экрану ряби:
LD BC,6144 LD HL,0 LD DE,16384 LD A,255 MET PUSH BC PUSH DE PUSH HL LDIR POP HL POP DE POP BC INC HL DEC A JR NZ,MET RET
Кроме рассмотренной команды LDIR в ассемблерных программах довольно широко используются и другие команды пересылок байтов:
LDDR - перемещение блока памяти с декрементом. Ее действие аналогично команде LDIR, только пересылается группа байтов, расположенных в сторону уменьшения адресов от ячейки, на которую указывает HL. Количество передаваемых байтов также определяется в BC.
LDI - пересылка содержимого одной ячейки памяти с инкрементом. Байт из ячейки, адресуемой регистровой парой HL, переносится в ячейку, адресуемую парой DE; содержимое HL и DE увеличивается на 1, а BC уменьшается на 1. Если в результате выполнения команды BC=0, то флаг P/V сбрасывается, в противном случае P/V=1.
LDD - пересылка содержимого одной ячейки памяти с декрементом. Действие аналогично команде LDI, только содержимое регистровых пар HL и DE уменьшается на 1.
Покончив с теорией, можно заняться более приятным делом и написать процедуры для скроллинга окон для всех направлений. Начнем со смещения окна вверх. Перед вызовом этой подпрограммы нужно определить уже известные по предыдущим примерам переменные ROW, COL, HGT и LEN, записав в них координаты и размеры окна. Предварительно не помешает убедиться, что окно не выходит за пределы экрана, так как в целях упрощения программы подобные проверки в ней не выполняются. Добавим, что это в равной мере относится и к другим процедурам скроллингов.
SCR_UP LD A,(COL) LD C,A LD A,(HGT) LD B,A LD A,(ROW) ; Значения из переменных ROW, COL и HGT умножаем на 8, ; то есть переводим знакоместа в пиксели SLA A SLA A SLA A SLA B SLA B SLA B DEC B ;потому что один ряд пикселей просто ; заполняется нулями SLA C SLA C SLA C PUSH AF PUSH BC CALL 8880 ;вычисляем адрес верхнего левого угла окна POP BC POP AF SCRUP1 INC A ;следующий ряд пикселей PUSH AF PUSH BC PUSH HL CALL 8880 ;вычисляем адрес POP DE PUSH HL LD A,(LEN) ;пересылаем столько байт, сколько ; умещается по ширине окна LD C,A LD B,0 LDIR POP HL POP BC POP AF DJNZ SCRUP1 LD (HL),0 ;в последний ряд пикселей записываем нули LD D,H LD E,L INC DE LD A,(LEN) ;по ширине окна, DEC A ; минус 1 RET Z ;выходим, если только одно знакоместо LD C,A LD B,0 LDIR ;иначе обнуляем и все остальные байты ряда RET
Обратите особое внимание на последние строки процедуры, где в нижний ряд пикселей записываются нулевые байты. Этот прием весьма распространен и применяется для заполнения любой области памяти произвольным значением. Чтобы понять идею, нужно хорошо представлять, как работает команда LDIR. Сначала в первый байт массива, адресуемый парой HL, заносится какой-то определенный байт, в DE переписывается значение из HL и увеличивается на 1, в BC задается уменьшенный на единицу размер заполняемого массива, а дальше с выполнением команды LDIR происходит следующее. Байт из первого адреса (HL) переписывается во второй (DE), затем DE и HL увеличиваются, то есть HL будет указывать на второй байт, а DE - на третий. На следующем круге число из второго адреса пересылается в третий, но после первого выполнения цикла второй байт уже содержит ту же величину, что и первый, поэтому второй и третий байты к этому моменту станут равны первому. И так далее, до заполнения всего массива.
Практическое применение такому методу найти нетрудно. Кроме процедур вертикального скроллинга окон он может использоваться, например, для очистки экрана, инициализации блоков данных и других нужд. Попробуйте сами написать процедуру, аналогичную оператору CLS, в которой участвовала бы команда LDIR.
Вернемся к рассмотрению подпрограмм скроллингов окон. Процедура сдвига окна вниз во многом похожа на предыдущую:
SCR_DN LD A,(COL) LD C,A LD A,(HGT) LD B,A LD A,(ROW) ADD A,B ;начинаем перемещать изображение не ; сверху, как в SCR_UP, а снизу SLA A SLA A SLA A DEC A SLA B SLA B SLA B DEC B SLA C SLA C SLA C PUSH AF PUSH BC CALL 8880 POP BC POP AF SCRDN1 DEC A ;следующий ряд пикселей (идем вверх) PUSH AF PUSH BC PUSH HL CALL 8880 POP DE PUSH HL LD A,(LEN) LD C,A LD B,0 LDIR POP HL POP BC POP AF DJNZ SCRDN1 LD (HL),0 LD D,H LD E,L INC DE LD A,(LEN) DEC A RET Z LD C,A LD B,0 LDIR RET
Теперь приведем подпрограмму, выполняющую скроллинг окна влево. Она уже совсем не похожа на предшествующие, но в принципе повторяет известную вам процедуру , описанную в разделе «Бегущая строка» предыдущей главы. Но это и понятно: ведь там мы также скроллировали окно, только оно имело фиксированные размеры в одно знакоместо высотой и 32 - шириной. Здесь же мы приводим универсальную процедуру, но и с ее помощью можно получить тот же эффект.
SCR_LF LD A,(HGT) ;количество повторений такое же, LD B,A ; сколько строк занимает окно LD A,(ROW) ;номер верхней строки SCRLF1 PUSH AF ;дальше все очень похоже на процедуру SCRLIN PUSH BC CALL 3742 LD A,(COL) LD B,A LD A,(LEN) DEC A ADD A,B ADD A,L LD L,A LD B,8 SCRLF2 PUSH HL LD A,(LEN) AND A SCRLF3 RL (HL) DEC HL DEC A JR NZ,SCRLF3 POP HL INC H DJNZ SCRLF2 POP BC POP AF INC A DJNZ SCRLF1 RET
И наконец, для полного комплекта, напишем соответствующую подпрограмму, выполняющую скроллинг окна вправо:
SCR_RT LD A,(HGT) LD B,A LD A,(ROW) SCRRT1 PUSH AF PUSH BC CALL 3742 LD A,(COL) ADD A,L LD L,A LD B,8 SCRRT2 PUSH HL LD A,(LEN) AND A SCRRT3 RR (HL) INC HL DEC A JR NZ,SCRRT3 POP HL INC H DJNZ SCRRT2 POP BC POP AF INC A DJNZ SCRRT1 RET
А сейчас рассмотрим программу, в которой демонстрируются возможности вертикального скроллинга вверх. После запуска вы увидите две быстро сменяющие друг друга картинки: сначала весь экран заполняется красивым орнаментом (фактурой), а затем средняя его часть стирается процедурой очистки окна. После этого строка за строкой снизу вверх начнет медленно перемещаться текст с правилами игры (Рисунок 6.6), который обычно вызывается из меню как один из кадров заставки.
СОХРАНЕНИЕ И ЗАГРУЗКА ТЕКСТОВ И ПРОГРАММ
СОХРАНЕНИЕ И ЗАГРУЗКА ТЕКСТОВ И ПРОГРАММ
Разобравшись с методами редактирования строк, пора узнать, как сохраняются результаты. Вотличие от Бейсика, редактор GENS позволяет сохранить не только весь текст целиком, но и произвольную его часть, для чего в команде P (Put text) прежде указывается начальная строка сохраняемого фрагмента, а затем - конечная. Самым последним параметром команды служит имя, под которым вы хотите сохранить текст. (У строчного редактора есть одно полезное, но в некоторых случаях опасное свойство: часть команд, будучи введенными без параметров, выполняется с параметрами предыдущей команды. Поэтому, на первых порах будьте особо внимательны.) Поскольку в этой команде нет необязательных параметров, то для записи всего текста можно задать границы номеров сохраняемых строк «с запасом» и ввести, например, такую строчку:
P1,20000,ASMTEXT
Если вы работаете с дисководом и имеете в своем распоряжении дисковую версию ассемблера GENS4D или gens4b, то можете записать исходный текст на дискету. Формат команды в этом случае останется, в общем-то, тем же самым, только имя файла должно начинаться с указания номера дисковода, к которому вы собираетесь обратиться. Например, для записи на дисковод A нужно ввести
P1,20000,1:ASMTEXT
Заметьте, что двоеточие между номером дисковода и именем обязательно.
Обратное действие - загрузка ранее созданного исходного текста - осуществляется с помощью команды редактора G (Get text). Формат ее похож на формат предыдущей команды, правда, значение имеет только последний параметр - имя файла. Поэтому для загрузки программы GAME с магнитофона следует ввести
G,,GAME
а для загрузки одноименного файла с дисковода B эта же команда примет вид
G,,2:GAME
По сравнению с оператором Бейсика LOAD у команды G есть одно существенное отличие: она не уничтожает уже имеющийся в памяти текст, а добавляет новый в конец. После объединения строки программы будут перенумерованы с номера 1 и с единичным шагом. Часто такая особенность команды G бывает не только полезна, но и просто необходима, однако если вы собираетесь поработать над новой программой, прежде чем ее загружать, надо убедиться, что редактор свободен от какого бы то ни было текста, а если нет, то удалить его (как это сделать, мы скажем чуть позже).
Для сохранения полученного машинного кода (нередко называемого объектным, хотя это и не совсем верно) можно воспользоваться командой O (Object). Она имеет синтаксис, аналогичный команде G, то есть для записи оттранслированной программы в файл MYPROG при работе с лентой нужно ввести строку
O,,MYPROG
а при работе с дисководом A эта команда запишется как
O,,1:MYPROG
Записывая тексты и программы на диск, вы иногда можете увидеть на экране надпись
File exists Delete (Y/N)?
сообщающую о том, что одноименный файл уже имеется на диске. В этом случае вам предлагается решить, удалять этот файл или отказаться от записи с тем, чтобы заменить дискету и попытаться сохранить текст еще раз. Если на этот запрос ответить нажатием клавиши Y, то файл на диске будет переписан, нажатие любой другой клавиши приведет к отказу от выполнения команды.
СОВМЕСТНОЕ УПРАВЛЕНИЕ КЛАВИАТУРОЙ ИДЖОЙСТИКОМ
СОВМЕСТНОЕ УПРАВЛЕНИЕ КЛАВИАТУРОЙ И ДЖОЙСТИКОМ
Прочитав название параграфа, многие наверняка подумали - а в чем тут собственно проблема, достаточно объединить вместе блоки управления клавиатурой и джойстиком и вроде бы все, можно пользоваться как тем так и другим. Не будем вас разочаровывать, так оно и есть, и в принципе подобную схему вполне можно использовать для совместного управления. Но при этом нужно помнить о том, что если потребуется изменить управляющие клавиши, вам придется вносить в текст программы довольно много изменений. То же самое можно сказать и о смене типа джойстика. Поэтому хотелось бы иметь универсальную процедуру опроса клавиатуры и джойстика, в которой перечисленные изменения можно было выполнить с наименьшей затратой сил. Ниже приводится программа, использующая подобную процедуру, обозначенную меткой KBDJOY. В ней на экран выводится вертолет с вращающимся винтом (Рисунок 8.2 а,б) и стрекочущим двигателем. Нажимая клавиши Q, A, O, P или наклоняя ручку джойстика, вы сможете легко убедиться в том, что программа работает как положено. А для усиления эффекта можно попробовать нажать какую-нибудь из клавиш и одновременно повернуть джойстик - вертолет послушно полетит по диагонали.
СОЗДАНИЕ ПРОСТЕЙШИХ ЗВУКОВ
СОЗДАНИЕ ПРОСТЕЙШИХ ЗВУКОВ
Вероятно, вы давно и с большим нетерпением ждете того момента, когда мы наконец соблаговолим поведать о том, как у компьютера «прорезать голос». До сих пор мы применяли некие акустические суррогаты, однако терпеть и дальше такое «безобразие» уже нет никакой возможности. Более детально и всесторонне вопросы создания мелодий и шумовых эффектов (в том числе и для ZX Spectrum 128) будут рассмотрены в десятой главе. Пока же мы предлагаем не очень сложный способ получения звуков, основанный на использовании подпрограммы ПЗУ, к которой в конечном итоге обращается интерпретатор при выполнении оператора BEEP. Эта подпрограмма располагается по адресу 949 и требует указания в регистровых парах DE и HL соответственно длительности и высоты звука. Методику расчета этих параметров для получения конкретных музыкальных звуков мы объясним позже, а сейчас нас интересуют простые звуковые эффекты, не имеющие конкретной высоты, поэтому данные значения можно подбирать просто на слух. Пожалуй, самая простая программа, которую можно сравнить с резонатором, настроенным на определенную частоту, выглядит следующим образом:
LD DE,40 LD HL,500 CALL 949 RET
Несмотря на некоторую трудность подбора параметров, все же попытаемся создать с помощью приведенной подпрограммы что-либо интересное, причем так, чтобы время звучания и высоту тона можно было свободно регулировать. Поскольку при этом возможны самые разные варианты, то получится множество звуковых эффектов, что, собственно говоря, нам и требуется.
Рассмотрим программу, основанную на приведенном выше примере, генерирующую звуки длительностью порядка 1-3 секунд. Она отличается тем, что высота тона изменяется в цикле, а продолжительность звучания можно регулировать не только изменением DE, но и занося различные значения в регистр B. Мы предполагаем использовать ее в дальнейшем, для чего присвоим ей собственное имя:
SND LD B,10 ;количество циклов LD HL,300 ;начальная частота звучания LD DE,8 ;длительность звука SND1 PUSH BC PUSH DE PUSH HL CALL 949 POP HL POP DE POP BC DEC HL ;или INC HL DJNZ SND1 RET
После запуска вы услышите звук, увеличивающийся по высоте, но если где-то в игре потребуется, чтобы с течением времени частота уменьшалась, достаточно команду DEC HL заменить на INC HL. Для извлечения более коротких звуков (0.1-0.3 сек.) достаточно задать в SND другие значения регистров:
SND LD B,90 LD DE,2 LD HL,150
Располагая этими программками, попробуйте поэкспериментировать, встроив, например, два резонатора в один цикл, либо, организовав несколько (в том числе и вложенных) циклов с одинаковыми или разными резонаторами. Уверены, что это занятие приведет вас ко многим «открытиям» и доставит немало приятных минут.
СОЗДАНИЕ ЗВУКОВЫХ ЭФФЕКТОВ
СОЗДАНИЕ ЗВУКОВЫХ ЭФФЕКТОВ
Различные звуковые и шумовые эффекты, которыми изобилуют компьютерные игры, достигаются через изменение по тому или иному закону частоты выводимого звука. Нетрудно догадаться, что высота звука напрямую зависит от продолжительности цикла задержки между командами вывода в порт динамика: чем больше задержка, тем более низким получится звук.
Техника вывода звука вам уже известна, поэтому без лишних слов сразу перейдем к делу и продемонстрируем несколько наиболее часто употребляемых в играх эффектов. Первый из них больше всего напоминает щебет птиц, особенно если его вызывать с небольшими и неравными промежутками времени:
TWEET LD A,(23624) ;определение цвета бордюра AND #38 RRA RRA RRA DI TWEET1 XOR 16 ;переключение 4-го бита OUT (254),A PUSH BC DJNZ $ ;цикл задержки POP BC DJNZ TWEET1 EI RET
Длительность эффекта перед обращением к процедуре TWEET задается в регистре B, например:
LD B,200 CALL TWEET RET
Прежде чем привести следующий пример, скажем несколько слов, относящихся не только к этой подпрограмме, но и ко всем остальным. Поскольку в реальных программах цвет бордюра обычно не изменяется и определен заранее, то он, как правило, не вычисляется в программе, а задается в явном виде загрузкой в аккумулятор кода нужного цвета. Вы также можете вместо первых строк от метки TWEET до команды DI просто написать XOR A для получения черного бордюра или, например, LD A,4 - для зеленого.
Другой интересный момент касается уже не самой программы, а собственно ассемблера. Вы, наверное, обратили внимание на запись
DJNZ $
Как известно, символ доллара при трансляции принимает значение текущего адреса размещения машинного кода, а точнее, адрес начала строки ассемблерного текста. Поэтому такая запись полностью равноценна записи
LOOP DJNZ LOOP
но позволяет обойтись без дополнительных меток.
После такого небольшого лирического отступления давайте продолжим «изобретение» звуковых эффектов.
Особо часто в игровых программах можно услышать множество разновидностей вибрирующих звуков. Получить такой эффект можно, периодически увеличивая и уменьшая частоту (то есть количество циклов задержки). Вибрация характеризуется двумя параметрами: собственной частотой и глубиной (амплитудой), поэтому для такой процедуры потребуется, кроме длительности звучания, задавать и некоторые другие входные данные. Сначала приведем текст подпрограммы для получения вибрирующего звука, а затем объясним, какие значения в каких регистрах следует разместить перед обращением к ней.
VIBR LD A,(23624) AND # 38 RRA RRA RRA LD C,A DI VIBR1 LD D,E ;продолжительность цикла спада (подъема) VIBR2 LD A,C XOR 16 LD C,A OUT (254),A LD A,H ;изменение частоты звука ADD A,L LD H,A VIBR3 DEC A ;цикл задержки JR NZ,VIBR3 DEC D JR NZ,VIBR2 LD A,L ;смена направления изменения частоты NEG LD L,A DJNZ VIBR1 EI RET
В регистр H нужно занести начальную частоту звука (имеется в виду, конечно, частота не в герцах, а в относительных единицах). Содержимое регистра E влияет на частоту вибрации: чем меньше его значение, тем быстрее спад будет сменяться подъемом и наоборот. В регистре B задается количество циклов вибрации, то есть в конечном счете - длительность звука, а в L заносится величина, определяющая глубину вибрации, или иначе, скорость изменения высоты звука. Мы предлагаем такие значения регистров:
LD H,100 LD E,120 LD B,4 LD L,1 CALL VIBR RET
однако это только один из многих возможных вариантов. Попробуйте поэкспериментировать и подобрать наиболее интересные варианты звучания, которые впоследствии сможете использовать в своих собственных разработках.
Приведем еще одну подпрограмму, создающую другой тип вибрации, при котором частота звука, достигнув наивысшей (или же низшей) точки, возвращается к начальной своей величине. Тем самым частотная характеристика имеет внешний вид, схожий с зубьями пилы. Подпрограмма, создающая похожий звук, имеется в известном пакете Suprcode и значится там под именем «Laser». Вот как примерно она может выглядеть:
LASER LD A,(23624) AND #38 RRA RRA RRA DI LASER1 PUSH BC LD L,H LASER2 XOR 16 OUT (254),A LD B,H DJNZ $ INC H ;другой вариант - DEC H DEC C JR NZ,LASER2 LD H,L POP BC DJNZ LASER1 EI RET
Прежде чем обратиться к данной процедуре, необходимо в регистр B загрузить количество «зубчиков пилы», в C - продолжительность каждого «зубца», а в регистре H задать исходную высоту звука. Например:
LD B,5 LD C,200 LD H,50 CALL LASER RET
Если вы работали с музыкальным редактором Wham, то, вероятно, задавались вопросом, как в одном звуковом канале удается получить сразу два тона различной высоты. Во многих играх, особенно последних лет, музыкальное сопровождение выполнено в аранжировке той или иной степени сложности. Иногда можно слышать не два, а три и более голосов (например, в DEFLEKTOR или MIG-29). Конечно, написание музыки на два голоса - дело вовсе непростое, но принцип получения подобных звуков знать все же стоит. Тем более, что таким способом можно создать ряд весьма недурных эффектов. Двуголосие достигается наложением двух различных частот, поэтому программа, генерирующая одновременно два тона, может выглядеть примерно так:
TWOTON LD A,(23624) AND # 38 RRA RRA RRA LD H,D LD L,E DI TWOTN1 DEC H ;задержка для получения первого тона JR NZ,TWOTN2 XOR 16 OUT (254),A ;извлечение первого звука LD H,D ;восстановление значения задержки ; для первого тона TWOTN2 DEC L ;задержка для получения второго тона JR NZ,TWOTN1 XOR 16 OUT (254),A ;извлечение второго звука LD L,E ;восстановление значения задержки ; для второго голоса PUSH AF LD A,B ;проверка окончания звучания OR C JR Z,TWOTN3 POP AF DEC BC JR TWOTN1 TWOTN3 POP AF EI RET
Перед обращением к процедуре в регистровой паре BC нужно указать длительность звучания, а в регистрах D и E - высоту звука соответственно в первом и втором голосах. Если в регистрах D и E задать близкие значения, то вместо двух различных тонов получится звук приятного тембра, слегка вибрирующий и как бы объемный. Послушайте, например, такое звучание:
LD BC,500 ;длительность звучания LD D,251 ;высота первого тона LD E,250 ;высота второго тона CALL TWOTON RET
Не меньшим спросом в игровых программах пользуются и различные шумовые эффекты, имитирующие выстрелы, разрывы снарядов, стук копыт и т. п. Такие звуки характеризуются отсутствием какой-то определенной частоты - в них присутствуют частоты всего спектра. Это так называемый «белый» шум. Но обычно в шуме все же преобладают тона определенной высоты, что позволяет отличить шипение змеи от грохота обвала. И это необходимо учитывать при создании нужного эффекта.
Первый звук этого типа, который мы хотим предложить, при подборе соответствующей длительности позволяет имитировать звуки от хлопков в ладоши до шипения паровоза, выпускающего пар. Продолжительность звучания задается в регистровой паре DE, однако ее значение не должно превышать 16384, так как в качестве генератора «случайных» чисел, определяющих частоту тона используются коды ПЗУ:
HISS LD A,(23624) AND #38 RRA RRA RRA LD B,A LD HL,0 ;начальный адрес ПЗУ DI HISS1 LD A,(HL) ;берем байт в аккумулятор AND 16 ;выделяем 4-й бит OR B ;объединяем с цветом бордюра OUT (254),A ;получаем звук INC HL ;переходим к следующему байту DEC DE ;уменьшаем значение длительности LD A,D OR E JR NZ,HISS1 ;переходим на начало, ; если звук не закончился EI RET
Следующий пример немного похож на предыдущий, но позволяет выделять более низкие частоты. При наличии воображения его вполне можно сравнить с отдаленными раскатами грома. Продолжительность звучания здесь определяется в регистре B:
CRASH LD A,(23624) AND #38 RRA RRA RRA DI LD HL,100 ;начальный адрес в ПЗУ CRASH1 XOR 16 OUT (254),A ;извлекаем звук LD C,A ;сохраняем значение аккумулятора LD E,(HL) ;получаем в паре DE INC HL ; продолжительность цикла задержки LD A,(HL) AND 3 ;ограничиваем величину старшего байта LD D,A CRASH2 LD A,D ;цикл задержки OR E JR Z,CRASH3 DEC DE JR CRASH2 CRASH3 LD A,C ;восстанавливаем значение аккумулятора DJNZ CRASH1 EI RET
Неплохие результаты можно получить, если изменять во времени среднюю частоту выводимого шума. Ниже приведен текст подпрограммы, построенной именно по такому принципу:
EXPLOS LD A,(23624) AND #38 RRA RRA RRA LD L,A DI EXPL1 PUSH BC PUSH DE EXPL2 PUSH DE EXPL3 LD B,E DJNZ $ ;задержка LD A,(BC) ;в паре BC один из первых 256 адресов ПЗУ AND 16 OR L OUT (254),A INC C DEC D JR NZ,EXPL3 POP DE ; Изменение высоты шума (понижение среднего тона; ; если заменить на DEC E, тон будет наоборот повышаться) INC E DEC D JR NZ,EXPL2 POP DE POP BC DJNZ EXPL1 ;повторение всего эффекта EI RET
Перед обращением к ней в регистр B заносится количество повторений эффекта (что позволяет получить звук, напоминающий описанный выше эффект «Laser»), в D задается длительность звучания и в E - величина, определяющая начальную среднюю частоту. Например, для создания звука, напоминающего взрыв бомбы, можно предложить такие значения регистров:
LD B,1 LD D,100 LD E,-1 CALL EXPLOS RET
а пулеметную очередь можно получить с другими исходными данными:
LD B,5 LD D,35 LD E,0 CALL EXPLOS RET
У всех перечисленных процедур есть один общий недостаток: на время звучания выполнение основной программы затормаживается. Существует несколько способов обхода этой неприятности. Наиболее удачным из них представляется использование прерываний. Конечно, в этом случае получить чистый тон окажется совершенно невозможно, но для создания звуковых имитаций это и не особенно важно.
Поясним суть идеи. Пятьдесят раз в секунду с приходом очередного сигнала прерываний программа приостанавливается и выполняется процедура, извлекающая серию коротких звуков - по звуку на прерывание. Понятно, что звуки должны быть действительно очень короткими, иначе толку от прерываний будет чуть.
Основная сложность в написании такой процедуры состоит, пожалуй, только в способе передачи параметров подпрограмме, генерирующей тон. Поскольку прерывания выполняются автономно и не зависят от работы основной программы, входные значения не могут передаваться через регистры, а только через память. То есть необходимо составить блок данных, описывающих высоту и длительность каждого отдельного звука - по два байта на каждый. Адреса начальной и текущей пары значений в блоке данных также должны передаваться через переменные, чтобы каждое очередное прерывание «знало», какие параметры требуется считывать. Кроме этого нужна еще одна переменная, которая будет выполнять роль флага разрешения извлечения звука. Эта же переменная может служить счетчиком циклов, если вы хотите иметь возможность многократно исполнять запрограммированный в блоке данных фрагмент.
Учитывая все сказанное, можно написать такую процедуру обработки прерываний:
INTERR PUSH AF ;сохраняем используемые PUSH BC ; в прерывании регистры PUSH HL INTER1 LD A,(REPEAT) AND A ; Если эффект прозвучал нужное количество раз, ; завершаем обработку прерывания JR Z,EXITI LD HL,(CURADR) ;определяем текущий адрес ; в блоке данных LD B,(HL) ;высота звука INC B DEC B ; Если встретился маркер конца блока данных, ; переходим к следующему повторению JR Z,EXITI0 INC HL LD C,(HL) ;длительность звука INC HL LD (CURADR),HL ;запоминаем текущий адрес CALL BEEP ;извлекаем звук EXITI POP HL ;восстанавливаем регистры POP BC POP AF JP 56 ;переходим к стандартному ; обработчику прерываний ; Переход к началу эффекта - повторение EXITI0 LD HL,(ADREFF) ;восстанавливаем начальный LD (CURADR),HL ; адрес блока данных LD HL,REPEAT DEC (HL) ;уменьшаем счетчик повторений JR INTER1 REPEAT DEFB 0 ;количество повторений эффекта ADREFF DEFW 0 ;начальный адрес блока данных эффекта CURADR DEFW 0 ;текущий адрес в блоке данных ; Извлечение звука BEEP XOR A BEEP1 XOR 16 OUT (254),A PUSH BC DJNZ $ POP BC DEC C JR NZ,BEEP1 RET
Составив процедуру обработки прерываний, нужно теперь позаботиться об управлении ею. В первую очередь необходимо установить второй режим прерываний, как это было показано в предыдущей главе:
IMON XOR A ;в начале на всякий случай LD (REPEAT),A ; запрещаем вывод звука LD A,24 ;код команды JR LD (65535),A LD A,195 ;код команды JP LD (65524),A LD HL,INTERR ;переход на процедуру LD (65525),HL ; обработки прерываний LD HL,#FE00 ;формируем таблицу векторов прерываний LD DE,#FE01 LD BC,256 LD (HL),#FF ;на адрес 65535 (#FFFF) LD A,H ;запоминаем старший байт адреса таблицы LDIR DI LD I,A ;загружаем регистр вектора прерываний IM 2 ;включаем 2-й режим EI RET
Сразу же напишем и процедуру восстановления первого режима прерываний, которая будет вызываться при окончании работы программы. Она вам уже известна, но тем не менее повторим:
IMOFF DI LD A,63 LD I,A IM 1 EI RET
Теперь можно написать блоки данных, характеризующие различные эффекты. При их составлении нужно учитывать две вещи: во-первых, как мы уже говорили, каждый звук должен быть достаточно коротким, чтобы он не задерживал выполнение основной программы (не более нескольких сотых долей секунды), а во-вторых, при увеличении первого параметра (высота тона) второй (длительность звучания) нужно уменьшать, иначе более низкие звуки окажутся и более продолжительными. Завершаться каждый блок данных обязан нулевым байтом, обозначающим конец звучания эффекта и переход на его начало. Приведем два приблизительных варианта:
EFF1 DEFB 200,5,220,4,200,5 DEFB 100,8,80,9,50,20 DEFB 0 EFF2 DEFB 50,20,100,6,200,3,100,6 DEFB 0
Наконец, напишем управляющую часть, которая позволит легко обратиться к любой подпрограмме: включения или выключения второго режима прерываний, а также активизации того или иного эффекта. Напомним, что для «запуска» любого из заданных в блоках данных эффектов необходимо сначала занести в переменные ADREFF и CURADR начальный адрес соответствующего блока и в переменной REPEAT указать количество повторений звука.
Чтобы любую процедуру было удобно вызывать даже из Бейсика, не имеющего ни малейшего представления о метках ассемблерного текста, применим распространенный прием, часто используемый в таких случаях и которым мы однажды уже воспользовались (см. программу ) - вставим в самом начале программы в машинных кодах ряд инструкций «длинного» перехода (JP) с указанием адресов каждой из «внешних», то есть вызываемых из другой программы, процедур. Тогда адреса обращения к любой из них будут увеличиваться с шагов в 3 байта (размер команды JP). Таким образом, наш пакет процедур будет выглядеть так:
ORG 60000 ; 60000 - включение второго режима прерываний JP IMON ; 60003 - переход к подпрограмме выключения 2-го режима прерываний JP IMOFF ; 60006 - включение эффекта1 JP ONEFF1 ; 60009 - включение эффекта2 ONEFF2 LD HL,EFF2 LD A,5 JR ONEFF ONEFF1 LD HL,EFF1 LD A,3 ONEFF LD (ADREFF),HL LD (CURADR),HL LD (REPEAT),A RET ; Блоки данных эффектов
; Инициализация второго режима прерываний
; Выключение второго режима прерываний
; Процедура обработки прерываний
А вот фрагмент программы на Бейсике, демонстрирующий использование приведенных звуковых эффектов:
10 INK 5: PAPER 0: BORDER 0: CLEAR 59999 20 RANDOMIZE : LET x=INT (RND*30)+1: LET y=INT (RND*20)+1 30 LET dx=1: IF RND>=.5 THEN LET dx=-1 40 LET dy=1: IF RND>=.5 THEN LET dy=-1 50 INK 2: PLOT 0,0: DRAW 255,0: DRAW 0,175: DRAW -255,0: DRAW 0,-175: INK 5 60 RANDOMIZE USR 60000 100 PRINT AT y,x; INK 8; OVER 1;"O": LET x1=x: LET y1=y: PAUSE 3 110 IF x=0 OR x=31 THEN RANDOMIZE USR 60006: LET dx=-dx 120 IF y=0 OR y=21 THEN RANDOMIZE USR 60009: LET dy=-dy 130 LET x=x+dx: LET y=y+dy 140 IF INKEY$<>"" THEN GO TO 200 150 PRINT AT y1,x1; INK 8; OVER 1;"O": GO TO 100 200 RANDOMIZE USR 60003
После запуска этой программки экран окрасится в черный цвет, по краю его будет нарисована рамка и в случайном месте возникнет шарик, который начнет метаться из стороны в сторону, отскакивая от «стенок». При соприкосновении с преградами будет раздаваться протяжный вибрирующий звук, причем разный в зависимости от того, в горизонтальную или в вертикальную «стенку» ударился мячик. Для остановки программы достаточно нажать любую клавишу.
СПРАЙТ-ГЕНЕРАТОР
СПРАЙТ-ГЕНЕРАТОР
Можно по разному создавать блоки данных для спрайтов, начиная с самого простого способа, когда изображение сначала рисуется на бумаге, а затем выписываются его коды байт за байтом. Можно воспользоваться приведенной нами в четвертой главе программой, для которой сначала создается фонт (например, в Art Studio), соответствующий одному или сразу нескольким спрайтам, после чего коды все равно требуется записать и только затем уж вводить в программу. Оба варианта требуют затрат большого труда и времени и оправдывают себя лишь в случаях небольших спрайтов (порядка 1 - 6 знакомест). Учитывая все это, мы сочли необходимым предложить программу, которая полностью исключает какие-либо записи, а формируемые ею кодовые блоки можно сразу встраивать в создаваемые вами игры.
Программа состоит из двух частей - бейсиковской и кодовой. Если вы работаете с магнитофоном, то программу на Бейсике можно исполнять сразу, если же с дисководом, то три строки текста следует заменить (какие именно, сказано ниже). Затем введите и оттранслируйте ассемблерную часть, создав соответствующий кодовый файл. Теперь можно работать со спрайтами. После ввода и старта программы на вашем экране появится меню. Если вы предварительно просмотрите текст программы, то легко обнаружите, какие функции она может выполнять, тем не менее коротко прокомментируем эти опции.
Load Screen - загрузка экранного файла
Create Sprite - создание спрайта
Save Sprite - сохранение спрайт-файла
New Sprite - удаление спрайтов из памяти для начала создания нового спрайт-файла
View Table - просмотр таблицы смещений спрайтов в спрайт-файле
Quit Program - выход из программы
Нажимая клавиши Q и A, можно перемещать курсор в виде инвертированной полоски вверх или вниз по строчкам меню. Отметив курсором нужный пункт, нажмите клавишу M для выполнения функции.
Прежде всего необходимо загрузить экранный файл, для чего предназначен первый пункт меню Load Screen. Внизу экрана появится запрос Screen name:, на который нужно ввести имя загружаемой картинки со спрайтами. После загрузки экранного файла программа снова выйдет в меню.
После этого можно «вырезать» с картинки спрайты, выбрав следующий пункт Create Sprite. Окно с меню исчезнет с экрана и останется только загруженная картинка и маленький пунктирный квадратик. С помощью клавиш Q, A, O и P поместите его в верхний левый угол выбранного спрайта и нажмите клавишу M, чтобы зафиксировать местоположение квадратика на экране. Затем, управляя теми же клавишами, расширьте его до нужных размеров, чтобы спрайт полностью поместился внутри отмеченной пунктиром области и еще раз нажмите клавишу M. Возврат в меню покажет, что спрайт успешно закодирован - можно создавать следующий. Если создано уже достаточно много спрайтов и все они имеют значительные размеры, то памяти может не хватить. В этом случае программа выдаст сообщение Out of memory! Вы можете сохранить полученный спрайт-файл, вызвав опцию Save Sprite, и начать создание следующего, предварительно очистив память, выбрав пункт New Sprite.
Перед сохранением спрайт-файла нужно будет ввести его имя, под которым он будет записан на внешний носитель, а перед удалением спрайтов из памяти потребуется подтвердить свое намерение, нажав клавишу Y.
Последняя опция Quit Program в особых комментариях не нуждается, поэтому скажем только, что во избежание случайного выхода (а следовательно, и потери данных) нужно будет также подтвердить или опровергнуть выбор.
Сообщим еще общие «эксплуатационные» характеристики спрайт-генератора: каждый вновь создаваемый спрайт может занимать площадь до 255 знакомест, если вам захочется чуть больше - описывающий прямоугольник все равно не позволит, сколько бы ни старались. Максимальное количество спрайтов для одного спрайт-файла - 22, после чего его необходимо сохранить. И последнее, создаются спрайты только прямоугольной формы.
Для того чтобы вам легче было разобраться в этой сервисной программе, приведем расшифровку используемых обозначений, а также дадим краткое описание ее основной части - функции создания спрайта:
Массивы:
m$(6,13) - наименования опций меню
s(22) - смещения спрайтов относительно начала спрайт-файла
Переменные:
spr - количество созданных спрайтов
addr - адрес следующего спрайта
col, row - координаты окон и спрайтов ( переменная row используется также для определения позиции курсора меню)
len, hgt - размеры окон и спрайтов
pap - цвет PAPER окон
k$ - символ нажатой клавиши
Константы:
scr - адрес «теневого» экрана
ad0 - адрес начала спрайт-файла
ramka, svscr, restor, clsv, setv, gtbl - адреса одноименных процедур
Опишем «центральную» подпрограмму ГЕНЕРАТОРА СПРАЙТОВ - подпрограмму создания спрайтов:
2010 - если создано 22 спрайта, сообщение о том, что спрайт-файл завершен. Необходимо его сохранить и начать новый.
2020 - вывод экранной картинки.
2030 - определение начальных значений переменных «вырезаемого» спрайта.
2040 - вывод по заданному размеру и в заданном месте экрана пунктирной рамки, отмечающей будущий спрайт.
2045..2090 - установка рамки в верхний левый угол «вырезаемого» спрайта.
2100 - удаление рамки и звуковой сигнал после нажатия клавиши M.
2110 - вывод рамки.
2130..2170 - выбор желаемого размера спрайта.
2200 - кодирование спрайта.
2210 - если процедура gtbl возвращает ненулевое значение, то рассчитывается величина смещения спрайта от начала спрайт-файла, а переменная addr указывает на конец спрайт-файла.
2220..2240 - выдается сообщение о нехватке памяти для создания спрайта заданных размеров. Можно сохранить спрайт-файл и начать новый или попытаться создать спрайт меньших размеров.
10 POKE 23693,40: BORDER 5: CLS 20 DIM m$(6,13): FOR n=1 TO 6: READ m$(n): NEXT n 30 DIM s(22): LET spr=0 40 LET scr=30000: LET ad0=36912: LET addr=ad0 50 LET ramka=65000: LET svscr=65003: LET restor=65006: LET clsv=65009: LET setv=65012: LET gtbl=65015 60 RANDOMIZE USR svscr 100 REM --- МЕНЮ --- 110 RANDOMIZE USR restor: LET row=6: LET col=8: LET len=15: LET hgt=13: LET pap=7: GO SUB 8000 120 LET row=0 130 IF row<0 THEN LET row=5 135 IF row>5 THEN LET row=0 140 FOR n=0 TO 5: PRINT PAPER 7; BRIGHT 1;AT 7+n*2,9;m$(n+1): NEXT n 150 PRINT INVERSE 1; BRIGHT 1;AT 7+row*2,9;m$(row+1) 160 PAUSE 0: LET k$=INKEY$ 170 IF k$="a" OR k$="A" THEN BEEP .01,20: LET row=row+1: GO TO 130 180 IF k$="q" OR k$="Q" THEN BEEP .01,20: LET row=row-1: GO TO 130 190 IF k$<>"m" AND k$<>"M" THEN GO TO 150 200 BEEP .01,20: GO SUB (row+1)*1000 210 GO TO 100 1000 REM --- ЗАГРУЗКА ЭКРАННОЙ КАРТИНКИ --- 1010 INPUT "Screen name: "; LINE n$ 1020 LOAD n$CODE 16384,6912 1030 RANDOMIZE USR svscr: RETURN 2000 REM --- СОЗДАНИЕ СПРАЙТОВ --- 2010 IF spr=22 THEN LET row=11: LET col=4: LET len=23: LET hgt=3: LET pap=3: GO SUB 8000: PRINT AT row+1,col+1; PAPER pap; BRIGHT 1;"Sprite-file complete!": BEEP 1,-20: PAUSE 0: RETURN 2020 RANDOMIZE USR restor 2030 LET spr=spr+1: LET row=12: LET col=15: LET len=1: LET hgt=1: POKE 23303,len: POKE 23304,hgt 2040 POKE 23301,col: POKE 23302,row: RANDOMIZE USR ramka 2045 PAUSE 0: LET k$=INKEY$ 2050 IF (k$="q" OR k$="Q") AND row>0 THEN RANDOMIZE USR ramka: LET row=row-1: GO TO 2040 2060 IF (k$="a" OR k$="A") AND row<24-hgt THEN RANDOMIZE USR ramka: LET row=row+1: GO TO 2040 2070 IF (k$="o" OR k$="O") AND col>0 THEN RANDOMIZE USR ramka: LET col=col-1: GO TO 2040 2080 IF (k$="p" OR k$="P") AND col<32-len THEN RANDOMIZE USR ramka: LET col=col+1: GO TO 2040 2090 IF k$<>"m" AND k$<>"M" THEN GO TO 2045 2100 RANDOMIZE USR ramka: BEEP .01,20 2110 POKE 23303,len: POKE 23304,hgt: RANDOMIZE USR ramka 2120 PAUSE 0: LET k$=INKEY$ 2130 IF (k$="a" OR k$="A") AND hgt<24-row AND len*(hgt+1)<256 THEN RANDOMIZE USR ramka: LET hgt=hgt+1: GO TO 2110 2140 IF (k$="q" OR k$="Q") AND hgt>1 THEN RANDOMIZE USR ramka: LET hgt=hgt-1: GO TO 2110 2150 IF (k$="o" OR k$="O") AND len>1 THEN RANDOMIZE USR ramka: LET len=len-1: GO TO 2110 2160 IF (k$="p" OR k$="P") AND len<32-col AND (len+1)*hgt<256 THEN RANDOMIZE USR ramka: LET len=len+1: GO TO 2110 2170 IF k$<>"m" AND k$<>"M" THEN GO TO 2120 2200 BEEP .01,20: POKE 23300,hgt*len: RANDOMIZE addr: LET ad=USR gtbl 2210 IF ad THEN LET s(spr)=addr-ad0: LET addr=ad: RETURN 2220 LET col=6: LET row=11: LET hgt=3: LET len=20: LET pap=2 2230 GO SUB 8000: PRINT AT row+1,col+3; PAPER pap; BRIGHT 1;"Out of memory!" 2240 LET spr=spr-1: BEEP 1,-20: PAUSE 0: RETURN 3000 REM --- СОХРАНЕНИЕ СПРАЙТ-ФАЙЛА --- 3010 IF NOT spr THEN RETURN 3020 INPUT "Sprite name: "; LINE n$ 3030 SAVE n$CODE ad0,addr-ad0 3040 RETURN 4000 REM --- УДАЛЕНИЕ СПРАЙТ-ФАЙЛА ИЗ ПАМЯТИ --- 4010 IF NOT spr THEN RETURN 4020 GO SUB 7000: IF k$<>"y" THEN RETURN 4030 LET spr=0: LET addr=ad0: RETURN 5000 REM --- ПРОСМОТР ТАБЛИЦЫ СМЕЩЕНИЙ СПРЙТОВ --- 5010 CLS : IF NOT spr THEN RETURN 5020 FOR n=1 TO spr: PRINT "Sprite No ";n,"Offset == ";s(n): NEXT n 5030 PAUSE 0: RETURN 6000 REM --- ВЫХОД ИЗ ПРОГРАММЫ --- 6010 GO SUB 7000: IF k$<>"y" THEN RETURN 6020 CLEAR : STOP 7000 REM --- ЗАПРОС --- 7010 LET row=11: LET col=5: LET len=21: LET hgt=3: LET pap=6: GO SUB 8000 7020 PRINT AT row+1,col+1; PAPER pap; BRIGHT 1; "Are you shure (Y/N)?" 7030 PAUSE 0: LET k$=INKEY$: IF k$="Y" THEN LET k$="y" 7040 BEEP .01,20: RETURN 8000 REM --- ОКНА --- 8010 POKE 23301,col: POKE 23302,row: POKE 23303,len: POKE 23304,hgt 8020 RANDOMIZE USR clsv: PRINT PAPER pap; BRIGHT 1;: RANDOMIZE USR setv 8030 LET l=len*8-1: LET h=hgt*8-1 8040 PLOT col*8,175-row*8: DRAW l,0: DRAW 0,-h: DRAW -l,0: DRAW 0,h 8050 RETURN 9000 REM --- ДАННЫЕ МЕНЮ --- 9010 DATA "Load Screen" 9020 DATA "Create Sprite" 9030 DATA "Save Sprite" 9040 DATA "New Sprite" 9050 DATA "View Table" 9060 DATA "Quit Program" 9900 REM --- АВТОСТАРТ ПРОГРАММЫ --- 9910 POKE 23693,40: BORDER 5: CLEAR 29999 9920 LOAD "sptgen"CODE 9930 RUN
Если вы работаете в системе TR-DOS, то несколько строк этой программы следует заменить на приведенные ниже:
1020 RANDOMIZE USR 15619: REM : LOAD n$CODE 16384,6912 3030 RANDOMIZE USR 15619: REM : SAVE n$CODE ad0,addr-ad0 9920 RANDOMIZE USR 15619: REM : LOAD "sptgen"CODE
и только после этого использовать.
Некоторые процедуры, как вы заметили, написаны на ассемблере и вызываются функцией USR. При использовании ряда подпрограмм в машинных кодах из Бейсика возникает проблема, как определить адреса обращения к ним. Можно, конечно, оттранслировать каждую из них отдельно, задав для каждой определенный начальный адрес или в одном исходном файле указать несколько директив ORG. Но при этом возникнут другие сложности, связанные с компоновкой программы. Можно также, оттранслировав весь пакет процедур как единое целое, просмотреть затем полученные коды с помощью дизассемблера и найти точки входа в каждую подпрограмму. Но при этом, если потребуется внести в текст какие-либо изменения (а особенно часто это придется делать на этапе отладки), то всю работу по определению адресов придется повторять с начала. В связи с этим мы предлагаем вам наиболее простой способ, часто применяемый в подобных ситуациях: в начале ассемблерного текста нужно вставить ряд команд JP, передающих управление всем процедурам пакета, к которым имеется обращение из Бейсика (либо из другого языка). Зная, что команда JP в памяти занимает 3 байта, несложно вычислить адрес любой процедуры по ее «порядковому номеру». Впоследствии мы еще не раз воспользуемся этим методом, поэтому мы и обратили на него ваше внимание.
Основная часть пакета - это подпрограмма GTBL, сохраняющая в памяти образ экрана в принятом для процедуры PTBL формате спрайтов. Подпрограмма OUT_BT также относится к ней. При вызове GTBL в переменной __SP сохраняется начальное состояние указателя стека SP. Делается это для корректного выхода в Бейсик в случае возникновения ошибки (Out of memory - нехватка памяти).
Подпрограмма RAMKA выводит на экран пунктирный прямоугольник, отмечающий границы создаваемого спрайта. Вывод производится по принципу XOR, поэтому при повторном обращении к процедуре прежний вид экрана полностью восстанавливается.
Подпрограмма SVSCR нужна для сохранения экранного изображения в памяти для последующего его восстановления процедурой RESTOR.
В пакет включены также две описанные ранее процедуры CLSV и SETV для очистки окна экрана и установки в нем постоянных атрибутов.
ORG 65000 ORIGIN EQU $ ;верхняя допустимая граница спрайт-файла ADDR EQU 23670 ;текущий адрес в спрайт-файле ATTR EQU 23695 ;значение атрибутов окна SCREEN EQU 30000 ;адрес «теневого» экрана N_SYM EQU 23300 ;рассчитанная в Бейсике площадь спрайта COL EQU 23301 ;координаты спрайта ROW EQU 23302 LEN EQU 23303 ;размеры спрайта HGT EQU 23304 ; 65000 JP RAMKA ; 65003 JP SVSCR ; 65006 JP RESTOR ; 65009 JP CLSV ; 65012 JP SETV ; 65015 GTBL CALL RESTOR ;восстанавливаем экранную картинку LD (__SP),SP ;запоминаем состояние стека для ; возврата при возникновении ошибки LD IX,(ADDR) ;адрес конца спрайт-файла ; Формирование заголовка LD A,(N_SYM) ;количество знакомест ; в создаваемом спрайте CALL OUT_BT ;записываем первый байт в спрайт-файл LD A,(ROW) ;вычисляем адрес атрибутов LD L,A LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL LD A,#58 ADD A,H LD H,A LD A,(COL) ADD A,L LD L,A LD DE,(LEN) LD BC,0 GTBL1 PUSH BC PUSH DE PUSH HL GTBL2 LD A,B CALL OUT_BT ;позиция по вертикали внутри спрайта LD A,C CALL OUT_BT ;позиция по горизонтали LD A,(HL) CALL OUT_BT ;байт атрибутов INC HL INC C DEC E JR NZ,GTBL2 POP HL LD DE,32 ;переходим к следующей строке ADD HL,DE POP DE POP BC INC B DEC D JR NZ,GTBL1 ; Данные состояния пикселей LD A,(HGT) LD B,A LD A,(ROW) GTBL3 PUSH AF PUSH BC CALL 3742 ;вычисляем адрес начального знакоместа LD A,(COL) ADD A,L LD L,A LD A,(LEN) LD B,A GTBL4 PUSH BC PUSH HL LD B,8 ;переписываем в спрайт-файл 8 байт ; знакоместа GTBL5 LD A,(HL) CALL OUT_BT INC H DJNZ GTBL5 POP HL INC HL ;переходим к следующему знакоместу POP BC DJNZ GTBL4 POP BC POP AF INC A ;переходим к следующей строке DJNZ GTBL3 PUSH IX ;возвращаем в Бейсик адрес POP BC ; конца спрайт-файла RET OUT_BT PUSH BC ;запись в спрайт-файл байта из A PUSH HL ; Проверка наличия свободной памяти PUSH IX POP HL LD BC,ORIGIN ;адрес конца свободной памяти ; для спрайт-файла AND A ;очистка флага CY перед вычитанием ; (если этого не сделать, результат будет неверен!) SBC HL,BC ;если текущий адрес достиг ORIGIN, JR NC,OUTRAM ; происходит выход в Бейсик POP HL POP BC LD (IX),A ;записываем байт в спрайт-файл INC IX ;увеличиваем адрес размещения кодов RET OUTRAM LD SP,(__SP) ;восстанавливаем значение стека LD BC,0 ;возвращаем в Бейсик код ошибки RET __SP DEFW 0 ;переменная для сохранения указателя стека ; Рисование прямоугольной пунктирной рамки RAMKA LD A,(ROW) PUSH AF CALL 3742 ;вычисляем адрес экрана LD A,(COL) ADD A,L LD L,A CALL HOR ;проводим верхнюю линию CALL VERT1 ;рисуем боковые стороны в первой ; строке окна LD A,(HGT) DEC A JR Z,RAMK2 ;обходим, если единственная строка LD B,A ;иначе рисуем боковые стороны по всей ; высоте окна POP AF RAMK1 PUSH AF CALL VERT ;заканчиваем предыдущую строку POP AF INC A ;переходим к следующей PUSH AF CALL 3742 ;вычисляем адрес экрана LD A,(COL) ADD A,L LD L,A CALL VERT ;ставим верхние точки CALL VERT1 ;заканчиваем вертикальный пунктир POP AF DJNZ RAMK1 ;повторяем PUSH AF RAMK2 POP AF ; Горизонтальная пунктирная линия HOR PUSH BC PUSH HL LD A,(LEN) ;рисуем пунктир по ширине окна LD B,A HOR1 LD A,%10011001 ;фактура пунктирной линии XOR (HL) ;объединяем с экранным изображением LD (HL),A ;возвращаем на экран INC HL DJNZ HOR1 POP HL POP BC RET ; Рисование двух точек для боковых сторон рамки VERT PUSH HL LD A,128 ;левая точка XOR (HL) LD (HL),A LD A,(LEN) ;ищем адрес правой стороны окна DEC A ADD A,L LD L,A LD A,1 ;правая точка XOR (HL) LD (HL),A POP HL RET ; Боковые стороны рамки по высоте знакоместа VERT1 INC H ;пропускаем 3 ряда пикселей INC H INC H CALL VERT ;ставим точки на левой и правой ; сторонах прямоугольника INC H CALL VERT ;повторяем для следующего ряда INC H ;делаем следующий промежуток INC H INC H RET ; Сохранение области видеобуфера в «теневом» экране SVSCR LD HL,16384 LD DE,SCREEN LD BC,6912 LDIR RET ; Восстановление изображения на экране RESTOR LD HL,SCREEN LD DE,16384 LD BC,6912 LDIR RET ; Подпрограмма очистки окна
; Подпрограмма установки атрибутов в окне
Спрайт из игры FIST
Рисунок 7.1. Спрайт из игры FIST
Рассмотрим пример вывода спрайта произвольной конфигурации (Рисунок 7.1). Не правда ли, многие узнали в нем одного из персонажей игры FIST. Все дело в том, что этот спрайт как нельзя лучше демонстрирует эффективность предложенной нами процедуры PTBL, поскольку его форма заметно отличается от прямоугольной. Управляющая часть программы получилась довольно короткой:
ORG 60000 ENT $ LD A,48 ;INK 0: PAPER 6 LD (23693),A XOR A ;BORDER 0 CALL 8859 CALL 3435 LD A,2 CALL 5633 LD B,10 ;ROW LD C,15 ;COL LD A,SPRPUT ;вывод с уничтожением предыдущего ; изображения LD HL,SPR1 ;чтение адреса спрайта SPR1 CALL PTBL RET
; Заголовок спрайта SPR1 DEFB 22 DEFB 0,3,48, 0,4,48, 1,3,48, 1,4,48, 2,2,48 DEFB 2,3,48, 2,4,48, 2,5,48, 2,6,48, 3,2,48 DEFB 3,3,48, 3,4,48, 4,0,48, 4,1,48, 4,2,48 DEFB 4,3,48, 4,4,48, 5,0,48, 5,1,48, 5,2,48 DEFB 5,3,48, 5,4,48 ; Данные спрайта DEFB 0,0,0,0,3,12,209,46 DEFB 0,0,0,0,192,32,224,240 DEFB 249,34,38,43,43,48,35,248 DEFB 16,112,56,8,16,48,16,240 DEFB 3,6,31,61,61,59,59,27 DEFB 254,255,255,255,255,255,255,255 DEFB 64,224,255,255,255,255,255,255 DEFB 0,0,255,192,224,243,252,240 DEFB 0,248,4,2,2,250,50,28 DEFB 31,7,5,6,7,15,15,31 DEFB 255,255,254,229,3,247,247,235 DEFB 240,0,0,0,252,254,254,254 DEFB 0,0,0,0,0,62,103,77 DEFB 0,0,0,0,0,0,249,255 DEFB 31,31,63,63,127,255,255,255 DEFB 235,219,219,219,252,224,193,131 DEFB 254,254,254,126,254,252,252,248 DEFB 65,67,71,69,34,30,0,0 DEFB 255,255,255,255,63,7,0,0 DEFB 255,254,252,248,240,224,0,0 DEFB 3,3,1,2,2,1,0,0 DEFB 248,248,152,198,1,255,0,0
Есть смысл немного прокомментировать приведенные выше числовые данные, которые полностью соответствуют описанному выше формату. В заголовке перечислены (например, для первой строки):
22 - общее количество знакомест в спрайте,
0 - координата Y первого выводимого на экран знакоместа, взятая относительно левого верхнего угла описывающего прямоугольника,
3 - координата X того же знакоместа,
48 - суммарные атрибуты знакоместа: PAPER 6, INK 0.
Далее идут тройки чисел, относящиеся к другим знакоместам спрайта и в последовательности, указанной выше: координата Y, координата X, суммарные атрибуты.
Спрайт «вертолет»
Рисунок 8.2. Спрайт «вертолет»
ORG 60000 ENT $ LD A,5 LD (23693),A XOR A CALL 8859 CALL 3435 ; Основная часть программы LD BC,#505 ;задаем исходное положение вертолета KEY PUSH BC CALL KBDJOY ;читаем данные из портов POP BC RRCA ;поворачиваем ручку джойстика вправо ; или нажимаем клавишу P - ; полет вертолета вправо JR NC,KEY1 INC C KEY1 RRCA ;поворачиваем ручку джойстика влево ; или нажимаем клавишу O - ; полет вертолета влево JR NC,KEY2 DEC C KEY2 RRCA ;поворачиваем ручку джойстика вниз ; или нажимаем клавишу A - ; полет вертолета вниз JR NC,KEY3 INC B KEY3 RRCA ;поворачиваем ручку джойстика вверх ; или нажимаем клавишу Q - ; полет вертолета вверх JR NC,KEY4 DEC B KEY4 RRCA ;при нажатии кнопки «огонь» джойстика ; или клавиши M - выход RET C ; Подпрограмма вывода на экран изображения вертолета в двух фазах, ; каждая из которых соответствует одному из положений винта XOR A ;формируем звуковой сигнал, OUT (254),A ; имитирующий работу двигателя CALL CHECK ;проверка достижения границ экрана LD A,16 OUT (254),A LD A,SPRXOR ;задаем режим вывода спрайта LD HL,WERT1 ;устанавливаем адрес спрайта PUSH BC PUSH HL CALL PTBL ;выводим вертолет в первой фазе LD BC,5 ;задаем задержку между фазами CALL 7997 ; вращения винта POP HL POP BC LD A,SPRXOR ;режим вывода спрайта PUSH BC PUSH HL CALL PTBL ;стираем вертолет в первой фазе POP HL POP BC XOR A OUT (254),A ; Вывод вертолета во второй фазе LD A,16 ;звуковой сигнал OUT (254),A LD A,SPRXOR ;режим вывода спрайта LD HL,WERT2 ;устанавливаем адрес спрайта ; с другим расположением винта PUSH BC PUSH HL CALL PTBL ;выводим спрайт во второй фазе LD BC,5 CALL 7997 POP HL POP BC LD A,SPRXOR ;режим вывода спрайта PUSH BC PUSH HL CALL PTBL ;стираем с экрана спрайт во второй фазе POP HL POP BC JR KEY ; Подпрограмма проверки границ экрана CHECK LD A,C AND A ;сравниваем координату X вертолета ; с заданной левой границей экрана JR NZ,CONT1 INC C CONT1 CP 29 ;сравниваем координату X вертолета ; с заданной правой границей экрана JR NZ,CONT2 DEC C CONT2 LD A,B ;задаем верхнюю границу экрана AND A ;сравниваем координату Y вертолета ; с заданной верхней границей экрана JR NZ,CONT3 INC B CONT3 CP 21 ;сравниваем координату Y вертолета ; с заданной нижней границей экрана RET NZ DEC B RET ; Подпрограмма чтения данных из портов клавиатуры и джойстика KBDJOY IN A,(31) ;опрашиваем порт джойстика LD E,A ;запоминаем полученные биты ; Проверяем, подключен ли порт джойстика (ручку невозможно ; повернуть сразу и вправо и влево - если оба бита установлены, ; порт не подключен) AND 3 CP 3 JR NZ,KBDJ1 ;если да, переходим к опросу клавиатуры LD E,0 ; иначе очищаем коллектор битов KBDJ1 LD HL,DKEY ;адрес блока данных клавиатуры KBDJ2 LD C,(HL) ;младший байт адреса порта INC C ;проверка на 0 (конец блока данных) DEC C LD A,E ;значение коллектора в аккумулятор RET Z ;выход, если конец данных INC HL LD B,(HL) ;старший байт адреса порта INC HL IN A,(C) ;читаем из порта CPL ;инвертируем биты AND (HL) ;проверяем конкретный бит INC HL JR Z,KBDJ3 LD A,(HL) ;если бит установлен, читаем код направления OR E ; и объединяем с коллектором LD E,A KBDJ3 INC HL JR KBDJ2 ;продолжаем чтение ; Данные управляющих клавиш: ; первое число - младший байт порта ; второе число - старший байт порта ; третье число - маска бита ; четвертое - код направления (аналогично кодам джойстика) DKEY DEFB #FE,#FB,1,8 ;Q - вверх DEFB #FE,#FD,1,4 ;A - вниз DEFB #FE,#DF,2,2 ;O - влево DEFB #FE,#DF,1,1 ;P - вправо DEFB #FE,#7F,4,16 ;M - «огонь» DEFB 0 ;метка конца блока данных
; Заголовок первой фазы спрайта «вертолет» WERT1 DEFB 7 DEFB 0,1,6,1,0,6,1,1,6,1,2,6 DEFB 2,0,6,2,1,6,2,2,6 ; Данные первой фазы спрайта «вертолет» DEFB 0,0,4,28,56,32,24,24 DEFB 0,0,0,1,2,2,4,4 DEFB 60,60,255,129,66,36,36,24 DEFB 0,0,0,128,64,64,32,32 DEFB 7,2,1,0,1,2,4,14 DEFB 255,36,36,255,0,0,0,0 DEFB 224,64,128,0,128,64,32,112 ; Заголовок второй фазы спрайта «вертолет» WERT2 DEFB 9 DEFB 0,0,6,0,1,6,0,2,6 DEFB 1,0,6,1,1,6,1,2,6 DEFB 2,0,6,2,1,6,2,2,6 ; Данные второй фазы спрайта «вертолет» DEFB 0,0,30,127,255,127,14,0 DEFB 0,0,0,129,231,129,24,24 DEFB 0,0,120,254,255,254,112,0 DEFB 0,0,0,1,2,2,4,4 DEFB 60,60,255,129,66,36,36,24 DEFB 0,0,0,128,64,64,32,32 DEFB 7,2,1,0,1,2,4,14 DEFB 255,36,36,255,0,0,0,0 DEFB 224,64,128,0,128,64,32,112
СПРАЙТЫ (ПРОГРАММА «ПРЫГАЮЩИЙ ЧЕЛОВЕЧЕК»)
СПРАЙТЫ (ПРОГРАММА «ПРЫГАЮЩИЙ ЧЕЛОВЕЧЕК»)
Прояснив в какой-то степени вопрос о перемещении по экрану символов неплохо теперь заняться спрайтами и решить некоторые общие проблемы. Среди них: создание специальных блоков данных, удобных для кодирования и последующего вывода на экран спрайтов, разработка эффективной подпрограммы вывода этих спрайтов и, наконец, формирование самого подвижного изображения.
Рассмотрим один из вариантов решения этих вопросов (так как позже будут изложены и другие). Вначале коротко о том, что мы увидим на экране. После запуска программы появится простенький пейзаж, состоящий из зеленой травы и ветки дерева, на которой висит спелое яблоко. От земли отталкивается человечек и в прыжке пытается достать это яблоко. Для получения более или менее правдоподобного эффекта движения оказалось достаточным выводить человечка всего в двух фазах (Рисунок 5.6 а, б). Разберем теперь программу, которая все это делает.
СТАТИЧЕСКИЕ ЗАСТАВКИ
СТАТИЧЕСКИЕ ЗАСТАВКИ
Сейчас в нашем распоряжении имеется уже достаточно средств, чтобы попытаться изобразить на экране нечто вполне осмысленное и несколько более привлекательное, чем просто точки или окружности.
Во время игры на экран обычно выводится масса различной информации. Играющему нет никакой необходимости задумываться над тем, к какому типу эта информация принадлежит, анализировать и классифицировать происходящее на экране. Достаточно правильно нажимать клавиши в соответствии с указаниями, данными в программе. Но совсем другое отношение ко всему происходящему должно быть у человека, планирующего свою собственную игру.
Все кадры, появляющиеся на экране во время игры, можно условно разделить на два типа: статические и динамические. Кпервым можно отнести такие, в которых изображение со временем не меняется. Это могут быть, например, отдельные кадры многокадровой заставки, такие как Правила Игры или Таблица Результатов. Сюда же можно отнести различные информационные панели: схемы лабиринтов, карты места боевых действий и т. п.
Динамическими заставками (или кадрами) будем называть такие, в которых с течением времени что-то изменяется на экране. В простейшем случае это может быть изменение цвета рамки или надписей, подвижный курсор, возможность ввода с клавиатуры. В более сложных динамических кадрах появятся элементы мультипликации, перемещения спрайтов и пейзажей.
О создании динамических кадров мы поговорим позже, а сейчас разберемся со способами изготовления статических заставок.
Как и при программировании на Бейсике, мы рекомендуем вам прежде всего нарисовать на листе клетчатой бумаги прямоугольник размером 32 клетки по горизонтали и 24 - по вертикали (то есть по размеру экрана в знакоместах) и внутри изобразить все то, что вы хотите увидеть на экране. А затем составить блоки данных DEFB так, чтобы их осталось только распечатать командой CALL 8252.
Строение экрана
Как мы уже говорили, важность знания структуры экранной области трудно переоценить. Умение быстро рассчитывать адрес в видеобуфере по координатам экрана понадобится нам в большинстве графических построений, особенно, если вы хотите научиться обходиться без опеки операционной системы, которая почти всегда оказывается не достаточно быстродействующей.
Строение экрана «на высоком уровне» вам уже должно быть известно (Рисунок 2.2), но тем не менее, напомним, из каких частей он состоит. Внешняя область, называемая бордюром, может только изменять свой цвет, никакую графическую информацию, за исключением быстро бегущих по нему полос, в эту область поместить невозможно. Внутри бордюра находится рабочий экран, сюда может быть выведена любая текстовая или графическая информация. Говоря об экране, мы всегда будем подразумевать именно эту его часть. Рабочий экран в свою очередь делится на основной экран и служебное окно, которое обычно занимает две нижние строки, но в некоторых случаях может увеличиваться или уменьшаться. Всего экран имеет 24 текстовые строки, и в каждой строке можно напечатать 32 символа. Эти стандартные площадки для вывода символов называются знакоместами. Любое изображение на экране состоит из маленьких квадратиков, называемых пикселями, и каждое знакоместо имеет размеры 8 ґ 8 таких элементарных «точек».
Строение экрана
Рисунок 2.2. Строение экрана
Теперь перейдем к более низкому уровню и посмотрим, как адресуется область видеобуфера. Вы, наверное, не раз наблюдали, как грузятся с магнитофона стандартные экранные файлы: область экрана заполняется не последовательно, строка за строкой, а довольно хитрым способом. Сначала один за другим появляются верхние ряды пикселей восьми первых текстовых строк, затем в этих же строках рисуются все вторые ряды и так далее, пока не сформируется изображение всей верхней трети экрана. Затем, в том же порядке, заполняется средняя часть экрана, а потом и нижняя. Итолько в самом конце последовательно выводятся атрибуты всех знакомест. Однако это говорит не о каком-то изощренном способе загрузки именно экранных файлов - они грузятся так же, как и любые другие, последовательно заполняя ячейки памяти от младших адресов к старшим. Это свидетельствует, напротив, о нелинейном строении экранной области памяти.
На первый взгляд такая организация экранной области кажется исключительно неудобной, особенно при решении задач определения адреса по заданным координатам. Но это лишь до тех пор, пока вы используете в расчетах только десятичные числа. Ведь даже начальный адрес видеобуфера 16384 в десятичном виде представляется просто «взятым с потолка». В таких случаях гораздо удобнее пользоваться несколько иным представлением числовой информации. Мы имеем в виду шестнадцатеричный формат чисел, который от десятичного отличается «емкостью» разрядов.
В десятичном представлении каждый разряд числа может изменяться от 0 до 9, а в шестнадцатеричном - от 0 до 15. Цифры от 0 до 9 при этом записываются так же, как и в десятичных числах, а дальше применяются буквы латинского алфавита от A до F. Вот соответствие чисел в десятичном и шестнадцатеричном форматах:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18... 0 1 2 3 4 5 6 7 8 9 A B C D E F 10 11 12...
Если адрес 16384 привести к шестнадцатеричному виду, то он вдруг окажется совершенно «ровным» - #4000 (знак # перед числом говорит о том, что оно представлено как шестнадцатеричное). Взгляните на схему, изображенную на Рисунок 2.3 и вы заметите явную закономерность в распределении адресного пространства видеообласти. В недалеком будущем мы подробно расскажем о методах вычисления адресов экрана, а сейчас перейдем к рассмотрению других не менее важных понятий.
Данные | Атриб. | Строка | ВИДЕОБУФЕР | Строка | Данные | Атриб. |
#4000 | #5800 | 0 | 0 | #401F | #581F | |
#4020 | #5820 | 1 | 1 | #403F | #583F | |
#4040 | #5840 | 2 | 2 | #405F | #585F | |
#4060 | #5860 | 3 | 3 | #407F | #587F | |
#4080 | #5880 | 4 | 4 | #409F | #589F | |
#40A0 | #58A0 | 5 | 5 | #40BF | #58BF | |
#40C0 | #58C0 | 6 | 6 | #40DF | #58DF | |
#40E0 | #58E0 | 7 | 7 | #40FF | #58FF | |
#4800 | #5900 | 8 | 8 | #481F | #591F | |
#4820 | #5920 | 9 | 9 | #483F | #593F | |
#4840 | #5940 | 10 | 10 | #485F | #595F | |
#4860 | #5960 | 11 | 11 | #487F | #597F | |
#4880 | #5980 | 12 | 12 | #489F | #599F | |
#48A0 | #59A0 | 13 | 13 | #48BF | #59BF | |
#48C0 | #59C0 | 14 | 14 | #48DF | #59DF | |
#48E0 | #59E0 | 15 | 15 | #48FF | #59FF | |
#5000 | #5A00 | 16 | 16 | #501F | #5A1F | |
#5020 | #5A20 | 17 | 17 | #503F | #5A3F | |
#5040 | #5A40 | 18 | 18 | #505F | #5A5F | |
#5060 | #5A60 | 19 | 19 | #507F | #5A7F | |
#5080 | #5A80 | 20 | 20 | #509F | #5A9F | |
#50A0 | #5AA0 | 21 | 21 | #50BF | #5ABF | |
#50C0 | #5AC0 | 22 | 22 | #50DF | #5ADF | |
#50E0 | #5AE0 | 23 | 23 | #50FF | #5AFF |
СТРУКТУРА АССЕМБЛЕРНОЙ СТРОКИ
СТРУКТУРА АССЕМБЛЕРНОЙ СТРОКИ
Как и в любом другом языке, программа на ассемблере начинается с набора исходного текста. Наверное, нет надобности говорить, что для этих целей редактор Бейсика, встроенный в компьютер, совершенно непригоден. Посему в первую очередь необходимо освоить специальный редактор ассемблерных текстов, в который вы попадаете сразу после запуска GENS. В этой главе мы научим вас вводить строки программы, просматривать полученный текст и, наконец, транслировать готовую программу.
Прежде всего нужно сказать о структуре строк исходного текста программы на ассемблере. Они состоят из нескольких полей и имеют следующий формат:
Номер Поле меток Поле мнемоник Поле операндов 12345 LABEL LD BC,1000
Каждое поле служит для определенных целей, поэтому мнемоника, например, не может быть записана в поле меток - это неминуемо приведет к ошибке.
Рассмотрим назначение каждого их этих полей.
Хотя строки в программах для GENS и нумеруются, но в ассемблере ссылки на номера строк, в отличие от Бейсика, нигде не применяются. Нумерация нужна исключительно для определения очередности следования строк, вывода листинга на экран или принтер и для вызова строк на редактирование. Допустимые значения этого поля - от 1 до 32767.
Но как же, спросите вы, в ассемблере отмечаются точки переходов, как обозначаются начала подпрограмм и данных, если отсутствуют ссылки на номера строк? Просто в ассемблере для этого используется другая методика, гораздо более удобная, как вы почувствуете позже. Здесь для любого рода ссылок используются специальные имена, задаваемые программистом, которые называются метками. Метки записываются в следующем после номера строки поле - в поле меток. Конечно, это поле не обязательно должно заполняться в каждой строке. Метки расставляются только там, где в них действительно возникает необходимость. Дальше мы более подробно поговорим о них и вообще о методах адресации, а пока перейдем к следующему полю строки.
Как мы уже говорили, в ассемблере команды микропроцессора представляются в виде сокращений английских слов и такие аббревиатуры называются мнемониками или мнемокодами. При наборе строк программы мнемоники команд должны располагаться обязательно в своем поле, иначе GENS не сможет их распознать, что приведет к появлению ошибки. В этом же поле записываются и специальные инструкции - директивы ассемблера, о которых мы также обязательно расскажем, но несколько позже. При необходимости это поле, так же, как и предыдущее, может быть пустым.
Если у команды имеются какие-либо операнды, они записываются в следующем и последнем поле строки - в поле операндов. Таким образом полная команда может занимать в строке программы одно или два поля.
Точки
Обратимся, как и прежде, к огромному вместилищу различных процедур в машинных кодах, к ПЗУ компьютера. Оператор Бейсика PLOT реализует подпрограмма, имеющая адрес 8933. Понятно, что для получения точки на экране необходимо указать, как минимум, ее координаты. Поэтому перед обращением к процедуре рисования точек следует занести в регистр C смещение по горизонтали, а в B- по вертикали, отсчитывая пиксели, как и Бейсике, от левого нижнего угла экрана. Например, для получения результата, аналогичного выполнению оператора PLOT 45,110 нужно вызвать такую последовательность команд микропроцессора:
LD C,45 LD B,110 CALL 8933
Так как регистры C и B относятся к одной паре, то часто можно сокращать запись, загружая их не последовательно, а одновременно. При этом удобнее воспользоваться шестнадцатеричной системой счисления:
LD BC,#6E2D ;45 = #2D, 110 = #6E CALL 8933
Чтобы нарисовать точку с определенными атрибутами так же, как и при выводе символов, можно воспользоваться подпрограммой , описанной в разделе , или установить временные цвета, изменив системную переменную ATTR_T (23695). Сразу заметим, что это в равной степени относится и к подпрограммам рисования линий и окружностей.
Для примера поставим точку красного цвета на желтом фоне. (Задавая атрибуты при выводе графики, помните, что в Speccy цвета определяются для целого знакоместа.) Рассчитав значения байта атрибутов для заданной комбинации цветов, получим число 50 (2+6ґ8). Перед выводом точки занесем это число в ячейку 23695, предварительно подготовив экран подпрограммой из раздела :
ORG 60000 ENT $ CALL SETSCR LD A,50 LD (23695),A LD BC,#6E2D CALL 8933 RET
ТРАНСЛЯЦИЯ БОЛЬШИХ ПРОГРАММ
ТРАНСЛЯЦИЯ БОЛЬШИХ ПРОГРАММ
Особенно много сложностей возникает при трансляции больших программ, и в первую очередь это касается тех, кто еще не успел обзавестись дисковой операционной системой TR-DOS и адаптированной к ней версией ассемблера gens4b или GENS4D. Однако это не означает, что если текст программы, таблица меток, исполняемый файл и сам ассемблер не умещаются в памяти, то мечту о создании серьезных игровых программ можно похоронить. Ассемблер GENS4 предоставляет возможность получения достаточно объемистых исполняемых модулей, сопоставимых с размерами управляющей части фирменных программ. Безусловно, это потребует от вас некоторого терпения, но если есть желание, все остальное приложится.
Конечно, можно транслировать разные модули программы раздельно и определять глобальные адреса, запрашивая вывод таблицы меток (см. ). Однако подобный метод вряд ли можно назвать хоть сколько-нибудь удобным и прибегать к нему имеет смысл лишь в исключительных случаях, когда все остальное не помогает. Мы же советуем воспользоваться другой возможностью, имеющейся в ассемблере GENS4.
Идея заключается в том, чтобы не загружать в память исходный текст программы, а транслировать его непосредственно с внешнего носителя. Это позволит высвободить максимальное пространство, так как именно текстовый файл имеет наибольшие размеры по сравнению с таблицей меток и исполняемым файлом. Если ваш компьютер не снабжен дисководом и вы вынуждены работать с лентой, то прежде всего потребуется создать текстовый файл в специальном формате, пригодном для трансляции таким способом. При этом GENS разбивает исходный текст на небольшие фрагменты, а затем считывает и транслирует файл по частям. Считывание происходит в специально предназначенный для этих целей буфер, размер которого необходимо указать в самом начале работы. Введите в редакторе команду C (Change buffers - изменить буферы). На экране появится запрос:
Include buffer?
на который нужно ввести размер входного буфера для включаемых файлов в байтах. Введите в ответ на него, например, значение 1000. Затем вы увидите еще один запрос - Macro buffer? - который нас пока не интересует, поэтому его можно пропустить, просто нажав Enter. После этого загрузите текст программы в память и сохраните его снова, но уже не обычным образом, а с помощью команды T. Ее формат похож на формат команды P для сохранения текста:
T[нач.строка],[конечн.строка],[имя_файла]
Например, чтобы перевести весь исходный текст во включаемый формат и записать его в файле с именем INCL, нужно ввести команду
T1,20000,INCL
Теперь очистите память командой Z и напишите одну-единственную строку:
10 *F INCL
Обратите внимание, что команда ассемблера *F (не путайте с командами редактора) должна находиться в поле меток, а имя файла записывается следом за ней после единичного пробела. Для начала трансляции введите команду редактора A, например:
A16,5400
Конечно, размер таблицы меток в вашем случае может потребоваться совершенно иной, но важно то, что он обязательно должен быть указан, так как при единственной строке в памяти GENS для этих нужд зарезервирует примерно 100 байт, которых хватит от силы на пару десятков меток. Размер таблицы желательно задать максимально близким к тому, который будет использован, и чтобы узнать его, может понадобиться сначала оттранслировать программу «вхолостую», использовав ключ 2. Для окончательной трансляции желательно применить ключ 16, который отведет максимум памяти для исполняемого модуля.
Таким образом можно обрабатывать исходные тексты программ, превосходящие размеры всей свободной памяти компьютера. Ведь текст может располагаться не в одном, а в нескольких файлах еще до обработки их командой T. Например, программа MOON занимает 3 файла. Создайте для каждого из них три включаемых файла с именами MOON1, MOON2 и MOON3, а затем введите три строки
10 *F MOON1 20 *F MOON2 30 *F MOON3
которые будут транслироваться уже описанным способом.
При использовании такого метода трансляции нужно помнить еще об одном. Если вы создали включаемые файлы, но не стали их транслировать сразу же, а решили отложить эту работу до следующего раза, то перед началом ассемблирования необходимо будет заново указать размер буфера для включения, причем он должен в точности совпадать с указанным при сохранении программы командой T.
Обладателям дисковой системы TR-DOS повезло несравненно больше. При работе с диском не нужно переводить файлы в специальный формат, достаточно воспользоваться командой *F с указанием имени файла, которое должно начинаться, как и в прочих командах работы с внешней памятью, с указания номера дисковода, например, строка
10 *F 1:BATTY
оттранслирует непосредственно с диска, находящегося на дисководе A, текстовый файл с именем BATTY. Правда, и в этом случае потребуется задать размер таблицы меток достаточных размеров, но и не слишком большой, чтобы осталось больше места для исполняемого файла. С этой же целью также желательно указать ключ 16.
Еще один существенный плюс использования для трансляции дисковода (помимо скорости считывания) заключается в том, что если исполняемый модуль перекрывает всю свободную память, уже полученный машинный код «сбрасывается» на диск порциями по мере заполнения памяти. Однако в этом случае необходимо указать и третий параметр в команде редактора A - имя выходного файла, то есть того файла, в который будет записана оттранслированная программа. К примеру, это может выглядеть так:
A16,8000,2:BATT.EXE
После ассемблирования текста исполняемый модуль будет сохранен на дискете в дисководе B под именем BATT.EXE.
ТРАНСЛЯЦИЯ ПРОГРАММЫ
ТРАНСЛЯЦИЯ ПРОГРАММЫ
Оттранслируйте теперь полученный исходный текст. Введите команду A безо всяких параметров. На экране должна появиться примерно такая надпись:
HiSoft GEN Assembler ZX Spectrum ZX Microdrive Version 4.1b
Copyright (C) HiSoft 1987 V4.1 All right reserved
Pass 1 errors: 00
Pass 2 errors: 00
Table used: 13 from 104
Текст «шапки», естественно, может быть и другим, все зависит от версии, которой вы пользуетесь. Здесь нам важно, в общем-то, одно- то, что ни в первом, ни во втором проходе ассемблирования (Pass) ошибок (errors) не обнаружено. Надо сказать, что ассемблер обрабатывает исходный текст в два прохода: на первом проверяются синтаксические ошибки и выстраивается таблица меток (ее размер показывает самая нижняя строка сообщения Table used); во втором проходе генерируется машинный код программы.
УДАЛЕНИЕ ТЕКСТА
УДАЛЕНИЕ ТЕКСТА
Для удаления ненужных строк исходного текста в редакторе GENS имеется несколько возможностей. Первая из них в точности повторяет способ избавления от лишних строк в Бейсике: достаточно набрать номер удаляемой строки и нажать Enter.
Но, как вы понимаете, такой способ приемлем для удаления двух-трех строчек, когда же требуется ликвидировать значительную часть текста, подобная методика превращается в настоящую пытку. Фирма HISOFT учла интересы программистов и включила в редактор возможность удаления произвольной группы строк. Для этих целей служит команда D (Delete - удалить) с двумя параметрами, разделенными запятой. Первым указывается номер начальной строки удаляемого фрагмента, а затем - номер последней удаляемой строки. Например, для исключения из текста строк, начиная с сотой и по стопятидесятую включительно, нужно ввести команду
D100,150
Обращаться с этой командой нужно достаточно осторожно, так как выполняется она сразу же после ввода, безо всяких дополнительных подтверждающих запросов, а восстановить удаленный текст можно будет только если вы успели сохранить его на ленте или дискете. Ни в коем случае не следует использовать команду D без параметров, потому как при этом будут взяты параметры предыдущей введенной команды (параметры по умолчанию), а они могут далеко не соответствовать желаемым.
В редакторе GENS есть еще одна полезная команда, предназначенная для удаления текста. Это команда Z. С ее помощью можно быстро уничтожить сразу весь текст. Она не требует никаких параметров, а после ввода просит подтвердить ваши намерения запросом
Delete text (Y/N)?
Если вы не передумали стирать текст, нажмите клавишу Y, в противном случае - любую другую клавишу.
УПРАВЛЕНИЕ ДЖОЙСТИКОМ
УПРАВЛЕНИЕ ДЖОЙСТИКОМ
В динамичных играх управление спрайтами с помощью Kempston-джойстика часто оказывается более удобным, чем от клавиатуры - нужно помнить всего лишь о четырех сторонах света, да в пылу борьбы не забыть, что есть еще кнопка «огонь».
Наверное, вам известно, что в Бейсике определить положение ручки джойстика можно с помощью функции IN 31. В ассемблере для этих же целей удобнее всего применять команду IN A,(31), после выполнения которой в аккумуляторе появится некоторое число, отдельные биты которого и определяют «статус» джойстика. Значения имеют не все биты, а только 5 младших, причем, в отличие от клавиатуры, в нейтральном положении все биты сброшены в 0 (конечно, если порт джойстика вообще подключен), а установка какого-то бита в 1 означает поворот ручки или нажатие кнопки «огонь». При диагональном наклоне ручки будут установлены сразу два бита. В табл. 8.2 показано соответствие пяти младших битов, получаемых в аккумуляторе после выполнения команды IN A,(31), направлениям наклона ручки джойстика и нажатию кнопки «огонь». Три старших бита не определены, поэтому в таблице они заменены знаками вопроса.
Таблица 8.2. Значения битов порта джойстика.
Направление | Код |
Вправо | ???00001 |
Влево | ???00010 |
Вверх | ???00100 |
Вниз | ???01000 |
Огонь | ???10000 |
Давайте сначала на примере простой программки перемещения точки, аналогичной той, которую мы описали в начале первого раздела данной главы, рассмотрим принцип использования джойстика. Причем, как и в случае с клавиатурой, точку на экран будем ставить с помощью бейсик-программы.
ORG 60000 XY EQU 23296 JOY LD HL,(XY) ;в регистре H вертикальная координата, ; а в L - горизонтальная IN A,(31) ;читаем из порта джойстика RRCA ;проверяем бит 0 JR NC,JOY1 ;если в 0, переходим к проверке ; следующего бита INC L ;увеличиваем горизонтальную координату JOY1 RRCA ;аналогично проверяем остальные биты JR NC,JOY2 DEC L ;уменьшаем горизонтальную координату JOY2 RRCA JR NC,JOY3 DEC H ;увеличиваем вертикальную координату JOY3 RRCA JR NC,JOY4 INC H ;уменьшаем вертикальную координату JOY4 LD (XY),HL ;новые координаты передаем ; бейсик-программе RRCA RET NC JP 3435 ;при нажатии кнопки «огонь» ; экран очищается
Чтобы точка, перемещаясь по экрану, не ушла за его пределы, необходимо ввести ограничения. Как это осуществить, покажем на конкретном примере, в котором мы используем приведенный выше принцип для воспроизведения реальной игровой ситуации, например, для управления самолетом. Прежде всего создадим три спрайта (блоки SAM1, SAM2 и SAM3), первый из которых будет соответствовать полету самолета прямо, второй повороту вправо (самолет при этом должен слегка наклониться) и, наконец, третий - его повороту влево. Таким образом, наклоняя ручку джойстика в ту или иную сторону, вы можете в приведенной ниже программе легко изменять положение спрайта на экране.
ORG 60000 ENT $ LD A,5 LD (23693),A XOR A CALL 8859 CALL 3435 ; Основная часть программы LD BC,#505 ;в регистре B вертикальная координата Y, ; а в C горизонтальная JOY IN A,(31) ;читаем данные из порта джойстика LD E,A ;освобождаем аккумулятор ; для проверки границ LD HL,SAM1 ;задаем адрес первого спрайта RRC E ;сдвигаем E вправо на один бит JR NC,JOY1 LD A,C CP 30 ;задаем границу перемещения вправо JR NC,JOY1 ;если правая граница достигнута, ; то увеличивать X уже нельзя - ; переходим на метку JOY1 INC C ;увеличиваем координату X, что соответствует ; перемещению самолета вправо LD HL,SAM2 ;задаем адрес второго спрайта JOY1 RRC E JR NC,JOY2 LD A,C CP 1 JP M,JOY2 DEC C ;уменьшаем координату X LD HL,SAM3 ;задаем адрес третьего спрайта JOY2 RRC E JR NC,JOY3 LD A,B CP 22 JR NC,JOY3 INC B ;увеличиваем координату Y JOY3 RRC E JR NC,JOY4 LD A,B CP 1 JP M,JOY4 DEC B ;уменьшаем координату Y JOY4 RRC E RET C ;если кнопка «огонь» нажата - ; выходим из программы ; Вывод на экран по принципу XOR одного из трех спрайтов самолета PUSH BC PUSH HL LD A,SPRXOR ;устанавливаем режим вывода XOR CALL PTBL ;печатаем один из самолетов LD BC,10 ;вводим задержку CALL 7997 POP HL POP BC PUSH BC LD A,SPRXOR ;устанавливаем режим вывода XOR CALL PTBL ;стираем изображение самолета POP BC JR JOY ;переходим в начало программы ; для изменения координат
; Заголовок данных спрайта первого самолета, соответствующего полету вперед и назад SAM1 DEFB 4 DEFB 0,0,5,0,1,5,1,0,5,1,1,5 ; Данные для первого самолета DEFB 0,0,5,0,95,254,56,66 DEFB 0,0,160,0,250,127,28,66 DEFB 61,1,1,5,13,0,0,0 DEFB 188,128,128,160,176,0,0,0 ; Заголовок данных спрайта второго самолета, соответствующего повороту вправо SAM2 DEFB 4 DEFB 0,0,5,0,1,5,1,0,5,1,1,5 ; Данные для второго самолета DEFB 0,0,5,0,11,95,78,35 DEFB 0,0,160,0,244,62,28,8 DEFB 29,1,1,2,2,0,0,0 DEFB 176,128,128,192,224,192,0,0 ; Заголовок данных спрайта третьего самолета, соответствующего повороту влево SAM3 DEFB 4 DEFB 0,0,5,0,1,5,1,0,5,1,1,5 ; Данные для третьего самолета DEFB 0,0,5,0,47,124,56,16 DEFB 0,0,160,0,208,250,114,196 DEFB 13,1,1,3,7,3,0,0 DEFB 184,128,128,64,64,0,0,0
УПРАВЛЕНИЕ СПОМОЩЬЮ КЛАВИАТУРЫ
УПРАВЛЕНИЕ С ПОМОЩЬЮ КЛАВИАТУРЫ
При разработке игровых программ немыслимо обойтись без опроса клавиатуры. Действительно, чтобы во время игры управлять спрайтами, перемещая их хотя бы по четырем направлениям, программа обязана безошибочно различать одну из четырех нажатых клавиш. Например, O должна соответствовать движению влево, P - вправо, Q - вверх и A - вниз. В Бейсике, как вы помните, для этой цели мы пользовались функцией INKEY$, а одна из возможных программ, «узнающих», к примеру, символ P, могла бы выглядеть так:
100 IF INKEY$<>"P" THEN GO TO 100
Соответствующий ей фрагмент ассемблерной программы имеет следующий вид:
XOR A LD (23560),A ;в системную переменную LAST_K (код ; последней нажатой клавиши) заносится 0 LOOP LD A,(23560) ;из этой системной переменной ; считывается значение кода нажатой клавиши CP "P" ;сравнение двух кодов - находящегося ; в регистре A и символа P
JR NZ,LOOP ;если результат сравнения не равен 0, ; то переход на метку LOOP, если 0, RET ; то выход
Надо заметить, что эта программка уже неоднократно применялась нами для разных целей, например, для выхода из циклов, для перехода к кадрам в многокадровой заставке, но она может использоваться и как образец для создания более сложных программ, например, следующей:
KEY XOR A LD (23560),A MET1 LD A,(23560) CP "P" ;сравнение двух кодов ; Если результат сравнения не равен нулю (то есть нажата не P), ; то переход на метку MET2, после которой проверяются нажатия других клавиш JR NZ,MET2 LD DE,TXT1 PRINT LD BC,5 ;вывод на экран символа, CALL 8252 ; соответствующего нажатой клавише LD A,13 RST 16 JR KEY ;переход на начало программы MET2 CP "O" ;проверка нажатия клавиши O
JR NZ,MET3 LD DE,TXT2 JR PRINT MET3 CP "Q" ;проверка нажатия клавиши Q
JR NZ,MET4 LD DE,TXT3 JR PRINT MET4 CP "A" ;проверка нажатия клавиши A
JR NZ,MET5 LD DE,TXT4 JR PRINT MET5 CP "0" ;проверка нажатия клавиши 0
JR NZ,MET1 ;если коды не совпадают, ; повторяем все сначала RET ; иначе - выход из программы ; Данные для печати TXT1 DEFM "KEY P" TXT2 DEFM "KEY O" TXT3 DEFM "KEY Q" TXT4 DEFM "KEY A"
После того как вы нажмете клавишу P, O, Q или A, программа напечатает в левом верхнем углу экрана одну из фраз, перечисленных в блоке данных, например, «KEY Q».
В игровых программах, как вы знаете, часто требуется опрашивать несколько клавиш одновременно, например, чтобы выполнять сложные перемещения спрайтов, включающие помимо вертикальных и горизонтальных еще и диагональные направления. Для подобных ситуаций приведенные выше способы опроса клавиатуры оказываются непригодными и, чтобы заставить все-таки спрайты перемещаться в любую сторону, придется воспользоваться командой IN, с помощью которой выполняется ввод данных из какого-либо порта. Существует несколько способов чтения из портов, но нас будут интересовать только два из них:
IN reg,(C) - ввод байта из порта и помещение его в регистр, причем reg - один из регистров A, B, C, D, E, H или L, а адрес порта содержится в паре BC (в C - младший байт адреса, в B - старший).
IN A,(port) - ввод байта из порта с номером port и помещение его в аккумулятор. При этом полный 16-разрядный адрес порта составляется из значения port (младший байт) и значения аккумулятора (старший байт).
Применяя одну или другую команду, можно получить два способа опроса клавиатуры. Первый из них очень похож на использование функции IN в Бейсике. Напомним, что все клавиши группируются по полурядам, то есть по 5 клавиш. Каждому полуряду соответствует определенный порт. Адреса «клавиатурных» портов отличаются только старшим байтом, а младший всегда равен 254 (#FE). Все эти адреса представлены в табл. 8.1 в десятичной, шестнадцатеричной и двоичной нотации. Предположим, что нам нужно определить нажатие клавиши M, тогда в регистровую пару BC необходимо записать адрес 32766. Из порта считываем значение для полуряда, а затем, чтобы определить нажатие конкретной клавиши, проверяем соответствующий ей бит (от бита 0 - для крайних клавиш до 4-го бита - для центральных). Так как клавиша M занимает третье место от края, то она будет определяться состоянием 2-го бита полученного из порта байта. Если этот бит окажется сброшенным в 0, это будет означать, что клавиша нажата. (Из-за упрощенной аппаратной реализации клавиатуры, примененной в ZX Spectrum, достоверно (в общем случае) можно определить одновременное нажатие не более двух каких-либо клавиш - Примеч. ред.)
Таблица 8.1. Адреса портов для опроса клавиатуры
Полуряд | DEC | HEX | BIN |
Space...B | 32766 | 7FFE | 01111111 11111110 |
Enter...H | 49150 | BFFE | 10111111 11111110 |
P...V | 57342 | DFFE | 11011111 11111110 |
0...6 | 61438 | EFFE | 11101111 11111110 |
1...5 | 63486 | F7FE | 11110111 11111110 |
Q...T | 64510 | FBFE | 11111011 11111110 |
A...G | 65022 | FDFE | 11111101 11111110 |
CS...V | 65278 | FEFE | 11111110 11111110 |
Второй способ принципиально не отличается от первого. Перед чтением в аккумулятор помещается старший байт адреса соответствующего порта, а младший байт задается в явном виде в команде IN:
KEY LD A,#7E ;в аккумулятор заносится старший байт ; адреса порта #7EFE IN A,(254) ;считывание из порта (254 или #FE - ; младший байт адреса) BIT 2,A ;проверка нажатия третьей от края ; клавиши (M) JR NZ,KEY RET
Рассмотрим программу, в которой при нажатии клавиш Q, A, O и P изменяются координаты точки на экране. Сами точки будем ставить в бейсик-программе, которую напишем позже, но подразумевая использование процедуры из Бейсика, воспользуемся для передачи координат точки, как и раньше, областью буфера принтера, определив адрес передаваемых параметров константой XY.
ORG 60000 XY EQU 23296 KEY LD HL,(XY) ;запись координат точки в HL ; В регистр A заносится старший байт полуряда, ; в котором располагается клавиша Q
LD A,251 IN A,(254) ;читаем из порта значения для полуряда ; Проверка бита 0 (команду RRCA вместо BIT здесь удобнее применять ; потому, что клавиша Q в полуряду занимает крайнее положение) RRCA ; Если клавиша не нажата (на что указывает установленный бит), ; то следующую команду пропускаем JR C,KEY1 ; Увеличиваем значение вертикальной координаты, которое находится в регистре H INC H KEY1 LD A,253 IN A,(254) RRCA ;клавиша A
JR C,KEY2 DEC H ;уменьшаем вертикальную координату KEY2 LD A,223 IN A,(254) RRCA ;клавиша P
JR C,KEY3 INC L ;увеличиваем горизонтальную координату ; Так как клавиши P и O находятся в одном полуряду, ; то выполнять команду IN дважды нет необходимости KEY3 RRCA ;клавиша O
JR C,KEY4 DEC L ;уменьшаем горизонтальную координату KEY4 LD (XY),HL LD A,127 IN A,(254) BIT 2,A RET NZ ;выход, если клавиша M не нажата JP 3435 ; иначе очищаем экран
Чтобы увидеть эту процедуру в действии, необходимо дополнить ее небольшой бейсик-программкой, задача которой состоит только в том, чтобы ставить на экране точку в соответствии с координатами (XY), полученными в ассемблерной программе.
100 POKE 23296,100: POKE 23297,100 110 PLOT PEEK 23296, PEEK 23297 120 RANDOMIZE USR 60000: GO TO 110
Попробуйте ее ввести и исполнить, а затем понажимайте клавиши Q, A, O и P - по экрану в разных направлениях потянутся четкие прямые линии подобно использованию функции PEN в графическом редакторе. Нажав клавишу M, в любой момент можно очистить экран и начать рисовать новую «картину».
В заключение этого раздела приведем еще один пример управления с помощью клавиатуры, с которым мы иногда встречаемся, загружая те или иные игровые программы. Он полезен еще и тем, что дает вариант решения некоторых побочных проблем, таких, например, как учет ограничений на перемещение курсора (или спрайта) по экрану, введение дополнительных функций управления и некоторые другие.
УПРАВЛЕНИЕ ТРАНСЛЯЦИЕЙ
УПРАВЛЕНИЕ ТРАНСЛЯЦИЕЙ
Основная задача ассемблера- трансляция исходных текстов, поэтому о том, как получается исполняемый файл, не помешает знать несколько больше того, что вам уже известно. При обработке небольших программ обычно бывает вполне достаточно просто ввести команду A, но иногда может потребоваться, например, узнать адреса некоторых меток, вывести листинг ассемблирования на экран или распечатать его на принтере. А как быть, если какая-то часть программы должна размещаться в экранной области памяти или на месте системных переменных? Или размер программы получается настолько большим, что перекрывает не только исходный текст, но и коды самого ассемблера? Оказывается, GENS позволяет справиться и с подобными трудностями! Нужно только дать ему соответствующее задание. Для этого необходимо указать в команде A дополнительные параметры, которые мы раньше пропускали.
Условные и безусловные переходы
Алгоритм циклов с использованием регистров, отличных отB, принципиально не отличается от порядка выполнения команды DJNZ и может быть выражен словами «уменьшить содержимое регистра и перейти на начало цикла, если не ноль». Как записывается в ассемблерной мнемонике первая часть этого предложения, вам, наверное, уже понятно: если использовать в качестве счетчика, скажем, регистр E, то нужно написать команду DEC E. Что же касается переходов, то в системе команд микропроцессора имеется не одна инструкция (как в Бейсике оператор GO TO), а целых две. Одна из них универсальна и позволяет переходить по любому выбранному адресу, другая же предназначена для более коротких переходов на расстояния, не превышающие 126-127 байт (так же, как и для команды DJNZ). Первая инструкция записывается двумя буквами JP (сокращение от Jump - перепрыгнуть), а вторая имеет мнемоническое обозначение JR (Jump Relative - относительный переход). Сначала приведем несколько примеров безусловных переходов:
JP 3435 ;переход по абсолютному адресу 3435 JP LABEL ;переход на метку LABEL JR $+35 ;относительный переход вперед на ; расстояние в 35 байт JR LABEL ;переход на ту же самую метку LABEL
Сразу может возникнуть вопрос, зачем для выполнения одного и того же действия нужны разные команды? Во-первых, команда JR короче JP и занимает в памяти два байта вместо трех (не улыбайтесь: такая, на первый взгляд, мелочная экономия в итоге может вылиться в килобайты!), а во-вторых, команды «коротких» переходов вы сможете оценить в полной мере, если вам когда-нибудь доведется писать программы или процедуры в машинных кодах, которые позволительно загружать по любому удобному адресу. Применение команды JR в таких подпрограммах избавит вас от необходимости выполнения предварительной их настройки на адрес загрузки. Именно так написано большинство процедур из пакетов Supercode и NewSupercode, что значительно облегчает работу с ними. Поэтому при написании собственных программ старайтесь использовать команду JR везде, где это только позволяет расстояние, оставляя команде JP переходы к дальним адресам.
Теперь относительно условных переходов. Эти команды также начинаются с JP или JR, но в поле операндов записывается мнемоника проверки одного из возможных флагов и после запятой - имя метки или абсолютный адрес, например:
JP Z,8252 ;переход по адресу 8252, если ; установлен флаг нуля JR NC,MAIN ;относительный переход на метку MAIN, ; если флаг переноса сброшен
Перечислим мнемоники всех возможных условий:
Z - если ноль (установлен флаг нуля Z);
NZ - если не ноль (флаг нуля Z сброшен);
C - если перенос (установлен флаг переноса CY);
NC - если нет переноса (флаг переноса CY сброшен);
M - если отрицательный результат (установлен флаг знака S);
P - если результат положительный (флаг знака S сброшен);
PE - если четность или переполнение (установлен флаг P/V);
PO - если нет четности/переполнения (флаг P/V сброшен).
Для флагов H и N условия отсутствуют, так как они используются только в неявном виде командами коррекции двоично-десятичных чисел.
Здесь нужно еще добавить, что в команде JP возможно применение всех перечисленных мнемоник условий, а с командой JR допускаются только первые четыре: Z, NZ, C и NC.
Зная все это, можно наконец написать цикл. В общем виде он будет выглядеть так:
LD E,N ;заносим в регистр E счетчик ; количества повторений цикла N LOOP PUSH DE ;сохраняем его в стеке ......... ;тело цикла POP DE ;восстановление счетчика DEC E ; и уменьшение его на единицу JR NZ,LOOP ;переход на начало цикла, если счетчик ; не обнулился (в самом общем случае ; здесь может находиться инструкция ; JP NZ,LOOP)
Установка размера таблицы меток
Среди сообщений GENS на экране изредка может появиться:
No Symbol table space!
говорящее о том, что ассемблеру не хватило памяти для размещения таблицы меток. Это может показаться странным, особенно если ваша программа имеет небольшие размеры и свободной памяти на самом деле остается еще килобайт 15-20. Авсе дело в том, что GENS перед началом трансляции исходного текста выделяет под таблицу не все пространство, а определенных размеров буфер, величина которого рассчитывается исходя из размера текста программы, находящейся в памяти. Чем больше строк вы ввели, тем больше окажется и таблица. В большинстве случаев рассчитанного объема оказывается вполне достаточно, но если при небольших размерах текста он будет насыщен метками и прочими именами, то тогда, скорее всего, и появится указанное выше сообщение.
Бороться с подобной ситуацией поможет второй параметр, задаваемый в команде A. Он соответствует размеру создаваемой таблицы в байтах. Например, если вы считаете, что таблицы в две с половиной тысячи байт будет достаточно, введите для начала ассемблирования команду
A,2500
При необходимости, конечно, можете указать также и ключи трансляции.
К этой возможности можно прибегать и в тех случаях, когда реальный размер таблицы меток получается намного меньше рассчитанного ассемблером. Таким образом, например, можно высвободить дополнительное пространство для размещения исполняемого кода.
Вложенные циклы
Сохранение счетчика совершенно необходимо также при организации вложенных циклов. Приведем пример небольшой программки, заполняющей экран однородной фактурой, в которой использован принцип вложения циклов. Во внешнем цикле будем подсчитывать количество заполненных строк, а внутренний, в общем, аналогичен приведенному выше примеру:
ORG 60000 ENT $ LD HL,UDG LD (23675),HL ;адресация области символов UDG LD A,8 LD (23693),A ;синяя «бумага», черные «чернила» LD A,1 CALL 8859 ;синий бордюр CALL 3435 ;подготовка экрана LD A,2 CALL 5633 LD B,22 ;заполняем 22 строки экрана OUTSID PUSH BC ;внешний цикл LD B,32 ;по 32 символа в строке ; --------- INSIDE LD A,144 ;внутренний цикл RST 16 ;выводим код символа A из набора UDG DJNZ INSIDE ;конец внутреннего цикла ; --------- POP BC DJNZ OUTSID ;конец внешнего цикла RET ; Данные символа A (UDG) UDG DEFB 119,137,143,143,119,153,248,248
ВНЕСЕНИЕ В ТЕКСТ ИЗМЕНЕНИЙ
ВНЕСЕНИЕ В ТЕКСТ ИЗМЕНЕНИЙ
Следующий по важности момент после набора строк- редактирование исходного текста. Ведь трудно себе представить, что какая-то программа может быть написана одним заходом, сразу начисто, без ошибок и опечаток. Для этого нужно научиться быстро изменять текст строк, не переписывая их целиком, вставлять недостающие команды, удалять лишние и, наконец, записывать программу на внешний носитель, чтобы плоды вашего труда не пропали даром.
Начнем со способов внесения правок в уже имеющийся текст. Для вызова строки на редактирование служит команда E (Edit) с номером нужной строки (Рисунок 3.1). Давайте в нашей программке подправим, например, строку 20, изменив число, загружаемое в регистровую пару BC. Введите команду E20. То, что вы увидели, возможно, несколько удивило вас. На экране появилась вызванная строка, но курсор на ней не задержался, а проскочил ниже, напечатав только номер. Весь остальной текст куда-то подевался. Может создаться такое впечатление, что теперь необходимо набирать весь текст заново. Однако, это только впечатление, и на самом деле строка сейчас готова к редактированию.
«Волна»
Предлагаем вам еще один интересный эффект, иногда встречающийся в игровых программах: по строке текста от левого к правому краю экрана как бы пробегает волна, буквы приподнимаются, затем опускаются и наконец занимают свое первоначальное положение.
Для осуществления этого эффекта прежде всего потребуется написать две подпрограммы вертикального скроллинга отдельных знакомест экрана: одну для перемещения символа вверх, а другую - для движения изображения вниз. Принцип такого рода скроллингов исключительно прост. Например, для сдвига изображения на один пиксель вверх нужно байт из второго ряда пикселей перенести на место байта первого ряда, затем байт из третьего ряда поместить во второй и так далее, а самый нижний ряд пикселей заполнить нулями. Правда, при этом потеряется содержимое самого верхнего байта, но для текстовой строки это не особенно важно, так как обычно буквы сверху и снизу имеют по пустому ряду точек (ведь между строками текста должен быть какой-то зазор). Скроллинг букв вниз аналогичен, только перемещать байты знакоместа нужно, начиная с нижнего края.
Подпрограмма вертикального скроллинга знакоместа может выглядеть так:
UP CP 32 ;проверка позиции перемещаемого знакоместа RET NC ;выход, если больше или равна 32 LD HL,(AD_LIN) ;получаем адрес экрана начала строки PUSH AF OR L LD L,A PUSH HL LD D,H ;копируем адрес в DE LD E,L LD B,7 ;повторяем 7 раз UP1 INC H ;в HL - адрес байта следующего ряда LD A,(HL) ;переносим из (HL) в (DE) LD (DE),A INC D ;переходим к следующему ряду DJNZ UP1 LD (HL),0 ;обнуляем самый нижний ряд POP HL POP AF RET
Перед обращением к этой (а также и к следующей) подпрограмме в аккумулятор нужно занести горизонтальную позицию знакоместа в строке. Если заданная позиция выходит за пределы экрана, то есть не попадает в диапазон от 0 до 31, подпрограмма не должна выполняться. Для этого в самом ее начале проверяется обозначенное условие и в случае его невыполнения происходит условный выход из подпрограммы (команда RET NC). Как видите, условными могут быть не только переходы. Сразу заметим, что по условию можно также вызывать процедуры, используя команды типа
CALL S,ADDR
где S - любое из возможных условий, а ADDR - абсолютный адрес или метка.
После проверки возможности выполнения подпрограммы в регистровую пару HL загружается адрес начала строки экрана, по которой будет пробегать «волна». Этот адрес рассчитывается заранее и помещается в двухбайтовую переменную AD_LIN, которая в программе будет задаваться инструкцией ассемблера DEFW. В остальных строках не встретилось ничего нового, поэтому, надеемся, вполне достаточно кратких комментариев.
Подпрограмма скроллинга вниз похожа на первую, но все же есть и некоторые отличия. Мы говорили, что начинать такой скроллинг нужно с нижнего края знакоместа, однако можно поступить и несколько иначе, например, так:
DOWN CP 32 ;начало такое же, как и в RET NC ; предыдущей подпрограмме LD HL,(AD_LIN) PUSH AF OR L LD L,A PUSH HL XOR A ;в аккумуляторе 0 EX AF,AF' ;отправляем его в альтернативный AF' LD B,7 DOWN1 LD A,(HL) ;считываем байт текущего ряда EX AF,AF' ;меняем аккумулятор на альтернативный LD (HL),A ;записываем в текущий ряд INC H ;переходим к следующему ряду DJNZ DOWN1 EX AF,AF' ;запись последнего байта LD (HL),A ; в самый нижний ряд POP HL POP AF RET
Раньше мы уже упоминали команду EX AF,AF', сейчас же показали ее практическое применение. Напомним, что она выполняет действие, аналогичное команде EXX, только меняет на альтернативный аккумулятор, а заодно и флаговый регистр (этим иногда тоже можно пользоваться, сохраняя флаги для последующих операций). Советуем внимательно изучить подпрограмму DOWN и проследить за «эволюциями» аккумулятора в данном примере. Это поможет вам лучше понять принцип работы многих других подпрограмм, с которыми вы еще встретитесь.
После того, как основные процедуры скроллингов созданы, можно приступить к написанию подпрограммы, формирующей саму «волну». Алгоритм создания этого эффекта совсем несложен: первый символ, находящийся в «голове» синусоиды нужно сдвинуть, например, вверх, тогда следующие два пойдут вниз и последний - снова вверх. Получив таким образом синусоиду, смещаем ее начало на одну позицию и повторяем все с самого начала до тех пор, пока «волна» не пройдет по всему экрану и не скроется за его пределами. Вот программа, создающая на экране описываемый эффект:
ORG 60000 XOR A ;инициализация переменных: LD (HEAD),A ; начальной позиции «волны» CALL 3742 LD (AD_LIN),HL ; и адреса строки экрана WAVE LD HL,HEAD LD A,(HL) INC (HL) ;увеличивать или уменьшать можно не ; только содержимое регистров или ; регистровых пар, но и значение в ; ячейке памяти, адресованной парой HL CP 35 ;ушла ли «волна» за пределы экрана? RET Z CALL UP ;первый символ вверх DEC A CALL DOWN ;второй - вниз DEC A CALL DOWN ;третий тоже вниз DEC A CALL UP ;последний - вверх LD BC,5 CALL 7997 ;небольшая задержка (PAUSE 5) JR WAVE HEAD DEFB 0 ;позиция «головы» синусоиды AD_LIN DEFW 0 ;адрес экрана начала строки
Данная подпрограмма создает эффект «волны» в самой верхней строке экрана, а чтобы получить то же самое в другой строке, нужно перед командой
CALL 3742
добавить
LD A,N
где N - номер требуемой строки.
Подпрограмма в таком виде пригодна для вызова из Бейсика. Напечатайте в верхней строке какой-нибудь текст, желательно большими буквами, а затем выполните оператор
RANDOMIZE USR 60000
Если же вы захотите все необходимые приготовления выполнить в ассемблере, то у вас уже достаточно знаний, чтобы справиться с этой задачей самостоятельно и без особых трудностей.
в первую очередь тем, кого
Эта книга адресована в первую очередь тем, кого уже перестал удовлетворять несколько ограниченный и неповоротливый Бейсик и кто мечтает наконец научиться писать программы на ассемблере. Книга рассчитана на достаточно подготовленного читателя, прошедшего «боевое крещение» Бейсиком, а новичкам в программировании мы можем порекомендовать первую книгу из серии «Как написать игру для ZX Spectrum». Надеемся также, что и профессионалы смогут найти здесь для себя некоторые зерна истины.
Как и в предшествующей книге, речь здесь пойдет преимущественно об игровых программах, однако хотим вас предупредить заранее, что ассемблер - штука серьезная и нам не раз придется погружаться в пучины мудреных понятий и терминов. Но со своей стороны мы обещаем сделать эти погружения не слишком головокружительными, смягчив суровую необходимость занимательными примерами.
Возможно, вас несколько смутили только что прочитанные строки, да и раньше вам, быть может, не раз приходилось слышать, мол, писать программы на ассемблере невероятно сложно. Но, право, не так страшен ассемблер, как его малюют, а что касается сложностей, так вспомните свои первые шаги в том же Бейсике.
Конечно, между этими двумя языками существуют различия, причем принципиальные. Так, действие почти каждого оператора Бейсика можно сразу увидеть на экране [1], чего не скажешь об ассемблере. Здесь, если вы хотите получить какой-то видимый невооруженным глазом результат, нужно, засучив рукава, создавать целую программу. И если в Бейсике достаточно только набрать текст программы, чтобы она заработала, то ассемблерный текст нужно еще специальным образом обработать - оттранслировать (трансляция исходных текстов на ассемблере в принципе очень похожа на обработку бейсик-программ с помощью компиляторов.) Однако надеемся, что после внимательного изучения данного пособия обозначенные выше сложности не покажутся вам серьезным препятствием.
Вообще же, основная сложность ассемблера заключается, пожалуй, в одном: текст программы получается очень длинным из-за того, что каждая инструкция выполняет какое-то одно элементарное действие типа сложения или вычитания, поэтому для получения ощутимого результата приходится писать длинный ряд инструкций. Зато, в отличие от Бейсика, понять смысл каждой команды, как нам кажется, значительно проще. Во всяком случае, мы попытаемся и вам передать нашу уверенность в этом.
Поскольку исходные тексты программ на ассемблере получаются весьма громоздкими, мы не смогли поместить в книге ни одной полноценной игры, компенсировав этот недостаток массой небольших, но очень полезных примеров и фрагментов игровых программ. И если вы уже имеете некоторый опыт в создании компьютерных игрушек, то вам не составит большого труда собрать свою собственную программу из приведенных в книге «кирпичиков».
Еще одна сложность ассемблера состоит в том, что в нем отсутствуют привычные сообщения об ошибках. Ассемблер будет «ругаться» только тогда, когда вы попытаетесь подсунуть ему несуществующую инструкцию или еще что-нибудь в этом роде. А так можно шутки ради написать заведомо неработоспособную программу и ассемблер ничтоже сумняшеся оттранслирует ее, да еще подбодрит вас сообщением, мол, ошибок нет, программа получилась - просто блеск. Но вот что любопытно: этот, казалось бы, неприятный недостаток обращается в несравненное достоинство, совершенно недоступное никакому другому языку! Освоив ассемблер, вы наверняка в один прекрасный момент почувствуете, что значит истинная свобода. Ведь теперь вы не будете скованы никакими условностями, и никто не рявкнет вдруг из-за спины: «это нельзя, то нельзя!» Отныне все условности для себя будете создавать вы сами и никто не помешает в любой момент отбросить их в сторону.
Теперь по сложившейся традиции необходимо сказать несколько слов о структуре книги. Будем исходить из предположения, что вам уже известны составные части игровой программы и способы их создания, и мы лишь мельком «пробежимся» по этому материалу, сосредоточив основное внимание на средствах достижения той или иной цели. Вначале мы продемонстрируем, как можно получить те же результаты, что и при использовании операторов Бейсика, а затем все чаще станем применять методы, недоступные языкам высокого уровня и дающие совершенно уникальные эффекты.
вводит вас в мир игровых программ, показывает, из каких основных частей они состоят: заставки, игрового пространства, блока взаимодействия с играющим, блока оценки игровой ситуации и блока музыкального сопровождения игры. В ней дается общее представление о каждой из этих частей и их функциях, приводятся примеры и рисунки.
Как вы понимаете, прежде чем приступать к изучению нового языка, необходимо четко уяснить, что он собой представляет, усвоить несколько новых понятий и на самых элементарных примерах научиться писать исходные тексты. Поэтому во рассказывается о том, что такое машинные коды и ассемблер, чем они отличаются друг от друга и каковы преимущества и недостатки ассемблера. Вы познакомитесь с организацией памяти ZX Spectrum, с регистрами и регистровыми парами, то есть со всем тем, без чего невозможно написать даже самую маленькую ассемблерную программку.
В , шаг за шагом, мы научим вас вводить и редактировать программы в ассемблере GENS4, расскажем о структуре строки исходного текста, трансляции программ, о сохранении и удалении текстов, и о многом другом, с чем вы столкнетесь буквально в первые минуты знакомства с ассемблером.
показывает, как подготовить экран к работе, то есть установить его атрибуты, бордюр, а при необходимости очистить от ненужных символов; как сделать на нем разные надписи, вывести числа и создать простейшие изображения. При этом каждый, даже самый маленький шаг, сопровождается программой и подробными комментариями к ней. После нескольких уроков приводится вполне самостоятельная программа статической заставки, на примере которой вы узнаете, как создаются блоки данных для вывода на экран простейших спрайтов, всевозможных рамок и различных надписей не только латинскими, но и русскими буквами.
В мы обсудим вопрос о том, как заставить двигаться по экрану отдельные символы, целые строки и небольшие спрайты. Особое внимание уделено принципам организации циклов, составлению и использованию подпрограмм, в соответствии с которыми разработано несколько полезных для вашего будущего творчества процедур. Показано, как сформировать и заставить двигаться окна, «растворить» изображение на экране, сделать настоящую мультипликационную картинку.
посвящена многокадровой заставке и проблемам, которые возникают при ее создании. В частности, приводятся программки, формирующие простые звуки, а также делающие из стандартного набора символов высокие и широкие буквы. Если же вы не чувствуете в себе особых способностей к рисованию, мы предложим простой способ создания изображения названия игры. Значительное внимание уделено скроллингу окон во всех направлениях даются примеры и пояснения к их использованию. В конце главы приводится полноценная многокадровая заставка, которую с небольшими изменениями или без таковых, вы сможете использовать в своих игровых программах.
Из сказанного может создаться впечатление, что мы слишком уж много внимания уделяем заставкам, но на это есть по меньшей мере две причины: во-первых, заставка, как-никак - это лицо любой игровой программы, а во-вторых, на этих примерах проще всего изучать команды машинного языка. Поэтому к концу шестой главы вы усвоите большую их часть и когда в следующей, седьмой главе, перейдете к изучению игрового пространства, можно будет заняться чистым творчеством, не отвлекаясь на техническую сторону программирования.
В мы познакомим вас с основой основ любой игровой программы - игровым пространством и детально рассмотрим круг вопросов, связанных с формированием и быстрым выводом на экран пейзажей и спрайтов. Чтобы облегчить их создание, предлагается простая программа, которая позволяет легко получить готовые к использованию спрайты и даже целые спрайт-файлы. Подробно изложена проблема восстановления фона при движении спрайтов в игровом пространстве и описаны пять наиболее употребительных способов, каждый из которых проиллюстрирован примером. В заключительном разделе показано, что элементы игрового пространства можно формировать не только непосредственно на экране, но и в памяти компьютера, и после окончания построения изображения быстро выводить его на экран, создавая многоплановые мультипликационные картинки.
показывает, как управлять спрайтами во время игры, перемещая их во всех направлениях, в том числе по диагонали. В зависимости от сюжета и цели игры вы получаете возможность остановить свой выбор на управлении от клавиатуры или одном из типов джойстиков. Но можете воспользоваться и универсальной процедурой, благодаря которой спрайты одинаково реагируют как на клавиши, так и на повороты ручки джойстика.
В следующей, , речь идет о том, как оценивать действия играющего и его противников на протяжении всей игры: начислять очки, следить за количеством «жизней» и оставшимися ресурсами, и вообще за всем тем, что поддается подсчету. А для того, чтобы вычисления не вызывали больших проблем, показано, как воспользоваться услугами «зашитой» в ПЗУ компьютера программой, называемой калькулятором. С ее помощью можно выполнять математические действия не только над целыми, но и над дробными числами. Наконец, детально рассмотрена такая малопонятная для многих начинающих программистов вещь, как прерывания. Показано, как написать свою собственную процедуру обработки прерываний на примере контроля оставшегося до конца игры времени.
Из вы узнаете о нескольких различных способах извлечения звуков. В ней мы покажем, как получить отдельные акустические эффекты для звукового оформления игры, а также, как заставить компьютер проигрывать целые музыкальные фразы. Обладатели компьютеров ZX Spectrum 128 и Scorpion ZS 256 получат представление о работе с трехканальным музыкальным сопроцессором. В последнем разделе главы приводится краткое описание второй версии музыкального редактора Wham - Wham FX, предназначенного для программирования мелодий, исполняемых музыкальным сопроцессором.
Завершает книгу , в которой будут обсуждены некоторые «профессиональные» возможности ассемблера GENS4. К ним относится работа с макросами, условная трансляция, методика ассемблирования программ больших размеров, состоящих, возможно, из нескольких исходных файлов.
Изначально мы планировали включить в книгу подробное описание популярной игры Tetris и на ее примере показать, как создается полноценная программа на ассемблере, с чего нужно начать и чем закончить. Но увы, ограничение объема не позволило сделать этого. Тем не менее, мы все же надеемся, что данная книга не последняя в серии «Как написать игру» и позже удастся опубликовать не только упомянутую программу, но и многие другие, не вошедшие в настоящее издание.
Ввод имени играющего
Рисунок 8.1. Ввод имени играющего
Представим себе, что в конце игры необходимо набрать имя играющего, чтобы затем записать его в раздел меню HI SCORE. Для этого, при достижении определенных результатов, вызывается кадр, в котором вы видите примерно такую таблицу, какая изображена на Рисунок 8.1. Далее, управляя курсором с помощью клавиш Q, A, O и P, требуется выбрать из таблицы буквы вашего имени, нажимая после каждой клавишу выбора M. При этом набранные буквы из таблицы будут переноситься в строку, расположенную ниже. Если какой-то символ набран неверно, его можно стереть, «нажав» в таблице букву d (delete), для печати пробела используется буква s (space), а для ввода имени и завершения этой части программы - буква e (enter). Надо сказать, что такой способ ввода имени не самый удобный, однако он имеет право на существование в случаях, когда играющий еще плохо знаком с клавиатурой ZX Spectrum, но имеет некоторое представление о латинском алфавите.
ORG 60000 ENT $ XOR A CALL 8859 LD A,68 LD (23693),A CALL 3435 LD A,2 CALL 5633 ; Очистка строки для ввода имени LD HL,NAME LD DE,NAME+1 LD BC,19 LD (HL)," " LDIR ; Вывод таблицы символов в рамке CALL TABL CALL LINES LD A,68 LD (23693),A LD BC,#506 ;начальные координаты курсора в таблице LD E,0 ;номер символа в строке ввода SET 3,(IY+48) ;режим ввода прописных букв ; Управление курсором и печать выбранного символа в строку KEYS CALL SETCUR ;вывод курсора XOR A LD (23560),A WAIT LD A,(23560) ;ожидание нажатия клавиши AND A JR Z,WAIT CP "P" ;перемещение курсора на JR Z,RIGHT ; один шаг вправо CP "O" ;перемещение курсора JR Z,LEFT ; на один шаг влево CP "Q" ;перемещение курсора JR Z,UP ; на один шаг вверх CP "A" ;перемещение курсора JR Z,DOWN ; на один шаг вниз CP "M" ;печать выбранного символа JR Z,SELECT ; в строке ввода JR KEYS ; Перемещение курсора вправо RIGHT LD A,C ;проверка достижения курсором CP 24 ; правой границы таблицы JR NC,KEYS CALL RESCUR ;удаление курсора на прежнем месте INC C ;изменение положения курсора INC C CALL SETCUR ;установка курсора на букву таблицы JR KEYS ; Перемещение курсора влево LEFT LD A,C ;проверка достижения курсором CP 7 ; левой границы таблицы JR C,KEYS CALL RESCUR DEC C DEC C CALL SETCUR JR KEYS ; Перемещение курсора вверх UP LD A,B ;проверка достижения курсором CP 6 ; верхней границы таблицы JR C,KEYS CALL RESCUR DEC B DEC B CALL SETCUR JR KEYS ; Перемещение курсора вниз DOWN LD A,B ;проверка достижения курсором CP 11 ; нижней границы таблицы JR NC,KEYS CALL RESCUR INC B INC B CALL SETCUR JR KEYS ; Выбор символа, который затем будет напечатан в строке или выбор ; функции для редактирования этой строки SELECT PUSH BC PUSH DE CALL SND ;звуковой сигнал, издаваемый при ; перемещении символа из таблицы в ; набираемую строку POP DE POP BC LD A,B CP 11 JR NZ,MOVE ;печать символа LD A,C CP 20 JR Z,DELETE ;удаление символа в строке CP 22 JR Z,SPACE ;печать пробела в строке CP 24 RET Z ;выход из программы ; Перемещаем символ из таблицы в набираемую строку и смещаем курсор ; на позицию вправо, при этом делаем проверку того, чтобы символ ; не вышел за заданные границы строки (слева и справа). MOVE LD A,E CP 20 JP NC,KEYS LD D,0 PUSH BC PUSH DE LD A,B ;по вертикальной координате курсора ; определяем адрес данных строки ; таблицы (STR1, STR2, STR3 или STR4) SUB 5 LD HL,D_STR LD E,A ADD HL,DE LD E,(HL) INC HL LD D,(HL) EX DE,HL LD A,C ;по горизонтальной координате находим ; код символа в блоке данных SUB 6 LD C,A LD B,0 ADD HL,BC POP DE POP BC LD A,(HL) ;помещаем код символа в A LD HL,NAME ;определяем адрес в строке NAME ADD HL,DE ; для ввода символа LD (HL),A ;помещаем символ в строку ввода CALL PR_STR ;выводим строку ввода на экран INC E ;смещаем позицию ввода вперед JP KEYS ; Удаление неправильно набранного символа DELETE LD A,E ;проверка достижения начала строки ввода AND A JP Z,KEYS DEC E ;уменьшаем позицию ввода LD D,0 LD HL,NAME ADD HL,DE LD (HL)," " ;заменяем удаляемый символ пробелом CALL PR_STR JP KEYS ; Ввод пробела SPACE LD A,E ;проверка достижения конца строки ввода CP 20 JP NC,KEYS LD D,0 LD HL,NAME ADD HL,DE LD (HL)," " CALL PR_STR INC E ;увеличиваем позицию ввода JP KEYS ; Вывод курсора изменением байта атрибутов RESCUR LD A,68 ;PAPER 0, INK 4, BRIGHT 1 JR PRATTR ; Удаление курсора восстановлением байта атрибутов SETCUR LD A,79 ;PAPER 1, INK 7, BRIGHT 1 ; Вычисляем адрес атрибутов знакоместа и заносим ; по этому адресу байт из аккумулятора PRATTR LD L,B LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL PUSH AF LD A,H ADD A,#58 LD H,A LD A,L ADD A,C LD L,A POP AF LD (HL),A RET ; Подпрограмма печати таблицы символов TABL LD DE,STR LD BC,LENSTR JP 8252 ; Подпрограмма печати введенной строки PR_STR PUSH BC PUSH DE LD DE,STR5 LD BC,LENLIN CALL 8252 POP DE POP BC RET ; Подпрограмма рисования рамки LINES EXX PUSH HL LD A,66 LD (23695),A LD BC,#8A2C ;B = 138, C = 44 CALL 8933 LD DE,#101 LD BC,160 ;B = 0, C = 160 CALL 9402 LD DE,#FF01 LD BC,#3D00 ;B = 61, C = 0 CALL 9402 LD DE,#1FF LD BC,160 CALL 9402 LD DE,#101 LD BC,#3D00 CALL 9402 POP HL EXX RET ; Короткий звуковой сигнал SND LD B,30 LD HL,350 LD DE,2 SND1 PUSH BC PUSH DE PUSH HL CALL 949 POP HL POP DE POP BC SBC HL,DE DJNZ SND1 RET ; Данные таблицы символов STR DEFB 22,5,6 STR1 DEFM "1 2 3 4 5 6 7 8 9 0" ;символы через один пробел DEFB 22,7,6 STR2 DEFM "A B C D E F G H I J" DEFB 22,9,6 STR3 DEFM "K L M N O P Q R S T" DEFB 22,11,6 STR4 DEFM "U V W X Y Z . d s e" STR5 DEFB 22,19,5,16,5,">",16,2 NAME DEFM "····················" DEFB 16,5,"<" LENSTR EQU $-STR ;длина строки для печати таблицы LENLIN EQU $-STR5 ;длина строки ввода имени ; Адреса данных символов в таблице D_STR DEFW STR1,STR2,STR3,STR4
Эту программу можно рассматривать как вполне независимый кадр заставки. Если вы решите использовать ее в своей собственной игре, единственное, что вам потребуется, это перенести после выхода введенное имя из строки NAME в таблицу «рекордов». И, конечно же, вам нужно будет проследить, чтобы имена меток, задействованные в приведенной программе не повторялись в вашей игре. Естественно, что при необходимости наименования меток можно и заменить.
ВВОД ЭЛЕМЕНТА СЛУЧАЙНОСТИ
ВВОД ЭЛЕМЕНТА СЛУЧАЙНОСТИ
Любая игра потеряет всякий смысл, если действия компьютера можно будет предугадать на всех этапах развития сюжета. Чтобы придать персонажам видимость самостоятельности и непредсказуемости поведения, в игровых программах довольно широко используются так называемые случайные числа. Строго говоря, получить действительно случайные значения программным путем нет никакой возможности, вы можете лишь заставить компьютер вырабатывать более или менее длинную последовательность неповторяющихся величин, но в конце концов она все же начнет повторяться. Поэтому такие числа обычно называют псевдослучайными. ВБейсике для их получения используется функция RND, которая вырабатывает по определенному закону числа от 0 до 1 и далее они обычно преобразуются программными средствами в числа из заданного диапазона. В ассемблере работать с дробными величинами значительно сложнее, отчего программисты с этой целью редко прибегают к использованию подпрограмм ПЗУ, а пишут, как правило, свои аналогичные процедуры.
В качестве «случайных» чисел довольно часто используют последовательность кодов ПЗУ. Такой метод крайне прост и дает неплохую степень случайности в циклах. Если вы не забыли, именно такой метод мы применили в программе «растворения» символов, описанной в предыдущей главе. Сейчас же мы расскажем и о некоторых других способах получения псевдослучайных чисел.
Иногда «случайные» числа извлекают из системного регистра регенерации R. Поскольку его значение постоянно увеличивается после выполнения каждой команды микропроцессора, предугадать, что же он содержит в какой-то момент времени практически невозможно. Таким образом, простейший генератор случайных чисел может выглядеть так:
LD A,R
Но помните, что это справедливо только для достаточно разветвленных программ, особенно если их работа зависит от внешних воздействий (например, при управлении с помощью клавиатуры или джойстика). В коротких же циклах ни о какой непредсказуемости говорить не приходится.
Кроме того, есть и еще один недостаток использования регистра регенерации. Значение его никогда не превышает 127. Иными словами, седьмой бит этого регистра обычно «сброшен» в 0, и, дойдя до значения 127, он вновь обнуляется.
Однако, справедливости ради, стоит заметить, что это относится лишь к тем программам, в которых регистр R не изменяется принудительным образом. При желании вы можете установить его 7-й бит и тогда постоянно будете получать из него значения от 128 до 255. Правда, делается это обычно в целях защиты программ (например, такой метод применен в игре NIGHT SHADE), но это уже совсем другая тема.
Когда требуется получить наибольшую степень случайности, прибегают к математическим расчетам. Разберем одну из таких «математических» подпрограмм. Несмотря на ее простоту, она вырабатывает все же достаточно длинную последовательность неповторяющихся значений, чтобы их можно было рассматривать в качестве случайных.
RND255 PUSH BC PUSH DE PUSH HL ; Регистровая пара HL загружается значением из счетчика «случайных» чисел ; (это может быть, например, системная переменная 23670/23671, ; которая используется Бейсиком для тех же целей) LD HL,(ADDR) LD DE,7 ;дальше следует расчет очередного ; значения счетчика ADD HL,DE LD E,L LD D,H ADD HL,HL ADD HL,HL LD C,L LD B,H ADD HL,HL ADD HL,BC ADD HL,DE LD (ADDR),HL ;сохранение значения счетчика «случайных» ; чисел для последующих расчетов LD A,H ;регистр A загружается значением ; старшего байта счетчика POP HL POP DE POP BC RET ADDR DEFW 0
Эта процедура возвращает в аккумуляторе «случайные» числа от 0 до 255. Однако в подавляющем большинстве случаев нужно иметь возможность получать значения из произвольного диапазона. С этой целью дополним подпрограмму RND255 расчетами по ограничению максимального значения и назовем новую процедуру просто RND. Перед обращением к ней в регистре E задается верхняя граница вырабатываемых «случайных» чисел. Например, для получения в аккумуляторе числа от 0 до 50 в регистр E нужно загрузить значение 51:
RND CALL RND255 LD L,A LD H,0 LD D,H CALL 12457 LD A,H RET
Здесь вновь появилась еще одна подпрограмма ПЗУ, расположенная по адресу 12457. Она выполняет целочисленное умножение двух чисел, записанных в регистровых парах DE и HL. Произведение возвращается в HL. Если в результате умножения получится число, превышающее 65535, то будет установлен флаг CY, иначе выполняется условие NC. Проверка переполнения может оказаться полезной, когда перемножаются не известные заранее величины. В подпрограмме RND это условие проверять не нужно, так как оба сомножителя не превышают величины 255 (H и D предварительно обнуляются).
После того, как мы получили в свое распоряжение подпрограмму генерации случайных чисел, рассмотрим один занимательный пример ее применения. Представьте себе некое подобие мишени, состоящей, как и положено, из окружностей и ряда цифр, характеризующих заработанные вами очки при попадании в ту или иную ее часть. Вы нажимаете любую клавишу компьютера и в ту же секунду раздается звук, очень похожий на пролетающую мимо вашего уха пулю, а в мишени появляется отверстие с рваными краями. Нажимаете еще раз - снова попадание, но уже совсем в другом месте (Рисунок 6.8) и изображение отверстия тоже стало каким-то другим. Повторив эту процедуру много раз, вы легко можете убедиться в том, что «пули», как и при настоящей стрельбе, ложатся на мишень совершенно случайно. То же самое можно сказать и о характере отверстий. Отсюда ясно, что программа, реализующая эту игрушку, должна вырабатывать для каждого выстрела три случайных числа: координаты X и Y места попадания и номер изображения для пулевого отверстия. Теперь можно обратиться к самой программе и кратко ее прокомментировать.
ВВОД СТРОКИ
ВВОД СТРОКИ
Давайте напишем небольшую программку, находясь в редакторе GENS. Для этого загрузите и запустите ассемблер, как было сказано выше, для определенности выбрав адрес загрузки равным 25000.
Вводить строки можно двумя способами: так же, как и в Бейсике, то есть каждый раз набирать номер и текст, или воспользоваться автоматической нумерацией, что на наш взгляд несравненно удобнее. Для автоматической нумерации строк нужно на подсказку редактора (символ > с курсоромC или L) ввести команду I (Insert - режим вставки строк) и нажать Enter. Первая строка будет иметь номер 10 и последующие номера будут увеличиваться с шагом 10. Если вы пожелаете изменить порядок нумерации строк, введите ту же команду I, но с двумя параметрами, разделенными запятой: первое число - начальный номер, второе - шаг. Например, I100,5.
Итак, введите команду I с параметрами или без - после старта GENS оба параметра по умолчанию равны 10, поэтому в самом начале работы их можно и не указывать.
Так как в первой строке метки нам не понадобятся, сразу перейдем к полю мнемоник. Переход к следующему полю строки осуществляется вводом символа табуляции, который получается при одновременном нажатии клавиш Caps Shift и 8. (Вместо символов табуляции можно вводить просто один или несколько пробелов - после окончательного ввода строки они автоматически будут заменены кодами табуляции, и при просмотре листинга текст будет выглядеть аккуратно выровненным по границам полей.) Нажмите эту комбинацию клавиш и увидите, как курсор перескочит вправо сразу на несколько позиций печати. Теперь можно набрать какую-нибудь инструкцию.
Как правило, программы на ассемблере начинаются с указания начального адреса, то есть того адреса, с которого полученная программа будет загружаться в память. Это достигается включением в программу директивы ORG (от англ. Origin - начало), следом за которой пишется десятичный или шестнадцатеричный адрес. (При записи шестнадцатеричных чисел они должны начинаться с символа #, например, #3FC.) Наберите в первой строке эту директиву и укажите после символа табуляции адрес 60000. Если вы случайно нажмете не ту клавишу, удалить последний введенный символ можно обычным образом, нажав клавиши Caps Shift/0 или Delete. Но вот переместить курсор к любому символу строки стандартными методами не удастся: ведь клавиши Caps Shift/8, как вы уже знаете, отвечают за ввод табуляции, а Caps Shift/5 удалит весь текст строки и переместит курсор в самое ее начало. Закончите ввод нажатием клавиши Enter, после чего строка должна принять вид
10 ORG 60000
а ниже появится номер следующей строки с курсором.
Аналогичным образом введите еще две строки:
20 LD BC,1000 30 RET
Теперь нам нужно вернуться в строчный редактор GENS. Для этого достаточно нажать клавишу Edit или Caps Shift/1. На экране снова появится подсказка, на которую можно вводить команды редактора.
ВЫХОД ИЗ GENS
ВЫХОД ИЗ GENS
Убедившись, что ошибок нет (по крайней мере, грубых), неплохо было бы проверить работоспособность получившейся программы. Работая с GENS вы в любой момент можете временно попрощаться с ассемблером и выйти в Бейсик, дав редактору команду B (Bye - «до встречи» или Basic, если вам так легче запомнить).
После выхода из GENS4 мы рекомендуем вам прежде всего очищать экран оператором CLS или простым нажатием клавиши Enter. Дело в том, что эта версия пользуется для вывода на экран встроенным драйвером, позволяющим выводить в строке до 51 символа мелким шрифтом, и при выходе стандартный канал вывода по каким-то причинам не сразу восстанавливается. Поэтому до очистки экрана оператор PRINT может не дать ожидаемых результатов.
Вероятно, вы обратили внимание на то, что предложенный пример в точности повторяет программку, которую мы недавно вводили с помощью оператора POKE. Поверьте, что сделали это мы не от скудости воображения, а для того, чтобы вы имели возможность удостовериться в идентичности полученных тогда и сейчас результатов. Введите с клавиатуры строку
FOR n=60000 TO 60003: PRINT PEEK n: NEXT n
и увидите столбик чисел, совпадающих с теми, которые мы вводили ранее вручную.
Снова вызовите редактор GENS и введите команду L. Как видите, текст набранной программки никуда не исчез.
Выполнение команд сдвига аккумулятора
Рисунок 5.2. Выполнение команд сдвига аккумулятора
Похожие сдвиги могут быть получены и с другими регистрами (в том числе и с аккумулятором), либо в ячейке памяти, адресованной регистровой парой HL. Вот эти команды:
Команда RLC S выполняет действие, аналогичное RLCA. В качестве операнда S могут использоваться регистры A, B, C, D, E, H или L, а также адрес (HL), если записать инструкцию RLC (HL). Во всех остальных командах можно применять тот же набор операндов (немного опережая события, добавим, что во всех этих командах могут участвовать также и ячейки памяти, адресованные индексными регистрами, но поскольку мы их еще не рассматривали, применять такие инструкции пока не будем).
Инструкция RRC S выполняется аналогично команде RRCA, но помимо аккумулятора применима к любым перечисленным операндам.
Команды RR S и RL S подобны описанным командам RRA и RLA соответственно.
Весьма широко применяются команды SLA S (сдвиг влево) и SRL S (сдвиг вправо). Они отличаются от прочих команд сдвигов тем, что освобождающийся бит при любых условиях заполняется нулевым значением, в результате чего их вполне можно рассматривать как операции умножения или деления на 2 соответственно. «Вытесняемый» бит при этом, как и при других сдвигах переходит во флаг переноса.
Еще одна операция «деления на 2 со знаком» выполняется командой SRA S, которая смещает вправо только 7 младших битов, а старший оставляет без изменения. Младший бит, как и положено, вытесняется во флаг CY. Все команды этой группы воздействуют уже не только на флаг переноса, но и на все прочие. Флаги H и N, так же, как и при сдвигах аккумулятора, сбрасываются в 0.
На Рисунок 5.3 приведены схемы всех перечисленных сдвигов. Рассмотрите этот рисунок внимательно, и вам, наверное, уже станет понятно в общих чертах, как можно получить эффект плавно движущейся строки. Теперь остается, пожалуй, только одна сложность: научиться быстро и безошибочно определять адреса в экранной области, соответствующие началу любой строки. Конечно, вы можете воспользоваться из второй главы, но тогда не сможете написать универсальной подпрограммы «на все случаи жизни».
Выполнение команд сдвига регистров
Рисунок 5.3. Выполнение команд сдвига регистров
Алгоритм расчета адресов экрана достаточно сложен, поэтому, как и прежде, обратимся за помощью к ПЗУ, благо в «прошивке» Speccy имеются необходимые процедуры. Для получения начального адреса любой строки экрана можно обратиться к подпрограмме, расположенной по адресу 3742. Перед обращением к ней в аккумулятор необходимо поместить номер строки экрана. На выходе в регистровой паре HL получится искомый адрес, зная который, уже несложно рассчитать и любой другой адрес в пределах данной строки. Каждая строка имеет длину 32байта, которые расположены последовательно, и вмещает 8 рядов пикселей. При переходе к следующему ряду адрес видеобуфера увеличивается на 256, то есть увеличивается только старший байт адреса, а младший остается без изменений.
Для примера покажем, как рассчитать адрес второго байта сверху в 5-й строке и 11-й позиции экрана (иначе, в позиции печати, определяемой директивой Бейсика AT 5,11):
LD A,5 ;номер строки CALL 3742 ;получаем в HL начальный адрес LD A,L ;берем значение младшего байта адреса OR 11 ;добавляем смещение в 11 байт (знакомест) LD L,A ;возвращаем в младший байт INC H ;увеличиваем адрес на 256 и тем самым ; получаем адрес второго байта в ; знакоместе сверху
Теперь можно написать программу, дающую эффект бегущей строки. Для определенности будем скроллировать 21-ю строку экрана:
ORG 60000 LD A,21 ;21-я строка экрана SCRLIN CALL 3742 ;получаем ее адрес в HL ; Так как строка должна бежать слева направо, то раньше нужно сдвигать ; последние байты, поэтому определяем адрес конца строки LD A,L OR 31 LD L,A LD C,8 ;высота строки 8 пикселей SCRL1 LD B,32 ;длина строки 32 байта AND A ;очистка флага CY PUSH HL ;сохраняем адрес SCRL2 RL (HL) ;последовательно сдвигаем все байты DEC HL DJNZ SCRL2 POP HL ;восстанавливаем адрес INC H ;переходим к следующему ряду пикселей DEC C ;повторяем JR NZ,SCRL1 RET
Но это еще не все, ведь данная процедура сдвинет строку только на один пиксель влево, а для перемещения ее на знакоместо потребуется выполнить приведенную подпрограмму 8 раз. Однако прежде чем мы продолжим создание полноценного эффекта, поясним смысл некоторых использованных команд.
Возможно, вам не совсем ясно, что в данной подпрограмме делает инструкция AND A. Как сказано в комментарии к этой строке, она очищает флаг переноса. Собственно, это и все, что нам от нее требуется, но для чего это нужно? Посмотрите на схему перемещения битов командой RL S и увидите, что при ее выполнении бит из CY переходит в младший бит операнда, в то время как старший сохраняется во флаге переноса. Этим и обусловлен выбор именно команды RL (HL), ведь нам нужно скроллировать не отдельный байт, а целую цепочку байтов, значит, вытесняемый бит должен быть сохранен для следующей команды сдвига. Но сдвигая самый первый байт в цепочке, мы должны убедиться, что в младшем бите появится 0, поэтому и нужно сбросить флаг CY. Если этого не сделать, то в конце концов в скроллируемой строке может появиться какой-то нежелательный «мусор» в виде «включенных» пикселей.
Надеемся, что этих объяснений достаточно, а если нет, то попытайтесь мысленно проследить, что происходит в результате выполнения команды RL (HL) на каждом «витке» цикла, обозначенного меткой SCRL2, какие биты и куда при этом сдвигаются.
А сейчас напишем небольшую тестовую программку на Бейсике, проверяющую работоспособность нашей процедуры. Постарайтесь после этого самостоятельно переписать ее на ассемблере, а когда справитесь с задачей, перелистните несколько страниц и проверьте себя, сравнив полученный результат с , данным в конце этого раздела.
10 INK 6: PAPER 0: BORDER 0: CLS 20 LET a$="Examine yourself how you know the assembler!" 30 FOR i=1 TO LEN a$ 40 PRINT AT 21,31; INK 0; a$(i) 50 FOR j=1 TO 8 60 RANDOMIZE USR 60000 70 NEXT j 80 NEXT i 90 FOR i=1 TO 256: RANDOMIZE USR 60000: NEXT i
Используя команды сдвигов, можно придумать великое множество интересных эффектов. Приведем маленький пример наиболее простого из них - циклического скроллинга отдельного знакоместа, и предоставим вам возможность пофантазировать и развить эту идею.
ORG 60000 LD B,8 LD HL,16384 ROL RRC (HL) INC H DJNZ ROL RET
Чтобы посмотреть, как эта программка работает, напечатайте в Бейсике в левом верхнем углу экрана какой-нибудь символ и в цикле вызывайте процедуру. Изображение должно многократно «провернуться» вокруг вертикальной оси, причем уходящие вправо точки будут вновь появляться с левого края. Если заменить команду RRC (HL) на RLC (HL), то скроллинг будет выполняться в обратную сторону.
ВЫВОД БУКВЕННЫХ И ЦИФРОВЫХ СИМВОЛОВ
ВЫВОД БУКВЕННЫХ И ЦИФРОВЫХ СИМВОЛОВ
Из предыдущего описания вы, возможно, сделали вывод, что сейчас нам предстоит долгая и кропотливая работа над созданием подпрограммы вывода символов на экран, затем придется писать еще одну процедуру, устанавливающую цвета печатаемых букв и цифр, а напоследок, собрав остатки угасающих сил, придумывать способ, как выводить сразу целые строки... Ипосле всего этого решить, что гораздо проще и эффективнее воспользоваться оператором Бейсика PRINT и забыть об ассемблере как о кошмарном сне.
Так вот, смеем вас заверить, что ничего подобного вам не грозит. Очень скоро вы убедитесь, что большинство операций, доступных Бейсику, в ассемблере выполняется почти так же просто. Ведь, как мы уже говорили, в ПЗУ компьютера имеются необходимые подпрограммы для выполнения всех бейсиковских операторов. Поэтому во многих случаях достаточно знать лишь две вещи: первое - по какому адресу расположена та или иная подпрограмма, и второе - как этой подпрограмме передать требуемые параметры. Ну и, конечно же, нужно представлять, каким образом вообще вызываются подпрограммы в ассемблере. А выполняет это действие команда микропроцессора CALL (звать, вызывать), которую можно сравнить с известным вам оператором GO SUB. Только вместо номера строки после команды указывается адрес перехода (еще раз напомним, что адреса обозначаются числами в диапазоне от 0 до 65535).
Сначала разберемся, что требуется для печати символов.
Общаясь с Бейсиком, вы могли заметить, что оператор PRINT весьма универсален и используется для многих целей. С его помощью можно выводить символы и строки не только на основной экран, но и в служебное окно, если написать PRINT #0 или PRINT #1. В системе TR-DOS этот же оператор применяется для записи в файлы прямого и последовательного доступа, а для вывода на принтер имеется другая его разновидность - оператор LPRINT.
Для многих, вероятно, не будет новостью, что LPRINT, в сущности, это уже некоторое излишество Бейсика, так как часто удобнее бывает заменять его на PRINT #i, где i=3, который выводит информацию в поток #3 (подробно о каналах и потоках можно прочитать в [2]), то есть на принтер. Если же номер потока в операторе PRINT не конкретизирован, то по умолчанию вывод осуществляется в поток #2 - на основной экран.
В то время как в Бейсике нужный поток устанавливается автоматически, в ассемблере программист сам должен позаботиться о своевременном и правильном включении текущего потока. Для этой цели в ПЗУ имеется специальная подпрограмма, расположенная по адресу 5633 (или в шестнадцатеричном формате - #1601). Перед ее вызовом в аккумулятор следует поместить номер требуемого потока. Вы, наверное, еще не забыли, что для занесения в какой-либо регистр или регистровую пару некоторого значения используется команда LD. Таким образом, назначить поток #2 для вывода на основной экран можно всего двумя командами микропроцессора:
LD A,2 CALL 5633
После этого можно что-нибудь написать на экране.
Подпрограмма, соответствующая оператору PRINT (или LPRINT) располагается по адресу 16, а перед ее вызовом в регистре A следует указать код выводимого символа. То есть, чтобы напечатать, например, букву A, загрузим в аккумулятор код 65 и вызовем подпрограмму с адресом 16:
LD A,65 CALL 16
Теперь остается дописать команду RET и программка, печатающая на экране букву A, будет, в принципе, готова. Но, прежде чем привести законченный текст, хотелось бы сказать еще вот о чем. Во-первых, для определения кода нужного символа не обязательно каждый раз заглядывать в таблицу, можно предложить ассемблеру самостоятельно вычислять коды. Достаточно нужный символ заключить в кавычки. В нашем случае это будет выглядеть так:
LD A,"A"
И еще один момент.
Если вы дизассемблируете даже целую сотню фирменных игрушек, то вряд ли где-то обнаружите инструкцию CALL 16, хотя добрая половина из них не отказывает себе в удовольствии попользоваться возможностями ПЗУ. Объясняется это тем, что в системе команд микропроцессора Z80 для вызова подпрограмм помимо CALL имеется еще одна инструкция, более ограниченная в применении, но зато и более эффективная. Это команда RST. Она отличается от CALL, в сущности, только одним: с ее помощью можно обратиться лишь к нескольким первым, причем строго фиксированным, адресам. В частности, к адресу 16. А основное преимущество этой команды состоит в том, что она очень компактна и занимает в памяти всего один байт вместо трех, требуемых для размещения кодов команды CALL. Поэтому вместо
CALL 16
значительно выгоднее писать
RST 16
Подводя итог сказанному, можно написать программку, которая будет работать подобно оператору PRINT #2;"A". Загрузите GENS и в редакторе наберите такой текст:
10 ORG 60000 20 LD A,2 30 CALL 5633 40 LD A,"A" 50 RST 16 60 RET
Чтобы проверить работу этой программки, оттранслируйте ее, введя в редакторе команду A, а затем выйдите в Бейсик и, предварительно очистив экран, запустите ее с адреса 60000 оператором RANDOMIZE USR 60000.
После этого можете поэкспериментировать, подставляя в строке 40 другие значения для регистра A. Посмотрите, что получится, если указать коды псевдографических символов, UDG или ключевых слов Бейсика, которые имеют значения от 128 до 255. Правда, в этом случае придется отказаться от символьного представления кодов и нужно будет вводить непосредственные числовые величины. Тем не менее, вы убедитесь, что команда RST 16 превосходно справляется с поставленной задачей и работает точно так же, как работал бы в этом случае и оператор PRINT.
Эксперименты экспериментами, но здесь мы должны сделать небольшое предупреждение. В программировании на ассемблере имеется множество «подводных камней», поэтому не очень удивляйтесь, если после очередного опыта ваша программа «улетит» в неизвестном направлении. Дабы избежать стрессов, вызванных подобной неприятностью, всегда перед стартом программы сохраняйте измененный исходный текст, а лучше - не заходите в своих изысканиях чересчур далеко, пока не изучите книгу до конца. В свое время мы, по возможности, расскажем обо всех (ну, по крайней мере, о многих) «крутых поворотах», подстерегающих программистов-ассемблерщиков на пути создания полноценных программ.
Сделав первый, самый трудный шаг, двинемся дальше и несколько усложним нашу программку. Попробуем напечатать символ в определенном месте экрана, например, в 10-й строке и 8-м столбце, то есть попытаемся воспроизвести оператор PRINT AT 10,8;"X". Оказывается, и это в ассемблере сделать не многим труднее, чем в Бейсике.
Помимо обычных «печатных» символов (так называемых, ASCII-кодов), псевдографики, UDG и токенов ( ключевых слов) Бейсика существует ряд специальных кодов, которые не выводятся, а служат для управления печатью. Часто их так и называют - управляющие символы. Они имеют коды от 0 до 31, хотя при выводе на экран используются не все, а только некоторые из них.
Директиве AT соответствует управляющий символ с кодом 22. И кроме этого кода необходимо вывести еще два, указывающих номера строки и столбца на экране. То есть, команду RST 16 нужно выполнить трижды:
LD A,22 RST 16 LD A,10 RST 16 LD A,8 RST 16
После этого можно вывести и сам символ:
LD A,"X" RST 16
Управляющие коды имеются и для всех прочих директив оператора PRINT: TAB, INK, PAPER, FLASH, BRIGHT, OVER, INVERSE, а также для запятой и апострофа. В табл. 4.1 приведены значения всех управляющих кодов, а также указано, какие байты требуется передать в качестве параметров. Как видите, для кодов 6, 8 и 13 дополнительных данных не требуется, коды 16...21 нуждаются еще в одном байте, а 22 и 23 ожидают ввода двух значений. Обратите внимание, что код 23 (TAB), вопреки ожиданиям, требует не одного, а двух байт, хотя на самом деле роль играет только первый из них, а второй игнорируется и может быть каким угодно (на это в таблице указывает вопросительный знак).
Таблица 4.1. Коды управления печатью
Код | Байты параметров | Значение |
6 | - | Запятая |
8 | - | Забой |
13 | - | Перевод строки (апостроф) |
16 | colour | Цвет INK |
17 | colour | Цвет PAPER |
18 | flag | FLASH |
19 | flag | BRIGHT |
20 | flag | INVERSE |
21 | flag | OVER |
22 | Y, X | Позиция AT |
23 | X, ? | Позиция TAB |
Допустимые значения для параметров следующие:
colour - 0...9 flag - 0 или 1 X - 0...31 Y - 0...21
Теперь напишем на ассемблере пример, соответствующий оператору
PRINT AT 20,3; INK 1; PAPER 5, BRIGHT 1; "OK."
Для большей наглядности снабдим нашу программку комментариями. В ассемблере комментарии записываются после символа «точка с запятой» (;), который может находиться в любом месте программы. Весь текст от этого символа до конца строки при трансляции пропускается и на окончательном машинном коде никак не сказывается. Само собой, при наборе примеров вы можете пропускать все или часть комментариев, тем более, что в книге многие из них даны на русском языке, а GENS, к сожалению, с кириллицей не знаком.
10 ORG 60000 20 LD A,2 ; вывод на основной экран (PRINT #2). 30 CALL 5633 40 ;---------------- 50 LD A,22 ; AT 20,3 60 RST 16 70 LD A,20 80 RST 16 90 LD A,3 100 RST 16 110 ;---------------- 120 LD A,16 ; INK 1 130 RST 16 140 LD A,1 150 RST 16 160 ;---------------- 170 LD A,17 ; PAPER 5 180 RST 16 190 LD A,5 200 RST 16 210 ;---------------- 220 LD A,19 ; BRIGHT 1 230 RST 16 240 LD A,1 250 RST 16 260 ;---------------- 270 LD A,"O" ; печать трех символов строки OK.
280 RST 16 290 LD A,"K" 300 RST 16 310 LD A,"." 320 RST 16 330 RET
Не правда ли, получилось длинновато? Даже не верится, что этот пример после трансляции будет занимать в памяти меньше полусотни байт. А на самом деле его можно сократить еще в несколько раз. Для этого нужно воспользоваться подпрограммой ПЗУ, позволяющей выводить строки символов, да научиться формировать такие строки в программе.
Ассемблер предоставляет несколько директив для определения в программе текстовых строк и блоков данных. Вот они:
DEFB - через запятую перечисляется последовательность однобайтовых значений; DEFW - через запятую перечисляется последовательность двухбайтовых значений; DEFM - в кавычках задается строка символов; DEFS - резервируется (и заполняется нулями) область памяти длиной в указанное число байт.
Эти директивы чем-то напоминают оператор Бейсика DATA, но в отличие от него не могут располагаться в произвольном месте программы. Мы уже говорили, что ассемблер, как никакой другой язык, «доверяет» программисту. Это, в частности, объясняется тем, что микропроцессор не способен сам отличить, к примеру, код буквы A от кода команды LD B,C - и то и другое обозначается десятичным числом 65. Поэтому недопустимо размещать блоки данных, скажем, внутри какой-либо процедуры, так как в этом случае они будут восприниматься микропроцессором как коды команд, и чтобы избежать конфликтов, все данные лучше размещать в самом конце программы или уж, по крайней мере, между процедурами, после команды RET.
Для преобразования вышеприведенного примера выпишем последовательность кодов, выводимых командой RST 16, следом за директивой DEFB:
DEFB 22,10,8,16,1,17,5,19,1,"O","K","."
Подпрограмма вывода последовательности кодов располагается по адресу 8252 и требует передачи двух параметров: адрес блока данных перед обращением к ней нужно поместить в регистровую пару DE, а длину строки - в BC. И если вычисление второго параметра не должно вызвать трудностей, то об определении адреса стоит поговорить.
В предыдущей главе, в разделе «Структура ассемблерной строки», мы упоминали о существовании такого понятия как метка, но еще ни разу им не воспользовались - не было особой надобности. Но теперь нам без них просто не обойтись. Как уже говорилось, метки ставятся в самом начале строки, в поле, которое мы до сих пор пропускали, и служат для определения адреса первого байта команды или блока данных, записанных следом. Имена меток в GENS должны состоять не более чем из шести символов (если метка состоит более чем из 6 символов, лишние при трансляции автоматически отбрасываются, поэтому более длинные имена возможны, но исключительно ради наглядности), среди которых могут быть такие:
0...9 A...Z a...z _ [ ] \ # $ и Ј
но помните, что они не могут начинаться с цифры или знака #. Кроме того, метки не должны совпадать по написанию с зарезервированными словами, то есть с именами регистров и мнемониками условий. Например, недопустимо использование метки HL, однако, благодаря тому, что GENS делает различие между строчными и прописными буквами, имя hl вполне может быть меткой. Ниже перечислены все зарезервированные слова в алфавитном порядке:
Зарезервированные слова GENS
$ A AF AF' B BC C D E E H HL I IX IY L M NC NZ P PE PO R SP Z
И еще один очень важный момент, связанный с метками. Их имена должны быть уникальными, то есть одно и то же имя не может появляться в поле меток дважды. Хотя, конечно, ссылок на метку может быть сколько угодно.
Можно наконец переписать наш пример с использованием блока данных, присвоив последнему имя TEXT:
ORG 60000 LD A,2 CALL 5633 LD DE,TEXT ;в регистровую пару DE записывается ; метка TEXT, соответствующая адресу ; начала блока данных. LD BC,12 ;в регистровую пару BC заносится число, ; соответствующее количеству кодов ; в блоке данных. CALL 8252 ;обращение к подпрограмме ПЗУ, ; которая печатает строку на экране. RET TEXT DEFB 22,10,8,16,1,17,5,19,1,"O","K","."
Не удивляйтесь, что в этом примере отсутствуют номера строк. В дальнейшем мы везде будем приводить тексты программ именно в таком виде, во-первых, потому, что нумерация не несет никакой смысловой нагрузки, а во-вторых, многие фрагменты в ваших программах, скорее всего, будут пронумерованы совершенно иначе.
В заключение этого раздела расскажем еще об одной полезной подпрограмме ПЗУ, связанной с печатью символов. Вы знаете, что в Бейсике при использовании временных атрибутов в операторе PRINT их действие заканчивается после выполнения печати, и следующий PRINT будет выводить символы с постоянными атрибутами. В ассемблере же команда RST 16 временные установки не сбрасывает и для восстановления печати постоянными атрибутами нужно вызвать подпрограмму, расположенную по адресу 3405. Продемонстрируем это на таком примере:
ORG 60000 LD A,2 CALL 5633 LD DE,TEXT1 ;печать текста, обозначенного меткой LD BC,16 ; TEXT1, длиной в 16 байт. CALL 8252 CALL 3405 ;восстановление постоянных атрибутов. LD DE,TEXT2 ;печать текста, обозначенного меткой LD BC,11 ; TEXT2, длиной в 11 байт. CALL 8252 RET TEXT1 DEFB 22,3,12,16,7,17,2 DEFM "TEMPORARY" TEXT2 DEFB 22,5,12 DEFM "CONSTANT"
После трансляции и выполнения этой программки вы увидите на экране две надписи: верхняя (TEMPORARY) выполнена с временными атрибутами (белые буквы на красном фоне), а нижняя (CONSTANT) - постоянными.
Вывод изображения из «теневого» экрана
Рисунок 7.4. Вывод изображения из «теневого» экрана
Поясним, как это достигается. В блоке данных, описывающих пейзаж (в программе он обозначен меткой D_LAND), указывается положение каждого дома (или другого объекта) на трассе, протяженность дома, его «удаленность» от дороги и адрес другого блока, задающего его внешний вид. Программа просматривает данные пейзажа и, исходя из положения автомобиля (он всегда выводится в центре окна), выбирает только те дома, которые целиком или частично попадают в окно экрана, то есть если правый угол дома не выходит за левую границу и левый - за правую, а остальные пропускает. После этого начинается вывод объектов в «теневой» экран: сначала дома, затем рисуется дальний тротуар, автомобиль, ближний тротуар и, наконец, фонари. Как только построение закончено, окно «теневого» экрана переносится в физический видеобуфер и тем самым изображение становится видимым.
Так как объем книги не безграничен, в программе заданы только два типа зданий, а протяженность пейзажа измеряется 200 знакомест. Но вы можете дополнить графику, составив из имеющихся спрайтов другие дома. Попробуйте увеличить пробег автомобиля, введите новые спрайты, изображающие деревья, решетки ограды, арки, попытайтесь создать загородный пейзаж. Для этого вам нужно будет только расширить блоки данных, формат которых подробно описан в тексте программы. Не бойтесь экспериментировать, и результат, уверены, порадует вас.
Несмотря на относительную сложность программы, в ней не встретится неизвестных доселе команд. Единственное нововведение - это процедура ПЗУ, находящаяся по адресу 8020, которая служит для проверки нажатия клавиши Break (Caps Shift/Space). Подпрограмма не требует никаких входных параметров и сообщает о том, что Break нажата установкой на выходе флага переноса. В противном случае выполняется условие NC.
До того, как мы приведем текст программы, обращаем ваше внимание, что вызывается она только из Бейсика, так как ассемблер GENS пользуется своим собственным внутренним стеком и поскольку он может оказаться в любом месте (все зависит от адреса загрузки GENS), то в результате «теневой» экран может перекрыть стек, а что из этого следует, догадаться нетрудно.
ORG 60000 ; Адрес «теневого» (или виртуального) экрана = #8000 V_OFFS EQU #40 ;старший байт смещения адреса ; «теневого» экрана относительно ; начального адреса физического экрана LD A,69 LD (23693),A XOR A CALL 8859 CALL 3435 LD A,2 CALL 5633 ;здесь необходима только для правильного ; вывода атрибутов основного экрана ; Инициализация переменных MAIN LD A,2 LD (X_LAMP),A LD HL,0 LD (X_LAND),HL CALL FRAME ;рамка вокруг окна MAIN1 CALL CLS_V ;очистка «теневого» экрана ; Формирование игрового поля в «теневом» экране CALL LAND ;вывод заднего плана (дома) CALL LINE1 ;рисование дальнего тротуара CALL PUTCAR ;автомобиль CALL LINE2 ;ближний тротуар CALL LAMPS ;фонари ; ---------------- CALL PUTVRT ;вывод окна виртуального экрана ; на физический CALL 8020 ;проверка нажатия клавиши Break
RET NC ;выход из программы, если нажата LD HL,(X_LAND) ;изменение координаты автомобиля INC HL LD (X_LAND),HL LD DE,200 ;если пройдено расстояние AND A ; меньше 200 знакомест, SBC HL,DE JR NZ,MAIN1 ; то движение продолжается LD BC,100 ;иначе - пауза 2 сек. CALL 7997 JR MAIN ; и переход к началу пути ; Рисование горизонтальных линий белого цвета, изображающих тротуары LINE1 LD L,#40 LD H,#4A+V_OFFS JR LINE3 LINE2 LD L,#60 LD H,#4C+V_OFFS LINE3 PUSH HL ;адрес экрана здесь не рассчитывется, ; а задается в явном числовом виде LD D,H LD E,L INC DE LD (HL),255 ;сплошная линия (если изменить число, ; получится пунктирная) LD BC,31 LDIR ;проводим линию POP HL LD A,H ;расчет адреса атрибутов SUB V_OFFS AND #18 RRCA RRCA RRCA ADD A,#58+V_OFFS LD H,A LD D,H LD E,L INC DE LD (HL),7 ;INK 7, PAPER 0 LD BC,31 LDIR RET ; Вывод автомобиля в виртуальный экран PUTCAR LD BC,#90E ;координаты фиксированы (B = 9, C = 14) LD HL,SPR7 ;маска LD A,SPRAND ;по принципу AND CALL PTBL LD HL,SPR6 ;автомобиль LD A,SPROR ;по принципу OR JP PTBL ; Рисование в виртуальном экране дорожных фонарей LAMPS LD HL,X_LAMP LD C,(HL) ;горизонтальная координата самого левого ; на экране фонаря LD B,7 ;вертикальная координата фиксирована PUSH HL LAMPS1 LD HL,SPR9 ;маска LD A,SPRAND CALL PTBL LD HL,SPR8 ;фонарь LD A,SPROR CALL PTBL LD A,20 ;следующий через 20 знакомест ADD A,C LD C,A CP 28 JR C,LAMPS1 ;закончить вывод? POP HL DEC (HL) ;уменьшение координаты на 2 DEC (HL) RET P ;выход, если начальная координата ; не отрицательна LD (HL),18 ;иначе задаем начальную координату RET X_LAMP DEFB 0 ; Подпрограмма очистки виртуального экрана CLS_V LD H,#40+V_OFFS LD L,0 LD D,H LD E,L INC DE LD (HL),0 LD BC,6144 LDIR ; Атрибуты LD (HL),1 ;INK 1, PAPER 0 LD BC,767 LDIR RET ; Формирование изображения здания, скомбинированного из нескольких ; спрайтов (адрес соответствующего блока данных в паре HL) PUT_B LD A,(HL) ;количество блоков в доме INC HL PUT_B1 PUSH AF PUSH BC LD A,(HL) ;горизонтальная координата блока INC HL ADD A,C CP 2 ;проверка выхода за пределы окна JR C,PUT_B3 CP 29 JR NC,PUT_B3 LD C,A LD A,(HL) ;вертикальная координата блока INC HL ADD A,B ;проверка выхода за пределы окна JP M,PUT_B4 LD B,A LD E,(HL) ;адрес спрайта, изображающего блок INC HL LD D,(HL) INC HL PUSH HL EX DE,HL LD A,SPRPUT CALL PTBL ;выводим спрайт в «теневой» экран POP HL PUT_B2 POP BC POP AF DEC A JR NZ,PUT_B1 ;следующий блок RET PUT_B3 INC HL ;обход, если блок выходит за пределы окна PUT_B4 INC HL INC HL JR PUT_B2 ; Вывод помещающейся в окно части пейзажа X_LAND DEFW 0 ;координата автомобиля на трассе LAND LD HL,D_LAND ;блок данных, описывающих ; местоположение и внешний вид домов LAND1 LD BC,(X_LAND) LD E,(HL) ;горизонтальная координата дома на трассе INC HL LD D,(HL) INC HL LD A,D OR E RET Z ;0 - маркер конца блока данных LD A,(HL) ;протяженность дома в знакоместах INC HL PUSH HL PUSH DE EX DE,HL ;вычисляем координату дальнего конца дома ADD A,L LD L,A LD A,H ADC A,0 LD H,A INC HL AND A SBC HL,BC ;сравниваем с текущим положением ; автомобиля POP DE JR C,LAND3 ;если дом выходит за левый край окна LD HL,28 ADD HL,BC SBC HL,DE JR C,LAND3 ;если дом выходит за правый край окна EX DE,HL SBC HL,BC ;вычисление экранной координаты дома LD C,L POP HL LD B,(HL) ;вертикальная экранная координата дома INC HL LD E,(HL) ;адрес блока данных, описывающих дом INC HL LD D,(HL) PUSH HL EX DE,HL CALL PUT_B ;вывод изображения дома в «теневой» экран POP HL LAND2 INC HL JR LAND1 ;следующий дом LAND3 POP HL ;обход, если изображение дома не ; попадает в окно экрана INC HL INC HL JR LAND2 ; ВНИМАНИЕ! В процедуре PTBL есть изменения! PTBL SPRPUT EQU 0 SPROR EQU #B6 SPRAND EQU #A6 SPRXOR EQU #AE PUSH HL LD (MODE),A LD A,(HL) INC HL PUSH HL LD L,A LD H,0 LD E,L LD D,H ADD HL,HL ADD HL,DE POP DE ADD HL,DE EX DE,HL PTBL1 PUSH AF PUSH BC LD A,(HL) INC HL PUSH HL ADD A,B CP 24 JR NC,PTBL4 PUSH DE CALL 3742 POP DE ; +++ Добавление +++ LD A,V_OFFS ADD A,H LD H,A ; +++ EX (SP),HL LD A,(HL) EX (SP),HL ADD A,C CP 32 JR NC,PTBL4 ADD A,L LD L,A LD B,8 PUSH HL PTBL2 LD A,(DE) MODE NOP LD (HL),A INC DE INC H DJNZ PTBL2 POP BC LD A,B AND #18 SRA A SRA A SRA A ; +++ Изменение: вместо ADD A,#58 +++ ADD A,#58+V_OFFS ; +++ LD B,A POP HL INC HL LD A,(HL) DEC HL LD (BC),A PTBL3 POP BC POP AF INC HL INC HL DEC A JR NZ,PTBL1 POP HL RET PTBL4 LD HL,8 ADD HL,DE EX DE,HL POP HL JR PTBL3 ; Вывод окна «теневого» экрана на физический PUTVRT LD A,8 ;отступ сверху (в пикселях) LD B,12*8 ;высота окна (в пикселях) PUT_V1 PUSH AF PUSH BC LD C,4*8 ;отступ слева (в пикселях) CALL 8880 ;адрес физического экрана LD D,H ;сохраняем в DE LD E,L LD A,V_OFFS ADD A,H ;в HL - соответствующий адрес LD H,A ; «теневого» экрана LD BC,24 ;ширина окна - 24 знакоместа LDIR POP BC POP AF INC A ;следующий ряд пикселей DJNZ PUT_V1 ; Перенос атрибутов LD DE,#5824 ;адрес верхнего левого угла окна LD L,E ; в области атрибутов LD A,D ADD A,V_OFFS ;вычисляем соответствующий адрес LD H,A ; в «теневом» экране LD B,12 PUT_V2 PUSH BC LD BC,24 LDIR ;переносим 24 байта (по ширине окна) LD BC,8 ;переходим к следующей ADD HL,BC ; строке экрана (32-24 = 8) EX DE,HL ADD HL,BC EX DE,HL POP BC DJNZ PUT_V2 RET ; Рисование рамки вокруг окна ; (подпрограммы LABS1 и PRINT описаны в предыдущем разделе) FRAME LD IX,DFRAME ;Данные для построения рамки
; Блоки дома: ; Окно 1 (с рамой) SPR1 DEFB 4 DEFB 0,0,65, 0,1,65, 1,0,65, 1,1,65 DEFB 255,193,193,193,255,193,193,193 DEFB 255,7,7,7,7,7,7,7 DEFB 193,193,193,255,255,255,255,255 DEFB 7,7,7,255,255,255,255,255 ; Окно 2 (без рамы) SPR2 DEFB 4 DEFB 0,0,65, 0,1,65, 1,0,65, 1,1,65 DEFB 255,252,252,252,252,252,252,252 DEFB 255,63,63,63,63,63,63,63 DEFB 252,252,252,255,255,255,255,255 DEFB 63,63,63,255,255,255,255,255 ; Окно с балконом SPR3 DEFB 4 DEFB 0,0,65, 0,1,65, 1,0,65, 1,1,65 DEFB 255,193,193,193,193,193,193,193 DEFB 255,7,7,7,7,7,7,7 DEFB 191,182,182,182,182,182,128,255 DEFB 251,219,219,219,219,219,3,255 ; Духовые окна на крыше дома SPR4 DEFB 4 DEFB 0,0,65, 0,1,65, 1,0,65, 1,1,65 DEFB 0,0,255,255,254,254,254,254 DEFB 0,0,255,255,127,127,127,127 DEFB 255,255,255,255,153,153,255,255 DEFB 255,255,255,255,153,153,255,255 ; Подвальные окна SPR5 DEFB 2 DEFB 0,0,65, 0,1,65 DEFB 255,255,255,255,240,255,255,255 DEFB 255,255,255,255,15,255,255,255 ; Автомобиль SPR6 DEFB 10 DEFB 0,0,66, 0,1,66, 0,2,66, 0,3,66, 0,4,66 DEFB 1,0,66, 1,1,66, 1,2,66, 1,3,66, 1,4,66 DEFB 0,15,24,48,56,62,63,57 DEFB 0,255,48,48,56,60,255,249 DEFB 0,255,48,48,56,60,255,249 DEFB 0,255,48,48,56,60,255,243 DEFB 0,192,96,32,32,48,248,248 DEFB 63,63,62,29,1,0,0,0 DEFB 255,255,239,183,240,224,0,0 DEFB 255,255,255,255,0,0,0,0 DEFB 255,255,247,237,15,7,0,0 DEFB 248,252,124,184,128,0,0,0 ; Маска для спрайта «автомобиль» SPR7 DEFB 10 DEFB 0,0,66, 0,1,66, 0,2,66, 0,3,66, 0,4,66 DEFB 1,0,66, 1,1,66, 1,2,66, 1,3,66, 1,4,66 DEFB 224,192,128,131,128,128,128,128 DEFB 0,0,0,135,129,0,0,0 DEFB 0,0,0,135,129,0,0,0 DEFB 0,0,0,135,129,0,0,0 DEFB 31,15,15,15,135,3,3,3 DEFB 128,128,128,128,192,252,254,255 DEFB 0,0,0,0,0,7,15,255 DEFB 0,0,0,0,0,255,255,255 DEFB 0,0,0,0,0,224,240,255 DEFB 1,1,1,1,3,63,127,255 ; Фонарный столб SPR8 DEFB 7 DEFB 0,0,70, 0,1,70, 0,2,70, 1,1,70 DEFB 2,1,70, 3,1,70, 4,1,70 DEFB 0,0,127,64,63,0,0,0 DEFB 0,0,239,146,17,16,16,16 DEFB 0,0,252,4,248,0,0,0 DEFB 16,16,16,16,16,16,16,16 DEFB 16,16,16,16,16,16,16,16 DEFB 16,16,16,56,56,56,56,56 DEFB 56,56,56,56,56,56,56,56 ; Маска для спрайта «фонарный столб» SPR9 DEFB 7 DEFB 0,0,70, 0,1,70, 0,2,70, 1,1,70 DEFB 2,1,70, 3,1,70, 4,1,70 DEFB 255,0,0,0,0,128,255,255 DEFB 255,0,0,0,0,68,199,199 DEFB 255,1,1,1,1,3,255,255 DEFB 199,199,199,199,199,199,199,199 DEFB 199,199,199,199,199,199,199,199 DEFB 199,199,131,131,131,131,131,131 DEFB 131,131,131,131,131,131,131,131 ; Данные для формирования зданий: ; 1-й байт - количество блоков в доме ; Далее следуют данные для каждого блока ; 1-й байт: горизонтальная координата блока в доме относительно ; верхнего левого угла изображения ; 2-й байт: вертикальная координата блока в доме ; 3-й и 4-й байты: адрес спрайта блока D_BLD1 DEFB 25 DEFW #000,SPR4,#002,SPR4,#004,SPR4,#006,SPR4,#008,SPR4 DEFW #200,SPR3,#202,SPR2,#204,SPR1,#206,SPR2,#208,SPR3 DEFW #400,SPR3,#402,SPR2,#404,SPR1,#406,SPR2,#408,SPR3 DEFW #600,SPR1,#602,SPR2,#604,SPR1,#606,SPR1,#608,SPR1 DEFW #800,SPR5,#802,SPR5,#804,SPR5,#806,SPR5,#808,SPR5 D_BLD2 DEFB 15 DEFW #000,SPR3,#002,SPR1,#004,SPR2 DEFW #200,SPR3,#202,SPR1,#204,SPR2 DEFW #400,SPR3,#402,SPR1,#404,SPR2 DEFW #600,SPR1,#602,SPR1,#604,SPR2 DEFW #800,SPR5,#802,SPR5,#804,SPR5 ; Данные для формирования пейзажа: ; 1-й и 2-й байты (слово): горизонтальная координата дома ; 3-й байт: протяженность дома в знакоместах ; 4-й байт: вертикальная координата дома ; 5-й и 6-й байты (слово): адрес данных для формирования дома D_LAND DEFW 1,#10A,D_BLD1,14,#FE0A,D_BLD1,25,#FE0A,D_BLD1 DEFW 39,#106,D_BLD2,46,#10A,D_BLD1,63,6,D_BLD2 DEFW 70,10,D_BLD1,90,#10A,D_BLD1,101,10,D_BLD1 DEFW 112,#FF0A,D_BLD1,123,#FE0A,D_BLD1,138,#106,D_BLD2 DEFW 145,#FE06,D_BLD2,152,#FF06,D_BLD2,159,6,D_BLD2 DEFW 166,#106,D_BLD2,178,#FE0A,D_BLD1,190,#FE06,D_BLD1 DEFW 0 ; Данные рамки вокруг окна (формат описан в предыдущем разделе) DFRAME DEFB 0,3,0,0, 1,4,0,24, 2,28,0,0 DEFB 3,3,1,12@#80, 3,28,1,12@128 DEFB 5,3,13,0, 1,4,13,24, 4,28,13,0 DEFB -1 ; Элементы рамки D_SYMB DEFB 127,213,159,191,253,182,248,181 DEFB 255,85,255,255,85,170,0,255 DEFB 254,83,249,245,121,181,121,181 DEFB 249,181,249,181,249,181,249,181 DEFB 249,117,249,245,81,169,3,254 DEFB 249,189,255,191,213,170,192,127
ВЗАИМОДЕЙСТВИЕ С ИГРАЮЩИМ
ВЗАИМОДЕЙСТВИЕ С ИГРАЮЩИМ
Думается, ни для кого не секрет, в чем кроется причина популярности компьютерных игр. Ведь в них любой играющий с помощью несложных манипуляций может влиять на события, происходящие на экране и тем самым без особого вреда для здоровья почувствовать себя в роли супергероя или суперзлодея. Икаким бы сложным ни был сюжет игры, устройства управления остаются одними и теми же - клавиатура или джойстик.
Приведем несколько примеров. В игре RIVER RAID можно ускорять или замедлять полет вашего самолетика, поворачивать его вправо или влево. И для этого достаточно лишь наклонить ручку джойстика в нужном направлении, а нажав кнопку «огонь», можно выпустить по объекту противника ракету.
Особенно поражают реальностью происходящего игры, посвященные какому-либо виду спортивной борьбы (FIST, ORIENTAL GAMES и другие). В них с помощью все тех же пяти клавиш или джойстика можно заставить главного персонажа выполнять множество различных приемов, прыжков, ударов и прочих телодвижений.
Подобные примеры можно продолжать до бесконечности, но для нас важно другое: все эти изменения на экране возможны благодаря особой части игровой программы, которая носит название блок взаимодействия с играющим. Когда вы нажимаете на какую-либо из управляющих клавиш или наклоняете ручку джойстика, программа реагирует должным образом и обычно переключается на выполнение той части, которая «заведует» выполнением соответствующего действия. Так, например, если ваш танк может поворачивать вправо и влево, а время от времени еще и стрелять, то в блоке управления такой программы должно быть ровно три части, каждая из которых отвечает за свой «участок работы».
У вас может создаться впечатление, что вся задача сводится лишь к тому, чтобы отслеживать нажатия клавиш или наклон ручки джойстика да в ответ на воздействие извне изменять направление движения управляемого объекта. Однако это пока еще не все. В реальной игре, когда по экрану движется созданный вами человечек, самолет или любой другой персонаж, возникает масса проблем, которые нужно решать. Прежде всего необходимо восстанавливать фон позади движущегося объекта, чтобы за ним не протянулся след девственно чистого экрана. А что делать, если движущийся объект «натолкнулся» на какое-то препятствие: стенку, дерево или встретился лицом к лицу со своим противником. Наконец, как поступить с врагом, в которого попал снаряд, выпущенный из вашей пушки. На все эти вопросы мы постараемся дать ответ на страницах нашей книги.
Для начала нужно сказать несколько
ЗАГРУЗКА GENS4
Для начала нужно сказать несколько слов о самой программе. Ее часто можно встретить в пакете с монитором-отладчиком MONS под общим названием DEVPAC. Этот пакет состоит из трех файлов:
Загрузчик на Бейсике (изначально пакет DEVPAC не содержал загрузчика на Бейсике, а появился он там благодаря Н.Родионову [2] - Примеч. ред.);
Ассемблер GENS4 (или GENS3);
Монитор-отладчик MONS4 (MONS3).
На самом деле вовсе не обязательно загружать в память весь пакет. Во-первых, отладчик нам сейчас не нужен - он понадобится позже, когда мы начнем дизассемблировать (то есть получать исходные тексты) программы в машинных кодах, а во-вторых, GENS (так же, впрочем, как и MONS) может быть загружен по любому удобному адресу, отличному от заданного в загрузчике. Поэтому чаще достаточно загружать только коды ассемблера, введя с клавиатуры, например, такую строку:
LOAD "GENS4"CODE 25000
После этого (или до) опустите RAMTOP на один байт ниже адреса загрузки ассемблера, выполнив оператор
CLEAR 24999
и теперь, чтобы GENS можно было запустить командой RUN, введите строку
10 RANDOMIZE USR 25000
Если вас по какой-то причине не вполне устраивает предложенный адрес, можете заменить его на любой другой. Помните только, что он не должен быть слишком низким (чтобы не повредить бейсик-систему) или слишком высоким (чтобы поместился не только сам GENS, но и текст будущей программы, который расположится следом за кодами ассемблера). Допустимый диапазон адресов загрузки лежит примерно в пределах от 24500 до 54000.
Загрузочная картинка к игре JUGGERNAUT
Рисунок 1.1. Загрузочная картинка к игре JUGGERNAUT
Часто только это изображение и называют заставкой, хотя на самом деле это лишь первая ее часть, помогающая игроку морально подготовиться к тяготам предстоящего сражения или лишениям, связанным с поисками сокровищ. Однако в солидных игровых программах заставка чаще всего разбивается на несколько частей, именуемых кадрами. Объясняется это очень просто: перед началом игры на экране должно появиться столь большое количество информации, что «впихнуть» ее в один кадр практически невозможно. Поэтому после окончания загрузки программы на экране появляется основной кадр заставки, называемый меню, в котором дается список действий, необходимых для настройки программы или для получения той или иной информации (Рисунок 1.2).
ЗАСТАВКА
ЗАСТАВКА
Вы знаете, что заставкой обычно называют картинку, появляющуюся во время загрузки игровой программы. Такие картинки имеют тот же смысл, что и рекламные щиты у дверей кинотеатров. Значительную площадь, как правило, занимает название игры, а все, что изображено на экране, так или иначе связано с ее сюжетом. Здесь же обычно сообщается о фирме-изготовителе, авторах программы и годе ее создания (Рисунок 1.1).
ЗВУКОВОЕ СОПРОВОЖДЕНИЕ ИГРЫ
ЗВУКОВОЕ СОПРОВОЖДЕНИЕ ИГРЫ
Трудно даже назвать игру, тем более фирменную, где бы совсем не использовались те или иные звуки. Речь может идти лишь о том, что в одних играх музыкального сопровождения меньше, а в других каждое нажатие клавиши приводит ко все новым музыкальным шедеврам. Как образцы блестящего музыкального оформления стоит назвать такие игры как DEFINDER OF THE CROWN, PINK PONG, FLYING SHARK. Мы привели данный список с той мыслью, что, прочитав книгу до конца и накопив некоторый опыт, вам будет вполне по силам дизассемблировать эти игры и найти, а затем использовать лучшие образцы компьютерной музыки в своих программах. Ведь чего греха таить, не все, даже очень хорошие программисты, рождаются с музыкальными способностями.
В отличие от других частей игровой программы, выделить какой-то особый музыкальный блок довольно трудно, поскольку разные по сложности и назначению звуки формируются и в заставке, и в блоке взаимодействия с играющим, и даже в блоке оценки игровой ситуации. Тем не менее, все, что связано с музыкальным оформлением игры, можно рассматривать с точки зрения программирования как нечто, напоминающее отдельный блок. Все дело в том, что разные звуки и музыкальные фрагменты формируются одними и теми же подпрограммами и отличие может состоять лишь в размерах блоков данных (в особенности это справедливо для подпрограмм, создающих звуки в компьютере ZX Spectrum 128).
Говоря о звуках в компьютерных играх, никак нельзя пройти мимо возможностей музыкального сопроцессора, с помощью которого можно создавать не просто интересные звуковые эффекты, но и настоящие музыкальные произведения с полноценной оркестровой аранжировкой. На страницах этой книги мы постараемся поделиться своим опытом в данной области, подробно расскажем о программировании звуковых эффектов, сопровождая все это примерами ассемблерных программ с их подробным описанием.