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

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

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

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

ГЛАВА ПЯТАЯ,
в которой изображения становятся подвижными

Как вы понимаете, никакая игра, даже с очень изысканной графикой, не станет достаточно интересна, если все изображения на экране будут неподвижны и персонажи не будут подавать признаков жизни. Для «оживления» картинок в программировании используется тот же принцип, что и в мультипликации: в одно и то же место экрана последовательно помещаются слегка отличающиеся друг от друга изображения, показывающие разные фазы движения одного объекта.

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

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

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

Таблица 5.1. Результаты операции сравнения
Результат сравненияСостояние флаговМнемоника условияперехода
A = XZ = 1Z
A <> XZ = 0NZ
Беззнаковое сравнение (числа от 0 до 255)
A < XCY = 0C
A >= XCY = 0NC
Сравнение с учетом знака (числа от −128 до +127)
A < XS = 1P
A >= XS = 0M

Используя операцию сравнения, можно написать цикл, в котором регистр 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:

  1. RLCA — циклический сдвиг влево. После выполнения этой команды старший бит переходит в младший и дублируется во флаге переноса CY (то есть он будет установлен, если 7-й бит перед выполнением команды был в 1 и сброшен, если 7-й бит имел нулевое значение).
  2. RRCA — циклический сдвиг аккумулятора вправо. Эта команда в точности противоположна предыдущей: младший бит переходит в старший и повторяется во флаге CY.
  3. RLA — это тоже циклический сдвиг, но не совсем обычный. В этой команде флаг переноса рассматривается как еще один дополнительный бит аккумулятора: 7-й бит перемещается в CY, а предыдущее значение флага переноса переносится в младший бит.
  4. RRA — эта команда аналогична предыдущей с тем отличием, что движение битов происходит в обратном направлении, то есть слева направо.

На рис. 5.2 показаны схемы направлений перемещения битов во всех четырех перечисленных командах. После их выполнения все основные флаги (кроме CY, конечно) остаются без изменений, а во флагах N и H появляются нули.

Рис. 5.2. Выполнение команд сдвига аккумулятора

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

Команда RLC S выполняет действие, аналогичное RLCA. В качестве операнда S могут использоваться регистры A, B, C, D, E, H или L, а также адрес (HL), если записать инструкцию RLC (HL). Во всех остальных командах можно применять тот же набор операндов (немного опережая события, добавим, что во всех этих командах могут участвовать также и ячейки памяти, адресованные индексными регистрами, но поскольку мы их еще не рассматривали, применять такие инструкции пока не будем).

Инструкция RRC S выполняется аналогично команде RRCA, но помимо аккумулятора применима к любым перечисленным операндам.

Команды RR S и RL S подобны описанным командам RRA и RLA соответственно.

Весьма широко применяются команды SLA S (сдвиг влево) и SRL S (сдвиг вправо). Они отличаются от прочих команд сдвигов тем, что освобождающийся бит при любых условиях заполняется нулевым значением, в результате чего их вполне можно рассматривать как операции умножения или деления на 2 соответственно. «Вытесняемый» бит при этом, как и при других сдвигах переходит во флаг переноса.

Еще одна операция «деления на 2 со знаком» выполняется командой SRA S, которая смещает вправо только 7 младших битов, а старший оставляет без изменения. Младший бит, как и положено, вытесняется во флаг CY. Все команды этой группы воздействуют уже не только на флаг переноса, но и на все прочие. Флаги H и N, так же, как и при сдвигах аккумулятора, сбрасываются в 0.

На рис. 5.3 приведены схемы всех перечисленных сдвигов. Рассмотрите этот рисунок внимательно, и вам, наверное, уже станет понятно в общих чертах, как можно получить эффект плавно движущейся строки. Теперь остается, пожалуй, только одна сложность: научиться быстро и безошибочно определять адреса в экранной области, соответствующие началу любой строки. Конечно, вы можете воспользоваться рис.&nbsp2.3 из второй главы, но тогда не сможете написать универсальной подпрограммы «на все случаи жизни».

Рис. 5.3. Выполнение команд сдвига регистров

