Уточки не прощают


Добрый день!


Раз гоблин написал про бесконечные шарики, то я напишу про бесконечных уточек. Напишу здесь как создавалась Апогей-дема 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 комментариев

avatar
www.pouet.net/prod.php?which=64019
Ссылка на поует
Исходники вместе с ассемблером
yadi.sk/d/CwwLGJW6bEVKi
avatar
Слушай, Kakos_nonos , а напиши вводную статью про «Апогей», ммм? Начиная от как с нуля вывести «hello world», какие компиляторы использовать, как загрузить в эмулятор полученный бинарь, и заканчивая чем-то более продвинутым?

Я думаю так появилось бы релизов на 1 больше точно ;)
avatar
Да, я планирую такое сделать. Даже начал писать уже, но как-то завяло, надо продолжить.
avatar
Код типа такого:
> 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 какой-то, только советский.
  • lvd
  • +6
avatar
Да, можно так, в будущем учту.
avatar
Во славу Уточек!
  • nyuk
  • +5
avatar
Kabardcomp Ducks You.
avatar
Настоящие программисты кодеры, ассемблеры не юзают!



18:58 LessNick: «HelloWorld» для Апогея (86РК, Микроши, Партнёра, ...) без ассемблера ;)

M
21
07
00
CD
18
F8
76
48
45
4C
4C
4F
20
57
4F 
52
4C
44
21
00
.
G
avatar
самый лаконичный туториал, чо :)
avatar
Блин! парсер лох! вырезал, что это сказал DDp
avatar
Клавиатура AER'a-SUPERCODER'a?
avatar
Для тех, кто в уме не умеет дизасмить :)
(M видимо включает ввод кодов с нулевого адреса RAM)
LD HL,7; адрес строчки с буквами
CALL #F818 ;(пзушечный вызов, наверное, который печатает)
HALT; процессор виснет, ибо прерываний в апогее не предусмотрено
DB «HELLO WORLD!»,0; строка как раз по адресу 7
avatar
-МИКРОША- мой первый комп… но я был слишком мелкий и глюпый чтобы там ченить делать кроме как гамать… ну да, уровни в лестнице перерисовывал… в граф реде чето калякал… кодинг очень туго шел. Спасибо, написал первый HELLO WORLD на микроше!
avatar
мой первый ящ
avatar
Кстати, прикольно, в схеме апогея (и многих похожих) обошлись без КР580ВК28/38, ценой чему стало отсутствие адр. пр-ва ввода-вывода (все IO-чипы типа ВВ55 в пространстве памяти — ну чем не 6502?) и без КР580ВН59 ценой отсутствия прерываний (это не Z80, где можно влепить IM 1 и знай подавай себе кадровые стробы, надо выдавать на шину аж целую команду CALL xxyy). В специалисте избавились ещё и от КР580ГФ24, который формировал дурацкие тактовые импульсы амплитудой 12 вольт, сделали врукопашную на ЛА8 и пуллапах на 12в.
avatar
вот ты меня сейчас в говно пытался превратить таким страшным колдунством? ))
а если я начну тебе объяснять как demo22 работает? )
avatar
Без обид, но мне кажется, он и так знает.
avatar
Не знает, я его спрашивал лично :)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.