Jak jsem 20 let vařil vejce

Takže pár technických faktů a zajímavostí…

Originální kód jsem nezkoumal, vše bylo naprogramováno od základu nové. Z ROM (Monitoru) není využitá jediná funkce (jen v loaderu). Z původního tap souboru jsem vytáhl jen grafiku. Obrázky jsou uložené v 8 bitovém formátu, jinak by se do paměti nevlezly. Zkoušel jsem i různé RLE a slovníkové komprese, ale pro tak malé obrázky se nakonec ukázalo jako nejefektivnější právě ten původní způsob uložení.
Pro vykreslování používám jednu offscreen obrazovku a 1kB cache (ramdisk). Před vykreslením se do ramdisku převede vybraný sprite do 6 bitového formátu a pak se teprve kreslí do offscreen. Každý sprite může být vykreslen s 0/2/4 pixelovým offsetem, případně zrcadlově obrácený. Vykreslovat je možné OR metodou nebo SET metodou – pro SET metodu se jen navíc vymaže v pozadí obdélník (respektuje se bitové posunutí). Všechny grafické rutiny počítají s COLORACE, tedy přehazují barvy pro sudé/liché řádky. Když je nastaven černobílý režim, tak se vybírá jen podmnožina kombinací z možných atributů. Mnoho kódu se modifikuje za pochodu, jelikož je popis jednotlivých obrazovek seřazen podle použitého spritu a vykreslovacího módu, tak se to časově vyplatí.

Co se týká animovaných objektů, tak ty také vykresluji nejprve do offscrenu a současně sestavuji seznam regionů, které se musejí ve videoram aktualizovat. Nad to všechno se (v offscreenu) překreslí jediný sprite s maskou – Dizzy. Ten je vždy nad všemi objekty a v tomto si troufám říct, že vypadá PMD verze lépe než ta ZX. Všechny regiony se pak jedním šmahem překopíruji do videoram. Pokud má objekt jemný horizontální posun (např. žralok), připraví se v ramdisku všechny jeho posunuté varianty. Dizzy se ale nepřipravuje, ten se posouvá “za pochodu”, těch dat je totiž opravdu hodně. Na rozdíl od ZX verze v paměti nejsou ani zrcadlově obrácené sprity Dizzyho – ten se taky otáčí během vykreslování do offscreenu.

Animace jsou vykreslovány XOR metodou, ale v některých případech (když objekt nezasahuje do okolí nebo je na jednom místě), používám rychlejší SET metodu – např. voda, nepohybující se oheň.

Další optimalizaci přineslo samotné řízení objektů (oheň, voda, netopýři atd.). Pokoušel jsem se udělat univerzální model jednoho objektu, který by různými skripty a nastavením nahradil vše. Ovšem bylo to tak pomalé a nabubřelé, že nakonec má každý objekt svůj vlastní nativní kód složený z volání elementárních funkcí. A tady jsem od srpna loňského roku přistoupil k vytvoření kooperativního multitaskingu. Inspirovalo mě využití příkazu “yield” v C# v prostředí Unity3D. Takže pro každý objekt se spustí vlákno, a když má splněno, instrukce RST 0 předá ovládání dalšímu vláknu na zadaný počet “ticků”, po uplynutí této doby kód pokračuje za instrukcí RST 0. Např. vlákno netopýrů vypadá takto:

obNetopyr: 
   ; pohne s netopýrem v DE je deskriptor pohybu (odkud kam, jak rychle)
   CALL thrSpriteMoveDE 
   ; sprite index = (sprite index + 1) mod 4
   LXI H, BATINDEX
   INR M
   MOV A,M
   ANI 3
   LXI H, .table
   RST 3
   XCHG
   SHLD thrStack.ptr           ; adresa spritu
   MVI A, clWhite              ; bílá barva
   CALL thrUpdateSprite.update ; překreslení v offscreenu
   LXI B, .pain                ; adresa, kam se skočí, pokud je kolize s Dizzym
   RST 0                       ; "yield"
   DB 2 + 128                  ; uspí na 2 ticky + detekuje se kolize s Dizzym
   JMP obNetopyr
