Как написать игру на ассемблере для ZX Spectrum/Глава 05
- ГЛАВА ПЯТАЯ,
- в которой изображения становятся подвижными
Как вы понимаете, никакая игра, даже с очень изысканной графикой, не станет достаточно интересна, если все изображения на экране будут неподвижны и персонажи не будут подавать признаков жизни. Для «оживления» картинок в программировании используется тот же принцип, что и в мультипликации: в одно и то же место экрана последовательно помещаются слегка отличающиеся друг от друга изображения, показывающие разные фазы движения одного объекта.
В этой главе мы объясним, как заставить двигаться сначала отдельные символы, а затем и целые спрайты. А в завершение напишем полноценную динамическую заставку, которую в слегка измененном виде вполне можно использовать в какой-нибудь конкретной игре.
Как вы знаете, в программах на Бейсике для получения эффекта движения обычно используются циклы FOR…NEXT и операторы переходов GO TO и GO SUB, а также условный оператор IF…THEN, поэтому прежде всего нам необходимо выяснить, какие средства имеются в языке ассемблера для получения тех же результатов.
ОРГАНИЗАЦИЯ ЦИКЛОВ В АССЕМБЛЕРЕ
Способы организации циклов в ассемблере существенно отличаются от бейсиковского оператора FOR…NEXT. Здесь нет столь простых решений, зато вы можете получить множество разновидностей циклов, отвечающих любым требованиям.
Простые циклы
Рассмотрим сначала наиболее простой случай — получить заданное количество повторений некоторого фрагмента программы. Для этих целей в наборе команд микропроцессора имеется специальная инструкция DJNZ, которую можно расшифровать как «уменьшить и перейти, если не ноль». В этой команде уменьшается и проверяется на равенство нулю регистр B, который можно рассматривать в качестве счетчика количества циклов, а переход осуществляется по адресу, определенному меткой, указанной в команде DJNZ.
Проиллюстрируем цикл с использованием команды DJNZ на примере программки, печатающей на экране ряд из 32-х звездочек:
ORG 60000 ENT $ CALL 3435 ;Подготовка экрана к печати LD A,2 CALL 5633 LD B,32 ;В регистре B - количество повторений LOOP LD A,"*" ;Печать символа «*» RST 16 DJNZ LOOP ;Уменьшение регистра B на 1 и если ; B не равно 0, переход на метку LOOP RET
Такой способ организации циклов наиболее прост и удобен, но у него есть два существенных ограничения. Во-первых, как вы понимаете, количество повторений здесь не может превышать максимального значения для регистров, то есть 256 (если изначально в B записан 0), а во-вторых, между адресом начала цикла и командой DJNZ может быть расстояние не более 126 байт. Со вторым ограничением можно справиться, например, если тело цикла оформить как подпрограмму, тогда между меткой начала цикла и командой DJNZ будет находиться единственная инструкция CALL.
Еще раз напоминаем вам о необходимости сохранять регистры, которые могут быть изменены. В данном случае нужно позаботиться о сохранении регистра B, иначе цикл может никогда не закончиться, то есть произойдет «зависание» компьютера. В приведенном выше примере мы не сделали этого только потому, что команда RST 16 все регистры данных, за исключением аккумулятора, оставляет без изменений, но это не всегда справедливо при использовании других процедур ПЗУ. Если вы не совсем уверены в том, что какая-то подпрограмма сохраняет нужные регистры, лучше на всякий случай перестраховаться. Таким образом, в общем виде цикл может выглядеть так:
LD B,N ;Указываем количество повторений МЕТКА PUSH BC ;Сохраняем счетчик цикла ......... ;Тело цикла, которое может быть выделено ; в отдельную подпрограмму, и тогда ; здесь будет располагаться единственная ; инструкция CALL POP BC ;Восстановление счетчика DJNZ МЕТКА ;Уменьшение счетчика и если не конец, ; переход на начало цикла
Вложенные циклы
Сохранение счетчика совершенно необходимо также при организации вложенных циклов. Приведем пример небольшой программки, заполняющей экран однородной фактурой, в которой использован принцип вложения циклов. Во внешнем цикле будем подсчитывать количество заполненных строк, а внутренний, в общем, аналогичен приведенному выше примеру:
ORG 60000 ENT $ LD HL,UDG LD (23675),HL ;адресация области символов UDG LD A,8 LD (23693),A ;синяя «бумага», черные «чернила» LD A,1 CALL 8859 ;синий бордюр CALL 3435 ;подготовка экрана LD A,2 CALL 5633 LD B,22 ;заполняем 22 строки экрана OUTSID PUSH BC ;внешний цикл LD B,32 ;по 32 символа в строке ; --------- INSIDE LD A,144 ;внутренний цикл RST 16 ;выводим код символа A из набора UDG DJNZ INSIDE ;конец внутреннего цикла ; --------- POP BC DJNZ OUTSID ;конец внешнего цикла RET ; Данные символа A (UDG) UDG DEFB 119,137,143,143,119,153,248,248
Проверка условий. Флаги
Иногда бывает удобнее задавать счетчик цикла не в регистре B, а в каком-то другом. Это немногим сложнее описанного способа, но тут нам понадобятся, по меньшей мере, еще две новые команды: одна для уменьшения значения выбранного регистра и другая, похожая на бейсиковский оператор IF…THEN, для проверки условия достижения конца цикла.
Для изменения содержимого регистров на единицу имеется два типа команд: INC (Increase) — увеличение и DEC (Decrease) — уменьшение. Попутно заметим, что эти же команды применимы не только к отдельным регистрам, но и к регистровым парам. Например, для уменьшения на 1 значения аккумулятора нужно написать команду
DEC A
а для других регистров или регистровых пар, как вы догадываетесь, достаточно вместо A написать другое имя. Команды увеличения INC пишутся аналогичным образом.
Несколько сложнее дела обстоят с проверкой условий. Это еще один момент, имеющий со средствами Бейсика лишь очень отдаленное сходство. Чтобы объяснить, как микропроцессор реагирует на то или иное условие, прежде всего нужно ввести еще одно новое понятие — флаги.
Строго говоря, с принципом флагов вы уже встречались и в Бейсике. Вспомните, например, команду POKE 23658,8, которая включает режим ввода прописных букв. Флаги применяются в тех случаях, когда нужно передать какое-то сообщение из одной части программы в другую. Установив бит 3 (число 8) системной переменной FLAGS2, мы посылаем драйверу клавиатуры сообщение о том, что все вводимые буквы нужно переводить в верхний регистр.
Вернемся теперь к флагам микропроцессора. Здесь они имеют то же назначение и передают программе сообщения о выполнении или невыполнении различных условий. Как вы знаете, компьютер — машина бескомпромиссная и приемлет лишь два ответа: либо да, либо нет, а все остальное от лукавого. Поэтому для сообщения о принятии того или иного решения достаточно одного-единственного бита: если бит установлен в 1, то решение положительное, если он сброшен в 0, то это означает, что условие не выполнено. Другое дело, что самих проверяемых условий может быть несколько, и для каждого из них зарезервирован свой бит специального регистра, который так и называется: флаговый, или регистр F. Кстати, это и есть тот самый загадочный регистр, который образует пару совместно с аккумулятором.
Сначала перечислим все возможные условия, проверяемые микропроцессором, а затем кратко поясним каждое из них в отдельности. Вот они:
- ноль (флаг Z);
- перенос (флаг CY);
- четность/переполнение (флаг P/V);
- отрицательный результат, знак (флаг S);
- вычитание (флаг N);
- вспомогательный перенос (флаг H).
Всего, как видите, можно проверить 6 условий, и в регистре флагов F задействовано, соответственно, 6 битов, а два остались без применения. На рис. 5.1 показано, какой из битов за какое условие ответственен. Крестиками обозначены неиспользуемые биты.
Флаг нуля Z (zero) устанавливается в том случае, если в результате выполнения последней команды получился нулевой результат. Флаг переноса CY (carry) говорит о том, что в итоге арифметической операции или же после сдвига регистра произошел перенос. Например, при сложении чисел 254 и 2 должно получиться 256, но восьми разрядов регистров хватает только для чисел от 0 до 255, поэтому реально мы получим число 0, а старший девятый бит перейдет во флаг CY. Кроме того, после такой операции будет установлен и флаг нуля. Вообще же эти два флага наиболее важны и поэтому используются чаще других. В принципе, почти всегда можно обойтись только ими, лишь изредка и, скорее, для удобства прибегая к проверке остальных флагов.
Флаг четности/переполнения P/V (parity/overflow) — единственный, на который возложена индикация не одного, а сразу двух различных условий. Это возможно потому, что они относятся к двум принципиально различным типам команд и вместе никогда не встречаются. Флаг четности устанавливается, если в результате логической операции в байте оказалось установлено в 1 четное количество битов, а условие переполнения выполняется тогда, когда после арифметического действия изменился знак операнда.
Флаг S (sign) тоже достаточно важен и после флагов Z и CY используется наиболее часто. Он устанавливается, если при выполнении арифметической или логической операции получился отрицательный результат.
Остальные два флага — N (negative) и H (half-carry) используются довольно редко и преимущественно при работе с так называемыми двоично-десятичными числами.
Надо сказать, что далеко не все команды микропроцессора оказывают какое-то влияние на флаги, а некоторые команды изменяют лишь отдельные биты флагового регистра. Например, все описанные до сих пор типы команд, за исключением упомянутых INC и DEC, вообще на флаги не воздействуют.
По мере надобности мы еще будем возвращаться к вопросу о флагах, а сейчас продолжим прерванный разговор о циклах.
Условные и безусловные переходы
Алгоритм циклов с использованием регистров, отличных от B, принципиально не отличается от порядка выполнения команды DJNZ и может быть выражен словами «уменьшить содержимое регистра и перейти на начало цикла, если не ноль». Как записывается в ассемблерной мнемонике первая часть этого предложения, вам, наверное, уже понятно: если использовать в качестве счетчика, скажем, регистр E, то нужно написать команду DEC E. Что же касается переходов, то в системе команд микропроцессора имеется не одна инструкция (как в Бейсике оператор GO TO), а целых две. Одна из них универсальна и позволяет переходить по любому выбранному адресу, другая же предназначена для более коротких переходов на расстояния, не превышающие 126—127 байт (так же, как и для команды DJNZ). Первая инструкция записывается двумя буквами JP (сокращение от Jump — перепрыгнуть), а вторая имеет мнемоническое обозначение JR (Jump Relative — относительный переход). Сначала приведем несколько примеров безусловных переходов:
JP 3435 ;переход по абсолютному адресу 3435 JP LABEL ;переход на метку LABEL JR 535 ;относительный переход вперед на ; расстояние в 35 байт JR LABEL ;переход на ту же самую метку LABEL
Сразу может возникнуть вопрос, зачем для выполнения одного и того же действия нужны разные команды? Во-первых, команда JR короче JP и занимает в памяти два байта вместо трех (не улыбайтесь: такая, на первый взгляд, мелочная экономия в итоге может вылиться в килобайты!), а во-вторых, команды «коротких» переходов вы сможете оценить в полной мере, если вам когда-нибудь доведется писать программы или процедуры в машинных кодах, которые позволительно загружать по любому удобному адресу. Применение команды JR в таких подпрограммах избавит вас от необходимости выполнения предварительной их настройки на адрес загрузки. Именно так написано большинство процедур из пакетов Supercode и NewSupercode, что значительно облегчает работу с ними. Поэтому при написании собственных программ старайтесь использовать команду JR везде, где это только позволяет расстояние, оставляя команде JP переходы к дальним адресам.
Теперь относительно условных переходов. Эти команды также начинаются с JP или JR, но в поле операндов записывается мнемоника проверки одного из возможных флагов и после запятой — имя метки или абсолютный адрес, например:
JP Z,8252 ;переход по адресу 8252, если ; установлен флаг нуля JR NC,MAIN ;относительный переход на метку MAIN, ; если флаг переноса сброшен
Перечислим мнемоники всех возможных условий:
- Z — если ноль (установлен флаг нуля Z);
- NZ — если не ноль (флаг нуля Z сброшен);
- C — если перенос (установлен флаг переноса CY);
- NC — если нет переноса (флаг переноса CY сброшен);
- M — если отрицательный результат (установлен флаг знака S);
- P — если результат положительный (флаг знака S сброшен);
- PE — если четность или переполнение (установлен флаг P/V);
- PO — если нет четности/переполнения (флаг P/V сброшен).
Для флагов H и N условия отсутствуют, так как они используются только в неявном виде командами коррекции двоично-десятичных чисел.
Здесь нужно еще добавить, что в команде JP возможно применение всех перечисленных мнемоник условий, а с командой JR допускаются только первые четыре: Z, NZ, C и NC.
Зная все это, можно наконец написать цикл. В общем виде он будет выглядеть так:
LD E,N ;заносим в регистр E счетчик ; количества повторений цикла N LOOP PUSH DE ;сохраняем его в стеке ......... ;тело цикла POP DE ;восстановление счетчика DEC E ; и уменьшение его на единицу JR NZ,LOOP ;переход на начало цикла, если счетчик ; не обнулился (в самом общем случае ; здесь может находиться инструкция ; JP NZ,LOOP)
Операция сравнения
Программируя на Бейсике, вы привыкли, что в циклах можно задавать любые граничные значения управляющей переменной. Оказывается, в ассемблере это также возможно, хотя здесь и есть ряд ограничений, касающихся выбора регистра и, естественно, диапазона границ его изменения. В этом случае в качестве счетчика удобнее всего использовать аккумулятор, что избавит от необходимости применения дополнительных команд пересылок между регистрами. Перед началом цикла нужно занести в аккумулятор стартовое значение счетчика, а в конце сравнивать его с числом, до которого он должен измениться. Команда сравнения величины регистра A (и только его!) с числовым значением или с содержимым другого регистра записывается как CP (compare — сравнить), а в поле операндов помещается число или имя регистра, например:
CP 5 ;сравнить значение в аккумуляторе с числом 5
или
CP D ;сравнить содержимое аккумулятора с регистром D
Операция сравнения исключительно важна и ее применение, конечно, далеко не ограничивается только циклами, поэтому мы сочли необходимым привести возможные результаты сравнения регистра A с операндом X и используемые при этом мнемоники условий для переходов в табл. 5.1.
Результат сравнения | Состояние флагов | Мнемоника условияперехода |
A = X | Z = 1 | Z |
A <> X | Z = 0 | NZ |
Беззнаковое сравнение (числа от 0 до 255) | ||
A < X | CY = 0 | C |
A >= X | CY = 0 | NC |
Сравнение с учетом знака (числа от −128 до +127) | ||
A < X | S = 1 | P |
A >= X | S = 0 | M |
Используя операцию сравнения, можно написать цикл, в котором регистр A изменяется, например, от 12 до 24:
LD A,12 ;задаем начальное значение в аккумуляторе CYCLE PUSH AF ;сохраняем в стеке ......... ;тело цикла POP AF ;восстановление аккумулятора INC A ;увеличение счетчика CP 25 ;сравниваем содержимое регистра A ; с числом 25 JR C,CYCLE ;переход на начало, если меньше 25 ; (меньше или равно 24)
В данном примере ничего принципиально не изменится, если команду JR C,CYCLE заменить на JR NZ,CYCLE — результат будет тем же, но если шаг цикла окажется отличным от единицы, то второй вариант может не сработать, поэтому предпочтительнее все же применять проверку флага переноса, а не нуля.
Приведенный вариант организации циклов уже почти повторяет такие строки Бейсика:
FOR N=12 TO 24 .............. NEXT N
Основное отличие, пожалуй, состоит лишь в одном: ассемблерный цикл в любом случае выполнит хотя бы один проход, независимо от граничных условий, ведь проверка здесь находится в конце цикла. Поэтому, чтобы исключить такой нежелательный эффект, в тех случаях, когда заранее неизвестны границы цикла, необходимо проверку производить в самом начале. Это лишь немногим усложнит программу, и она примет следующий вид:
;Предположим, что значение аккумулятора до начала цикла неизвестно CYCLE CP 25 ;проверяем на достижение конечного ; значения, то есть выполнение условия ; A > 24 (A >= 25) JR NC,AROUND ;если да, обходим цикл PUSH AF ;сохраняем счетчик в стеке ......... ;тело цикла POP AF ;восстановление значения счетчика INC A ; и увеличение его на 1 JR CYCLE ;безусловный переход на начало AROUND ......... ;продолжение программы
Прежде чем продолжить разговор о циклах, скажем еще несколько слов об особенностях использования в ассемблере чисел со знаком. Вы, конечно, понимаете, что ни в какую ячейку памяти невозможно записать отрицательное значение. Попробуйте в редакторе GENS ввести, а затем оттранслировать команду
LD A,-1
после чего просмотрите память, например, функцией Бейсика PEEK. На первом месте вы увидите байт 62, это код самой команды, а следом за ним вместо -1 обнаружите число 255. Казалось бы, что с отрицательными числами в машинных кодах иметь дело совершенно невозможно, но тем не менее, можно считать, что старший бит в байте или двухбайтовом значении иногда будет играть роль знака: если он установлен, то число отрицательное, а если сброшен — положительное. Таким образом, применение чисел со знаком в ассемблере в достаточной степени условно, но все же возможно. А следить за тем, какой тип чисел применяется в каждом конкретном случае должен сам программист.
«Длинные» циклы
Нередко могут понадобиться циклы с количеством повторений значительно больше 256. На первый взгляд решение такой проблемы может показаться весьма тривиальным: достаточно использовать для размещения счетчика не отдельный регистр, а регистровую пару. Это так, но не совсем. Дело в том, что команды увеличения или уменьшения содержимого регистровых пар, оказывается, никак не влияют на флаги. Не получится использовать и операцию сравнения, разве только проверять регистры, составляющие пару, раздельно — сначала один, затем другой. Поэтому в таких случаях применяется совершенно иной подход.
Наиболее простой вариант состоит в проверке на обнуление регистровой пары: она будет содержать нулевое значение только в том случае, если оба ее регистра будут равны нулю. Иными словами, алгоритм такого цикла с использованием в качестве счетчика, для определенности, пары BC можно сформулировать так: «если регистр B не равен нулю ИЛИ регистр C не равен нулю, то перейти на начало цикла». Команда «ИЛИ» выглядит так же, как и в Бейсике — OR. Правда, в ассемблере она служит в основном совершенно для других целей, о чем мы обязательно расскажем. Но сначала приведем общий вид программы, иллюстрирующий «длинные» циклы:
LD BC,NN ;записываем в пару BC счетчик MET PUSH BC ;сохраняем ......... ;выполняем тело цикла POP BC ;восстанавливаем значение счетчика DEC BC ; и уменьшаем его на единицу LD A,B ;проверка условия завершения цикла OR C JR NZ,MET
Логические операции
О команде OR стоит поговорить более подробно, так как она очень часто будет встречаться в дальнейшем. Наряду с двумя другими командами AND и XOR она относится к логическим операциям, присущим только машинному языку и не имеющим аналогии, по крайней мере, в стандартном «Спектрум-Бейсике». Эти три операции воздействуют на отдельные биты (разряды двоичного числа), изменяя их в соответствии с одним из трех возможных принципов поразрядного сложения. Сразу же скажем, что все три операции могут выполняться только на регистре A и изменяют флаги Z, P/V и S. Флаги CY и N после выполнения любой операции сбрасываются в 0 независимо от результата, а флаг H устанавливается после AND и сбрасывается после OR или XOR.
Принцип поразрядного объединяющего ИЛИ (OR) заключается в том, что если хотя бы в одном из двух двоичных чисел определенный разряд не равен нулю, то соответствующий разряд результата также будет ненулевым. Иными словами, если хотя бы в одном из объединяющихся байтов установлен бит, то и в результирующем байте данный бит будет установлен, а ноль получится лишь в том случае, если и там и там бит сброшен. Например, при сложении по принципу OR чисел 1001 и 1100 получится число 1101:
1 0 0 1 1 1 0 0 ---------- 1 1 0 1
Теперь, наверное, должно быть понятно, что после выполнения команд
LD A,B OR C
нулевой результат в аккумуляторе получится лишь тогда, когда оба регистра (B и C) будут содержать 0.
При сложении двух чисел по принципу поразрядного исключающего ИЛИ (XOR) нулевой бит может получиться в двух случаях: если соответствующий бит установлен в обоих слагаемых байтах либо если оба бита сброшены. Если же в одном из чисел бит установлен, а в другом сброшен, в результате получим установленный бит. Для тех же двух чисел результат сложения по XOR будет таким:
1 0 0 1 1 1 0 0 ---------- 0 1 0 1
этот принцип используется, например, при выводе текста и графики в режиме OVER 1.
Принцип поразрядного И (AND) состоит в том, что 1 в каком-то бите может получиться лишь тогда, когда данный бит установлен в обоих числах. В любом другом случае в результате получится нулевой бит:
1 0 0 1 1 1 0 0 ---------- 1 0 0 0
Команды, использующие принцип AND, применяются, в тех случаях, когда требуется в двоичном числе выделить несколько битов, оставив их неизменными, а остальные сбросить в 0. В дальнейшем вы увидите, что такие команды очень полезны, например, при различных графических построениях.
ПЕРЕМЕЩЕНИЯ СИМВОЛОВ
В очень многих фирменных игровых программах используются те или иные способы движения отдельных букв, строк, или даже целых текстовых экранов. Так, например, в OCEAN CONQUER осуществляется побуквенный вывод текста (что-то вроде «печатающего квадрата», рассмотренного в [1]), в F-16 COMBAT PILOT можно увидеть бегущую строку, а в играх OVERLORD, ZULU WAR и многих других — скроллинги (перемещения) экранов с текстами. И в этом разделе мы рассмотрим некоторые из подобных эффектов, делающих игровые программы более интересными.
Печатающий квадрат
Чтобы создать программу этого эффектного вывода символов на экран, напоминающего работу пишущей машинки или телетайпа, достаточно воспользоваться простым циклом, содержащим команду DJNZ. Вначале два слова о том, что мы должны увидеть на экране, а затем рассмотрим особенности программы и новые команды микропроцессора, которые нам понадобятся для осуществления этого замысла.
После запуска программы слева направо начнет передвигаться синий квадратик, следом за которым будут постепенно появляться буквы заданного текста. Вывод желательно сопроводить звуком, имитирующим стук клавиш.
Поскольку такой способ печати текстов может встречаться в игровой программе неоднократно, необходимо создать универсальную процедуру, которая сможет выводить любой заданный текст. В этом случае перед ее вызовом достаточно будет лишь указать нужные параметры в некоторых регистрах. Какие это должны быть параметры? Во-первых, конечно, сам текст, а точнее, адрес строки символов. Другим параметром может быть длина выводимой строки, но от этого значения можно избавиться несколькими способами (вообще же везде, где это только возможно, нужно стараться избегать лишних параметров). Например, можно вставить байт длины строки непосредственно перед ее началом, но это не слишком удобно. Более распространенный способ — ограничить строку, вставив в ее конец какой-нибудь конкретный байт, служащий маркером конца текста. Обычно для этих целей используется байт 0 и такой формат строки называют ASCIIZ-форматом. Существуют и другие способы задания строк, но о них мы поговорим чуть позже.
Используя при выводе строк способом «печатающего квадрата» ASCIIZ-строки, перед вызовом процедуры будем заносить их адреса в регистровую пару HL. Алгоритм самой подпрограммы может быть таким: считываем из строки очередной символ и если он равен нулю, заканчиваем вывод; печатаем символ; следом выводим пробел с синим фоном и возвращаем позицию печати на один символ назад; получаем короткий звук, имитирующий стук пишущей машинки и делаем небольшую задержку; возвращаемся на начало цикла. Кроме этого, перед завершением программы нужно стереть с экрана синий квадрат, для чего достаточно просто вывести пробел.
Программа, выполняющая все эти действия будет выглядеть примерно таким образом:
T_TAPE LD A,(HL) ;читаем из строки очередной символ AND A ;проверяем на 0 JR NZ,TTAPE1 ;если нет, продолжаем LD A," " ;стираем изображение RST 16 ; «печатающего квадрата» RET ;выход TTAPE1 RST 16 ;печатаем считанный символ INC HL ;перемещаем указатель текущего ; символа на следующий PUSH HL ;сохраняем в стеке значение указателя LD DE,TXTCUR ;формируем изображение LD BC,4 ; «печатающего квадрата» CALL 8252 CALL 3405 ;сбрасываем временные атрибуты XOR A ;в аккумуляторе 0 OUT (254),A ;«выключаем» динамик ; ------------------ ; Задержка (PAUSE 5) LD BC,5 ;пауза 5/50 секунды CALL 7997 ;вызов подпрограммы PAUSE ; ------------------ LD A,%00010000 ;устанавливаем 4 бит аккумулятора OUT (254),A ;«включаем» динамик POP HL ;восстанавливаем значение указателя JR T_TAPE ;переход на начало цикла TXTCUR DEFB 17,1," ",8
Как вы видите, в этой небольшой процедуре появилось несколько новых команд, поэтому помимо кратких комментариев дадим еще и более основательные пояснения.
Команда LD A,(HL) в принципе очень похожа на известную вам LD A,(Address), но отличается от нее тем, что в аккумулятор загружается значение не из какой-то конкретной ячейки памяти, а из той, на которую указывает регистровая пара HL. Например, если в HL записать число 16384, то в регистр A загрузится содержимое ячейки, находящейся по адресу 16384, то есть выполнится команда LD A,(16384). Эту инструкцию очень удобно применять в тех случаях, когда значение адреса для чтения или записи заранее неизвестно и может быть любой переменной величиной. Попутно скажем, что существует и обратная команда, то есть LD (HL),A, которая записывает содержимое аккумулятора по адресу, указанному в HL.
Существенное преимущество этого типа команд состоит еще и в том, что кроме аккумулятора таким способом можно пересылать и содержимое любого другого регистра данных, в том числе и регистров H и L, например, LD L,(HL) или LD (HL),H. И даже более того, по адресу, указанному в HL, можно записывать не только содержимое регистров, но и непосредственные числовые значения, конечно, не превышающие 255, скажем, LD (HL),153. Что же касается аккумулятора, то для него имеется возможность адресации не только с помощью пары HL, но также и BC либо DE, и эти команды еще не раз появятся в нашей книге.
В приведенной процедуре встретились две логические команды AND A и XOR A, хотя в данном примере они применены и не совсем по назначению, а скорее для сокращения машинного кода. Первая из них выполняет то же действие, что и команда CP 0, а вторая заменяет инструкцию LD A,0 и такой способ записи среди программистов считается хорошим стилем. В самом деле, при объединении аккумулятора по принципу AND с самим собой, ноль может получиться лишь тогда, когда он имеет нулевое значение (кстати, с тем же успехом можно пользоваться инструкцией OR A). Весьма ценно и то, что при этом содержимое аккумулятора не изменяется, а команда воздействует только на флаги, и в частности, на флаг Z, который нам и нужно проверить. Другая команда, XOR A, обнулит содержимое аккумулятора при любом его изначальном значении. Ведь, как мы уже говорили, при объединении двух чисел по принципу XOR любой бит будет сброшен в 0, если соответствующие биты одинаковы в обоих числах. То есть при объединении по XOR двух одинаковых величин результат всегда будет нулевым. Запомните это обстоятельство, поскольку в дальнейшем мы всегда будем пользоваться описанными приемами. Единственный случай, когда команда LD A,0 незаменима, это если нужно сохранить значение флагового регистра для последующей проверки. Помните, что все логические команды воздействуют на флаги, а инструкции загрузки регистров — нет.
Вы, наверное, знаете, что для получения звука в Бейсике, кроме оператора BEEP, можно воспользоваться командой OUT, которая записывает число в указанный порт. Динамик в Spectrum’е подключен к порту 254, а за его «включение» или «выключение» ответственен 4-й бит посылаемого байта. Остальные биты несут другую нагрузку, а здесь нас интересуют еще биты 0, 1 и 2, которые, как уже было сказано в разделе «Подготовка экрана к работе из ассемблера» главы 4, определяют цвет бордюра. При заданных в процедуре значениях аккумулятора 0 и 16 получится черный бордюр, но его легко изменить на любой другой цвет, просто прибавив к коду нужного цвета значение «бита для динамика» (точнее, код цвета объединяется со значением 4-го бита аккумулятора по принципу OR, для чего, кстати, логические команды в основном и используются). Быстрое чередование значения этого бита приводит к появлению звука. Более подробно о получении различных звуковых эффектов мы расскажем в главе 10.
В процедуре «печатающий квадрат» показано применение еще одной полезной подпрограммы ПЗУ, которая вызывается при интерпретации оператора PAUSE. Перед обращением к ней необходимо в пару BC поместить величину задержки, измеряемую в 50-х долях секунды — так же, как и в Бейсике. Если в BC поместить 0, то получится бесконечная пауза, прерываемая только при нажатии на любую клавишу (опять же, как в Бейсике).
Теперь приведем пример использования данной процедуры и напечатаем на экране таким способом какой-нибудь конкретный текст. Например:
ORG 60000 ENT $ CALL SETSCR ;подготавливаем экран LD A,22 ;помещаем текущую позицию печати в ; начало 5-й строки экрана (AT 5,0) RST 16 LD A,5 RST 16 XOR A RST 16 LD HL,TEXT ;указываем адрес ASCIIZ-строки с текстом CALL T_TAPE ;выводим методом телетайпа RET TEXT DEFM "COMPUTER Sinclair ZX-Spectrum" DEFB 0 SETSCR ......... ;подпрограмма из раздела «Подготовка ; экрана к работе из ассемблера» главы 4 T_TAPE .........
Бегущая строка
Теперь покажем, как можно создать на ассемблере еще один эффект, который, несмотря на относительную простоту реализации, пользуется в игровых программах довольно большой популярностью. В книге [1] мы показывали, как добиться эффекта бегущей строки средствами Бейсика, однако интерпретатор не позволил нам добиться плавного движения букв. В ассемблере же можно без особого труда устранить этот недостаток, правда, для этого нам придется познакомиться еще с одним типом команд, выполняющих различные виды сдвигов битов в регистрах или в памяти.
Смысл сдвигов заключается в том, что все биты, не меняя своего относительного положения, смещаются вправо или влево. В зависимости от типа команды уходящие «за край» биты могут появляться с противоположной стороны (циклические сдвиги) либо теряться (нециклические или простые сдвиги). Например, после циклического сдвига влево числа 11001001 получится результат 10010011, а после простого сдвига вправо этого же значения — ?1100100. Знак вопроса на месте 7-го бита означает, что его значение зависит от результата предыдущей операции, а точнее, от состояния флага переноса CY; если он был установлен, то в 7-м бите появится единица, в противном случае - 0.
Существуют четыре команды для смещения битов в регистре A:
- RLCA — циклический сдвиг влево. После выполнения этой команды старший бит переходит в младший и дублируется во флаге переноса CY (то есть он будет установлен, если 7-й бит перед выполнением команды был в 1 и сброшен, если 7-й бит имел нулевое значение).
- RRCA — циклический сдвиг аккумулятора вправо. Эта команда в точности противоположна предыдущей: младший бит переходит в старший и повторяется во флаге CY.
- RLA — это тоже циклический сдвиг, но не совсем обычный. В этой команде флаг переноса рассматривается как еще один дополнительный бит аккумулятора: 7-й бит перемещается в CY, а предыдущее значение флага переноса переносится в младший бит.
- RRA — эта команда аналогична предыдущей с тем отличием, что движение битов происходит в обратном направлении, то есть слева направо.
На рис. 5.2 показаны схемы направлений перемещения битов во всех четырех перечисленных командах. После их выполнения все основные флаги (кроме CY, конечно) остаются без изменений, а во флагах N и H появляются нули.
Похожие сдвиги могут быть получены и с другими регистрами (в том числе и с аккумулятором), либо в ячейке памяти, адресованной регистровой парой HL. Вот эти команды:
Команда RLC S выполняет действие, аналогичное RLCA. В качестве операнда S могут использоваться регистры A, B, C, D, E, H или L, а также адрес (HL), если записать инструкцию RLC (HL). Во всех остальных командах можно применять тот же набор операндов (немного опережая события, добавим, что во всех этих командах могут участвовать также и ячейки памяти, адресованные индексными регистрами, но поскольку мы их еще не рассматривали, применять такие инструкции пока не будем).
Инструкция RRC S выполняется аналогично команде RRCA, но помимо аккумулятора применима к любым перечисленным операндам.
Команды RR S и RL S подобны описанным командам RRA и RLA соответственно.
Весьма широко применяются команды SLA S (сдвиг влево) и SRL S (сдвиг вправо). Они отличаются от прочих команд сдвигов тем, что освобождающийся бит при любых условиях заполняется нулевым значением, в результате чего их вполне можно рассматривать как операции умножения или деления на 2 соответственно. «Вытесняемый» бит при этом, как и при других сдвигах переходит во флаг переноса.
Еще одна операция «деления на 2 со знаком» выполняется командой SRA S, которая смещает вправо только 7 младших битов, а старший оставляет без изменения. Младший бит, как и положено, вытесняется во флаг CY. Все команды этой группы воздействуют уже не только на флаг переноса, но и на все прочие. Флаги H и N, так же, как и при сдвигах аккумулятора, сбрасываются в 0.
На рис. 5.3 приведены схемы всех перечисленных сдвигов. Рассмотрите этот рисунок внимательно, и вам, наверное, уже станет понятно в общих чертах, как можно получить эффект плавно движущейся строки. Теперь остается, пожалуй, только одна сложность: научиться быстро и безошибочно определять адреса в экранной области, соответствующие началу любой строки. Конечно, вы можете воспользоваться рис. 2.3 из второй главы, но тогда не сможете написать универсальной подпрограммы «на все случаи жизни».
Алгоритм расчета адресов экрана достаточно сложен, поэтому, как и прежде, обратимся за помощью к ПЗУ, благо в «прошивке» Speccy имеются необходимые процедуры. Для получения начального адреса любой строки экрана можно обратиться к подпрограмме, расположенной по адресу 3742. Перед обращением к ней в аккумулятор необходимо поместить номер строки экрана. На выходе в регистровой паре HL получится искомый адрес, зная который, уже несложно рассчитать и любой другой адрес в пределах данной строки. Каждая строка имеет длину 32 байта, которые расположены последовательно, и вмещает 8 рядов пикселей. При переходе к следующему ряду адрес видеобуфера увеличивается на 256, то есть увеличивается только старший байт адреса, а младший остается без изменений.
Для примера покажем, как рассчитать адрес второго байта сверху в 5-й строке и 11-й позиции экрана (иначе, в позиции печати, определяемой директивой Бейсика AT 5,11):
LD A,5 ;номер строки CALL 3742 ;получаем в HL начальный адрес LD A,L ;берем значение младшего байта адреса OR 11 ;добавляем смещение в 11 байт (знакомест) LD L,A ;возвращаем в младший байт INC H ;увеличиваем адрес на 256 и тем самым ; получаем адрес второго байта в ; знакоместе сверху
Теперь можно написать программу, дающую эффект бегущей строки. Для определенности будем скроллировать 21-ю строку экрана:
ORG 60000 LD A,21 ;21-я строка экрана SCRLIN CALL 3742 ;получаем ее адрес в HL ; Так как строка должна бежать слева направо, то раньше нужно сдвигать ; последние байты, поэтому определяем адрес конца строки LD A,L OR 31 LD L,A LD C,8 ;высота строки 8 пикселей SCRL1 LD B,32 ;длина строки 32 байта AND A ;очистка флага CY PUSH HL ;сохраняем адрес SCRL2 RL (HL) ;последовательно сдвигаем все байты DEC HL DJNZ SCRL2 POP HL ;восстанавливаем адрес INC H ;переходим к следующему ряду пикселей DEC C ;повторяем JR NZ,SCRL1 RET
Но это еще не все, ведь данная процедура сдвинет строку только на один пиксель влево, а для перемещения ее на знакоместо потребуется выполнить приведенную подпрограмму 8 раз. Однако прежде чем мы продолжим создание полноценного эффекта, поясним смысл некоторых использованных команд.
Возможно, вам не совсем ясно, что в данной подпрограмме делает инструкция AND A. Как сказано в комментарии к этой строке, она очищает флаг переноса. Собственно, это и все, что нам от нее требуется, но для чего это нужно? Посмотрите на схему перемещения битов командой RL S и увидите, что при ее выполнении бит из CY переходит в младший бит операнда, в то время как старший сохраняется во флаге переноса. Этим и обусловлен выбор именно команды RL (HL), ведь нам нужно скроллировать не отдельный байт, а целую цепочку байтов, значит, вытесняемый бит должен быть сохранен для следующей команды сдвига. Но сдвигая самый первый байт в цепочке, мы должны убедиться, что в младшем бите появится 0, поэтому и нужно сбросить флаг CY. Если этого не сделать, то в конце концов в скроллируемой строке может появиться какой-то нежелательный «мусор» в виде «включенных» пикселей.
Надеемся, что этих объяснений достаточно, а если нет, то попытайтесь мысленно проследить, что происходит в результате выполнения команды RL (HL) на каждом «витке» цикла, обозначенного меткой SCRL2, какие биты и куда при этом сдвигаются.
А сейчас напишем небольшую тестовую программку на Бейсике, проверяющую работоспособность нашей процедуры. Постарайтесь после этого самостоятельно переписать ее на ассемблере, а когда справитесь с задачей, перелистните несколько страниц и проверьте себя, сравнив полученный результат с ответом, данным в конце этого раздела.
10 INK 6: PAPER 0: BORDER 0: CLS 20 LET a$="Examine yourself how you know the assembler!" 30 FOR i=1 TO LEN a$ 40 PRINT AT 21,31; INK 0; a$(i) 50 FOR j=1 TO 8 60 RANDOMIZE USR 60000 70 NEXT j 80 NEXT i 90 FOR i=1 TO 256: RANDOMIZE USR 60000: NEXT i
Используя команды сдвигов, можно придумать великое множество интересных эффектов. Приведем маленький пример наиболее простого из них — циклического скроллинга отдельного знакоместа, и предоставим вам возможность пофантазировать и развить эту идею.
ORG 60000 LD B,8 LD HL,16384 ROL RRC (HL) INC H DJNZ ROL RET
Чтобы посмотреть, как эта программка работает, напечатайте в Бейсике в левом верхнем углу экрана какой-нибудь символ и в цикле вызывайте процедуру. Изображение должно многократно «провернуться» вокруг вертикальной оси, причем уходящие вправо точки будут вновь появляться с левого края. Если заменить команду RRC (HL) на RLC (HL), то скроллинг будет выполняться в обратную сторону.
«Волна»
Предлагаем вам еще один интересный эффект, иногда встречающийся в игровых программах: по строке текста от левого к правому краю экрана как бы пробегает волна, буквы приподнимаются, затем опускаются и наконец занимают свое первоначальное положение.
Для осуществления этого эффекта прежде всего потребуется написать две подпрограммы вертикального скроллинга отдельных знакомест экрана: одну для перемещения символа вверх, а другую — для движения изображения вниз. Принцип такого рода скроллингов исключительно прост. Например, для сдвига изображения на один пиксель вверх нужно байт из второго ряда пикселей перенести на место байта первого ряда, затем байт из третьего ряда поместить во второй и так далее, а самый нижний ряд пикселей заполнить нулями. Правда, при этом потеряется содержимое самого верхнего байта, но для текстовой строки это не особенно важно, так как обычно буквы сверху и снизу имеют по пустому ряду точек (ведь между строками текста должен быть какой-то зазор). Скроллинг букв вниз аналогичен, только перемещать байты знакоместа нужно, начиная с нижнего края.
Подпрограмма вертикального скроллинга знакоместа может выглядеть так:
UP CP 32 ;проверка позиции перемещаемого знакоместа RET NC ;выход, если больше или равна 32 LD HL,(AD_LIN) ;получаем адрес экрана начала строки PUSH AF OR L LD L,A PUSH HL LD D,H ;копируем адрес в DE LD E,L LD B,7 ;повторяем 7 раз UP1 INC H ;в HL - адрес байта следующего ряда LD A,(HL) ;переносим из (HL) в (DE) LD (DE),A INC D ;переходим к следующему ряду DJNZ UP1 LD (HL),0 ;обнуляем самый нижний ряд POP HL POP AF RET
Перед обращением к этой (а также и к следующей) подпрограмме в аккумулятор нужно занести горизонтальную позицию знакоместа в строке. Если заданная позиция выходит за пределы экрана, то есть не попадает в диапазон от 0 до 31, подпрограмма не должна выполняться. Для этого в самом ее начале проверяется обозначенное условие и в случае его невыполнения происходит условный выход из подпрограммы (команда RET NC). Как видите, условными могут быть не только переходы. Сразу заметим, что по условию можно также вызывать процедуры, используя команды типа
CALL S,ADDR
где S — любое из возможных условий, а ADDR — абсолютный адрес или метка.
После проверки возможности выполнения подпрограммы в регистровую пару HL загружается адрес начала строки экрана, по которой будет пробегать «волна». Этот адрес рассчитывается заранее и помещается в двухбайтовую переменную AD_LIN, которая в программе будет задаваться инструкцией ассемблера DEFW. В остальных строках не встретилось ничего нового, поэтому, надеемся, вполне достаточно кратких комментариев.
Подпрограмма скроллинга вниз похожа на первую, но все же есть и некоторые отличия. Мы говорили, что начинать такой скроллинг нужно с нижнего края знакоместа, однако можно поступить и несколько иначе, например, так:
DOWN CP 32 ;начало такое же, как и в RET NC ; предыдущей подпрограмме LD HL,(AD_LIN) PUSH AF OR L LD L,A PUSH HL XOR A ;в аккумуляторе 0 EX AF,AF' ;отправляем его в альтернативный AF' LD B,7 DOWN1 LD A,(HL) ;считываем байт текущего ряда EX AF,AF' ;меняем аккумулятор на альтернативный LD (HL),A ;записываем в текущий ряд INC H ;переходим к следующему ряду DJNZ DOWN1 EX AF,AF' ;запись последнего байта LD (HL),A ; в самый нижний ряд POP HL POP AF RET
Раньше мы уже упоминали команду EX AF,AF', сейчас же показали ее практическое применение. Напомним, что она выполняет действие, аналогичное команде EXX, только меняет на альтернативный аккумулятор, а заодно и флаговый регистр (этим иногда тоже можно пользоваться, сохраняя флаги для последующих операций). Советуем внимательно изучить подпрограмму DOWN и проследить за «эволюциями» аккумулятора в данном примере. Это поможет вам лучше понять принцип работы многих других подпрограмм, с которыми вы еще встретитесь.
После того, как основные процедуры скроллингов созданы, можно приступить к написанию подпрограммы, формирующей саму «волну». Алгоритм создания этого эффекта совсем несложен: первый символ, находящийся в «голове» синусоиды нужно сдвинуть, например, вверх, тогда следующие два пойдут вниз и последний — снова вверх. Получив таким образом синусоиду, смещаем ее начало на одну позицию и повторяем все с самого начала до тех пор, пока «волна» не пройдет по всему экрану и не скроется за его пределами. Вот программа, создающая на экране описываемый эффект:
ORG 60000 XOR A ;инициализация переменных: LD (HEAD),A ; начальной позиции «волны» CALL 3742 LD (AD_LIN),HL ; и адреса строки экрана WAVE LD HL,HEAD LD A,(HL) INC (HL) ;увеличивать или уменьшать можно не ; только содержимое регистров или ; регистровых пар, но и значение в ; ячейке памяти, адресованной парой HL CP 35 ;ушла ли «волна» за пределы экрана? RET Z CALL UP ;первый символ вверх DEC A CALL DOWN ;второй - вниз DEC A CALL DOWN ;третий тоже вниз DEC A CALL UP ;последний - вверх LD BC,5 CALL 7997 ;небольшая задержка (PAUSE 5) JR WAVE HEAD DEFB 0 ;позиция «головы» синусоиды AD_LIN DEFW 0 ;адрес экрана начала строки UP ......... DOWN .........
Данная подпрограмма создает эффект «волны» в самой верхней строке экрана, а чтобы получить то же самое в другой строке, нужно перед командой
CALL 3742
добавить
LD A,N
где N — номер требуемой строки.
Подпрограмма в таком виде пригодна для вызова из Бейсика. Напечатайте в верхней строке какой-нибудь текст, желательно большими буквами, а затем выполните оператор
RANDOMIZE USR 60000
Если же вы захотите все необходимые приготовления выполнить в ассемблере, то у вас уже достаточно знаний, чтобы справиться с этой задачей самостоятельно и без особых трудностей.
«Растворение» символов
Продолжим изучение новых команд и приемов программирования, а заодно рассмотрим еще один интересный эффект, который можно назвать «растворение» символов. Он используется, например, в таких играх, как LODE RUNNER, THE DAM BUSTER и других. Возможно, он заинтересует и вас. Суть его состоит в том, что при переходе от одной картинки, формируемой программой, к другой происходит не мгновенная очистка экрана, как оператором CLS, а изображение постепенно как бы растворяется. Это очень напоминает таяние снега. Подпрограмма, создающая такой эффект удивительно проста и коротка:
ORG 60000 ENT $ THAW LD B,8 ;экран очищается за 8 циклов LD DE,0 ;адрес начала кодов ПЗУ THAW1 LD HL,#4000 ;адрес начала экранной области PUSH DE THAW2 LD A,(DE) ;берем «случайный» байт из ПЗУ AND (HL) ;объединяем с байтом из видеобуфера LD (HL),A ;помещаем обратно в видеобуфер INC HL ;переходим к следующим адресам INC DE LD A,H ;проверяем, нужно ли повторять цикл CP #58 ;если прошли еще не весь видеобуфер ; (#5800 - адрес начала области атрибутов) JR NZ,THAW2 ; то повторяем PUSH BC LD BC,1 CALL 7997 ;PAUSE 1 POP BC POP DE LD HL,100 ADD HL,DE ;увеличиваем адрес в ПЗУ на 100 EX DE,HL ;меняем HL на DE DJNZ THAW1 ;повторяем цикл JP 3435 ;окончательно очищаем экран
Сначала объясним смысл вновь встретившихся команд, а затем более подробно расскажем о принципе работы программы.
Как мы говорили в самом начале книги, микропроцессор способен выполнять элементарные арифметические операции над числами. Сложению соответствует мнемоника ADD (Addition — сложение) а вычитанию — SUB (Subtraction). В этих операциях может участвовать регистр A (арифметические операции над однобайтовыми числами) или пара HL (при сложении или вычитании двухбайтовых чисел). К содержимому аккумулятора можно прибавлять (или вычитать) значение другого регистра или непосредственную числовую величину, а к паре HL можно только прибавлять и только содержимое другой регистровой пары (кроме AF, конечно). Результат арифметического действия получается в аккумуляторе или в регистровой паре HL соответственно.
Помимо обычных операций сложения и вычитания существуют их разновидности — сложение и вычитание с переносом (или, как еще говорят, с заемом). Они отличаются тем, что в операции принимает участие еще и флаг переноса: при сложении он прибавляется к результату, а при вычитании — отнимается. Записываются такие команды с мнемоникой ADC или SBC. В отличие от обычного вычитания в операции вычитания с переносом регистровая пара HL вполне может участвовать.
Есть небольшая особенность в записи этих команд: операция вычитания с участием аккумулятора выглядит не SUB A,S или SBC A,S, как это можно было ожидать, а просто SUB S или SBC S. То есть имя регистра A не пишется. Во всех остальных командах нужно указывать и имя. Вот некоторые примеры:
ADD A,7 ;прибавить к содержимому аккумулятора 7 ADC A,C ;прибавить к аккумулятору регистр C и флаг CY SUB B ;вычесть из аккумулятора значение регистра B SBC 127 ;вычесть из аккумулятора число 127 и флаг CY ADD HL,HL ;удвоить значение регистровой пары HL ADC HL,DE ;сложить содержимое пар HL и DE и добавить ; значение флага CY SBC HL,BC ;вычесть с учетом флага переноса BC из HL
Все перечисленные команды изменяют основные флаги за исключением команды ADD HL,SS, которая влияет лишь на флаг переноса.
Другая новая команда, встретившаяся в подпрограмме «растворения» экрана — это команда EX DE,HL. Она выполняет самое элементарное действие: обменивает содержимым регистровые пары HL и DE. То, что раньше было в HL, переходит в DE и наоборот. Правда, в данном случае она использована просто для пересылки полученного в HL результата в пару DE. Такой, вроде бы, нелепый ход объясняется отсутствием в системе команд микропроцессора Z80 инструкций для пересылки значений между регистровыми парами (типа LD DE,HL), поэтому команда EX DE,HL иногда может заменять последовательность
LD D,H LD E,L
что не только сокращает запись, но и несколько ускоряет работу программы.
Теперь вернемся к нашему примеру и посмотрим, как он работает. Основную роль здесь выполняет команда AND (HL), объединяющая байт из ПЗУ с байтом из экранной области памяти. В данном случае коды ПЗУ можно рассматривать как некоторую последовательность «случайных» чисел, поэтому в результате операции AND мы получим в аккумуляторе байт из видеобуфера, но некоторые биты в нем окажутся «выключенными», а какие именно — предсказать довольно трудно. Каждый следующий байт экрана объединяется с другим байтом из ПЗУ, отчего уже после первого прохождения цикла часть изображения пропадет. После второго прохода на экране останется еще меньше «включенных» пикселей. Но для этого нужно изменить последовательность «случайных» чисел, чего легче всего добиться изменением начального адреса в ПЗУ. В нашем примере он просто увеличивается на 100 байт. Поскольку нет гарантии, что после установленных восьми циклов все изображение окончательно исчезнет, в самом конце программа переходит на процедуру полной очистки экрана. Кстати, обратите внимание, что вместо последовательности
CALL 3435 RET
стоит единственная команда
JP 3435
Это позволительно делать практически всегда, когда за командой безусловного вызова следует безусловный же выход из подпрограммы. Исключение составляют лишь некоторые особые случаи, о которых мы поговорим, когда в этом возникнет необходимость.
В подпрограмме «растворения» экрана есть еще один довольно интересный момент — это способ организации внутреннего цикла. Зная длину области данных видеобуфера, которая равна 6144 байт, можно было бы задать количество повторений в явном виде, но в данном случае, поскольку адрес экранной области начинается с ровного шестнадцатеричного значения (то есть младший байт адреса равен нулю) и размер обрабатываемого блока также кратен 256, достаточно проверять только старший байт адреса на достижение определенного значения, а именно, #58, так как с адреса #5800 начинается область хранения атрибутов для каждого знакоместа экрана.
Ответ на задачу
А сейчас, как мы и обещали, даем ответ на задачу, поставленную в параграфе «Бегущая строка» и приводим текст программы на ассемблере, соответствующий предложенному фрагменту на Бейсике. Не стоит очень расстраиваться, если вы обнаружите в чем-то расхождения, ведь любую, даже очень небольшую программу, можно написать тысячью и одним способом. Поэтому, если ваша программа работает, можете считать, что с поставленной задачей вы справились.
ORG 60000 ENT $ LD A,6 ;подготовка экрана LD (23693),A XOR A CALL 8859 CALL 3435 LD A,2 CALL 5633 LD HL,TEXT ;адрес текстовой строки MAIN1 LD DE,PR_AT ;позиционирование курсора, черный LD BC,5 ; (совпадающий с фоном) цвет символов CALL 8252 LD A,(HL) ;чтение очередного символа строки AND A JR Z,MAIN3 ;если 0, закончить вывод RST 16 INC HL PUSH HL ; Восьмикратное (по ширине символов в пикселях) скроллирование строки влево LD B,8 MAIN2 PUSH BC LD A,21 CALL SCRLIN CALL PAUSE ;задержка для получения более ; плавного смещения строки POP BC DJNZ MAIN2 POP HL JR MAIN1 ; Скроллинг, пока вся строка не исчезнет за левым краем экрана (0 = 265 раз) MAIN3 LD B,0 MAIN4 PUSH BC LD A,21 CALL SCRLIN CALL PAUSE POP BC DJNZ MAIN4 RET PAUSE LD BC,1 JP 7997 SCRLIN ......... PR_AT DEFB 22,21,31,16,0 TEXT DEFM "Examine yourself how you know the assembler!" DEFB 0
ОКНА
Пожалуй, найдется не так уж много игровых программ, в которых в той или иной форме не использовались бы окна. Вы, наверное, уже хорошо знакомы с этим термином, но тем не менее поясним, что именно мы будем под ним подразумевать. Окно — это некоторая прямоугольная часть рабочего экрана, внутри которой можно производить различные преобразования независимо от внешней области. Например, можно изменить цвет окна, вывести в него какой-нибудь текст, переместить внутри него изображения и многое другое. Некоторые игры буквально напичканы окнами: в одном происходит сражение, в другом сообщается, сколько времени вам осталось «жить», в третьем… Да что перечислять, вы и сами это знаете не хуже нас. Поговорим лучше о том, как создаются окна и как получаются всевозможные эффекты, связанные с ними.
Формирование окна
Прежде всего нам нужно разобраться, каким образом задаются окна. Для этого необходимо знать, какие параметры требуются для их описания.
В простейшем случае (и если вам доводилось работать с Laser Basic’ом, то это должно быть хорошо известно) достаточно определить четыре переменные: ROW — позиция по вертикали верхнего края окна, COL — горизонтальная координата левого края окна, LEN — ширина окна и HGT — его высота. Чаще все четыре параметра задаются в знакоместах, но иногда (как, например, в редакторе Art Studio) — в пикселях, правда, реализовать такие окна несравненно сложнее.
Если вы хотите иметь возможность заключать окна в рамки, причем с индивидуальным рисунком для каждого из них, то потребуется еще один параметр — тип рамки. Он может задаваться символом, содержащим рисунок рамки, просто ее порядковым номером или каким-то другим способом, а ноль, например, будет говорить об отсутствии окаймления.
Большее количество переменных может понадобиться для определения окон со сложной внутренней структурой. Например, окно может иметь собственное название, заголовок, выделяемый цветом и постоянно присутствующий в окне. Такие типы окон часто можно встретить в меню игровых (MICRONAUT ONE) или прикладных (Art Studio) программ (рис. 5.4). Кроме того, окно может быть как бы приподнятым над общим фоном экрана и отбрасывать на него «тень».
Как видите, можно придумать множество типов окон, но давайте сначала посмотрим, как получить наиболее простые из них и напишем подпрограммы, выполняющие наиболее распространенные преобразования в окнах. Первую из них, которая выполняет очистку заданной области экрана, назовем в соответствии с аналогичной процедурой Laser Basic’а CLSV. Перед обращением к ней необходимо заполнить 4 переменные, под которые нужно зарезервировать память инструкцией ассемблера DEFB:
COL DEFB 0 ROW DEFB 0 LEN DEFB 0 HGT DEFB 0
Обратите внимание, что переменные должны располагаться именно в том порядке, в котором они указаны; менять их местами или разбрасывать по тексту программы недопустимо. Это условие введено для упрощения всех последующих процедур, работающих с окнами, о чем мы еще скажем при детальном разборе подпрограмм. Другое упрощение касается допустимых значений переменных: во-первых, окно не может иметь нулевые размеры ни по ширине, ни по высоте, а во-вторых, оно не должно выходить за пределы экрана.
Такие допущения сделаны не только для упрощения подпрограмм и сокращения их размеров. Это позволяет также и несколько увеличить их быстродействие, так как все необходимые проверки возлагаются на программиста. Кстати, в большинстве игровых программ операции с окнами (да и многие другие) производятся как раз безо всяких проверок корректности задаваемых параметров. Ведь подобные действия нужны, в основном, лишь на стадии отладки программы, а когда работа закончена, они становятся «мертвым грузом», только замедляющим выполнение процедур.
Итак, приводим подпрограмму очистки окна экрана:
CLSV LD BC,(LEN) ;чтение сразу двух переменных: ; C = LEN, B = HGT LD A,(ROW) CLSV1 PUSH AF PUSH BC CALL 3742 ;адрес начала строки экрана LD A,(COL) ;прибавляем смещение ADD A,L ; COL по горизонтали LD L,A LD B,8 ;в каждой строке 8 рядов пикселей CLSV2 PUSH HL LD E,C ;счетчик циклов в E, равный ширине окна XOR A ;в аккумуляторе 0 CLSV3 LD (HL),A ;обнуляем очередной байт видеобуфера INC HL ;переходим к следующему DEC E ;пока не дойдем до правого края окна JR NZ,CLSV3 POP HL INC H ;переходим к следующему ряду пикселей DJNZ CLSV2 POP BC POP AF INC A ;переходим к следующей строке экрана DJNZ CLSV1 ;повторяем, пока не дойдем ; до нижнего края окна RET
Эта подпрограмма только очищает окно, но никак не влияет на его цвет. Для изменения атрибутов нужна другая процедура, которую назовем SETV. Здесь нам потребуется дополнительная переменная для хранения байта атрибутов, которую нужно определить в программе строкой
ATTR DEFB 0
и перед обращением к подпрограмме SETV занести в нее необходимое значение.
Прежде чем привести текст подпрограммы, скажем несколько слов о способе расчета требуемого адреса в области атрибутов экрана. Эта задача намного проще определения адреса в области данных, так как атрибуты имеют линейную организацию. Поэтому достаточно номер строки экрана умножить на 32, то есть на длину полной строки экрана, добавить величину смещения от левого края (горизонтальную позицию интересующего знакоместа) и полученную величину сложить с адресом начала области атрибутов. Наиболее сложным моментом расчетов может показаться операция умножения и это было бы действительно так, не будь один из сомножителей числом, равным степени 2, что позволяет свести задачу к простому сложению.
Вот текст подпрограммы, выполняющей установку атрибутов в окне экрана:
SETV LD DE,#5800 ;адрес начала области атрибутов экрана LD BC,(LEN) ;C = LEN, B = HGT LD A,(ROW) LD L,A ;расчет адреса левого верхнего угла окна LD H,0 ; в области атрибутов экрана ADD HL,HL ;умножаем на 32 (2 в 5-ой степени) ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,DE ;полученное смещение складываем ; с началом области атрибутов LD A,(COL) ;добавляем горизонтальное смещение окна ADD A,L LD L,A LD A,(ATTR) ;в аккумуляторе байт атрибутов SETV1 PUSH BC PUSH HL SETV2 LD (HL),A ;помещаем в видеобуфер INC HL DEC C ;до правого края окна JR NZ,SETV2 POP HL POP BC LD DE,32 ;переходим к следующей строке ADD HL,DE ; (длина строки 32 знакоместа) DJNZ SETV1 ;повторяем, пока не дойдем до нижнего ; края окна RET
Еще одним довольно распространенным видом преобразований, выполняемых с окнами, является инвертирование изображения. Подпрограмма, реализующая это действие очень похожа на процедуру очистки окна, только в этом случае байт данных из видеобуфера инвертируется в аккумуляторе и затем помещается обратно на свое место. Как вы знаете, инверсия — это изменение состояния битов на противоположное: если бит установлен, он сбрасывается и наоборот. Такую операцию можно выполнить, например, с помощью команды
XOR 255
однако микропроцессор имеет для этих целей специальную инструкцию CPL, которая и выполняется быстрее и занимает в памяти всего один байт. Ею мы и воспользуемся в подпрограмме INVV:
INVV LD BC,(LEN) LD A,(ROW) INVV1 PUSH AF PUSH BC CALL 3742 LD A,(COL) ADD A,L LD L,A LD B,8 INVV2 PUSH HL LD E,C INVV3 LD A,(HL) ;читаем байт из видеобуфера CPL ;инвертируем байт в аккумуляторе LD (HL),A ;возвращаем обратно в видеобуфер INC HL DEC E JR NZ,INVV3 POP HL INC H DJNZ INVV2 POP BC POP AF INC A DJNZ INVV1 RET
Поскольку здесь говорится об окнах, задаваемых с точностью до знакоместа, есть возможность несколько ускорить процедуру инвертирования изображения, что может оказаться существенным при работе с большой площадью экрана. Ведь вместо того, чтобы инвертировать каждый байт данных, можно просто поменять местами цвета INK и PAPER в области атрибутов и вместо восьми байт для каждого знакоместа обрабатывать только один. Для успешного решения этой задачи еще раз напомним значения битов в байте атрибутов: биты 0, 1 и 2 отвечают за цвет «чернил» INK, биты 3, 4 и 5 определяют цвет «бумаги» PAPER, 6-й бит задает уровень яркости BRIGHT, а старший 7-й бит включает или выключает мерцание FLASH.
Вот эта подпрограмма:
INVA LD DE,#5800 ;начало как в процедуре SETV LD BC,(LEN) LD A,(ROW) LD L,A LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,DE LD A,(COL) ADD A,L LD L,A INVA1 PUSH BC PUSH HL INVA2 LD A,%11000000 ;маскируем 2 старших бита атрибутов, ; которые не будут изменяться AND (HL) ;выделяем их из суммарных атрибутов LD B,A ;запоминаем их в регистре B LD A,%00111000 ;маскируем биты для цвета PAPER AND (HL) ;выделяем их RRCA ;сдвигаем на место битов цвета INK RRCA RRCA OR B ;объединяем с выделенными ранее битами LD B,A ;снова запоминаем LD A,%00000111 ;маскируем биты для цвета INK AND (HL) ;выделяем их RLCA ;сдвигаем на место битов цвета PAPER RLCA RLCA OR B ;объединяем все атрибуты LD (HL),A ;возвращаем их в видеобуфер INC HL DEC C JR NZ,INVA2 POP HL POP BC LD DE,32 ADD HL,DE DJNZ INVA1 RET
Довольно часто в игровых программах применяется зеркальное отображение окон. Подобная процедура имеется и в Laser Basic’е. Ее выполняет оператор .MIRV, поэтому и нашу подпрограмму мы назовем так же. Перед обращением к ней, как и во всех предшествующих процедурах необходимо определить переменные ROW, COL, HGT и LEN с теми же ограничениями, о которых мы уже говорили.
MIRV LD BC,(LEN) LD A,(ROW) MIRV1 PUSH AF PUSH BC CALL 3742 ;в HL - адрес экрана LD A,(COL) OR L LD L,A ;начальный адрес левого края окна ; В DE получаем соответствующий адрес противоположного края окна LD D,H LD A,C ADD A,L DEC A LD E,A LD B,8 ;8 рядов пикселей MIRV2 PUSH DE PUSH HL PUSH BC SRL C ;делим ширину окна на 2 JR NC,MIRV3 ;продолжаем, если разделилось без остатка INC C ;иначе увеличиваем счетчик на 1 MIRV3 LD A,(HL) ;получаем байт с левой стороны CALL MIRV0 ;зеркально отображаем его PUSH BC ;запоминаем его LD A,(DE) ;берем байт с правой стороны CALL MIRV0 ;отображаем POP AF ;восстанавливаем предыдущий байт ; в аккумуляторе LD (HL),B ;записываем «правый» байт ; на левую сторону окна LD (DE),A ; и наоборот INC HL ;приближаемся с двух сторон DEC DE ; к середине окна DEC C JR NZ,MIRV3 ;повторяем, если еще не дошли до середины POP BC POP HL POP DE INC H ;переходим к следующему ряду пикселей INC D DJNZ MIRV2 POP BC POP AF INC A ;переходим к следующей строке экрана DJNZ MIRV1 RET ; Подпрограмма зеркального отображения байта в аккумуляторе MIRV0 RLA RR B ;отображенный байт получится в B RLA RR B RLA RR B RLA RR B RLA RR B RLA RR B RLA RR B RLA RR B RET
В этой процедуре применены уже достаточно сложные средства, о которых стоит поговорить особо. Ядром ее является внутренняя подпрограмма MIRV0, которая переворачивает байт: 7-й бит переходит в 0-й, 6-й — в 1-й и т. д. Исходный байт на входе в нее помещается в аккумулятор, а «перевернутый» получаем на выходе в регистре B. Поясним, как это происходит. После выполнения команды RLA биты аккумулятора сдвигаются влево и вытесняемый бит переходит во флаг переноса, который используется в качестве пересылочного буфера. При выполнении же команды RR B бит из флага переноса попадает в младший бит регистра B и на следующих этапах постепенно перемещается к левому краю, то есть в сторону старшего бита. После выполнения восьми пар команд сдвигов все биты из аккумулятора перейдут в регистр B, но окажутся записанными в обратном, «зеркальном» порядке. Добавим, что вместо команд
RLA RR B
с тем же результатом можно использовать команды
RRA RL B
На первый взгляд кажется, что подпрограмма MIRV0 написана не слишком экономично. Раньше мы призывали вас использовать все доступные методы оптимизации программы, а теперь вдруг размахнулись вместо того, чтобы заключить одни и те же повторяющиеся команды в цикл. Однако, мы должны заметить, что существует оптимизация не только размеров программы; иногда бывает гораздо важнее оптимизировать ее по времени исполнения. И в данном случае важность достижения наивысшего быстродействия процедуры зеркального отображения окна значительно «перевешивает» стремление к сокращению ее объема. Попробуйте переписать подпрограмму MIRV0, использовав один из возможных способов организации циклов, и результат будет заметен даже при отображении сравнительно небольших окон.
Желанием ускорить работу программы обусловлен и выбор команды сдвига аккумулятора. Ведь с тем же успехом можно было написать любую другую инструкцию, например, RL A или SLA A, но если вы загляните в Приложение I, то заметите, что команда RLA выполняется в два раза быстрее других команд сдвига, всего за 4 такта (столько же времени требует и команда RLCA, которую также можно использовать в данной подпрограмме).
Другим интересным моментом подпрограммы является сохранение в стеке с последующим восстановлением отображенного байта внутри цикла MIRV3. Как мы уже сказали, требуемый байт получается в регистре B и применение команды PUSH BC поэтому должно быть понятно. Но затем почему-то использована инструкция POP AF. Это не ошибка, так и должно быть. Дело в том, что после второго вызова подпрограммы MIRV0 регистр B оказывается занят значением другого «перевернутого» байта, пары HL и DE также содержат нужную информацию. Свободными остаются только регистры C и A, но C связан в пару с занятым B, к тому же это младший регистр, а нам нужно восстановить из стека значение старшего. По счастью, аккумулятор в регистровой паре AF как раз занимает «старшее» место, а состояние флагов в данном случае нас не интересует. Именно это и делает возможным применение единственной инструкции POP AF вместо ряда пересылок между регистрами. Как видите, не всегда обязательно восстанавливать из стека ту же регистровую пару, которая была до этого сохранена командой PUSH.
Зеркальное отображение окон не будет полноценным, если «переворачивать» только пиксели, нужно также отображать и атрибуты. Процедура, выполняющая такую операцию, в общем должна быть похожа на только что описанную, она будет даже несколько проще благодаря тому, что сами байты в этом случае отображать уже не требуется. Если вам понятен принцип работы предыдущей подпрограммы, то с этой вы разберетесь без труда, посему приводим ее исходный текст без лишних предисловий и комментариев:
MARV LD BC,(LEN) LD A,(ROW) LD L,A LD H,0 ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL ADD HL,HL LD DE,#5800 ADD HL,DE LD A,(COL) ADD A,L LD L,A LD D,H LD A,C ADD A,L DEC A LD E,A MARV1 PUSH BC PUSH DE PUSH HL SRL C JR NC,MARV2 INC C MARV2 LD A,(HL) EX AF,AF' LD A,(DE) LD (HL),A EX AF,AF' LD (DE),A INC HL DEC DE DEC C JR NZ,MARV2 POP DE POP HL LD BC,32 ADD HL,BC EX DE,HL ADD HL,BC POP BC DJNZ MARV1 RET
Динамическое окно
Продемонстрируем использование подпрограмм CLSV и SETV на примере довольно часто используемого способа вывода окна: оно появляется не внезапно, а как бы выплывает на поверхность экрана или приближается издалека. При этом изменяются не только размеры окна, но и его цвет.
Универсальная программа, создающая таким способом окно любого размера и в произвольном месте экрана, потребует достаточно сложных вычислений с привлечением не только операций сложения и умножения, но и деления. Поэтому, чтобы упростить задачу, напишем программу для конкретного окна. А чтобы вы могли лучше понять, как она работает, сначала сделаем ее в Laser Basic’е и только потом перепишем на ассемблере:
10 INK 5: PAPER 0: BORDER 0: CLS 20 FOR n=7 TO 1 STEP -1 30 LET m=7-n 40 .ROW=5+n:.COL=2+2*n 50 .HGT=2+2*m:.LEN=1+4*m 60 LET attr=8*n+64: POKE 23693,attr 70 .CLSV:.SETV 80 NEXT n
После этого можно обвести окно рамкой, а внутри что-нибудь написать, но это мы уже сделаем в ассемблерной программе.
Поскольку рамки вокруг окон рисуются довольно часто, прежде чем привести текст программы, напишем универсальную процедуру, выводящую в выбранном месте экрана прямоугольник произвольного размера. Этой процедурой мы еще воспользуемся впоследствии, поэтому ее также желательно иметь в отдельном библиотечном файле. Перед обращением к ней в регистрах B и C нужно задать координаты верхнего левого угла прямоугольника соответственно по вертикали и горизонтали, а в H и L — его высоту и ширину. Поскольку позже мы предложим более быстродействующую процедуру, то чтобы не возникло путаницы с именами, назовем ее BOX_0, а в имени другой процедуры изменим индекс.
BOX_0 PUSH HL CALL 8933 ;PLOT - верхний левый угол POP BC PUSH BC LD DE,#101 ;верхняя линия LD B,0 CALL 9402 ;DRAW POP BC PUSH BC LD D,-1 ;правая линия LD C,0 CALL 9402 POP BC PUSH BC LD E,-1 ;нижняя линия LD B,0 CALL 9402 POP BC LD DE,#101 ;левая линия LD C,0 CALL 9402 LD HL,10072 EXX RET
Если в приведенной процедуре вам что-нибудь не совсем ясно, вернитесь к четвертой главе и еще раз просмотрите раздел «Рисование графических примитивов».
Используя подготовленные процедуры, можно написать программу «всплывающего» окна:
ORG 60000 ENT $ CALL SETSCR ;подготовка экрана DYNW LD B,7 DYNW1 LD A,7 ;вычисление промежуточной переменной ; для расчета размеров окна SUB B LD C,A LD A,B ;расчет переменной ROW ADD A,5 LD (ROW),A LD A,B ;расчет переменной COL ADD A,A ADD A,2 LD (COL),A LD A,C ;расчет переменной HGT ADD A,A ADD A,2 LD (HGT),A LD A,C ;расчет переменной LEN ADD A,A ADD A,A INC A LD (LEN),A LD A,B ;расчет байта атрибутов RLCA RLCA RLCA OR %01000000 ;64 - повышенная яркость LD (ATTR),A PUSH BC LD BC,3 CALL 7997 ;небольшая задержка перед выводом окна CALL CLSV ;очистка окна CALL SETV ;установка атрибутов окна POP BC DJNZ DYNW1 LD DE,D_ATTR ;установка атрибутов линий рамки LD BC,6 CALL 8252 LD BC,#7625 ;B = 118, C = 37 LD HL,#5EBE ;H = 94, L = 190 CALL BOX_0 ;рамка вокруг окна LD DE,TEXT LD BC,END-TEXT JP 8252 ;печать текста в окне SETSCR ......... BOX_0 ......... CLSV ......... SETV ......... COL DEFB 0 ROW DEFB 0 LEN DEFB 0 HGT DEFB 0 ATTR DEFB 0 D_ATTR DEFB 16,0,17,1,19,1 TEXT DEFB 22,9,12,16,6,17,1,19,1 DEFM "Program" DEFB 22,11,8,16,4 DEFM "DYNAMIC••WINDOW" DEFB 22,15,8,16,5 DEFM "Saint-Petersburg" DEFB 22,17,14,16,3 DEFM "1994" END
«Размножающиеся» окна
Рассмотрим программу, которая последовательно выводит на экран цепочку разноцветных окон различных размеров и которую при желании можно использовать как основу для создания оригинальной заставки. Самое последнее окно, появляющееся на экране, постепенно закрашивается черным цветом, оставляя только желтый контур, а затем в нем выводится заданный текст в виде «бегущей строки» (рис. 5.5).
Программа состоит из двух основных частей. На первом этапе из блока данных считываются параметры окон и выполняются уже известные процедуры CLSV и SETV. В конце блока данных установлен байт со значением -1 (255), при считывании которого программа переходит ко второму этапу — формированию «бегущей строки». Такое решение позволяет легко изменять не только размеры, цвет и местоположение окон, но и добавлять другие или убирать лишние, что особенно важно при отладке программы. Текст «бегущей строки» дан в формате ASCIIZ, и это также дает возможность как угодно изменять его без коррекций в самой программе.
Несмотря на довольно большой размер программы, в ней не встретится никаких неизвестных команд, поэтому приводим ее текст лишь с краткими комментариями:
ORG 60000 ENT $ CALL SETSCR ; Вывод последовательности окон LD HL,COORD ;адрес блока данных параметров окон PW1 LD A,(HL) ;последовательное считывание параметров CP -1 ;проверка достижения конца блока данных JR Z,PW2 ;если да, переходим ко второму этапу INC HL LD (COL),A LD A,(HL) INC HL LD (ROW),A LD A,(HL) INC HL LD (LEN),A LD A,(HL) INC HL LD (HGT),A LD A,(HL) ;последний параметр - цвет окна INC HL RLCA ;сдвигаем на место атрибута PAPER RLCA RLCA OR 7 ;добавляем цвет INK 7 LD (ATTR),A PUSH HL PUSH BC XOR A OUT (254),A ;получаем «щелчок» LD BC,5 CALL 7997 ;PAUSE 5 LD A,16 OUT (254),A CALL CLSV ;вывод окна CALL SETV POP BC POP HL JR PW1 ;переход к следующему окну PW2 LD A,6 LD (23695),A ;устанавливаем временные атрибуты LD BC,#8780 ;B = 135, C = 128 LD HL,#3848 ;H = 56, L = 72 CALL BOX_0 ;прямоугольник вокруг последнего окна ; «Бегущая строка» в последнем окне LD HL,TEXT ;адрес текста «бегущей строки» PW3 LD A,22 ;AT 8,24 RST 16 LD A,8 RST 16 LD A,24 RST 16 LD A,16 ;INK 0 RST 16 XOR A RST 16 LD A,(HL) ;считываем очередной символ AND A ;дошли до конца? RET Z ;если да, то завершаем программу RST 16 ;выводим считанный символ на экран INC HL LD B,8 ;сдвигаем строку на 8 пикселей влево PW4 PUSH BC PUSH HL CALL SCROL ;скроллинг строки на 1 пиксель влево LD BC,1 CALL 7997 ;PAUSE 1 POP HL POP BC DJNZ PW4 JR PW3 ;переходим к выводу следующего символа SCROL LD HL,18448+8 ;заранее рассчитанный адрес экрана ; конца «бегущей строки» LD C,8 SCROL1 LD B,8 AND A PUSH HL SCROL2 RL (HL) DEC HL DJNZ SCROL2 POP HL INC H DEC C JR NZ,SCROL1 RET SETSCR ......... SETV ......... CLSV ......... BOX_0 ......... COL DEFB 0 ROW DEFB 0 LEN DEFB 0 HGT DEFB 0 ATTR DEFB 0 ; Данные для всех окон. Параметры записаны в таком порядке: ; COL, ROW, LEN, HGT и последнее число - код цвета окна (PAPER) COORD DEFB 27,11,4,4,7 DEFB 26,12,4,4,4 DEFB 24,13,4,4,1 DEFB 23,15,4,4,2 DEFB 21,17,5,3,3 DEFB 19,18,5,3,5 DEFB 17,20,4,2,6 DEFB 14,21,4,2,2 DEFB 12,19,3,3,7 DEFB 10,18,3,3,1 DEFB 9,17,3,3,4 DEFB 7,15,4,4,3 DEFB 5,13,4,4,2 DEFB 4,12,4,4,5 DEFB 3,11,4,4,6 DEFB 1,8,5,5,1 DEFB 2,5,5,5,7 DEFB 3,4,5,5,2 DEFB 4,3,5,5,5 DEFB 6,2,5,5,3 DEFB 8,1,8,5,4 DEFB 11,0,6,5,6 DEFB 13,1,6,6,1 DEFB 15,3,7,6,3 DEFB 16,5,9,7,6 ; «Закрашивание» последнего окна DEFB 20,8,1,1,0 DEFB 19,7,3,3,0 DEFB 18,6,5,5,0 DEFB 17,6,7,5,0 DEFB 16,5,9,7,0 DEFB 255 ;------------------- TEXT DEFM "Sinclair Research Ltd. 1982" DEFM "••••••Program••W•I•N•D•O•W" DEFM "••*•Saint-Petersburg••1994••*" DEFM "•••••••••" DEFB 0
СПРАЙТЫ (ПРОГРАММА «ПРЫГАЮЩИЙ ЧЕЛОВЕЧЕК»)
Прояснив в какой-то степени вопрос о перемещении по экрану символов неплохо теперь заняться спрайтами и решить некоторые общие проблемы. Среди них: создание специальных блоков данных, удобных для кодирования и последующего вывода на экран спрайтов, разработка эффективной подпрограммы вывода этих спрайтов и, наконец, формирование самого подвижного изображения.
Рассмотрим один из вариантов решения этих вопросов (так как позже будут изложены и другие). В начале коротко о том, что мы увидим на экране. После запуска программы появится простенький пейзаж, состоящий из зеленой травы и ветки дерева, на которой висит спелое яблоко. От земли отталкивается человечек и в прыжке пытается достать это яблоко. Для получения более или менее правдоподобного эффекта движения оказалось достаточным выводить человечка всего в двух фазах (рис. 5.6 а, б). Разберем теперь программу, которая все это делает.
а | б |
Рис. 5.6. Фазы движения человечка |
Прежде всего закодируем отдельные элементы двух изображений человечка, воспользовавшись, как и в предыдущей главе, определяемыми пользователем символами UDG. В результате кодирования получится блок данных, первому байту которого присвоим метку UDG:
UDG DEFB 0,0,3,4,102,68,35,62 ;A (144) DEFB 0,0,192,32,166,34,204,56 ;B (145) DEFB 31,15,7,7,3,7,6,6 ;C (146) DEFB 112,96,192,96,192,224,224,224 ;D (147) DEFB 6,6,6,4,8,0,0,0 ;E (148) DEFB 96,96,96,32,16,0,0,0 ;F (149) DEFB 3,4,6,4,195,236,127,55 ;G (150) DEFB 192,32,160,32,195,55,126,108 ;H (151) DEFB 7,7,7,14,28,12,6,14 ;I (152) DEFB 224,96,224,112,56,48,96,112 ;J (153) DEFB 8,16,108,254,190,158,78,60 ;Яблоко (154) DEFB 17,85,255,0,0,0,0,0 ;Трава (155)
Справа от каждой строки указаны коды, которые понадобятся нам для составления трех других блоков данных. Первый из них с меткой PEJZ мы используем для создания примитивного пейзажа, состоящего, как мы уже говорили, из зеленой травы и яблока (маленькую ветку проще получить программным путем).
PEJZ DEFB 22,13,13,16,4 DEFB 155,155,155,155,155,155 DEFB 22,8,15,16,2,17,0 DEFB 154
Можно заметить, что кроме кодов UDG в блок включены также и управляющие символы, устанавливающие позицию печати и изменяющие цвета спрайтов.
Второй и третий блоки — это как раз те два спрайта, которые должны попеременно появляться на вашем экране, создавая иллюзию движения:
SPR1 DEFB 6 DEFB 0,0,7,144,0,1,7,145 DEFB 1,0,7,146,1,1,7,147 DEFB 2,0,7,148,2,1,7,149 SPR2 DEFB 6 DEFB 0,0,0,32,0,1,0,32 DEFB 1,0,7,150,1,1,7,151 DEFB 2,0,7,152,2,1,7,153
Для вывода этих спрайтов на экран можно было бы, как и раньше, воспользоваться подпрограммой 8252, однако основная идея применения спрайтов заключается в том, чтобы их можно было легко перемещать по экрану. Но поскольку спрайт обычно состоит из нескольких знакомест, которые имеют строго фиксированное положение внутри него, то при использовании процедуры 8252 пришлось бы перекодировать координаты всех составляющих его фрагментов. Сами понимаете, что это не слишком удобно, особенно если спрайт состоит из десятка-другого знакомест. Это означает, что для вывода спрайтов на экран необходимо создать самостоятельную процедуру, которую мы назовем PUT.
Теперь следует сказать несколько слов о построении блоков данных SPR1 и SPR2, поскольку они тесно связаны с подпрограммой PUT и их формат, естественно, должен быть согласован с ее работой. Опишем их структуру. Первый байт соответствует количеству фрагментов, входящих в спрайт (в нашем примере как для первого, так и для второго спрайта — это число 6). Далее следуют параметры для каждого фрагмента, занимающие по 4 байта:
1-й байт - относительная вертикальная координата данного знакоместа в спрайте; 2-й байт - относительная горизонтальная координата данного знакоместа в спрайте; 3-й байт - байт суммарных атрибутов знакоместа; 4-й байт - код символа соответствующего фрагмента спрайта.
Поскольку подпрограмма PUT нам потребуется не только в данном примере, сохраните ее в виде отдельного библиотечного файла. Она имеет следующий вид:
PUT LD E,(HL) ;считываем количество фрагментов спрайта PUT1 INC HL LD A,22 RST 16 LD A,B ;регистр B содержит координату ROW ADD A,(HL) ;прибавляем к ней относительную ; координату внутри спрайта RST 16 INC HL LD A,C ;в C - горизонтальная координата COL ADD A,(HL) ;складываем ее с относительной ; координатой, взятой из блока данных RST 16 INC HL LD A,(HL) ;считываем байт атрибутов знакоместа LD (23695),A INC HL LD A,(HL) ;берем код выводимого символа RST 16 DEC E JR NZ,PUT1 ;переходим к выводу следующего ; фрагмента RET
Перед вызовом этой процедуры необходимо в регистровой паре HL указать начальный адрес блока данных, соответствующего выводимому спрайту, а в регистрах B и C задать координаты экрана по вертикали и горизонтали.
Наконец соберем все части программы в единое целое, добавив несколько уже знакомых процедур, и напишем небольшую часть, управляющую движением человечка:
ORG 60000 ENT $ CALL SETSCR LD HL,UDG LD (23675),HL CALL FON CYCLE LD HL,SPR1 ;вывод спрайта 1 LD BC,#A0F ;B = 10, C = 15 CALL PUT CALL PAUSE ;небольшая задержка LD HL,SPR2 ;вывод спрайта 2 LD BC,#A0F CALL PUT CALL PAUSE ;снова задержка ; Выход из цикла при нажатии клавиши Space LD A,(23560) ;системная переменная LAST_K, в которой ; хранится код последней нажатой клавиши CP " " JR NZ,CYCLE ;повтор RET ; Задержка (PAUSE 12) PAUSE LD BC,12 JP 7997 ; Рисование пейзажа FON LD DE,PEJZ LD BC,19 CALL 8252 EXX PUSH HL LD A,6 LD (23695),A LD BC,#7078 ;B = 112, C = 120 CALL 8933 LD DE,#101 ;D = 1, E = 1 LD BC,10 CALL 9402 LD BC,#757D ;B = 117, C = 125 CALL 8933 LD E,#FF01 ;D = -1, E = 1 LD BC,#F0F ;B = 15, C = 15 CALL 9402 POP HL EXX RET SETSCR ......... PUT ......... PEJZ ......... SPR1 ......... SPR2 ......... UDG .........
Эффект движения здесь создается с помощью простого цикла, начинающегося с метки CYCLE, в теле которого по очереди выводятся то первый спрайт, изображающий стоящего человечка, то второй, показывающий человечка в прыжке. Скорость смены фаз движения регулируется подпрограммой PAUSE.
МУЛЬТИПЛИКАЦИОННАЯ ЗАСТАВКА
Разговор о подвижных изображениях хотелось бы завершить чем-то законченным, что можно прямо или с вашими дополнениями использовать в игровых программах. Но при этом текст будущей ассемблерной программы не должен быть уж слишком длинным. Учитывая все это, предлагаем один из вариантов динамической заставки, в которой создается настоящая мультипликационная картинка (рис. 5.7). По двум лестницам вверх и вниз передвигаются два маленьких человечка, поворачиваясь к вам то лицом, то спиной. Вся заставка окружена рамкой из шариков, переливающихся всеми цветами радуги. Когда смотришь на эту подвижную картинку, то не верится, что создана она довольно простыми средствами, уже рассмотренными нами в предыдущих разделах. Разберем теперь текст программы и подробно прокомментируем некоторые ее части, представляющие особый интерес.
Начнем, как и раньше, с составления блоков данных. Самое простое — это формирование блока, содержащего тексты, без которых не обходится ни одна заставка, будь она статическая или динамическая:
TEXT DEFB 22,2,9,16,6 DEFM "Welcome to the" DEFB 22,5,7,16,7 DEFM "L•I•T•T•L•E••M•A•N" DEFB 22,8,9,16,4 DEFM "0. START GAME" DEFB 22,10,9 DEFM "1. KEYBOARD" DEFB 22,12,9 DEFM "2. KEMPSTON" DEFB 22,14,9 DEFM "3. INSTRUCTIONS" DEFB 22,16,9 DEFM "4. DEFINE KEYS" DEFB 22,19,8,16,3 DEFM "Press key 0 to 4"
Поскольку с выводом текстов мы уже неоднократно сталкивались и подробно их комментировали, то не будем лишний раз заострять на этом внимание и сразу перейдем к следующему блоку. Для кодирования отдельных элементов подвижных изображений вновь воспользуемся символами UDG, определяющими отдельные фрагменты будущих спрайтов.
UDG DEFB 3,3,15,3,6,7,2,1 ; A (144) DEFB 224,224,248,224,176,240,32,19 ; B (145) DEFB 13,29,53,37,7,6,14,0 ; C (146) DEFB 208,216,200,208,240,112,48,56 ; D (147) DEFB 7,7,31,7,13,15,4,3 ; E (148) DEFB 192,192,240,192,96,224,64,128 ; F (149) DEFB 11,27,19,11,15,14,12,28 ; G (150) DEFB 176,184,172,164,224,96,112,0 ; H (151) DEFB 3,3,15,0,4,4,0,1 ; I (152) DEFB 224,224,248,0,16,16,0,192 ; J (153) DEFB 6,27,27,27,7,6,14,0 ; K (154) DEFB 176,120,248,240,240,112,48,56 ; L (155) DEFB 7,7,31,0,8,8,0,3 ; M (156) DEFB 192,192,240,0,32,32,0,128 ; N (157) DEFB 13,30,31,15,15,14,12,28 ; O (158) DEFB 96,216,216,216,224,96,112,0 ; P (159) DEFB 0,28,38,79,95,127,62,28 ; Q (160) DEFB 16,16,16,127,16,16,16,16 ; R (161) DEFB 8,8,8,254,8,8,8,8 ; S (162)
Следующий блок данных — это то, чем будет манипулировать программа, создавая на экране непрерывное движение маленьких симпатичных человечков:
SPR1 DEFB 4,0,0,7,144,0,1,7,145,1,0,4,146,1,1,4,147 SPR2 DEFB 4,0,0,7,148,0,1,7,149,1,0,4,150,1,1,4,151 SPR3 DEFB 4,0,0,7,152,0,1,7,153,1,0,4,154,1,1,4,155 SPR4 DEFB 4,0,0,7,156,0,1,7,157,1,0,4,158,1,1,4,159 SPR5 DEFB 2,0,0,6,161,0,1,6,162
Каждая из строк SPR1…SPR4 соответствует одной из фаз движения человечка, а SPR5 — фрагменту лестницы. Последний используется не только для рисования лестниц в статической картинке, но и для восстановления изображения позади бегущего человечка. Формат данных в этих блоках вам уже знаком. Он выбран точно таким же, как и в программе ПРЫГАЮЩИЙ ЧЕЛОВЕЧЕК, следовательно, для вывода всех спрайтов на экран можно воспользоваться подпрограммой PUT, которую мы уже описали. Кроме блоков данных нам понадобятся еще несколько переменных, необходимых для управления перемещением человечков по экрану:
ROW DEFB 0 ;позиция по вертикали левого человечка DIRECT DEFB 0 ;направление движения POSIT DEFB 0 ;фаза движения
Позже мы объясним смысл этих переменных более подробно.
После того, как мы разобрались с надписями и спрайтами, можно переходить к основной программе, которая, как и в большинстве игр, содержит две части: статическую и динамическую. Первая устанавливает атрибуты экрана и формирует на нем все неподвижные элементы заставки: текст, лестницы и рамки вокруг них (рис. 5.7). Во многих играх вместо загрузки дополнительных фонтов новый набор символов формируется программой. Например, можно предложить следующий способ получения шрифта Bold, буквы в котором имеют утолщенное начертание:
BOLD LD HL,15616 ;адрес стандартного набора символов LD DE,END ;метка конца программы; по этому ; адресу расположится новый фонт LD BC,#300 ;счетчики циклов: B = 3, C = 0 (256) PUSH DE BOLD1 LD A,(HL) ;байт из стандартного набора SRL A ; смещаем на 1 бит (пиксель) влево OR (HL) ; и объединяем с его прежним значением LD (DE),A ;помещаем в новый фонт INC HL ;переходим к следующему байту INC DE DEC C JR NZ,BOLD1 ;в общей сложности повторяем цикл DJNZ BOLD1 ; 768 раз POP HL DEC H ;уменьшаем адрес нового шрифта на 256 LD (23606),HL ; и заносим в системную ; переменную CHARS RET
После получения нового шрифта можно сформировать на экране все неподвижные части заставки:
SCREEN LD B,3 ; Рисование окон WIND LD C,3 CALL STAIRS LD C,27 CALL STAIRS INC B LD A,B CP 19 JR C,WIND ; Рисование рамок вокруг лестниц LD DE,ATRBOX LD BC,4 CALL 8252 LD BC,#1414 ;B = 20, C = 20 CALL 8933 CALL BOX LD BC,#14D4 ;B = 20, C = 212 CALL 8933 CALL BOX ; Печать текста LD DE,TEXT LD BC,UDG-TEXT JP 8252 ; Данные атрибутов рамок ATRBOX DEFB 16,7,17,1 ; Рисование фрагмента лестницы STAIRS LD A,17 RST 16 XOR A RST 16 LD HL,SPR5 JP PUT ; Рисование рамок BOX EXX PUSH HL LD BC,#8700 ;B = 135, C = 0 LD DE,#101 ;D = 1, E = 1 CALL 9402 LD BC,23 ;B = 0, C = 23 CALL 9402 LD BC,#8700 LD D,-1 CALL 9402 LD BC,23 LD E,-1 CALL 9402 POP HL EXX RET
Подпрограмма SCREEN не содержит ничего такого, что бы нами еще не рассматривалось, поэтому отдельно мы ее комментировать не будем, а ограничимся уже приведенными пояснениями и перейдем к динамической части заставки.
В первую очередь напишем подпрограмму, печатающую в заданной позиции экрана элемент, из которого составлена рамка вокруг всего изображения:
; Вывод на экран шарика цвета E в позицию BC PRINT LD A,22 RST 16 LD A,B DEC A RST 16 LD A,C DEC A RST 16 LD A,16 RST 16 LD A,E RST 16 LD A,160 RST 16 RET
Значительно сложнее заставить двигаться человечков, да чтобы они при этом еще переступали с ноги на ногу и поворачивались к вам то лицом, то спиной. Для этих целей введены переменные:
ROW — определяет вертикальную позицию левого человечка. Вертикальная координата правого легко вычисляется, так как они движутся в противофазе: когда один идет вверх, другой движется вниз и наоборот. Горизонтальные же координаты обоих человечков во время движения не изменяются.
DIRECT — задает направление движения также для левого человечка. Если она имеет значение 1, левый человечек идет вниз, а правый — вверх. При изменении направлений движения она меняет знак и становится равной минус 1 (255).
POSIT — сообщает программе, в какой из двух фаз выводить изображения человечков. При ее значении, равном 0, выводятся спрайты SPR2 и SPR4, а если она равна 1 — SPR1 и SPR3. Иными словами, эта переменная отвечает за то, на какую ногу в данный момент должен наступить каждый человечек.
Стоит сказать несколько слов о том, какими программными средствами изменяются эти переменные. Наиболее просто получается переключение переменной POSIT. Для этого достаточно использовать команду XOR 1, инвертирующую значение младшего бита. Чтобы изменить знак в переменной DIRECT, можно поступить следующим образом: инвертировать все ее биты, например, командой CPL, а затем увеличить ее на 1. Можно именно так и поступить, но лучше воспользоваться специальной командой, как раз предназначенной для изменения знака в аккумуляторе. Эта команда записывается мнемоникой NEG (Negative — отрицательный). Что касается ROW, то она изменяется простым сложением с переменной DIRECT.
Теперь приводим тексты соответствующих подпрограмм:
; Перемещение человечков MAN LD A,(ROW) LD B,A LD A,(DIRECT) LD D,A CP 1 ;проверка направления движения JR NZ,MAN1 CALL PRMAN1 LD E,17 ;нижняя граница координаты ROW JR MAN2 MAN1 CALL PRMAN2 LD E,3 ;верхняя граница координаты ROW MAN2 LD HL,POSIT ;изменение переменной POSIT LD A,(HL) XOR 1 LD (HL),A LD HL,ROW LD A,(HL) CP E ;проверка необходимости смены LD A,D ; направления движения JR NZ,MAN3 NEG ;изменение знака в аккумуляторе LD (DIRECT),A ;изменение переменной DIRECT MAN3 ADD A,(HL) ;изменение переменной ROW ; (адресуемой парой HL) LD (HL),A RET ; Левый человечек идет вниз, правый - вверх PRMAN1 DEC B ;координата ROW LD C,3 ;горизонтальная координата левого ; человечка LD HL,SPR5 ;восстановление фона CALL PUT INC B LD A,(POSIT) AND A LD HL,SPR1 JR NZ,PRM11 LD HL,SPR2 PRM11 CALL PUT LD A,22 ;вычисление вертикальной координаты SUB B ; правого человечка LD B,A LD C,27 ;горизонтальная координата правого ; человечка LD HL,SPR5 ;восстановление фона CALL PUT DEC B DEC B LD A,(POSIT) AND A LD HL,SPR4 JR Z,PRM12 LD HL,SPR3 PRM12 JP PUT ; Левый человечек идет вверх, правый - вниз PRMAN2 INC B INC B LD C,3 LD HL,SPR5 CALL PUT DEC B DEC B LD A,(POSIT) AND A LD HL,SPR3 JR NZ,PRM21 LD HL,SPR4 PRM21 CALL PUT LD A,19 SUB B LD B,A LD C,27 LD HL,SPR5 CALL PUT INC B LD A,(POSIT) AND A LD HL,SPR2 JR Z,PRM22 LD HL,SPR1 PRM22 JP PUT
Теперь объединим рассмотренные подпрограммы и напишем управляющую часть:
ORG 60000 ENT $ LD HL,UDG LD (23675),HL CALL BOLD LD A,15 LD (23693),A XOR A CALL 8859 CALL 3435 LD A,2 CALL 5633 ; Инициализация переменных LD A,4 LD (ROW),A LD A,1 LD (DIRECT),A LD (POSIT),A CALL SCREEN LD A,17 RST 16 XOR A RST 16 ; Начало динамической части заставки MAIN LD E,7 ;код цвета шариков, окаймляющих ; рабочий экран ; Вывод рамки, состоящей из разноцветных шариков MAIN1 LD B,22 ;в регистрах BC - координаты экрана MAIN2 LD C,1 CALL PRINT LD C,32 CALL PRINT DJNZ MAIN2 LD C,31 MAIN3 LD B,1 CALL PRINT LD B,22 CALL PRINT DEC C JR NZ,MAIN3 PUSH BC PUSH DE CALL MAN ;вывод очередной фазы движения ; человечков POP DE POP BC PUSH BC LD B,9 ;счетчик цикла задержки перемещения ; человечков MAIN4 PUSH BC LD BC,1 CALL 7997 POP BC ; Проверка нажатия клавиш от 0 до 4. Если нажаты - выход на EXIT LD A,(23560) CP "4"+1 JR NC,MAIN5 ;проверка >"4" (>="4"+1) CP "0" JR NC,EXIT MAIN5 DJNZ MAIN4 POP BC DEC E JR NZ,MAIN1 JR MAIN EXIT LD (KEY),A ;сохраняем код клавиши для других ; частей многокадровой заставки, ; принцип построения которой будет ; рассмотрен в следующей главе POP BC RET ; Подпрограммы BOLD ......... SCREEN ......... MAN ......... PRMAN1 ......... PRMAN2 ......... PRINT ......... PUT ......... ; Блоки данных TEXT ......... UDG ......... SPR1...SPR5 ; Переменные ROW DEFB 0 DIRECT DEFB 0 POSIT DEFB 0 KEY DEFB 0 ; Адрес размещения нового фонта (обязательно в самом конце, ; чтобы коды набора не перекрыли коды программы) END