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

         

Окна в игре MICRONAUT ONE



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

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


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