Алгоритм расчета адресов экрана достаточно сложен, поэтому, как и прежде, обратимся за помощью к ПЗУ, благо в «прошивке» Speccy имеются необходимые процедуры. Для получения начального адреса любой строки экрана можно обратиться к подпрограмме, расположенной по адресу 3742. Перед обращением к ней в аккумулятор необходимо поместить номер строки экрана. На выходе в регистровой паре HL получится искомый адрес, зная который, уже несложно рассчитать и любой другой адрес в пределах данной строки. Каждая строка имеет длину 32 байта, которые расположены последовательно, и вмещает 8 рядов пикселей. При переходе к следующему ряду адрес видеобуфера увеличивается на 256, то есть увеличивается только старший байт адреса, а младший остается без изменений.

Для примера покажем, как рассчитать адрес второго байта сверху в 5-й строке и 11-й позиции экрана (иначе, в позиции печати, определяемой директивой Бейсика AT 5,11):

       LD    A,5         ;номер строки
       CALL  3742        ;получаем в HL начальный адрес
       LD    A,L         ;берем значение младшего байта адреса
       OR    11          ;добавляем смещение в 11 байт (знакомест)
       LD    L,A         ;возвращаем в младший байт
       INC   H           ;увеличиваем адрес на 256 и тем самым
                         ; получаем адрес второго байта в
                         ; знакоместе сверху

Теперь можно написать программу, дающую эффект бегущей строки. Для определенности будем скроллировать 21-ю строку экрана:

       ORG   60000
       LD    A,21        ;21-я строка экрана
SCRLIN CALL  3742        ;получаем ее адрес в HL
; Так как строка должна бежать слева направо, то раньше нужно сдвигать
;  последние байты, поэтому определяем адрес конца строки
       LD    A,L
       OR    31
       LD    L,A
       LD    C,8         ;высота строки 8 пикселей
SCRL1  LD    B,32        ;длина строки 32 байта
       AND   A           ;очистка флага CY
       PUSH  HL          ;сохраняем адрес
SCRL2  RL    (HL)        ;последовательно сдвигаем все байты
       DEC   HL
       DJNZ  SCRL2
       POP   HL          ;восстанавливаем адрес
       INC   H           ;переходим к следующему ряду пикселей
       DEC   C           ;повторяем
       JR    NZ,SCRL1
       RET

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

Возможно, вам не совсем ясно, что в данной подпрограмме делает инструкция AND A. Как сказано в комментарии к этой строке, она очищает флаг переноса. Собственно, это и все, что нам от нее требуется, но для чего это нужно? Посмотрите на схему перемещения битов командой RL S и увидите, что при ее выполнении бит из CY переходит в младший бит операнда, в то время как старший сохраняется во флаге переноса. Этим и обусловлен выбор именно команды RL (HL), ведь нам нужно скроллировать не отдельный байт, а целую цепочку байтов, значит, вытесняемый бит должен быть сохранен для следующей команды сдвига. Но сдвигая самый первый байт в цепочке, мы должны убедиться, что в младшем бите появится 0, поэтому и нужно сбросить флаг CY. Если этого не сделать, то в конце концов в скроллируемой строке может появиться какой-то нежелательный «мусор» в виде «включенных» пикселей.

Надеемся, что этих объяснений достаточно, а если нет, то попытайтесь мысленно проследить, что происходит в результате выполнения команды RL (HL) на каждом «витке» цикла, обозначенного меткой SCRL2, какие биты и куда при этом сдвигаются.

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

10 INK 6: PAPER 0: BORDER 0: CLS
20 LET a$="Examine yourself how you know the assembler!"
30 FOR i=1 TO LEN a$
40 PRINT AT 21,31; INK 0; a$(i)
50 FOR j=1 TO 8
60 RANDOMIZE USR 60000
70 NEXT j
80 NEXT i
90 FOR i=1 TO 256: RANDOMIZE USR 60000: NEXT i

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

       ORG   60000
       LD    B,8
       LD    HL,16384
ROL    RRC   (HL)
       INC    H
       DJNZ   ROL
       RET

Чтобы посмотреть, как эта программка работает, напечатайте в Бейсике в левом верхнем углу экрана какой-нибудь символ и в цикле вызывайте процедуру. Изображение должно многократно «провернуться» вокруг вертикальной оси, причем уходящие вправо точки будут вновь появляться с левого края. Если заменить команду RRC (HL) на RLC (HL), то скроллинг будет выполняться в обратную сторону.

«Волна»

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

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

Подпрограмма вертикального скроллинга знакоместа может выглядеть так:

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). Кроме того, окно может быть как бы приподнятым над общим фоном экрана и отбрасывать на него «тень».

Рис. 5.4. Окна в игре MICRONAUT ONE

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

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

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