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

       

СОЗДАНИЕ ЗВУКОВЫХ ЭФФЕКТОВ



СОЗДАНИЕ ЗВУКОВЫХ ЭФФЕКТОВ

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

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

TWEET LD A,(23624) ;определение цвета бордюра AND #38 RRA RRA RRA DI TWEET1 XOR 16 ;переключение 4-го бита OUT (254),A PUSH BC DJNZ $ ;цикл задержки POP BC DJNZ TWEET1 EI RET

Длительность эффекта перед обращением к процедуре TWEET задается в регистре B, например:

LD B,200 CALL TWEET RET

Прежде чем привести следующий пример, скажем несколько слов, относящихся не только к этой подпрограмме, но и ко всем остальным. Поскольку в реальных программах цвет бордюра обычно не изменяется и определен заранее, то он, как правило, не вычисляется в программе, а задается в явном виде загрузкой в аккумулятор кода нужного цвета. Вы также можете вместо первых строк от метки TWEET до команды DI просто написать XOR A для получения черного бордюра или, например, LD A,4 - для зеленого.

Другой интересный момент касается уже не самой программы, а собственно ассемблера. Вы, наверное, обратили внимание на запись

DJNZ $

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

LOOP DJNZ LOOP

но позволяет обойтись без дополнительных меток.

После такого небольшого лирического отступления давайте продолжим «изобретение» звуковых эффектов.



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


VIBR LD A,(23624) AND # 38 RRA RRA RRA LD C,A DI VIBR1 LD D,E ;продолжительность цикла спада (подъема) VIBR2 LD A,C XOR 16 LD C,A OUT (254),A LD A,H ;изменение частоты звука ADD A,L LD H,A VIBR3 DEC A ;цикл задержки JR NZ,VIBR3 DEC D JR NZ,VIBR2 LD A,L ;смена направления изменения частоты NEG LD L,A DJNZ VIBR1 EI RET

В регистр H нужно занести начальную частоту звука (имеется в виду, конечно, частота не в герцах, а в относительных единицах). Содержимое регистра E влияет на частоту вибрации: чем меньше его значение, тем быстрее спад будет сменяться подъемом и наоборот. В регистре B задается количество циклов вибрации, то есть в конечном счете - длительность звука, а в L заносится величина, определяющая глубину вибрации, или иначе, скорость изменения высоты звука. Мы предлагаем такие значения регистров:

LD H,100 LD E,120 LD B,4 LD L,1 CALL VIBR RET

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

Приведем еще одну подпрограмму, создающую другой тип вибрации, при котором частота звука, достигнув наивысшей (или же низшей) точки, возвращается к начальной своей величине. Тем самым частотная характеристика имеет внешний вид, схожий с зубьями пилы. Подпрограмма, создающая похожий звук, имеется в известном пакете Suprcode и значится там под именем «Laser». Вот как примерно она может выглядеть:

LASER LD A,(23624) AND #38 RRA RRA RRA DI LASER1 PUSH BC LD L,H LASER2 XOR 16 OUT (254),A LD B,H DJNZ $ INC H ;другой вариант - DEC H DEC C JR NZ,LASER2 LD H,L POP BC DJNZ LASER1 EI RET

Прежде чем обратиться к данной процедуре, необходимо в регистр B загрузить количество «зубчиков пилы», в C - продолжительность каждого «зубца», а в регистре H задать исходную высоту звука. Например:

LD B,5 LD C,200 LD H,50 CALL LASER RET

Если вы работали с музыкальным редактором Wham, то, вероятно, задавались вопросом, как в одном звуковом канале удается получить сразу два тона различной высоты. Во многих играх, особенно последних лет, музыкальное сопровождение выполнено в аранжировке той или иной степени сложности. Иногда можно слышать не два, а три и более голосов (например, в DEFLEKTOR или MIG-29). Конечно, написание музыки на два голоса - дело вовсе непростое, но принцип получения подобных звуков знать все же стоит. Тем более, что таким способом можно создать ряд весьма недурных эффектов. Двуголосие достигается наложением двух различных частот, поэтому программа, генерирующая одновременно два тона, может выглядеть примерно так:



TWOTON LD A,(23624) AND # 38 RRA RRA RRA LD H,D LD L,E DI TWOTN1 DEC H ;задержка для получения первого тона JR NZ,TWOTN2 XOR 16 OUT (254),A ;извлечение первого звука LD H,D ;восстановление значения задержки ; для первого тона TWOTN2 DEC L ;задержка для получения второго тона JR NZ,TWOTN1 XOR 16 OUT (254),A ;извлечение второго звука LD L,E ;восстановление значения задержки ; для второго голоса PUSH AF LD A,B ;проверка окончания звучания OR C JR Z,TWOTN3 POP AF DEC BC JR TWOTN1 TWOTN3 POP AF EI RET

