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

       

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



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

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

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


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