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

         

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



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

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

Содержание раздела