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

Материал из Emuverse
Данный материал защищён авторскими правами!

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

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

ГЛАВА ЧЕТВЕРТАЯ,
показывающая, как сделать надпись на экране и создать простейшие изображения

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

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

ВЫВОД БУКВЕННЫХ И ЦИФРОВЫХ СИМВОЛОВ

Из предыдущего описания вы, возможно, сделали вывод, что сейчас нам предстоит долгая и кропотливая работа над созданием подпрограммы вывода символов на экран, затем придется писать еще одну процедуру, устанавливающую цвета печатаемых букв и цифр, а напоследок, собрав остатки угасающих сил, придумывать способ, как выводить сразу целые строки… И после всего этого решить, что гораздо проще и эффективнее воспользоваться оператором Бейсика 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-Перевод строки (апостроф)
16colourЦвет INK
17colourЦвет PAPER
18flagFLASH
19flagBRIGHT
20flagINVERSE
21flagOVER
22Y, XПозиция AT
23X, ?Позиция 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) — постоянными.

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

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

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

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

Так для

INK 6: PAPER 0: BRIGHT 1: FLASH 0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

       PUSH  BC

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

       POP   HL

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

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

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

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

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

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

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

OUT 254,color

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

       LD    A,2
       OUT   (254),A

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

       LD    A,5
       CALL  8859

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

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

       CALL  3435
       LD    A,2
       CALL  5633

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

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

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

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

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

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

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

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

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

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

Execute 60000

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

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

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

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

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

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

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

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

РИСОВАНИЕ ГРАФИЧЕСКИХ ПРИМИТИВОВ

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

Точки

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

Чтобы нарисовать точку с определенными атрибутами так же, как и при выводе символов, можно воспользоваться подпрограммой ATTRIB, описанной в разделе «Подготовка экрана к работе из ассемблера», или установить временные цвета, изменив системную переменную ATTR_T (23695). Сразу заметим, что это в равной степени относится и к подпрограммам рисования линий и окружностей.

Для примера поставим точку красного цвета на желтом фоне. (Задавая атрибуты при выводе графики, помните, что в Speccy цвета определяются для целого знакоместа.) Рассчитав значения байта атрибутов для заданной комбинации цветов, получим число 50 (2+6×8). Перед выводом точки занесем это число в ячейку 23695, предварительно подготовив экран подпрограммой SETSCR из раздела «Подготовка экрана к работе из ассемблера»:

       ORG   60000
       ENT   $
       CALL  SETSCR
       LD    A,50
       LD    (23695),A
       LD    BC,#6E2D
       CALL  8933
       RET
SETSCR .........

Прямые линии

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

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.

Дуги

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

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

PLOT 100,80: DRAW 30,50,3

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

       LD    A,123
       CALL  11560

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

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

Окружности

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

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

CIRCLE 120,80,60

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

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

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

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

СТАТИЧЕСКИЕ ЗАСТАВКИ

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

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

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

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

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

Как и при программировании на Бейсике, мы рекомендуем вам прежде всего нарисовать на листе клетчатой бумаги прямоугольник размером 32 клетки по горизонтали и 24 — по вертикали (то есть по размеру экрана в знакоместах) и внутри изобразить все то, что вы хотите увидеть на экране. А затем составить блоки данных DEFB так, чтобы их осталось только распечатать командой CALL 8252.

Рис. 4.1. Пример статической заставки

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

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

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

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

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

20 RANDOMIZE USR 15619: REM : CAT

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

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

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

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

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

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

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

       LD    HL,UDG
       LD    (23675),HL

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Рис. 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 "<189>": 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 "<189>": 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).

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

Несколько слов о самой программе. Как вы заметили, здесь впервые встретилась директива ассемблера 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 Примеч. ред.)