.pain: 
   CALL thrPain ; při doteku způsobí úbytek života
   DB 1         ; jen 1 bod
   DW txtVampyr ; text, který se zobrazí, pokud v důsledku toho Dizzy umře

; adresy spritů netopýra
.table: DW A_NETOPYR1, A_NETOPYR2, A_NETOPYR3, A_NETOPYR2

A to už je hezky přehledné a jednoduché, že?

Hlavní smyčka je synchronizovaná časovačem 8251, ale v některých místech (kde je hodně animovaných objektů) už CPU mírně nestíhá. Tipuji, že původní hra byla celá skriptovaná (s ohledem na tolik verzí Dizzyho), ale skriptované objekty byly na PMD dost loudavé.

Stejný kód můžu spustit vícekrát – pro každé vlákno se uchovává kopie registrů, zásobník mají společný. To bylo velké odlehčení! Jak velikostí kódu, tak v zátěži CPU i samotném programování. Např. objevení džina doprovází 6 blesků, stačilo naprogramovat jeden s parametrem kudy se pohybuje a bylo to. Když už jsme u toho džina, ten byl asi nejsložitější. Než jsem se k němu dostal, dával jsem si vždy pozor, aby žádný sprite nepřesáhl okraj obrazovky (aby v paměti nepomazal kód okolo). Nakonec jsem ale usoudil, že lepší bude naučit grafickou knihovnu sprity ořezávat. To se vyplatilo i opic házející kokosy, krysy, která spadne do studny atd. No ale hlavně ty blesky – to bylo nejhorší…

Logika hry (reakce na použití předmětů a vlastně posun celým příběhem) je udělána takovým primitivním skriptem – inspirace byla v jazyku IL, který se používá u PLC automatů. Skript má pak k dispozici bitovou paměť a na stav jednotlivých bitů se může ptát i vlákno objektů. Příklad zápisu dějové linky – zabití čaroděje:

 ; zabití čaroděje - použití trojzubce v screenu ICETOWER na zadané pozici
 PUT_ITEM it_trojzubec, id_ICETOWER3, 25, 56 
 ; zobrazí text a současně nastaví bitZaks, ale jen, pokud byl bitZaks v 0
 _ONETIMETEXT bitZaks, txtZaksKilling
 ; zobrazí prsten 
 _SHOW it_prsten, 25, 56
 ; spustí dva obláčky
 _CLOUD 25, 70
 _CLOUD2 25, 90
 ; trojzubec se skryje z inventáře a to je vše
 _HIDEEXIT

Pod jednotlivými příkazy se skrývá definované makro, který zadaný příkaz přeloží na 1 – 3 bajty.

Nejvíce času zabralo samotné přetahování jednotlivých obrazovek. Jelikož jsem nezkoumal původní kód, nedalo se jen tak přetáhnout hotové deskriptory. Našel jsem na Internetu mapu hry (která mimochodem není úplně přesná) a udělal si nástroj, který mi pomáhá sestavit popis. Můžete sami vyzkoušet: LevelEditor.zip.

Velkým pomocníkem byl Macro assembler. Popis levelu je tvořen příkazy, které mají různou bitovou délku. Seznam příkazů je zde (výsledek experimentování pro co nejmenší velikost v paměti):