Перед обращением к процедуре в регистровой паре BC нужно указать длительность звучания, а в регистрах D и E - высоту звука соответственно в первом и втором голосах. Если в регистрах D и E задать близкие значения, то вместо двух различных тонов получится звук приятного тембра, слегка вибрирующий и как бы объемный. Послушайте, например, такое звучание:

LD BC,500 ;длительность звучания LD D,251 ;высота первого тона LD E,250 ;высота второго тона CALL TWOTON RET

Не меньшим спросом в игровых программах пользуются и различные шумовые эффекты, имитирующие выстрелы, разрывы снарядов, стук копыт и т. п. Такие звуки характеризуются отсутствием какой-то определенной частоты - в них присутствуют частоты всего спектра. Это так называемый «белый» шум. Но обычно в шуме все же преобладают тона определенной высоты, что позволяет отличить шипение змеи от грохота обвала. И это необходимо учитывать при создании нужного эффекта.

Первый звук этого типа, который мы хотим предложить, при подборе соответствующей длительности позволяет имитировать звуки от хлопков в ладоши до шипения паровоза, выпускающего пар. Продолжительность звучания задается в регистровой паре DE, однако ее значение не должно превышать 16384, так как в качестве генератора «случайных» чисел, определяющих частоту тона используются коды ПЗУ:

HISS LD A,(23624) AND #38 RRA RRA RRA LD B,A LD HL,0 ;начальный адрес ПЗУ DI HISS1 LD A,(HL) ;берем байт в аккумулятор AND 16 ;выделяем 4-й бит OR B ;объединяем с цветом бордюра OUT (254),A ;получаем звук INC HL ;переходим к следующему байту DEC DE ;уменьшаем значение длительности LD A,D OR E JR NZ,HISS1 ;переходим на начало, ; если звук не закончился EI RET



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

CRASH LD A,(23624) AND #38 RRA RRA RRA DI LD HL,100 ;начальный адрес в ПЗУ CRASH1 XOR 16 OUT (254),A ;извлекаем звук LD C,A ;сохраняем значение аккумулятора LD E,(HL) ;получаем в паре DE INC HL ; продолжительность цикла задержки LD A,(HL) AND 3 ;ограничиваем величину старшего байта LD D,A CRASH2 LD A,D ;цикл задержки OR E JR Z,CRASH3 DEC DE JR CRASH2 CRASH3 LD A,C ;восстанавливаем значение аккумулятора DJNZ CRASH1 EI RET

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

EXPLOS LD A,(23624) AND #38 RRA RRA RRA LD L,A DI EXPL1 PUSH BC PUSH DE EXPL2 PUSH DE EXPL3 LD B,E DJNZ $ ;задержка LD A,(BC) ;в паре BC один из первых 256 адресов ПЗУ AND 16 OR L OUT (254),A INC C DEC D JR NZ,EXPL3 POP DE ; Изменение высоты шума (понижение среднего тона; ; если заменить на DEC E, тон будет наоборот повышаться) INC E DEC D JR NZ,EXPL2 POP DE POP BC DJNZ EXPL1 ;повторение всего эффекта EI RET

Перед обращением к ней в регистр B заносится количество повторений эффекта (что позволяет получить звук, напоминающий описанный выше эффект «Laser»), в D задается длительность звучания и в E - величина, определяющая начальную среднюю частоту. Например, для создания звука, напоминающего взрыв бомбы, можно предложить такие значения регистров:

LD B,1 LD D,100 LD E,-1 CALL EXPLOS RET

а пулеметную очередь можно получить с другими исходными данными:

LD B,5 LD D,35 LD E,0 CALL EXPLOS RET

У всех перечисленных процедур есть один общий недостаток: на время звучания выполнение основной программы затормаживается. Существует несколько способов обхода этой неприятности. Наиболее удачным из них представляется использование прерываний. Конечно, в этом случае получить чистый тон окажется совершенно невозможно, но для создания звуковых имитаций это и не особенно важно.



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

Основная сложность в написании такой процедуры состоит, пожалуй, только в способе передачи параметров подпрограмме, генерирующей тон. Поскольку прерывания выполняются автономно и не зависят от работы основной программы, входные значения не могут передаваться через регистры, а только через память. То есть необходимо составить блок данных, описывающих высоту и длительность каждого отдельного звука - по два байта на каждый. Адреса начальной и текущей пары значений в блоке данных также должны передаваться через переменные, чтобы каждое очередное прерывание «знало», какие параметры требуется считывать. Кроме этого нужна еще одна переменная, которая будет выполнять роль флага разрешения извлечения звука. Эта же переменная может служить счетчиком циклов, если вы хотите иметь возможность многократно исполнять запрограммированный в блоке данных фрагмент.

Учитывая все сказанное, можно написать такую процедуру обработки прерываний:

