speccy.pl
Facebook Like


SPECCY.PL

[SPECCY.PL PARTY 2023.1]

[WIKI SPECCY.PL]
Polecamy

KOMITET SPOŁECZNY KRONIKA POLSKIEJ DEMOSCENY
PIXEL HEAVEN 2023
AYGOR
Forum ZX Spectrum
Zawartość panelu chwilowo niedostępna
Archiwum plików ZX Spectrum
Nawigacja
Oldschool demomaking, część 1

Autor: Mat of ESI

Oldschool demomaking czyli jak to drzewiej bywało.

Słowo wstępne

Niniejszy tekst otwiera cykl artykułów - jeszcze nie wiem jak długi - omawiających różne techniki używane przy pisaniu "oldskulowych" dem. Nie będzie tu raczej informacji o tym jak pisać "wypasione" efekty liczące cieniowaną grafikę 3D itp. Nie będzie to również typowy kurs programowania - teksty zakładają przynajmniej podstawową znajomość BASICa i assemblera Z80.

Scrollery - część 1

Na początek oczywiście element obowiązkowy każdego oldschoolowego dema - scroller zwany również scrollem. Czym jest scroller wie chyba każdy - to przesuwający się po ekranie dłuższy tekst. Scroller może być poziomy, pionowy, może się tylko przesuwać w jedną stronę, ale może też być bardziej wymyślny i dodatkowo podskakiwać itp. Scrollery można podzielić w zasadzie na 4 podstawowe rodzaje:

  • scrollery na atrybutach - do wyświetlania używane są atrybuty ekranu Spectrum, co daje możliwość wyświetlenie dużego scrollera z czcionką o pojedynczym punkcie o rozmiarze 8x8 pikseli
  • scrollery bitmapowe bezpośrednio w pamięci ekranu - dowolny rozmiar czcionki (z oczywistym ograniczeniem tego ile danych jesteśmy w stanie przetworzyć) i scroller generowany bezpośrednio w pamięci ekranu - rozwiązanie używane w zasadzie wyłącznie do prostych efektów przesuwanych w jednym kierunku
  • scrollery bitmapowe z buforem - efekt j.w., ale scroller generowany jest najpierw w buforze a dopiero z niego przerzucany na ekran dzięki czemu przerzucenie może następować w różne punkty ekranu co pozwala na uzyskanie podskakującego scrollera
  • scrollery składane - tutaj tak na prawdę nie występuje żadne przesuwanie danych a cały scroller jest składany bezpośrednio na ekran z poszczególnych znaków dzięki czemu można uzyskać w zasadzie dowolne efekty typu sinusoid, podskakujących znaków itp.

Na początek przykład wykonania najprostszego scrollera w BASICu.

10 let t$="                                  Tekst do scrolla w BASICu. Tekst do scrolla w BASICu. Tekst do scrolla w BASICu."
15 let x$=t$+t$
20 for i=1 to len t$
30 pause 10
40 print at 0,0;x$(i to i+31);
60 next i
70 goto 20

Uruchomienie tego programu wyświetli tekst wpisany w linii 10 w pierwszej linii ekranu z prędkością mniej-więcej jednego znaku na 10 ramek czyli około 0.2 sekundy. Efekt nie jest bardzo piękny - tekst wyraźnie skacze a nie sie przesuwa jednak w niektórych zastosowaniach nie ma potrzeby używania bardziej skomplikowanych efektów.

Działanie programu jest proste. Wyjaśnienia wymaga ciąg spacji na początku tekstu i użycie zmiennej X$ podwajającej cały tekst. Spacje oczywiście potrzebne są do tego, żeby początek tekstu nie wskakiwał od razu na ekran tylko "wjechał zza ekranu". Podwajanie potrzebne jest do tego, żeby tekst się ładnie "zapętlił" - gdyby użyć tylko jednej zmiennej po dojściu do końca tekstu zostałby on od razu zastąpiony początkiem. Podwojenie powoduje, że na koniec tekstu zaczyna się wsuwać jego początek i po dojechaniu do końca pierwszego przebiegu całość się zapętla. Można by to samo uzyskać komplikując bardziej pętlę ale podwojenie tekstu jest znacznie prostszym a równie skutecznym zabiegiem.

O ile zastosowanie powyższego efektu we własnym programie w BASICu do niewątpliwie jakiś efekt daleko mu jednak do tego, czego spodziewalibyśmy się po scrollerze w demie. Przejdźmy więc teraz do prostego scrollera w assemblerze.

