Уточки не прощают
Добрый день!
Раз гоблин написал про бесконечные шарики, то я напишу про бесконечных уточек. Напишу здесь как создавалась Апогей-дема Ducks not forgive, также приведу код.
Итак, начинаем. Начнём с алгоритма, но он хорошо известен — есть несколько видеобуфферов, уточка рисуется поочереди с первого до последнего и на экран выводятся буферы тоже по-очереди. В компьютерах, где видеопамять задана жестко, приходится хранить буфера в памяти и при каждом кадре копировать их в видеопамять. В Апогее же проще — мы просто меняем положение видеопамяти. Но об этом позже.
начнём с организации экрана. Он у нас в стандарте текстовый 78*30, но на экран выводятся только 64*25. Но у Апогея есть два знакогенератора — один — буквенно-симольный, другой — псевдографика 3*2 блока. Причём псевдографические символы в два раза уже чем стандартные, поэтому, чтоб включить псевдографику, нам надо перепрограммировать видеоадаптер
SET192:
LXI H,VG75+1
MVI M,0 ; Отключаем видео на время перепрограммирования
DCX H
MVI M,77 ; Устанавливаем параметры - 78 символов в строке
MVI M,59 ; 60 строк символов (символы псевдографики в два раза уже обычных)
MVI M,3 ; Высота символа 4 строки
MVI M,NREZH; Устанавливаем номер режима цветных аттрибутов (об этом позже)
INR L
MVI M,23h ; Запускаем видео
CALL WAITIN; Ожидание конца кадра перед программированием DMA. Иначе будут глюки
EI ; Переключаем знакогенератор. В Апогее он привязан к выходу EI процессора
LXI H,VT57+8 ; Программируем DMA
MVI M,$80
MVI L,4
LHLD VADDR ;Загружаем адрес видеопамяти
XCHG
MOV M,E ; Устанавливаем положение видеопамяти
MOV M,D
INR L
LXI D,VSIZE+$4000 ; Устанавливаем её размер
MOV M,E
MOV M,D
MVI L,8
MVI M,$A4 ; Сохраняем настройки
LXI H,VG75+1
MVI M,23H ; Запускаем видео
RET
WAITIN: ; Подпрограмма ожидания начала кадра (аналог HLT, ожидание кадрового прерывания)
LXI H, VG75+1
MOV A,M
WAIT1:
MOV A,M ; Загружаем регистр состаяния ВГ75
ANI $20 ; Проверяем бит
JZ WAIT1; Если надо, ждём дальше
RET
Всё, установили видеорежим, идём дальше. Что представляет из себя у нас экран? Это 4680 байт, 78*60 блоков, из которых показывается около 64*50, каждый блок это 3*2 символа. Нам надо как-то реализовать попиксельную прорисовку.
Делаем так, сначала напишем процедуру, которая берёт X,Y и возвращает адрес этого байта в видеопамяти. Надо ещё учитвыать, что адрес видеопамяти не постоянный, поэтому его надо читать из переменной.
GPA: ;B,C - X,Y
MOV A,C
ADD A
MOV L,A
MVI H,$EB
MOV E,M
INX H
MOV D,M
MOV L,B
MVI H,0
DAD D
XCHG
LHLD VADDR
DAD D
RET
Эта подпрограмма берёт в B,C координаты и в HL возвращает адрес этого байта. Но процндура на достаточная, она обращается к области памяти $EBXX, в которую необходимо предварительно занести смещения для строк.
MOVERAM:
LXI D,0
LXI H,$EB00
MVI A,60
LXI B,78
NTACK:
MOV M,E
INX H
MOV M,D
INX H
XCHG
DAD B
XCHG
DCR A
JNZ NTACK
RET
После выполнения этой процедуры в области $EBxx будут значения 0,78,156,234,312,390…
Разобрались с записью символов, идём дальше. Будем реализовавать вывод уточек. Как мы видим, уточку двигаются попиксельно, а не посимвольно. Можно конечно хранить картинки как массив точек, но вывод будет очень медленный. Поэтму лучше иметь в памяти все фазы со смещением.
На картинке 6 фаз уточки (3*2), столько же масок, и то же самое для отзеркаленных.
Перегоняем эту картинку в байты, пригодные для вывода на апогеевский экран и можно начинать писать процедуру рисования, но есть ещё одна проблема — в каждом символе 3 пикселя по ширине, поэтому нам нужна процедура нахождения x/3 и x mod 3. Вычислять через сдвиги и суммы очень долго, поэтому решил сделать таблицу, в которой всё будет.
DIV3:
.byte 0
.byte 0
.byte 0
.byte 1
.byte 1
.byte 1
.byte 2
.byte 2
.byte 2
.byte 3
.byte 3
.byte 3
.byte 4
.byte 4
.byte 4
.byte 5
.byte 5
.byte 5
.... ; И так далее
Теперь рисуем уточку.
DRAWIM: ; B,C - координаты утки (в пикселях), HL - адрес набора уток (смотрят вправа или влево)
MOV A,C ; Работаем над вертикальной координатой
ADI 13 ; Смещение в видимую область (не весь экран виден)
RAR ; Делим на два - вычисляем координату символа
JNC NOPLU1 ; Если был перенос - значит нужна утка со смещением пиклель вниз.
LXI D,105 ; Делаем смещение, чтоб указатель указывал на утку с +1 вниз смещением
DAD D
NOPLU1:
PUSH H
MOV C,A ; Сохраняем Y координату в пикселях
LXI H,DIV3 ; Устанавливаем указатель на таблицу деления на три
MVI D,0
MOV A,B ; Смещаем X
ADI 24 ; в видимую область.
MOV E,A ; А также
DAD D ; находим
MOV B,M ; X div 3
MOV D,A ; Пусть найденная Х координата полежит в D, а мы займемся нахождением X mod 3.
MOV A,B ; Для этого вычисляем X mod 3 = (X div 3) * 3
ADD A
ADD B
MOV E,A
MOV A,D
SUB E ; Нашли X mod 3.
POP H
LXI D,35 ; Дальше. Каждая уточка размером 35 байт. Нам надо сместится на 35*(X mod 3)
CPI 1 ; Делаем так. Примитивно, но быстро.
JNZ NOPLUX1
DAD D
NOPLUX1:
CPI 2
JNZ NOPLUX2
DAD D
DAD D
NOPLUX2:
PUSH H ; Всё. Нашли адрес нашей утки в памяти. Пока сохраним его в стеке
CALL GPA ; и наёдем адрес в втдеопамяти куда её рисовать.
POP B ; Нашли, возврящаем в ВС адрес утки и начинаем рисовать
PUSH H
PUSH B
LXI D,78-5 ; Размер утки - 7*5 знакомест. 78-5 это то, что надо прибавить к указателю чтоб он
MVI A,7 ; перешел к началу следующего ряда.
NECFIRMA: ; Развёрнутый цикл рисования ряда. Точнее пока это не ряд, это маска. Мы стираем то,
PUSH PSW ; сотрёт под собой утка.
LDAX B
ANA M
MOV M,A
INX H
INX B
LDAX B
ANA M
MOV M,A
INX H
INX B
LDAX B
ANA M
MOV M,A
INX H
INX B
LDAX B
ANA M
MOV M,A
INX H
INX B
LDAX B
ANA M
MOV M,A
INX H
INX B
DAD D
POP PSW
DCR A
JNZ NECFIRMA
POP H
LXI D,210
DAD D
MOV C,L
MOV B,H
POP H
LXI D,78-5
MVI A,7
NETFIRMA: ; Во, а это мы уже рисуем на освободившемся месте нашу маленькую желтенькую.
PUSH PSW
LDAX B
ORA M
MOV M,A
INX H
INX B
LDAX B
ORA M
MOV M,A
INX H
INX B
LDAX B
ORA M
MOV M,A
INX H
INX B
LDAX B
ORA M
MOV M,A
INX H
INX B
LDAX B
ORA M
MOV M,A
INX H
INX B
DAD D
POP PSW
DCR A
JNZ NETFIRMA
RET
Вот так. Разобрались с рисованием. Теперь надо задать движение по лиссажу.
Как вы заметили, при нажатии пробела форма лисажжу меняется, значит параметры должны быть изменяемыми.
Есть такие переменные в памяти:
BOB1:
.BYTE 160
BOB2:
.BYTE 73
GO1:
.BYTE 0
GO2:
.BYTE 0
BASE:
.BYTE 196,220
Вычисление происходит по такой формуле:
GO=GO+BASE
BOB=BOB+CARRY
То есть, BOB это указатель в синусной таблице, а BASE — это скорость, с которой указатель двигается по таблице.
Таблицы у нас две — одна максимумальное значение 180 (для x координаты), другая — макимум 84 (для вертикали)
Реализуем движение так:
<br />
LDA GO1 ; Считаем по формуле
MOV E,A
LDA BASE
ADD E
STA GO1
MVI A,0
RAL
MOV E,A
LDA BOB1
ADD E
STA BOB1
MOV E,A
MVI D,0
LXI H,SINE180
DAD D
MOV B,M ;Загружаем в В значение с таблицы
LDA GO2
MOV E,A
LDA BASE+1
ADD E
STA GO2
MVI A,0
RAL
MOV E,A
LDA BOB2
ADD E
STA BOB2
MOV E,A
MVI D,0
LXI H,SINE84
DAD D
MOV C,M ; Теперь С
LDA BOB1 ; По положении указателя определяем, куда в данный момент смотрит утка
ADI 64
LXI H,DUCKIM
JM NOAH ; И устанавливаем нужный указатель
LXI H,DUCKEN
NOAH:
CALL DRAWIM ; Рисуем уточку!
Вот, основной цикл рисования у нас есть, но это не всё. Надо ж ешё листать видеостраницами.
Алгоритм у нас такой:
1. Ожидаем начало кадра
2. Переводим видеостраницу на новый адрес (Эту процедуру надо делать только вначале кадра)
3. Переставляем указатель видео на новый адрес (Но само видео не двигаем)
4. Если номер видеостраницы — 10, сбрасываем на нулевую.
5. Рисуем.
6. Goto 1
Благодаря такому раскладу, утка рисуется не на той странице, которая сейчас показывается. Поэтому исключается такой эффект как сечение луча.
Реализуем.
WEWE:
CALL WSHPLAY ; Ждём начала кадра и играем в это время шум (если играет ударник)
CALL MOVEVRAM ; Переносим видеопамять туда, куда указывает указатель
CALL PLAYMUSIC ; Играем музыку
CALL $F81B ; Проверяем, не нажата ли клавиша
CPI 32
JZ CLEROMBUL ; если нажат пробел - идём на процедуру стирания экрана.
CALL RANDOSHA ; Вызываем процедуру случайных чисел. Просто так, чтоб при каждом нажатии было случайное значение
LDA NUMUS ; Загружаем номер страницы
INR A ; Увеличиваем
CPI 10
JZ ZACYKL ; если 10, идём обнулять.
STA NUMUS
LHLD ORBOS ; Загружаем указаткль на адрес видеопамяти.
INX H ; Увеличиваем на 2
INX H
SHLD ORBOS ; Сохраняем
JMP VOVAN ; Обходим процедуру сброса номера видеостраницы
ZACYKL: ; Обнуление видеостранцы
XRA A ; Обнуляем номер
STA NUMUS
CALL SETNEWLETTER ; Это для вывода буквы текста внизу
LXI H,MUSKUS ; Ставим указатель на первую позицию в списке.
SHLD ORBOS
VOVAN:
MOV E,M ; Сохраняем число, извлечённое из списка указателей
INX H ; в переменную VADDR, из неё в начале кадра берётся значение для установки видеопамяти.
MOV D,M
XCHG
SHLD VADDR
MVI M,YELLOW ; Первым байтом в видеостранице ставим байт, отвечающий за цвет. Чтоб всё было желтым.
; Тут идет код вычисления синуса и рисаования уток.
JMP WEWE ; Идём в начало цикла
; Далее, в конце
NUMUS:
.BYTE 0
ORBOS:
.WORD MUSKUS ; Указатель на адрес видеостраницы из списка ниже
MUSKUS:
.WORD VAAGH+(4680*0) ; Адреса видеостраниц
.WORD VAAGH+(4680*1)
.WORD VAAGH+(4680*2)
.WORD VAAGH+(4680*3)
.WORD VAAGH+(4680*4)
.WORD VAAGH+(4680*5)
.WORD VAAGH+(4680*6)
.WORD VAAGH+(4680*7)
.WORD VAAGH+(4680*8)
.WORD VAAGH+(4680*9)
.WORD VAAGH+(4680*9)
; И потом
VAAGH:
.INCLUDE EYES.ASM ; Это заглавная картинка с уткой.
Вот такой алгоритм получается:
Сначала видеопарять настраивается на VAAGH, где лежит картинка. Так она ждёт 388 интов, потом переходит на процедуру стирания экрана, ту же, что и при нажатии пробела. Эта процедура возвращяется не через стек, а по абсолютному адресу, поэтому возврат идёт на WEWE, метку начала рабочего цикла.
Вот процедура инициализации
LXI H,$E0FD ; Устанавливаем стек
SPHL
LXI H,VG75 + 1 ; Уводим курсор далеко-далеко (250:250) чтоб его не было видно
MVI M,$80
DCX H
MVI M,250
MVI M,250
CALL SET192 ; Устанавливаем режми псевдографики
call MOVERAM ; Заносим в область EBxx данные со смещениями строк
CALL NASTROINATISH ; Инициализируем плеер
ASW: ; Цикл с заставкой
CALL WSHPLAY
CALL PLAYMUSIC
CALL RANDOSHA
LHLD STEP ; Счётчика фреймов
INX H
SHLD STEP
LXI B,388 ; Проверяем не равен ли он 388
MOV A,H
CMP B
JNZ ASW ; Если хоты б один байт не равен - к началу цикла
MOV A,L
CMP C
JNZ ASW
JMP CLEROMBUL ; Идём стирать экран.
Теперь очистка экрана. Экран очищается стеком. Также, эта процедура заносит в переменные, отвечающие за форму лиссажу, случайные значения, которые сгенерировала процедура RANDOSHA.
CLEROMBUL:
LXI H,RANDNUM ; По этой метке - 4 случайных байта, сгенерированных рандошей
MOV A,M ; Записываем их в параметры лиссажу -скорость и начальный параметр.
ORI 128 ; Скорость ограничиваем, чтоб не была слишком медленной.
STA BASE
INX H
MOV A,M
ORI 128
STA BASE+1
INX H
MOV A,M
STA BOB1
INX H
MOV A,M
STA BOB2
LXI H,VAAGH+78 ; Начинаем очистку. Начальная позиция - конец первой строки, так как мы очищаем стеком, а стек идёт назад.
SHLD COMPLIKATION ; Сохраняем в переменной и
XRA A ; создаём переменную счётчика
STA BALL
BEACH:
CALL WSHPLAY ; Во время цикла очистки музыка должна играть
CALL PLAYMUSIC
LHLD COMPLIKATION
MVI D,10
ALONECODER: ; Это у нас цикл, проходящий по всем 10 видеостраницам
SPHL ; Устанавливаем стек в конец строки
MVI A,4
LXI B,YELLOW
PUSH B
LXI B,$0
NOTBEER: ; Делаем 4 раза по 9 PUSH-ей
PUSH B
PUSH B
PUSH B
PUSH B
PUSH B
PUSH B
PUSH B
PUSH B
PUSH B
DCR A
JNZ NOTBEER
PUSH B ; Потом ещё 2, и строка очищена.
PUSH B
LXI B,4680
DAD B
DCR D
JNZ ALONECODER
LHLD COMPLIKATION ; Переходим на следующую строку.
LXI D,78
DAD D
SHLD COMPLIKATION
LDA BALL
INR A
STA BALL
CPI 60 ; Проверяем, не кончились ли строки.
LXI H,$E0FD
SPHL
JZ STOP ; Если да, то выходим из процедуры
; Метка STOP находится перед меткой WEWE - основным циклом демы.
JMP BEACH
Вот, вобщем и всё. Правда, я не рассказал о выводе надписи внизу, но это скучно и неинтересно. Также не рассказал о проигрователе музыки, но это тема для отдельной статьи.
Если есть вопросы — спрашивайте, отвечу.
18 комментариев
Ссылка на поует
Исходники вместе с ассемблером
yadi.sk/d/CwwLGJW6bEVKi
Я думаю так появилось бы релизов на 1 больше точно ;)
> MOV E,A
> MVI D,0
> LXI H,SINE180
> DAD D
> MOV B,M
(встречается не менее 3 раз)
можно оптимизировать по скорости и одновременно по размеру (мнемоники Z80):
LD H,TABLE/256
LD L,A
LD B,(HL)
Таблицы, ессно, надо выровнять по 256 байт.
А вообще, зачот. Двигать экран в любое место памяти — на спектруме об этом можно только мечтать :)
Прям комодоре 64 какой-то, только советский.
программистыкодеры, ассемблеры не юзают!(M видимо включает ввод кодов с нулевого адреса RAM)
LD HL,7; адрес строчки с буквами
CALL #F818 ;(пзушечный вызов, наверное, который печатает)
HALT; процессор виснет, ибо прерываний в апогее не предусмотрено
DB «HELLO WORLD!»,0; строка как раз по адресу 7
а если я начну тебе объяснять как demo22 работает? )