INTERR PUSH AF ;сохраняем используемые PUSH BC ; в прерывании регистры PUSH HL INTER1 LD A,(REPEAT) AND A ; Если эффект прозвучал нужное количество раз, ; завершаем обработку прерывания JR Z,EXITI LD HL,(CURADR) ;определяем текущий адрес ; в блоке данных LD B,(HL) ;высота звука INC B DEC B ; Если встретился маркер конца блока данных, ; переходим к следующему повторению JR Z,EXITI0 INC HL LD C,(HL) ;длительность звука INC HL LD (CURADR),HL ;запоминаем текущий адрес CALL BEEP ;извлекаем звук EXITI POP HL ;восстанавливаем регистры POP BC POP AF JP 56 ;переходим к стандартному ; обработчику прерываний ; Переход к началу эффекта - повторение EXITI0 LD HL,(ADREFF) ;восстанавливаем начальный LD (CURADR),HL ; адрес блока данных LD HL,REPEAT DEC (HL) ;уменьшаем счетчик повторений JR INTER1 REPEAT DEFB 0 ;количество повторений эффекта ADREFF DEFW 0 ;начальный адрес блока данных эффекта CURADR DEFW 0 ;текущий адрес в блоке данных ; Извлечение звука BEEP XOR A BEEP1 XOR 16 OUT (254),A PUSH BC DJNZ $ POP BC DEC C JR NZ,BEEP1 RET



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

IMON XOR A ;в начале на всякий случай LD (REPEAT),A ; запрещаем вывод звука LD A,24 ;код команды JR LD (65535),A LD A,195 ;код команды JP LD (65524),A LD HL,INTERR ;переход на процедуру LD (65525),HL ; обработки прерываний LD HL,#FE00 ;формируем таблицу векторов прерываний LD DE,#FE01 LD BC,256 LD (HL),#FF ;на адрес 65535 (#FFFF) LD A,H ;запоминаем старший байт адреса таблицы LDIR DI LD I,A ;загружаем регистр вектора прерываний IM 2 ;включаем 2-й режим EI RET

Сразу же напишем и процедуру восстановления первого режима прерываний, которая будет вызываться при окончании работы программы. Она вам уже известна, но тем не менее повторим:

IMOFF DI LD A,63 LD I,A IM 1 EI RET

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

EFF1 DEFB 200,5,220,4,200,5 DEFB 100,8,80,9,50,20 DEFB 0 EFF2 DEFB 50,20,100,6,200,3,100,6 DEFB 0

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



Чтобы любую процедуру было удобно вызывать даже из Бейсика, не имеющего ни малейшего представления о метках ассемблерного текста, применим распространенный прием, часто используемый в таких случаях и которым мы однажды уже воспользовались (см. программу ) - вставим в самом начале программы в машинных кодах ряд инструкций «длинного» перехода (JP) с указанием адресов каждой из «внешних», то есть вызываемых из другой программы, процедур. Тогда адреса обращения к любой из них будут увеличиваться с шагов в 3 байта (размер команды JP). Таким образом, наш пакет процедур будет выглядеть так:

ORG 60000 ; 60000 - включение второго режима прерываний JP IMON ; 60003 - переход к подпрограмме выключения 2-го режима прерываний JP IMOFF ; 60006 - включение эффекта1 JP ONEFF1 ; 60009 - включение эффекта2 ONEFF2 LD HL,EFF2 LD A,5 JR ONEFF ONEFF1 LD HL,EFF1 LD A,3 ONEFF LD (ADREFF),HL LD (CURADR),HL LD (REPEAT),A RET ; Блоки данных эффектов

; Инициализация второго режима прерываний

; Выключение второго режима прерываний

; Процедура обработки прерываний

А вот фрагмент программы на Бейсике, демонстрирующий использование приведенных звуковых эффектов:

10 INK 5: PAPER 0: BORDER 0: CLEAR 59999 20 RANDOMIZE : LET x=INT (RND*30)+1: LET y=INT (RND*20)+1 30 LET dx=1: IF RND>=.5 THEN LET dx=-1 40 LET dy=1: IF RND>=.5 THEN LET dy=-1 50 INK 2: PLOT 0,0: DRAW 255,0: DRAW 0,175: DRAW -255,0: DRAW 0,-175: INK 5 60 RANDOMIZE USR 60000 100 PRINT AT y,x; INK 8; OVER 1;"O": LET x1=x: LET y1=y: PAUSE 3 110 IF x=0 OR x=31 THEN RANDOMIZE USR 60006: LET dx=-dx 120 IF y=0 OR y=21 THEN RANDOMIZE USR 60009: LET dy=-dy 130 LET x=x+dx: LET y=y+dy 140 IF INKEY$<>"" THEN GO TO 200 150 PRINT AT y1,x1; INK 8; OVER 1;"O": GO TO 100 200 RANDOMIZE USR 60003

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


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