Wszystkie przykłady kodu w assemblerze przystosowane są do assemblera pasmo (http://www.speccy.org/pasmo/) i w wypadku użycia innego assemblera mogą wymagać pewnego dopasowania.

Jako jeden z efektów scroller wymaga zewnętrznej pętli wywołującej. W naszym przykładzie wygląda on tak:

        org 32768

main_loop:
        halt
        call one_scroll
        ld bc,$7ffe
        in a,(c)
        and 1
        jr nz, main_loop
        ret

Pętla zaczyna się od rozkazu HALT synchronizującego wykonanie z początkiem ramki ekranu. Następnie wywoływana jest procedura generująca scroller a po powrocie odczytywana klawiatura i jeśli wciśnięta jest spacja (wyzerowany bit 0 na porcie $7FFE) program kończy działanie. W rzeczywistym zastosowaniu najpierw należałoby ustawić stosowną zawartość ekranu, kolory itp. Następnie zainicjować muzykę i w pętli wywoływać procedurę ją odtwarzającą. Poza odczytem spacji należałoby też obsłużyć inne interakcje z użytkownikiem - zmianę muzyczki, przełączanie efektów itp.

Najistotniejszym elementem naszego programu jest oczywiście procedura one_scroll, która generuje na ekranie kolejną klatkę scrollera.

Procedura składa się z trzech fragmentów. Pierwszy to przesuwanie wskaźnika tekstu tak aby co osiem kolejnych wywołań wskazywał na kolejną literę do wyświetlenia.

one_scroll:
        ld a,(bpos)
        or a
        jr z,move_text
        dec a
        ld (bpos),a
        jr do_scroll
move_text:
        ld a,7
        ld (bpos),a
text_adr: equ $+1  
        ld hl,text 
        ld a,(hl)  
        inc hl     
        ld (text_adr),hl
        cp 13
        jr nz,get_char
        ld hl,text
        ld a,(hl) 
        inc hl    
        ld (text_adr),hl

Na początku następuje odwołanie do zdefiniowanej na końcu kodu zmiennej bpos - przechowuje ona licznik kolejnych wywołań. Jeśli znajdzie się w niej zero oznacza to, że musimy przesunąć wskaźnik tekstu o jeden bajt. W przeciwnym wypadku licznik jest zmniejszany, zapisywany do zmiennej i następuje skok do do_scroll, gdzie wykonane zostanie przesunięcie zawartości ekranu o jeden piksel.

Jeśli przeskoczyliśmy kolejny znak (albo jeśli jest to pierwsze wykonanie - domyślną wartością bpos jest 0) do licznika ładowane jest 7 a następnie przesuwany jest wskaźnik do tekstu. Wskaźnik ten jest realizowany jako modyfikacja kodu (parametru instrukcji LD HL) zamiast definiowania kolejnej zmiennej. Przed przesunięciem licznika pobierany jest kod aktualnego znaku a po przesunięciu następuje sprawdzenie, czy ten kod to 13 - znacznik końca tekstu. Jeśli tak, to wskaźnik przestawiany jest na początek tekstu, pobierany kod pierwszego znaku i zapisywany wskaźnik do następnego znaku. Zmienna test_addr zawsze wskazuje na znak, który ma być wyświetlony przy zamknięciu kolejnego cyklu scrollera. Zmienna text to zdefiniowana dalej dyrektywą DEFM zawartość tekstu dla scrollera.

Po ustawieniu wskaźnika tekstu w A znajduje się kod znaku do wyświetlenia. Wyświetlanie nowego znaku może być rozwiązane na kilka sposobów. Jednym z nich jest kopiowanie go na prawą stronę scrollera bezpośrednio w ekran jednak w takiej sytuacji co 8 pikseli widać pojawiający się tam nowy znak. Można tego uniknąć zakrywając to miejsce atrybutem z takim samym kolorem tła i atramentu.

Lepszym rozwiązaniem jest zastosowanie bufora na pojedynczy znak. Dzięki temu niewielkim kosztem możemy przewijać całe 32 znaki (256 pikseli) ekranu i scroller wsuwał sie będzie z prawej strony ekranu. Przygotowanie danych w buforze wykonuje drugi fragment kodu.

get_char:
        ld h,0
        ld l,a
        add hl,hl
        add hl,hl
        add hl,hl
        ld de,$3C00
        add hl,de  

Kod znaku jest najpierw mnożony przez 8 - tyle bajtów zajmuje bitmapowa reprezentacja pojedynczego znaku. Następnie do wyliczonej wartości dodawane jest $3C00 (dec: 15360) - wskaźnik do systemowego zestawu znaków w ROMie. Zestaw znaków zaczyna się od adresu $3D00 (dec: 15616), ale w związku z tym, że używane kody znaków to ASCII i drukowane znaki zaczynają się od kodu 32 po przemnożeniu tej wartości przez 8 dostajemy 256 - czyli różnicę między $3C00 i $3D00. Gdyby zestaw znaków pobierany był z innego miejsca w pamięci i jego adres nie był wyrównany do 256 bajtów kod musiałby wyglądać trochę inaczej:

        sub 32
        ld h,0
        ld l,a
        add hl,hl
        add hl,hl
        add hl,hl
        ld de,zestaw_znakow
        add hl,de  

Najpierw należy odjąć 32 od kodu znaku a następnie dodać adres zestawu znaków. Ten sam efekt można uzyskać w nieco mniej czytelny sposób:

        ld h,0
        ld l,a
        add hl,hl
        add hl,hl
        add hl,hl
        dec h
        ld de,zestaw_znakow
        add hl,de  

Rozwiązanie to najpierw mnoży kod znaku a następnie wykonuje DEC H zmniejszając parę HL o 256 bajtów. Kod jest odrobine mniej czytelny, ale wykonuje sie o 3 takty szybciej (SUB 32 to 7 taktów, DEC H - 4).

Po wyliczeniu adresu znaku należy jego dane przenieść do bufora:

        ld de,buf  
        rept 8
          ldi 
        endm  

Adres znaku znajduje się w HL, do DE ładujemy bufor i wykonujemy 8 razy rozkaz LDI - kopiowania bajtu spod adresu w HL pod adres w DE.

Po przygotowaniu znaku przechodzimy do właściwej procedury scrollera. Do HL ładujemy adres na ekranie gdzie znajdować się ma ostatni z prawej znak scrollera - w to miejsce będziemy wsuwali dane z przygotowanego bufora (w tym przykładzie jest to adres ostatniej tekstowej linii ekranu - 20704 powiększony o 31). Do DE oczywiście adres bufora a do B licznik kolejnych linii scrollera.

do_scroll:
        ld hl,20704+31
        ld de,buf
        ld b,8   

Następnie zaczynamy pętlę zewnętrzną zliczającą linie ekranu. Zapamiętujemy adres akranu, do C ładujemy 32 - tyle bajtów ekranu musimy przesunąć w lewo. Następnie pobieramy bajt z bufora, przesuwamy go w lewo wysuwając najstarszy bit do znacznika CY (i wcześniejszą wartość CY wsuwając na najmłodszy bit - w wypadku bufora nie ma to znaczenia). Na koniec przesunięty bajt zapisujemy z powrotem do bufora.

loop1:
        push hl
        ld c,32
        ld a,(de)
        rla
        ld (de),a

Wewnętrzna pętla przesuwa zawartość ekranu - pobiera bajt z ekranu, przesuwa go w lewo (RLA) wsuwając na najmłodszy bit poprzednią wartość CY a najstarszy bit wysuwając do CY po czym odsyła ten bajt z powrotem w ekran. Następnie modyfikuje wskaźnik (używamy DEC L, bo wiemy, że nie przekroczymy granicy 256 bajtów i nie ma potrzeby użycia DEC HL - oszczędzamy w ten sposób kolejne 2 takty), zmniejsza licznik i zamyka pętlę.

loop2:
        ld a,(hl)
        rla
        ld (hl),a
        dec l
        dec c    
        jr nz,loop2

Przed zamknięciem zewnętrznej pętli zwiększany jest wskaźnik do bufora, odtwarzany ze stosu wskaźnik ekranu po czym modyfikowany jest on przy użyciu INC H. Organizacja pamięci ekranu Spectrum została przy projektowaniu układu ULA dobrana w taki sposób, żeby przy zachowaniu znaków 8x8 w standardowym układzie uprościć dostęp do pamięci stąd kolejne linie w ramach jednego znaku są przesunięte między sobą o 256 bajtów - o tyle modyfikuje parę HL wykonanie INC H.

        inc de
        pop hl
        inc h 
        djnz loop1
        ret

Zamknięcie pętli wykonuje kolejne 7 cykli przewijając w efekcie całe 32 znaki o jeden piksel.

Na końcu kodu znajdują się używane zmienne - bufor, licznik wykonań i sam tekst scrollera.

buf:    ds 8
bpos:   db 0
text:   defm "Tekst scrollera... wpisany jako parametr dla defm albo "
        defm "w dowolne miejsce w pamieci a wtedy zamiast text: defm "
        defm "nalezy uzyc text: equ adres_tekstu "
        db 13
        end 32768

Ostatnia dyrektywa informuje assembler Pasmo od jakiego adresu zaczyna się wykonanie programu. Ułatwia to szybkie testowanie dzięki temu, że jednym poleceniem

pasmo --tapbas scroller_01.asm scroller_01.tap

W efekcie, po uruchomieniu programu możemy zobaczyć, efekt jak poniżej:

generowany jest plik TAP dla emulatora w którym zapisany jest prosty loader (wczytujący i uruchamiający nasz program od adresu zdefiniowanego w END) oraz właściwy kod scrollera.

Powyższy kod można w prosty sposób zmodyfikować tak, żeby zamiast standardowych znaków z ROMu używał na przykład ich pogrubionej wersji. W tym celu fragment kodu

        rept 8
          ldi
        endm

należy zastąpić innym:

        rept 8
          ld a,(hl)
          sla a    
          or (hl)  
          ld (de),a
          inc l 
          inc de
        endm

Powyższy kod zamiast LDI pobiera bajt do A, przesuwa go w lewo uzupełniając zerem z prawej strony po czym wykonuje OR z zawartością pamięci spod HL. W efekcie np. z kombinacji bitów 01000010 uzyskujemy 11000110 (10000100 OR 01000010) co zastosowane na standardowych znakach z ROMu daje nam nieźle wyglądające pogrubienie znaków.

Wygenerowany bajt znaku zapisywany jest pod DE, i oba wskaźniki są zwiększane.

Ciekawie wyglądający efekt używany w starych demach to pogrubienie tylko dolnej połowy znaku. Uzyskuje się je w taki sposób:

        rept 4
          ldi 
        endm  
        rept 4
          ld a,(hl)
          sla a    
          or (hl)  
          ld (de),a
          inc l 
          inc de
        endm

Jak widać jest to połączenie dwóch wcześniejszych metod - najpierw cztery razy robimy LDI a potem kolejne cztery bajty pogrubiamy.

Prostą modyfikacją powyższego scrollera jest wersja wyświetlająca tekst znakami o podwójnej wysokości. Kod nie różni się niczym aż do miejsca w którym dane znaku kopiowane są do bufora. Tutaj kod ten wygląda tak:

        rept 8
          ld a,(hl)
          ld (de),a
          inc de   
          ld (de),a
          inc de
          inc l
        endm

Dane są po prostu przepisywane dwa razy co daje w efekcie znaki w matrycy 8x16 pikseli - podwójnej wysokości.

Kod scrollera różni się również tylko nieznacznie.

do_scroll:
        ld hl,20672+31
        ld de,buf
        ld b,2   
loop0:
        push bc
        push hl
        ld b,8 

Adres ładowany na początku do HL to tym razem koniec drugiej od dołu linii znaków. Dodana została również zewnętrzna pętla z licznikiem w B inicjowanym na 2 - tyle wierszy ekranu musimy przesunąć. Wewnątrz pętli jej licznik i wskaźnik ekranu są zapisywane na stos. A następnie wykonywana jest pętla przesuwająca jeden wiersz ekranu. Identyczna z poprzednią wersją scrollera.

loop1:
        push hl
        ld c,32
        ld a,(de)
        rla
        ld (de),a
loop2:
        ld a,(hl)
        rla
        ld (hl),a
        dec hl   
        dec c    
        jr nz,loop2
        inc de
        pop hl
        inc h 
        djnz loop1

Na zakończenie zamykana jest dodatkowa pętla zewnętrzna.

        pop hl
        ld bc,32
        add hl,bc
        pop bc   
        djnz loop0

Odtwarza ona ponownie HL i zwiększa ten wskaźnik o 32 przeskakując do kolejnego wiersza ekranu. Po odtworzeniu BC pętla jest zamykana kończąc całą procedurę.

Ostatnia różnica to deklaracja bufora:

buf:    ds 16

Jest on oczywiście dwa razy większy niż w poprzedniej wersji.

Tak jak w poprzedniej wersji modyfikując kod przygotowujący znak w buforze można uzyskać znaki pogrubione itp.

Cały scroller jest bardzo prosty, ale dzięki temu, że wraz z tekstem kod ma około 300 bajtów można go użyć nie tylko w demie ale też na przykład w loaderach, intrach itp. gdzie ilość wolnej pamięci jest ograniczona rozmiarem ładowanego programu głównego.

Archiwum z kodami źródłowymi oraz plikami .tap do pobrania.

sect0r
sect0r dnia 28.04.2013 13:56:47

text_adr: [b] equ $+1[/b]
Panowie o co tutaj chodzi?

Dodaj komentarz
Zaloguj się, aby móc dodać komentarz.
Oceny
Tylko zarejestrowani użytkownicy mogą oceniać zawartość strony
Zaloguj się , żeby móc zagłosować.

Brak ocen. Może czas dodać swoją?