; Bitový stream
; 1 iii iiii xxxx xxxx yyyy yyyy - vykreslí sprite iii iiii na pozici x,y)
; 01 xxxxxx yyyyyy - zkrácený zápis (stejné nastavení na jiném místě
; 001 - opakované vykreslení na pozici vedle - každý bit jeden sprite
; 0001 MSccc - nastavení módu, barva se nastavuje jen prvním příkazem
; 00001 yyyyy - x je stejné, k y se připočítá yyyyyyy
; 000001 LRTB - odteď se bude generovat zadané omezení
; 0000001 V bbbbbbb - podmíněné vykreslení (pokud bit bbbbbbb == V)
; 00000001 flag(4) left (7) right(7) top(7) bottom (7)
; 000000001 ccc xxxxxxx yyyyyyy + text - text

V assembleru mám makro CMD_SPRITE, které samo rozhodne, které z těchto příkazů je nejvhodnější použít (porovnává poslední hodnoty) a dalším makrem vytvoří bitový stream:

; vloží sprite
CMD_SPRITE MACRO id, x, y, mode, color, rp
 if (mode <> ___OLDMODE) || (color <> ___OLDCOLOR)
 bs_write 1, 3
 bs_write mode + color, 5
 endif

if (id == ___OLDID) && ( x == ___OLDX ) && (___OLDY <= y) && (___OLDY+64> y )
 bs_write 1,5
 bs_write (y - ___OLDY) , 6

elseif (id == ___OLDID) && ( x >= ___OLDX ) && ( x < ___OLDX + 64) && (ABS(___OLDY - (y)) <32) ;
 bs_write 1,2
 bs_write (x - ___OLDX), 6
 bs_write (y - ___OLDY) & 3fh, 6

elseif
 bs_write (128+id),8
 bs_write x, 8
 bs_write y, 8
 endif
___OLDMODE SET MODE
___OLDCOLOR SET color
___OLDX SET x
___OLDY SET y
___OLDID SET id
 ; opakování
 IF rp>1
 bs_write 1, 4 ; 001 + každý další bit jedno opakování + 0
 bs_write 0FFFFFEh, (rp-1)
 ENDIF
 ENDM

Mimochodem popis všech obrazovek zabírá přes 4000 řádků textu a celý překlad na slabších strojích trvá půldruhé minuty.

Kromě obrázků, jejichž pozice se musela doladit, aby se barevné atributy příliš nerušily (to bylo peklo!), je součástí popisu i omezení – stěny, přes které Dizzy nemůže projít. Na začátku jsem tato omezení psal ručně, ale to vážně nešlo. Takže každý sprite nese flagy o tom, jestli do levelu přidá levé/pravé/horní/dolní omezení ve své vlastní velikosti a pozici. Nakonec omezování pohybu tímto seznamem nebylo tak těžké naprogramovat, stačilo najít vždy nejbližší omezení zleva, zprava, zhora a zdola a vznikl obdélník, ve kterém se mohl Dizzy pohnout. Při každém posunutí se hledalo znova. U žraloka se toto omezení pohybovalo se žralokem, aby se dalo chodit jen po špičce ocasu. V prvotních verzích jsem měl i kód, který toto omezení mohl vykreslit, pro srovnání:

Hudba je primitivní 3 kanálová, tedy nevyužívám všech možností SAA1099. Obě hudby (titulní, i ta během hry) jsou v nějakém trackeru na youtubku, takže o to méně práce. Velikost obou skladeb dohromady jsou asi 2 kilobajty, a ta titulní je schovaná v paměti vedle videoram, odkud se překopíruje do ramdisku, když je potřeba. Kvůli hudbě jsem nemohl použít k přesouvání dat zásobník. Kdyby došlo k vyvolání přerušení v okamžiku, kdy SP ukazuje někam do dat, došlo by ke katastrofě. Zkoušel jsem kritické části ohraničit instrukcemi DI/EI, ale odneslo to právě plynulost přehrávání hudby.

No a jak dlouho jsem to dělal? Nejstarší uložená záloha je z roku 2014, takže hodně dlouho. Ale je to prokládané půlročními pauzami. Vždy, když jsem viděl, že to celé musím přeprogramovat, jsem toho otráveně nechal. Teď, nabitý zkušenostmi, půjdou další věci rychleji 🙂

A perlička na konec. Už před 20 lety jsem chtěl spouštět Dizzyho tak, že bych na PMD85 vytvořil emulátor C64! Dokonce jsem udělal pro PMD nahrávač C64 souborů a i vlastní “turbosystém”, do kterého se podařilo přehrát většinu her – a to byla radost načítat. Byť to byla hodně naivní představa, probudila ve mě lásku ke zkoumání a emulaci různých platforem, která se mě drží dodnes.