- Drukuj
- 25 May 2012
- Programowanie
- 6306 czytań
- 1 komentarz
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.
Zaloguj się , żeby móc zagłosować.
text_adr: [b] equ $+1[/b]
Panowie o co tutaj chodzi?