Retrocomputing by Macc Magyar English

Fejlesztői napló · 2015 — 2026

Head over Heels.

A régi Commodore 64 kedvenc, a Head over Heels natív portolása egy Motorola 68k-s Macintoshre: gépikód-visszafejtés, egy saját fejlesztésű C64 emulátor, és egy izometrikus renderelő motor a semmiből felépítve.

Ez az oldal a projekt fejlesztési naplója — a felfedezésekkel, kísérletekkel és zsákutcákkal együtt.

FORRÁS Commodore 64 CÉLGÉP Motorola 68k Mac INDULT 2015 NYELV C + 68k ASM

2015 Február 20.

Bele is vágnék a közepébe: kb. 8 éve egy akkori kollégám szerzett nekem egy régi Mac-et kerek 2500 Forintért, pontosabban egy Mac Classic II-t. Mivel mindig is szerettem a Macintosh gépeket, így nagy örömmel fedeztem fel a régi kis gépet, volt rajta egy használható rendszer, szépen boot-olt, bár programok nem nagyon voltak hozzá, de elsőre megkedveltem a kis fekete-fehér kijelzőt. Mivel nem sokat tudtam a gépről, így kicsit meglepődtem, hogy még szürke árnyalatokat sem tudott megjeleníteni, csak fekete v. fehér pixeleket. Na abban a pillanatban kapott el az az érzés, hogy régi nagy c64-es kedvencemet a Head over Heels-t jó lenne portolni erre a gépre. A gond csak az volt, hogy kb. semmit nem tudtam arról, hogy hogyan is lehetne ehhez hozzálátni...

Azóta eltelt jó néhány év, a gép egy szekrény alján porosodott, mikor újra elővettem már be sem kapcsolt... Azaz bekapcsolt, de csak néhány zavaros csík volt a képernyőn, a boot-gong meg sem szólalt. :(

Ekkor kezdődött el egy kisebb kutató munka, hogy mit is lehet kezdeni a helyzettel, és kiderült, hogy ezekben a gépekben szeretnek elfolyni az elöregedett elektrolit-kondenzátorok(?) - Tehát rendeltem újakat, a gépet szépen alkatrészeire szedtem, megpucoltam, régi kondikat leforrasztottam, újakat a megtisztított helyükre tettem, és láss csodát, újra működött szépen! Bla-bla-bla, mielőtt túl unalmas lenne az egész történet, átugranám az elhalálozó régi SCSI vinyók kálváriáját, a lényeg, hogy most már van négy öt tökéletesen működő öreg Mac-em (Mac Classic II, Mac LC, Mac LC III, Mac IIci, saját 13" RGB Apple Display-el + Mac LC 475), ebből kettőben vinyó helyett egy AztecMonster SCSI-CF CARD adapter dolgozik, ezeket használom most fejlesztésre...

Hosszas kutatómunka után, most már működő telepített fejlesztői környezet fut a régi gépeken (Metrowerks CodeWarrior 4), az online elérhető dokumentációkat mind letöltöttem, amit nem lehetett, megrendeltem (Inside Macintosh Volume IV, V, VI).

Jelenleg ott tartok, hogy elkészült első c64 intro jellegű programom Mac-re. Itt most tennék egy rövid kitérőt: c64-en tanultam programozni, csináltam rá sok intro-t, pár bénább demo-t, a lényeg hogy a hardver közvetlen kódolása, a spórolás a RAM-mal, és a bitek közvetlen manipulációja az agyamba égett. Ezek a dolgok máig hiányoznak a mai munkáimból. Éppen ezért ezeken a régi gépeken nem izgat különösebben egy ablakos program létrehozása gombokkal, listákkal, lassú recegős animációkkal. Ezek tonnaszám elérhetőek, a régi Mac-es játékok egyáltalán nem nyűgöznek le (azért akad persze 1-2 kivétel, de azok sem a jó vizuális effektjeikkel hatottak rám). Mivel az ősi c64-en szinte minden játék gyönyörűen, vaj sima animációkkal futott (50fps), nagyon hiányoltam a képernyővel szinkronban futó sima, gyors animációkat. Már-már beképzeltem, hogy ezeken a gépeken ilyet nem is lehet csinálni, de mint hamarosan kiderül, lehet...

Ugye C64-en, Amigán, vagy egy Game-Boy-on van egy csomó hardveres opciónk, hogy hogyan is manipuláljuk a képernyőn megjelenő tartalmat, eltolhatjuk bitenként erre vagy arra, csak egy pár bitet kell megváltoztatnunk egy regiszterben, vagy ugyanilyen egyszerűen váltogatunk képernyő üzemmódok között, megfelelő raszterpozíciónál megszakítást válthatunk ki, és lám kész is vannak a szép rasztertrükkök, de Mac-en (és most a régi motorola 68k procis szériáról beszélek) ezekből nincsen semmi, van egy halom képpontunk, egy felbontásunk (monitor függő), és a legjobb amit tehetünk ha az oprendszert megkerülve, közvetlen a Video RAM-ba irkálva, elfoglaljuk a terepet. Szóval bármi lehetséges, csak számoljon a proci jó gyorsan. Én 2 gépre koncentrálok most fejlesztés közben, az egyik (a munkaállomás) egy Mac LC III (1993-ból, 36 MB RAM, motorola 68030 25 MHz, 640x480 max. 32 bit/pixel kijelzővel) és egy Mac Classic II (1991-es, 10 MB RAM, motorola 68030 16 MHz, 512x342 1 bit/pixel), és az első benyomások elég meggyőzőek, már ami a sebességet illeti, pedig még teljesen optimalizálatlan a kódom, azaz sima C-ben íródott minimális assemblyvel keverve. Meggyőző alatt azt értem, hogy egy félképernyős stretch effekt 60 fps sebességgel fut, és még a rendelkezésre álló erőforrás felet sem használtam ki.

A Head over Heels port projekt

A mozgatórugója annak a sok mindennek amit most csinálok, a címben szereplő régi kedvenc portolásának megvalósíthatóságára való törekvés. Magyarul, ahhoz, hogy megszülethessen a játék Mac-en (Ha elkészül, szerintem a platform egyik legjobb játéka lesz, ami elsősorban az eredeti játék készítőinek érdeme), először is meg kell tanulnom gyorsan manipulálni a pixeleket a képernyőn, és büszkén jelenthetem ki, hogy ez meg is történt. Ez rengeteg elfeledett dokumentum átolvasásának az eredménye, amit még több on-line kutatás előzött meg.

Az egészben az volt a legproblémásabb, hogy amit akarok, az abból a szempontból rendhagyó, hogy a képernyő közvetlen módosítását legtöbben kifejezetten nem ajánlják (érthető okokból), ezért ezekről nagyon kevés információ áll rendelkezésre, valamint egy régi elfeledett, amúgy sem a legnépszerűbb platformról van szó. Az én szívemhez viszont nagyon közel áll, mégiscsak ugyanaz a szív dobog benne mint egy Amigában:)

Szóval minden adott, a HOH pont egy olyan játék, amihez nem kell semmi spéci video effekt, csak egy 1 bites képernyő, és némi erőforrás, mert a c64 azért rendesen megizzadt az izometrikus pálya renderelésével. Az eredeti játékból a grafikáját könnyen ki lehetett szedni, a felbontás különbség viszont kicsit gondolkodóba ejt: 320x200-ra optimalizált grafikát 512x342-n nem egyszerű megjeleníteni. 1:1-ben egyszerűen apró lenne az amúgy is kicsi (9") kijelzőn, 2-szeres felbontásban (ami viszont ideális a 640x480-hoz) meg már nem férünk el, úgyhogy jelenleg egy 150%-os nagyításon gondolkozom, ami egy sor további problémát vet fel, de ezt majd később.

Na kicsit átugranék a C64-es vonalra, mert ugye a portolni kívánt játék eredetije azon fut. Sok szempontból szerencsém van, mert a játék kb. 45 KB, így egy sima memória snapshot-tal kinyerhető minden információ. Kis ügyeskedéssel gyorsan meg lehet szabadítani a kinyert adatokat a feleslegtől, megvan a belépési pont is ($5B03). Kis matatás árán rögtön megvoltak a grafikai elemek (Jelen pillanatban a pislogás és a pálya pereme meg nincs meg), valamint a teljes disassembly. Ezt jókedvemben ki is nyomtattam, lett kb. 150 oldal. Jegyzetelni, átfutni jó lesz. A következő fázis a programkód értelmezése, vagyis visszafejtése. Ez egy igen jópofa feladat, és hozzá sem látnék, ha nem ismernem jól a c64-es gépikódját. Na de amitől ez a rész érdekes, hogy a kinyomtatott listán, főleg egy 8000 soros assembly programlistán eligazodni nem egyszerű. Ezt megkönnyítendő, írtam egy speciális c64 emulatort...

Saját c64 emulator

Na szóval, biztos sok hasonló létezik, nem tudom, nem találtam, de nekem egy olyan emulator kellett, amivel futás közben látom a teljes C64 memóriáját, nyomon követhessem a program futását, írást/olvasást, a regiszterek állapotát, a Stack-et oly módon, hogy a program belépési pontjától nyomon követhető legyen milyen szubrutinokat hívott meg a kód, ezt log-olja, disassembly-t keszit kommenttel (hogy éppen pontosan milyen érteket honnan, hova mozgat). Na most ez az emulator már eddig is sokat segített, pedig még szinte el sem készült. Segített megérteni, hogyan építi fel az izometrikus tér részleteit a kód, valamint, hogy a kód melyik része fut mikor épp ez történik, stb.

Custom C64 emulator
Custom C64 emulator

A terv

A folyamat neheze most következik, és ez válaszút elé állít... Két lehetőségem van: vagy vert izzadok hosszú időn át, és megfejtem a program működését, vagy az alapján amit látok, érzékelek megírom a játékot.

Nyilván egyik sem egyszerű, a kérdés az, van-e értelme az eredeti kódot visszafejteni, és ha igen, milyen mértékben. Mi az amit meg lehet figyelni egy játékon, és mi az amihez pontos számszerű adatok kellenek, hogy klónozni lehessen. Kérdés az is, hogy mennyire ragaszkodjak az eredetihez, és min kellene javítani?

Mivel a játék úgy jó, ahogy van, a legjobb lenne mindent úgy hagyni, ahogy van, de van pár dolog amin azért mégis szeretnék illetve változtatni kell:

Kódolas Mac-en

Miután félkész állapotba került az emulator, kis klíma változásként elkezdtem a kódolást a Mac-en. Nehezen vettem rá magam, mivel teljesen idegen környezet volt. Egyrészt sosem programoztam C-ben. Régebben sok forráskódot olvastam már, néhányon módosítgattam pár dolgot, de sosem írtam C programot, főleg nem a semmiből. Szerencsémre viszont a Mac-en futó CodeWarrior 4 eddig teljesen stabilnak bizonyult (korábban kínlódtam egy sort az MPW-vel, de szerencsére sokat fagyott, mert a CW jobb, és kisebb kódot is generál*), így viszonylag gyorsan eljutottam arra a szintre, hogy OS-ből indítható app, közvetlenül és zavartalanul írjon a képernyőre, VBL azaz Vertical Blanking Interrupt (megszakítást) használjak a sima animációkhoz, és szépen kilépjek a programból vissza a rendszerbe. Ennek a tesztelésnek lett az eredménye egy kis intro (amiről korábban már említést tettem), egy scroller-rel, és egy hullámzó "logo"-val vagy nevezzük annak aminek akarjuk... * Valamiért szeretek tudni minden byte-rol a programjaimban (már ahol ez lehetséges), és kifejezetten zavar, ha olyan kód van benne, amit nem is futtatok soha.

2015 Március 04.

Egy kis idő eltelt az utolsó mondat óta, közben létrejött pár dolog ami a projekthez tartozik. Folytatva a Mac-en kódolást, megszületett a 2-es számú Mac Intro is, melyben egy kicsit nagyobb grafikát sikerült szép gyorsan megmozgatni a képernyőn, (Mac Classic-on meg nem az igazi) valamint egy DYCP scroller is készült. Lásd majd a kepéken, JS demo-ban...

Kódvisszafejtés II.

Nagy lökést adott a kódvisszafejtésnek, hogy egy egyszerű script segítségével, az összes rutin kapott egy hívás listát, a kód és adatblokkok szét lettek választva és sorszámoztattam őket, valamint egy másik kis script-tel a program összes változóját is listába szedtem kiemelve azok elérésének helyét módját, ez utóbbi egy 5000 soros lista lett, kb. így kell elképzelni:

       7743 - STA $5B2C        ; $5b2c-be visszairas.
       a60a - LDA $5B2C
       a60f - STA $5B2C
5B2D:  796d - LDA $5B2D
5B2E:  5c1f - LDA $5B2E        ;4. frame-re var... (ill 4 frame utan nullazodik)
       5c26 - STA $5B2E
       95f9 - DEC $5B2E        ; csokkentjuk $5b2e-t, a frame counter-t
       95fe - INC $5B2E        ; ha negativ lett vissza 0-ra

; CODE block 3 ---------------------------------------------
5C6F:  5c54 - STA $5C6F
5C73:  5c4e - STA $5C73

; DATA block 4 ---------------------------------------------
5CE2:  5ccc - LDX $5CE2
5CE3:  5ccf - LDY $5CE3
5CE4:  5cd5 - LDX $5CE4
5CE5:  5cd8 - LDY $5CE5

Az előző kódrészlet értelmezése: Változó címe: hivatkozás címé - kód ami a változóra hivatkozik (írja és/v. olvassa).

A Rutinok hívás listájának összeállítása egyszerűen úgy történik, hogy a kódban megkeressük minden JSR, JMP és Bcc utasítást, és megnézzük hova mutat. A mutatott címnél feljegyezzük, hogy honnan ugrottunk oda. Végigfutva az egész kódon az eredmény egy hosszú lista amiben minden hivatkozott cím után van egy sor címünk, ahonnan hivatkoztunk rá. A kódban a hivatkozott kódrészlet elé raktam zárójelbe a hivatkozás helyet: [JMP] (JSR) {Bcc}

; CODE block 32 ---------------------------------------------
[7846, 8af1]				; JMP hivatkozas
(7bea, 7d9a, 8685, 8a82, 8ac4)		; JSR hivatkozas
.C:84d0  8D AF 84    STA $84AF		; szubrutin kod
.C:84d3  0A          ASL A
.C:84d4  0A          ASL A
.C:84d5  6D CE 84    ADC $84CE

Vannak meg indirekt ugrások a kódban, valamint táblázatból vett értékkel átírt ugró utasítások is, azokat majd a kód behatóbb elemzése után fogom tudni csak feltárni, de hát ugye ez egy nagy kirakós...

A változók (nevezzük így őket) összegyűjtése hasonló módon történik, azaz a kódot átnézzük, és kigyűjtünk minden olyan memória címet ahova ír/olvas a kód, és extraként feljegyezzük, hogy hol is történt ez a kódban, és milyen utasítással. Itt szintén marad egy vakfolt, mégpedig a táblázatokból feltöltött indirekt változók, ezeknek pontos műveleti területe (a változónak nevezett memóriaterület) egy ideig meg rejtély marad. A létrehozott lista nagyon hasznos, egyrészt átlátható melyik változóval hol és mit dolgozik a program, azt is lehet látni, hogy mennyire sűrűn használt egy változó, továbbá, megkönnyíti a konstansok megtalálását is (amiket sehol nem írunk, csak kiolvassuk az értekét). Ezek az információk mind-mind közelebb visznek a program megértéséhez. Csak érdekességképpen megjegyezném, hogy 952 változót használ a játék, továbbá 693 szubrutin hivast, 304 elágazást, és 1274 feltételes elágazást, mindezt 12299 kódsorban...

Apple Macintosh LC475

2015 Március 09.

Közben meglett az új Mac-em is, egy LC 475. Ez a pizzás doboz formájú LC sorozat legerősebb tagja. Azonos, 25 MHz-es órajelen átlagosan 3x gyorsabb mindenben mint az LC III-as, kivéve a lebegőpontos számításokat, mivel jelenleg egy M68LC040-es processzor van a gépben, amiből hiányzik az integrált FPU. A fullos proci, amiből semmi nem hiányzik, már úton van Németországból. Itt jegyezném meg, hogy ez a processzor, az utódjával együtt (68060) szerintem a valaha készített legszebb processzorok.

Motorola 68LC040
Motorola 68LC040teljes méret ↗

Na most a közvetlen VRAM manipulációs terveim kicsit megdőlni látszanak. Egyrészt tetszik, hogy amit a memóriába írok az rögtön meg is jelenik a képernyőn, másfelől viszont mivel az LC475 teljesen másképp kezeli a video memóriát, felvetődik a kérdés, érdemes-e a sok különböző architektúrára optimalizálni a kódot? Mivel a Head over Heels nem igényli az extrém gyors VRAM manipulálást, ezért egyértelműen NEM. Ezt a technikát inkább megtartom intro-k és demo effektek írásához. Tehát sokkal jobban járok, ha a rendszer kompatibilis képernyőkezelést választom (Offscreen GWorld), sebességben sokat nem vesztek, a játékban lévő animációkkal valószínűleg a Mac Classic is bőven elbír. Pont.

Izometrikus pálya renderelése

2015 Március 17.

Bármennyire is szórakoztató a c64-es kódvisszafejtés, rá kellett döbbennem, hogy túl hosszadalmas ezen a módon reprodukálni a játékot. Persze jó érzés, amikor pár órás kutatással megfejtek 1-2 algoritmust, és néhány ködös részlet kitisztul, mégis egy olyan komplex kód megfejtése ami egy izometrikus pályát tárol/renderel kihámozni az ismeretlenség homályából igencsak időigényes lenne, és annak hosszútávon már a munkám látná kárát.

Éppen ezért (és Krissz munkásságát, hatékonyságát látva) a talán hagyományosabb módját választottam a játékátírásnak: nézni, és leprogramozni:) Szóval legelőször az általam legrejtélyesebbnek ítélt problémát akartam megoldani, mégpedig az izometrikus pálya renderelését. Biztos vagyok benne, hogy sokfelé megoldása létezik a dolognak, így hat próbáltam valami használható információt fellelni a neten, ám a legrészletesebb leírás sem érintette az általam kérdéses részleteket. Leginkább sík, azaz 2 dimenziós tile-alapú pályák leképzéséről olvastam csak, azok meg teljesen egyszerűen 2 egymásba ágyazott for ciklussal megoldhatók. Viszont amikor függőleges irányba mozdul el egy test, akkor a for ciklusok kudarcot vallanak.

Két nap fejtörés után meglett a megoldás, és utólag nagyon örülök, hogy nem találtam meg más megoldását, mert sokkal jobb érzés mindig ha az ember maga jön rá a titok nyitjára. A megoldás fájdalmasan egyszerű: az egymásba ágyazott for ciklusokat, amik az éppen aktuális elem renderelő funkcióját hívják meg, nem is kell piszkálni. A renderelő rutinon kell csak kicsit változtatni úgy, hogy abban az esetben, ha az éppen kirajzolandó elem mögött van egy még le nem renderelt elem, akkor azt rendereljuk le először, és így tovább. Megpróbálom a problémát és megoldását vizuálisan is részletezni.

Az izometrikus nézettel sima 2D technológiával jeleníthetünk meg 3d-s teret, vagy egyszerű 2D-s játékteret jeleníthetünk meg kvázi 3d-ben. A szépsége a dolognak az egyszerűsége. Egy sima 2D-s tile alapú játékteret így alakítunk izometrikussá:

iso_1

Izometrikus térben, a sima 2D-s felülnézeti pályánk 3D hatasúvá válik. Ha az elemeket egymás tetejére pakoljuk, akkor valódi 3D-s teret alkothatunk, aminek megjelenítésére tökéletesen alkalmas az izometrikus nézet:

iso_2

Amíg minden elem rácsra van igazítva, addig a renderelés is pofon egyszerű, csak hátulról előre, és lentről felfelé kell haladni a rajzolással. Tehát például 3 egymásba ágyazott for ciklus szépen teszi a dolgát. Ha viszont mozgásnál szép folyamatos animációt akarunk látni, akkor nem ugorhatunk egyik pontról a másikra, hanem két rácspont között az utat több lepésben kell megtennünk, ahol a korábbi módszer már kudarcot vall:

iso_3

Ahhoz, hogy az elemeket minden helyzetben és kombinációban le tudjuk renderelni, csak annyi kell, hogy mielőtt bármelyik objektumot kirajzolnánk, ellenőrizni kell, hogy nincs-e mögötte olyan elem, ami meg nincs kirajzolva a képernyőre, ha van, akkor először azt kell kirakni. Hogy az átfedések vizsgálatát leegyszerűsítsem, és a renderelés ténylegesen a kamerához közeledve haladjon, a koordináta rendszert kicsit átalakítottam: isoX és isoY gyakorlatilag képernyő koordináták, míg az isoZ a képernyőtől való távolságot jelzi (példánkban minél nagyobb az isoZ, annál közelebb van hozzánk).

iso_4

Az új koordinátarendszerben számolva, a kirajzolandó objektumainkat sorba rendezzük Z,Y,X koordináták alapján növekvő sorrendben. Így a listában az első elem a leghátsó, legalsó elem lesz: tökéletes kiindulási pont. Egy ciklusra van csak szükségünk (a előbbi rendezés miatt), amely az elemek listáján végigmegy, és meghívja a renderelő funkciót az adott elem kirajzolásához. A renderelő funkció mielőtt kirajzolná az elemet, megnézi, van-e a fedésében olyan elem, ami esetleg még nincs kirajzolva, és ha igen, meghívja önmagát hogy az új elemet rajzolja ki előbb (és mivel rekurzív a függvény, az új elemnél is megnézi először, hogy van-e valami mögötte, és így tovább...).

Az izometrikus pálya renderelésének nehezén túl vagyok (legalábbis jól esik így hinni), most már csak az olyan apróságok kellenek, mint padlók, falak, ajtók renderelése. Majd azután jöhet a mozgás és animáció. De az nem ma lesz...

Grafika Remake 1.

2015 Március 21.

Most már biztos, hogy csak 2-szeres felbontás lesz! Sajnos kicsit nagy lesz 512x342-es felbontásnál, viszont a mozgások és az animációk időzítése miatt, irreális többletmunkát igényelne, ha 150%-os méretben is meg kéne csinálnom... Na ezen felbuzdulva, elkészítettem a falak remake-jét, lásd alább - A képre húzva az egeret, felsejlik az eredeti.

head over heels walls after
head over heels walls before
A falak — hover az eredetiért

A törekvésemen, miszerint az eredeti játékot a lehető legkevesebb módosítással reprodukáljam, egyre több csorba esik: először megpróbáltam a grafikát úgy felhúzni, hogy szinte azonos maradjon az eredetivel, csak az íveket, ferde vonalakat akartam simítani. Az eredmény elég kiábrándító lett: a vonalak túl vastagok voltak, és hiányolta a szemem a felbontástól elvárható részleteket. Ráadásul bizonyos elemeket kénytelen voltam részletezni, az meg nagyon elütött a korábbi technikával készített részektől. Ezért inkább elkezdtem következetesen elvékonyítani egyes vonalakat, és hozzáadni némi pixel koszt.

A pixelezés közben folyamatosan váltogattam az eredeti és új változat között és próbáltam úgy dolgozni, hogy hunyorítva szinte észrevehetetlen legyen a változás. Legalább úgy... Sokszor próbálkoztam 1-1 pixel hozzáadásával, törlésével, hogy az adott részlet kivehető legyen, térben jól helyezkedjen el. Bármilyen hihetetlen, de egy-egy pixel nagyon nagy jelentőséggel tud bírni, ha rossz helyen van simán előfordulhat, hogy megtörik egy ív, elváltozik egy arc, vagy csak megváltozik egy hossz. Tisztelem nagyon Bernie Drummond -ot aki negyed ennyi pixelből kihozta a grafikát, ott az előbb említettek hatványozottan érvényesek.

Volt egy olyan álmom is, hogy a készülő játék az eredetihez hűen szintén a lehető legkisebb futtatható állomány legyen. Magyarul legyen kicsi a file mérete. Az eredeti, kitömörítve is csupán 48 KB. Na most ez sok dolog miatt nem fog sikerülni. Kapásból duplázzuk a felbontást, ez ugye rögtön négyszeres növekedés a grafikai adatoknál. Ha még az animációkat is bővíteni akarom, az megint plusz adat. Ehhez hozzájön még az is, hogy az eredeti játék kényszerből (mert hogy nem volt több rendelkezésre álló RAM, és ragaszkodtak az 1 file-hoz, amiért áldom őket) olyan megoldásokat alkalmazott, hogy a 4 irányba néző objektumoknak csak 2 változatát tárolták, és futás közben tükrözték a figurát ha épp kellett. Ezzel is spóroltak sok memóriát, és file méretet. Ugyanez igaz a fal elemekre is, ajtókra... Na most 8 bites processzornál, frappáns módszerrel, egy 256 elemből álló fordító táblát generáltak, így kiolvasva a grafika egy byte-ját és azt indexként használva ezen a táblán, megkapták a 8 képpont tükörképet. Ez egy 68k processzornál szívás, mert a byte műveletek sok időt elpazarolnak, a grafika 4x annyi byte-ból van. 16 bites, 64 KB-os fordítótáblát is kellemetlennek tartanám, szóval 2 lehetőség tűnik ésszerűnek: eltárolom a grafikák tükörképét is, vagy a lassú byte alapú tükrözést elvégzem a program indításánál. Ez utóbbi nem csökkenti a program memória igényét, de legalább a file méret kisebb lesz, végül is ezt akartam... A lényeg, hogy futás időben nem tükrözök, mert nagyon lassítaná a játékot!

Néhány szó arról, miért is nem jó a 150%-os grafika méretnövelés: az eredeti játék pixelméretei, és sebessége tökéletes összhangban van a képernyő felbontásával. A képernyőn egy egységnyi terület bármely pontja a szomszédos terület azonos pontjától 16 pixelre van vízszintesen, és 8 pixelre függőlegesen. A térbeli függőleges egység 12 pixel magas. Ebből következik, hogy egységnyi távolságot az eredeti játék 8 lépésben tesz(tehet) meg síkban, és 6 lépésben függőlegesen, mindezt 12,5 fps sebességgel (50 Hz-es képfrissítésnél, minden negyedik frame-en volt mozgás). Ha duplázzuk a felbontást, akkor minden tökéletesen reprodukálható, csak duplázzuk a pixel elmozdulásokat. Ha finomítani akarom a mozgásokat, akkor ha minden második frame-en animálok, akkor arra is lehetőség van, mivel a dupla felbontásnál a lépésközöket tudom felezni. Tehát az eredeti játékban, a mozgás 1 fázisa alatt 2 pixelt lepek X, és 1 pixelt Y irányban, dupla felbontásnál ez 4 és 2 pixel, tehát tudom felezni, hogy növelhessem a másodpercenkénti képkockák számát. 150%-kal nagyított grafikánál, az alapegységek 24, 12 és 18 pixelre változnak. ezeket a távolságokat 8 egyenlő lépésben nehéz megtenni. Ha csak az lenne a cél hogy azonos idő alatt tegyen meg 1 egységnyi távolságot az ami mozog, meg lehetne oldani az fps módosításával, de akkor az animációink sebessége változna. Szóval megoldható, de sok módosítás és extra munka árán. "Nekem ez a kutya nem kell, ez gúnyolódik"

HOH Render Engine

2015 Április 1.

Az elmúlt tíz nap elég eseménydúsra sikeredett: először is nagyjából megterveztem a renderelőt, és a teljes játéklogikát, majd mikor már az utolsó kérdéses részlet is megoldódott, elkezdtem a programozást. Első körben kicsit átírtam a korábbi izometrikus objektum renderelőt, megváltozott a koordináta rendszer kissé, hogy egyes játék funkciókat mint például az összekapcsolt szobák, könnyebb legyen megvalósítani. Utána jött a maradék: padló, falak, ajtók. Bár semmiségnek tűnhet a tetszőleges elemekből álló izometrikus tér rendereléséhez képest, mégis legalább 2x olyan hosszú lett a kódja. Ezt bemutatandó, itt van ez a kis játszószoba:

North
door
East
door
South
door
West
door
X
size
Y
size
Main Room

Amit itt látunk, az gyakorlatilag egy háttér (kivéve persze az ajtókat), amit a renderelő 32 pixel széles oszlopokból allít össze, 2 menetben. Az egyik az északi fal mentén halad (bal felső) és magába foglalja a függőlegesen alatta levő padló és szegély részletet, a másik meg a keleti fal mentén halad hasonló módon. A renderelésen csavar egy kicsikét az tény, hogy a renderelő max 128x128 pixel meretű négyzetekből rakja ki a képet. Ez a "viewPort" módszer (ahogyan az eredeti játék is dolgozik) azért fontos, mert később, amikor majd kis elemek mozognak és animálódnak a játékban, ezeket a képernyő lehető legkissebb részletének frissítésével kell megoldani. Böngészőben, javascript-tel simán ki lehetne rajzolni a teljes kepernyőt minden frissítéskor, de a cél még mindíg a Mac Classic, ahol nem dúskálunk az erőforrásokban. A JS alapú verzó, amit be is fogok fejezni tulajdonképpen egy prototípus, ami alapjan majd elkészítem a C + ASM változatot. Ez persze a javascript kódban is meglátszik majd, hiszen sok programrészlet a végleges célplatform jellemzői alapján készül. Ugyanakkor vannak olyan részletek is, tipikusan a változók tárolásánál, ahol javascript-ben nem akartam bajlódni az utolsó bitek kispórolásával, ezekkel elég lesz játszadozni majd a CodeWarrior-ban.

A következő fázis most a renderelő befejezése lesz, olyan funkciók hozzáadásával mint a szövegek, grafikak képernyőre másolása, ami után elkészűlhet a menü rendszer, es a játék alatt a pontszámok/életek/stb. kiírása (OSD mint On Screen Display).

Bár az előző kis demón annyira nem látszik, de a renderelő lényege kész és nagyon jól működik (tökéleteset nem merek írni, 1 kis apró hibát már a demóban is észrevettem, és még tesztelnem kell berendezett szobában való mozgást is) képes az összes lehetséges szoba alaprajzot megjeleníteni az összes grafikai témában, képes bármilyen legális berendezést megjeleníteni, és abban a mozgó elemeket bárhol megjeleníteni úgy, hogy mindig a megfelelő elemek vannak takarásban. Legális alatt azt értem, hogy minden elemnek úgy kell elhelyezkednie, hogy ne lógjon bele más tárgyakba. A jaték során, erről az ütközésvizsgálat fog gondoskodni.

Helyzetjelentés

2015 Április 20.

Az elmúlt közel három hétben sokminden történt, többek között, elkészült a grafika 90%-a, már csak az animációk vannak hátra. Miután siketült belefáradni a kb 10 nap folyamatos pixelrajzolgatásba, hozzáláttam a következő fázisként beharangozott renderelő befejezéséhez - az gyorsan elkészült, nem volt egy komplex feladat, a lényeg már megvolt elötte is. Ezek után, elkezdtem a játék menürendszerét lekódolni, és örömmel jelentem, hogy ez is elkészült tegnap éjszaka. A program 640x480 es 512x342-es felbontasokra optimalizálva készül, a menüt is úgy állítottam össze, hogy ezen a két felbontáson legyen használható. A színektől eltekintve, gyakorlatilag teljesen azonos lett az eredetivel: http://iparigrafika.hu/hoh_proto/menu/

Menu remake screenshot
Menu remake screenshot

Lényegében már "csak" a játékmenet van hátra, és ez elég jó érzés, hiszen az összes olyan sallangot letudtam, amit más esetben a végére szoktam hagyni. Szóval a játékmenet: Játékosok irányítása, váltás a játékosok között, a pályák, azok adatainak tárolása, az ellenfelek mozgatása. Ezek lesznek a következő hullám főszereplői. Itt jegyezném meg, hogy ha ezt mind befejeztem, akkor következik a lényeg, hiszen amiről eddig beszéltem, az a Javascript verziója a játéknak, ami magától még nem fog Mac 68k platformon futni, tehát ezek után kezdődik egy kemény menet: C-ben újra létrehozni az egészet.

Game test screenshot
Game test screenshot

Helyzetjelentés újra

2017 Szeptember 24.

Meglepően sok idő telt el az utolsó frissítés óta, de semmi nem állt meg, sőt:)

Korábban már kifejtettem, hogy mennyire fontosnak tartom a kis file méretet, ezert idén év elején, elkezdtem kicsit foglalkozni az adattömörítéssel. Szívesen írnék róla bővebben, de megpróbáltam és eléggé rosszul ment: http://iparigrafika.hu/retrocomputing/packer/ (halkan megjegyezném, hogy az aktuális verzió már jobban tömörít, nincs olyan állomány a tesztgrafikák között, ahol a zip nyerne). Mindenesetre végül sikerült egy olyan tömörítő algoritmust összehozni, ami nagyon hatékony az én kisméretű 1 bites pixelgrafikáim esetében (konkrétan a zip-re köröket ver) és a kitömörítés is jó gyors (188 byte méretű 68k assembly kód, ennek készítéséről is szeretnek írni). Szóval a file mérete nem a grafika miatt lesz túl nagy, ha túl nagy lesz.

Az év elején készült még egy kis valami a régi gépekre, ez nem igazan köthető semmihez, csak szórakozás (sajnos a youtube széttömörítette, majd megoldom vagz nem):

Elkezdtem továbbá foglalkozni az animációkkal is, kb. 50%-on állok velük.

A hangokon is merengtem sokat. Nagyjából 2 megoldást tudnék elképzelni: Az egyik sample-lökkel működne, és kellene hozza írnom egy mod player (Igaz van egy forrásom hozza, de CodeWarrior alatt nem sikerült eddig lefordítanom). A másik megoldás, egy SID emulator írása (csak a hangchip emulációja kellene, mert nem az eredeti c64 kód játszaná le a zeneket, hangeffekteket). Az utóbbi megoldás azért szimpatikusabb, mert sokkal kisebb méretben meg lehetne valósítani mint hangmintákkal, és amúgy is nagyon szeretem az eredeti játék hangjait, de jó lenne azok kíséretében játszani. Az utóbbi hátránya, hogy egyelőre közelébe se kerültem annak, hogyan tudnám a SID chip-et emulálni. De mint eddig minden, ez is meg fog oldódni valahogyan, én optimista vagyok!

UPD: Az eltelt időben sok más vonalon is haladt a dolog. A kód visszafejtesben is előrébb járok, minden grafikai részlet meglett már rég (mint a pislogás pl. és a pálya szelei), továbbá a pálya adatok is megvannak, bár csak részlegesen tudtam eddig visszafejteni a kódot, ami az adatokból pályát hoz létre...

A hang és SID dolgok

2017 December 30.

Még mindig nem döntöttem el, hogyan valósítsam meg a hangokat a végleges játékban, viszont azért haladtam az alapokkal. Egy kisebb keresgélés után rátaláltam Hermit csodalatos jsSID player-ere, ahogy a neve is utal rá javascriptben van írva, böngészőben fut. Egyrészt már ez is nagyon sokat lendített előre a lehetőségek feltérképezésében, de elkezdtünk levelezni, és nagyon sok technikai segítséget kaptam Hermit-től a SID emulációval kapcsolatban. Ha már van emulátor a SID-hez, az nagyon jó, ezt majd később portolnám 68k assembly-be. A következő lépés az volt, hogy az eredeti c64 zenét, zenelejátszó rutint visszafejtettem - meglepően frappánsan ment, kb. 2-3 nap alatt meg is volt. Ebből készítettem egy javascript lejátszó rutint (a zenei adatokban eredetileg sok mutató volt közvetlen memóriacímekre, ezert azt is szét kellett boncolni, a mutatókat átírni, és hobbiból kicsit átstrukturálni): http://iparigrafika.hu/emu/hoh_sconv/hoh_jsplayer.html.

Van zenelejátszó, van SID emulator. Most kéne elkészítenem ezek portjait is a célplatformra (68k Mac), de addig is, kicsit úgy érzem necces 68030@16 MHz-en, úgy, hogy emellett még a játéklogikát, és grafika megjelenitését is, azaz a teljes játékot futtatni kell. Tehát megállás nélkül próbálok alternatívakat találni, és ilyenkor a leglehetetlenebb dolgok is eszembe jutnak, például milyen lenne ha a SID-et valahogy a régi Mac-hez tudnám kapcsolni. Mivel mostanában töltöttem pár órát Arduinozassal, kézenfekvő megoldásnak tűnt, ezert elkezdtem kutakodni SIDshield-ek ügyében, találtam kettőt (RealSIDShield, SID-Shield for Arduino), ami elég jónak tűnt, de azok csak lelkes próbálkozások voltak, nem lehetett megrendelni őket. Jó megoldás volt mindkettő, de valamiért nekem nem felelt meg. Zavart, hogy egy plusz IC-vel volt megoldva a SID és az Arduino közötti kommunikáció. Én még az Arduino-t is kihagynám a képletből legszívesebben, de annak van értelme, ha többféle számítógéppel terveznem összekötni a tákolmányomat. Tehát valamilyen gép -> soros csatlakozás -> Arduino -> SID. Magyarul az Arduino UNO-nak kellene (mert éppen ilyenem van) közvetlenül meghajtania a SID-et. Végül erre is sikerült találnom egy jó példát: http://www.deblauweschicht.nl/tinkering/mos6581_1.html. A kapcsolásban minimális módosítás után, és teljesen új kód használatával született meg az alábbi csoda:

Kicsikét bővebben a működéséről

a SID-nek a működéshez a következőkre van szüksége: +5V, +9V (SID 8580) vagy +12V (SID 6581). Kell neki továbbá egy 1 MHz-es jel (phi2), ami a belső működéséhez elengedhetetlen. Kell neki egy Reset (RES) jel. Kell 5 cím és 8 adatvonal. Jelezni kell neki továbbá, hogy írni vagy olvasni akarjuk (R/W), és azt is, hogy mikor állnak készen a bemeneti adatok (CS) (cím és adat íráshoz v. olvasáshoz).

SID original schematic
SID original schematic

Az Arduino-hoz a SID-et az alábbi (pin-láb) kiosztással csatlakoztattam:

Arduino  SID        Arduino  SID
5V_______25 (Vcc)   D2_______15 (D0)
GND______14 (GND)   D3_______16 (D1)
Vin______28 (Vdd)   D4_______17 (D2)
                    D5_______18 (D3)
A0_______ 9 (A0)    D6_______19 (D4)
A1_______10 (A1)    D7_______20 (D5)
A2_______11 (A2)    D8_______21 (D6)
A3_______12 (A3)    D9 pwm___ 6 (Phi2)
A4_______13 (A4)    D10______22 (D7)
A5_______ 5 (RES)   D11______ 8 (CS)
                    D12______ 7 (RW)

Az Arduino Vin kimenetét használom 9V-ként, ezért fontos, hogy ne USB-ről, hanem a tápcsatakozóra kötött 9V-os tápról kapjuk az áramot. Ezt a feszültséget nem szabályozza az Arduino, úgyhogy mindenképpen merjük ki mielőtt a SID-hez érhetne!

Mivel én SID 8580-nal "dolgozom", ezért a CAP1 és CAP2 -re kötött kondikat 22000pF (22nF)-ra cseréltem, és az 1K-s ellenállást nem használom a hangkimenetnél, az csak a 6581-nek kell. Az egész hangkimenet más mint ahogyan a fenti rajzon látszik, azt az eredeti C64 kapcsolási rajz alapján készítettem, lásd alább. A tápbemenetekhez közel elhelyeztem 1-1 .1uF-os kerámia kondit, a nem használt analóg bemenetekhez meg 1000pF-osakat.

SID Audio OUT original schematic
SID Audio OUT original schematic

A cím (A0-A4), az adatbusz (D0-D7), a RES, a R/W és a CS szerepét az Arduino egy-egy digitális kimenete látja el. Az 1 MHz-es jelet is könnyen előállíthatjuk az egyik PWM digitális kimeneten. Tehát nincs más dolgunk, mint hogy a megfelelő időben a megfelelő digitális kimeneteket állítgassuk.

A kódról röviden: nem kell külső lib, egyszerű mint a faék, és elég gyors.

#define pSID_RES  A5    // Reset
#define pSID_PHI2 9     // 1 MHz
#define pSID_CS   11    // Chip Select
#define pSID_RW   12    // Read/Write

#define pSID_D0   2     // Data
#define pSID_D1   3
#define pSID_D2   4
#define pSID_D3   5
#define pSID_D4   6
#define pSID_D5   7
#define pSID_D6   8
#define pSID_D7   10

#define pSID_A0   A0    // Address
#define pSID_A1   A1
#define pSID_A2   A2
#define pSID_A3   A3
#define pSID_A4   A4

Miután a rend kedvéért definiáltuk a használt pin-eket, beállítjuk mindet kimenetre, hiszen olvasni nem kívánunk (legalábbis én).

pinMode(pSID_RES, OUTPUT);
pinMode(pSID_PHI2, OUTPUT);
pinMode(pSID_CS, OUTPUT);
pinMode(pSID_RW, OUTPUT);

pinMode(pSID_D0, OUTPUT);
pinMode(pSID_D1, OUTPUT);
pinMode(pSID_D2, OUTPUT);
pinMode(pSID_D3, OUTPUT);
pinMode(pSID_D4, OUTPUT);
pinMode(pSID_D5, OUTPUT);
pinMode(pSID_D6, OUTPUT);
pinMode(pSID_D7, OUTPUT);

pinMode(pSID_A0, OUTPUT);
pinMode(pSID_A1, OUTPUT);
pinMode(pSID_A2, OUTPUT);
pinMode(pSID_A3, OUTPUT);
pinMode(pSID_A4, OUTPUT);

Az 1 MHz-es jel előállítása Arduino UNO-n, ami 16 MHz-en ketyeg:

TCNT0  =  0;                        // Reset timer
TCCR1A =  _BV(COM1A0);              // Set PORTB1 to toggle on OC1B compare match
TCCR1B =  _BV(WGM12);               // CTC mode, OCR1A as TOP
OCR1B  =  0b0000000;
OCR1A  =  0b0000111;                // Set counter TOP to 8-1 = 7. For a 16MHz. clock this = 1 Mhz.
TCCR1B |= _BV(CS10);                // set prescale to div 1 and start the timer

Az inicializálás végeként beállítjuk a CS és RW jeleket és reset jelet küldünk a SID-nek:

digitalWrite(pSID_CS, HIGH);        // Set SID Chip Select HIGH (idle)
digitalWrite(pSID_RW, LOW);         // Set SID R/W LOW (Write)
digitalWrite(pSID_RES, LOW);        // Set SID RESET LOW (reset)
delayMicroseconds( 200 );           // Wait a bit...
digitalWrite(pSID_RES, HIGH);       // Set SID RESET HIGH

Most már nincsen más dolgunk, csak kódból írni a SID regisztereket, és ugyanúgy fog viselkedni mint C64 alatt. Ezt megkönnyítendő, itt van pár segédfunkció:

void setData( byte data ) {
  digitalWrite(pSID_D0, data&0x01 );
  digitalWrite(pSID_D1, data&0x02 );
  digitalWrite(pSID_D2, data&0x04 );
  digitalWrite(pSID_D3, data&0x08 );
  digitalWrite(pSID_D4, data&0x10 );
  digitalWrite(pSID_D5, data&0x20 );
  digitalWrite(pSID_D6, data&0x40 );
  digitalWrite(pSID_D7, data&0x80 );
}
void setAddr( byte addr ) {
  digitalWrite(pSID_A0, addr&0x01 );
  digitalWrite(pSID_A1, addr&0x02 );
  digitalWrite(pSID_A2, addr&0x04 );
  digitalWrite(pSID_A3, addr&0x08 );
  digitalWrite(pSID_A4, addr&0x10 );
}
void writeSID( byte reg, byte val ) {
  setAddr( reg );                   // set addr pins
  setData( val );                   // set data pins
  digitalWrite(pSID_CS, LOW);       // data and addr ready, set CS LOW: data/addr read by SID
  digitalWrite(pSID_CS, HIGH);      // and set CS HIGH again...
}
void resetSID() {
  for( byte i=0; i<25; i++ ) writeSID( i, 0 );
}

A teljes Arduino sketch a Head over Heels zenéjével letölthető innen. Megjegyzéskent szerepeljen itt annyi, hogy EEPROM-ban tárolok egy változót, amit minden ujraindítás után növelek eggyel: ez a sorszáma a subtune-nak amit lejátszik. Összesen 14 van. Volt már olyan változat is ahol 3 ledet vezéreltem a 3 hangcsatorna GATE bitjének állapota alapjan...

Hozzátenném viszont, hogy a SID reset-elése nem 100%-os, nem minden esetben reset-elődik rendesen, aminek eredményeképpen az ADSR "inaktív", az envelope generator nem működik, és minden hangszín a gate bit bekapcsolásával egyidőben teljes hangerővel megszólal. Ha ezt kiderítettem miért csianlja, frissítem a cikket.

Mini Demo Mac Classic-ra

2020 Szeptember 18.

Olyan regen irtam, hogy mar szinte szegyellem folytatni, de most akkor ezen tul is lendulnek: szoval, tavaly a nagy karacsonyi szunetben vegre megvalosult egy apro almom, hogy egy anno javascript-ben megirt hullamzo scroller effect-et atportoltam Mac-re, 100% Assembly kod az egesz, es a magam szintjen eleg komplex kis 16 KB-os program lett.

A kiindulasi pont, a Javascript-ben megirt effekttel az 1. problema az volt, hogy a hullamgorbet rengeteg sin/cos szamitassal, szorzassal, osztassal vegezte a kod, es ebbol elegge sokra volt szukseg, es minden frame-ben, a rajzolas elott ezt ki kellett kalkulallni. A masodik problema pedig, hogy a karaktereket runtime forgatta (szamolta ki a pixeleket), ami elforgatott karakterenkent 4096x(sin/cos/osztas/szorzas/gyokvonas/stb). Ezek egyike sem portolhato egy 16 MHz-es gepre, ha tartani akarom a vsync-et.

Felmerult, hogy szepen kiszamolom elore az elforgatott karaktereket, es ugy... De a 720 KB-nyi adatmennyiseg, egy ilyen scrollerhez, egy ilyen regi gepen nevetsegesnek tunik nekem, aki amugy is mindig a leheto legkisebb file-meretre torekszik. Maradt az a megoldas, hogy M68k assemblyben forgatok. Ehhez ki kellett talalnom, egy kisebb, elore kiszamolt ertekeket tartalmazo tablazatokkal dolgozo eljarast. Az eredeti elforgato rutin, vegigfut a (64x64px) 4096 pixelen, kiszamolja a kozepponttol valo tavolsagot (pixelben), meghatarozza a ramutato vektornak a szoget, ehhez hozzaadja az elforgatas merteket, az uj szogbol, es a tavolsagbol visszaszamolja a forras pont helyet: amilyen pixelt ott talal az elforgatatlan karakteren, az lesz az adodd pixelen (ahonnan indultunk). Ez ugy lett leegyszerusitve, hogy 3 elore kiszamolt tablazatot hasznalok, az elso a pontok tavolsaga a kozeppontol, a masikban a szoguk, a harmadik, a szog alapjan, beszorozva a tavolsaggal megadja az elforgatott pixelpoziciot. Az osszes tabla, es grafikai adat amit hasznal a program 10 KB-ra tomoritve van tarolva (MPackerX ami a kis javascriptes prototipusbol app-pa fejlodte ki magat, lett commandline tool Mac OS, es Linux ala, valamint egy swift-re portolt Mac OS verzio, GUI-val), a maradek 6 KB az assembly kod, amiben maradt par extra, ha jol emlekszem (pl egy sajat PRINTF implementacio, igaz az kb 200 byte, egy debug font)

; RSRC     packed/unpacked    filename
; $80         476     2688    16x14_terminalfont_LR2.pkx
; $81         164     1116    apple_logo_96x93_LR2.pkx
; $82        1700    16896    64x48_funnyscrollfont.pkx
; $83        2560     8192    rotTA_NR2.pkx
; $84         564     2048    rotTRS_NR8.pkx
; $85        3174     8192    rotTR_.pkx
; $86         322     1024    distsc.pkx
; $87        1714     2434    sin_sina_mul_dirs_NR2.pkx
;-------------------------
;           10674    42590
		

Masik erdekesseg a rendereles (most atugrottam a hullamgorbe szamolasat, pedig az is erdekes), merthogy igaz csak 13 karaktert rajzolunk ki, de az a kepernyo tetejetol az aljaig barhol lehet. Es mivel nem csak kirajzolni kell, hanem torolni is (mivel EOR-ral rajzolunk, ua a rutin csinalja) ezert 26 karakter inkabb. Szoval Y poziciojuk alapjan rendezett sorban vegigfutunk a karaktereken, es rajzolunk (nem is volt erdekes igy utolag...).

A játékmenet fejlesztése

2026 Szeptember 29.

Hat év csend után végre a legnagyobb hiányzó darab is a helyére került: a játékmenet. A menü, a grafika, a renderelő és a hang (Hermit, azaz Horváth Mihály CSID_light SID emulátorával) már régóta megvolt, a szereplők irányítása, a tárgyak, az ellenfelek és a szobák logikája viszont nem. Ezt most Claude Opus 5.5-tel írattam meg, hasonlóan a Back to the PET-hez: az irányt, a szabályokat és a döntéseket én adtam, a játékmenet kódjának visszafejtését és megírását pedig ő végezte — az egészet a meglévő remake-re építve.

Head a kiinduló szobában
Head a kiinduló szobában

A kulcs egy felismerés volt, ami korábban elkerülte a figyelmemet: a C64-es Head over Heels rutinról rutinra a ZX Spectrum-os eredeti átirata. Ugyanaz a 18 bájtos objektumrekord, ugyanazok a táblázatok, ugyanaz a logika. A Spectrum-os változatnak pedig van egy nyilvános, felkommentezett disassembly-je, így az szolgált térképként, a C64-es lista pedig döntőbíróként: a teljes játékmenet-kódot utasításról utasításra összevetettük a kettőt. Ahol a C64-es port eltér a Spectrum-ostól, ott a remake a C64-est követi: nincs ugrás-hang, nincs pittyenés leérkezéskor, a C64 saját (kicsit hibás) véletlenszám-generátora dolgozik, és a győzelmi szoba időzítése is a C64-es. A 301 szoba pályaadatait a C64 natív pályaadataival is összevetettük: mind egyezik.

A kód sima C, struktúrákkal, osztályok nélkül, mert a végcél továbbra is a 68k assembly változat a régi Mac-ekre, és így szinte sorról sorra át lehet majd írni. A képernyő egy 640x480-as, 1 bites bitkép, amin csak a megváltozott területek rajzolódnak újra — pont úgy, ahogy az eredeti is csinálja, és ahogy egy régi Mac-en is kell majd.

Head és Heels egymáson
Head és Heels egymáson

30 képkocka másodpercenként

Az eredeti játék az 50 Hz-es gépen minden negyedik képkockában lép egyet, vagyis 12,5 fps-sel fut. A remake-ben minden lépés két fél lépésre bomlik (a koordináták duplázásával), így 30 fps-sel (60 Hz-en) fut, és itt jöttek jól a 2015-ben megrajzolt köztes animációs fázisok. Az útvonalak és az időzítések ugyanazok maradtak: lépésről lépésre összevetettük a 12,5 fps-es változattal. A sebesség így az NTSC-s C64-ével egyezik (a PAL-nál 20%-kal gyorsabb), fordításkor viszont választható a 25 fps is, ami pontosan a PAL sebessége.

A tesztelés nagy részét egy beépített tesztkörnyezet végezte: a program képernyő nélkül, előre megírt billentyűleütésekkel játszik, képernyőképeket ment, és lépésenként kiírja a szereplők állapotát. Egy véletlenszerűen játszó teszt mind a 301 szobában játszik egy kicsit, és közben figyeli az objektumlisták épségét — ez több olyan hibát is megfogott, amit kézzel játszva talán sosem vettem volna észre.

Könyvespolcok között
Könyvespolcok között

Ami az eredetiben nincs

Trainer módban: 99 fánk, Head éppen lő
Trainer módban: 99 fánk, Head éppen lő

A méretre továbbra is kényes vagyok: a macOS-es futtatható állomány kb. 120 KB, és ebben benne van az összes grafika (a fent említett saját tömörítővel) és a pályaadat. A Windows-os exe 159 KB.

Letöltés

Mindhárom platformra elkészült egy csomag, mindegyikben ott van az SDL2 is, telepíteni nem kell semmit. A csomagok az oldal alján vannak, mindig a legfrissebb változattal.

Csontvázak a falon
Csontvázak a falon

Térkép

A tesztkörnyezet egy újabb trükköt is kapott: minden szobát lefényképez úgy, ahogy a játék kezdetén kinéz — a tárgyakkal és az ellenfelekkel, de a szereplők nélkül. Ezekből készült el a játék térképe: mind a 301 szoba a helyén, a játék axonometrikus terében, a csak teleporttal elérhető szobacsoportok kicsit távolabb, a teleportok pedig vonalakkal összekötve.

Mi jön most? Játszani és tesztelni, és ha minden rendben van, jöhet az, amiért ez az egész elindult: a 68k-s változat a régi Mac-ekre.

Frissítés: ki kit takar?

2026 Október 1.

Játék közben észrevettem, hogy egy szereplő néha úgy is megállhat, hogy egy tárgy rajzolódik elé, pedig nem is mögötte áll. Ez a konkrét eset végül rendben volt: Heels a dob mögötti mezőn állt, csak a nézőpont miatt tűnt úgy, mintha mellette lenne. A keresgélés közben viszont előkerült egy valódi hiba, ami az eredeti rajzolási sorrendből jön, így a C64-es és a Spectrum-os játékban is ott van. Az eredeti a tárgyakat páronként összehasonlítva szúrja be egy rendezett listába, és ez néha ellentmondásos sorrendet ad: ha például a szereplő egy blokk szélén áll, a blokk mögött pedig egy másik tárgy van, a blokk széle a szereplő lábára vagy gallérjára rajzolódik. Az ajtónyílásban álló szereplőre pedig az ajtó alatti lépcső rajzolódhatott rá.

Előtte és utána: balra a blokk széle átrajzol Head gallérján, jobbra már jó a sorrend
Előtte és utána: a blokk széle Head gallérján

A remake most minden újrarajzolt területen a tárgyak térbeli helyzete alapján dönti el, mi kerül előre: ami közelebb van vagy feljebb, az később rajzolódik. Csak a rajzolás változott, az objektumlisták és a feldolgozás sorrendje maradt az eredeti, így a játékmenet lépésről lépésre ugyanaz. Egy teszt mind a 301 szobában végigvezeti Head-et az összes elérhető ponton, három magasságban, és minden helyzetben ellenőrzi a sorrendet. Egyetlen ajtónyílás maradt, ahol a szereplő egy blokk alatt áll, és három tárgy körben takarja egymást: ott nincs jó sorrend, így az úgy néz ki, mint eddig. A macOS-es futtatható állomány mérete közben nem nőtt, a letölthető csomagok pedig már a javított változatot tartalmazzák.

Frissítés: nagy szobák, nagy ablak

2026 Október 4.

Az előző frissítés óta több kisebb dolog is bekerült. Ha Heels felsétált egy blokkra, az előtte lévő, ugyanolyan magas blokk eltakarta a mancsa első pixeleit: mostantól a tárgy felett lévő szereplő mindig a tárgy előtt rajzolódik. Az állapotsor és a szoba 16 pixellel feljebb került, mert egy 640x480-as ablakban az állapotsor túl közel volt a kép széléhez. A PLAY THE GAME menüben pedig megjelent a CONTINUE: ha a szünetből (Esc, Esc) kilépünk a játékból, onnan folytathatjuk, ahol abbahagytuk — és mivel a játék kilépéskor el is mentődik, a CONTINUE a következő indításkor is ott lesz.

A nagyobb változás a nagy szobáké. Néhány szoba több szobából áll össze, és nagyobb a képernyőnél: a C64-en ezek egyszerűen kilógtak a képből. A remake most egy 1024x672-es vászonra rajzol, aminek a közepe a 640x480-as játékképernyő. Egy nagyobb ablak vagy a teljes képernyő a szoba többi részét is megmutatja, a menük és az állapotsor pedig a helyükön maradnak. Mind a 301 szobát lemértük: a legnagyobb is elfér a vásznon. Egy 640x480-as ablak ugyanannyit mutat, mint eddig.

A 7A1-es szoba egy nagyobb ablakban, a szaggatott vonal a 640x480-as játékképernyő széle
Egy nagy szoba egészben; szaggatott vonal: a 640x480-as képernyő

Amikor először láttam egy ilyen szobát egészben, a bal oldali ajtónak csak az egyik oszlopa volt meg. Kiderült, hogy ez nem rajzolási hiba: az eredeti játék a szoba felépítésekor csak azokat a tárgyakat hozza létre, amelyek belelógnak a 256 pixel széles látóterébe. Ami azon kívül esik, az nem csak nem látszik — nem is létezik. A 301 szobából 22-ben összesen 136 ilyen tárgy van: 17 ajtóoszlop, blokkok, dobok és néhány ellenfél (a fenti szobában a bal szélső ajtó oszlopa, két dob és egy ellenfél). A remake-ben most mind ott vannak. Ez tudatos eltérés az eredetitől: ezekben a képen kívül eső részekben a játék kicsit másképp megy, mint a C64-en, ahol a szereplő ott a semmin sétált keresztül.

Egy rajzolási hibát pedig az előző frissítés hozott: az ajtóoszlop doboza csak 18 egység magas, a képe viszont a boltív felét is hordja, 24 egységig. Ha egy szereplő egy elülső ajtó mögött zuhant lefelé, a boltív magasságában „az oszlop felett” volt, ezért elé rajzolódott. Most az ajtóoszlop a sorrend szempontjából olyan magas, mint a boltíve, így a boltív eltakarja a mögötte zuhanó szereplőt.

Előtte és utána: Head az ajtó mögött zuhan; balra a boltív elé rajzolódik, jobbra a boltív takarja
Előtte és utána: Head zuhan az ajtó mögött

A macOS-es futtatható állomány továbbra is kb. 120 KB. A letölthető csomagok már ezt a változatot tartalmazzák; a Linux arm64-es csomag eddig a játék indításakor lefagyott (ARM-on a C nyelv char típusa előjel nélküli), az új már jó.

E-papír, Playdate és egy kisebb játék

2026 Október 13.

Az előző frissítés óta eltelt kilenc nap mozgalmasra sikerült. Claude tovább dolgozott a projekten, és a játék most már e-papír kijelzőn és Playdate-en is fut, elkészült az 512x342-es változat a kompakt Mac-ekhez, 11%-kal kisebb lett úgy, hogy közben pontosan ugyanaz maradt, az adataihoz szerkesztők készültek, és a 68k-s Mac-es változat első darabja — a hang — már szól egy (emulált) Classic II-n. Sorjában:

Head over Heels e-papíron

Egy Raspberry Pi 5 egy Waveshare 7,5"-os e-Paper kijelzővel: 800x480 pixel, 1 bit, fekete-fehér — akárcsak a Classic II képernyője, csak papíron. A kérdés az volt, hogy egy ilyen panel bírja-e az eredeti játék másodpercenkénti 12,5 képét. Az e-ink változat PAL sebességgel fut, és az eredeti játék minden lépésének végén átad egy képet a panelnek, így a panel pontosan azokat a lépéseket látja, amiket a C64 is kirajzolt. A panelre csak a megváltozott téglalap megy ki (SPI-on, közvetlenül a Linuxon keresztül, külső könyvtár nélkül), és csak ez a téglalap frissül.

A panel saját, leggyorsabb részleges frissítése 380 ms, így csak minden negyedik kép jutott ki rá. A vezérlőnek viszont saját hullámforma is adható: milyen feszültséget kap egy pixel képkockáról képkockára, a régi és az új színétől függően. Egy rövid, 10 képkockás hullámforma 200 Hz-en 80 ms alatt lefut, és a panel lépést tart. Minden új képernyő (szoba, menüoldal) teljes frissítést kap, ami egyszer átvillantja a panelt, és eltünteti a korábbi képek szellemképét; ezalatt a játék vár, hogy senki ne induljon el vakon. Az F12 bármikor tisztára frissíti a panelt.

részleges frissítésfrissítési időmegjelenő képek
a panel saját hullámformája380 ms2,6 másodpercenként (4-ből 3 kimarad)
saját, 16 képkocka 200 Hz-en112 ms8,3 másodpercenként
saját, 10 képkocka 200 Hz-en80 ms11,4 másodpercenként (lépést tart)
A kiinduló szoba az e-papír kijelzőn, ahogy a labor fényképezőgépe látja
A kiinduló szoba az e-papíron, a labor fényképezőgépén át

E-ink kutatás: fényképezőgép figyeli a panelt

Két dolog nem volt rendben. A legnagyobb szobák a teljes frissítés után tönkrementek: kifakultak, és szürke csíkok futottak le a panel teljes magasságában. A mozgó szereplők pedig szürke nyomot hagytak maguk után. Találgatás helyett építettünk egy kis labort. Egy Sony A6000 a Mac USB-portján, gphoto2-vel távirányítva, lefényképezi a panelt (a macOS saját kamerakezelő szolgáltatásai azonnal lecsapnak minden fényképezőgépre, ezért ezeket egy ciklus 50 ms-onként leállítja, amíg a gphoto2 megnyitja a gépet). Egy panelpixel kb. 7,3 kamerapixel. A panelen megjelenített 16x10-es fehér pontrácsból kiszámolható, hogy melyik panelpixel hová esik a fotón: egy harmadfokú polinom, 0,6 kamerapixel pontossággal — ez egy panelpixel tizede. Ezután a panel mind a 384 000 pixele kiolvasható egy fotóból, és összevethető azzal, aminek ott kellene lennie: a kontraszt, a szürke „szellem” pixelek, és hogy a mozgó pixelek mennyire jutottak el a feketéig és a fehérig. Egy laborprogram a Pi-n a panel vezérlőjének minden fontos regiszterét beállítja: a hőmérsékletet, amit a vezérlő hisz, a boostert, a feszültségeket, a képfrekvenciát, a hullámformákat.

Az F98-as szoba teljes frissítés után: balra a régi booster-beállítással, kifakulva és csíkosan, jobbra a javítással
Az F98 a booster javítása előtt és után
a fotókon mérveelőtteutána
az F98 és a többi nagy szobatönkrementtiszta
Head sétál: kimaradt képek / maradó szürke pixelek49-ből 10 / 310 / 1
lift 10 másodpercig: kimaradt képek / maradó szürke pixelek145-ből 53 / 23934 / 302
teljes frissítés (a játék vár)1,5 s1,5 s (a saját gyorssal: 0,7 s)
A padló, ahol Head sétált, közelről: előtte világos foltok és Head szellemképe az ajtóban, utána tiszta
Head nyoma a padlón: előtte / utána
A 039-es szoba, miután a lift 10 másodpercig fel-le járt: előtte a lift szürke szellemképe, utána alig látszik
A lift szellemképe fekete padlón: előtte / utána

Menet közben kiderült, hogy a pontszám-képernyő 5 másodpercenként teljes frissítést csinált: egy képkockaszámláló bájt volt, és 256 képkockánként átfordult. Még nem próbáltuk ki: a panel hőmérsékletét (a saját hullámformáink nincsenek hőmérséklet szerint kompenzálva), a gyorsabb SPI-órajelet és a panel valódi VCOM feszültségét.

512x342: a kompakt Mac-ek képernyője

A 68k-s változat fő célpontja a kompakt Mac-ek (Plus, SE, Classic, Classic II) 512x342-es képernyője, így most már a C változat is lefordítható rá. Az elrendezése az, amit 2015-ben a JavaScript-es remake 512-es módjához készítettem: annak menüi, és az állapotsora a két alsó sarokban, 24x24-es mini sprite-okkal (ezek végig ott voltak a sprite-adatban, csak a régi táblázatban rossz helyen). A látótér az eredeti képernyő teljes, 256 pixeles szélessége és a 192 sorából a felső 171, duplázva; az alul kimaradó 21 sorban van az eredetiben az állapotsor. Az ablak csak az 512x342 egész számú többszöröseit veheti fel, így a pixelek élesek maradnak.

Az 512x342-es változat: Head a kiinduló szobában, a mini sprite-ok a két alsó sarokban
Az 512x342-es változat

Kisebb, és pontosan ugyanaz

Egy átvizsgálás, hogy hol lehetne rövidebb a kód, 160 helyet talált, többnyire többször megírt kódot: a szoba négy falát, a négy ütközésvizsgáló rutint, a menüoldalakat. A játék kódszegmense 92 256 bájtról 81 696-ra csökkent, hat helyett öt 16 KB-os lapra; a macOS-es futtatható állomány 120 KB-ról 103 KB-ra, benne az összes grafikával, hanggal és pályaadattal. A grafikát most az MPackerX új változata tömöríti (optimális parse-olással, ugyanabban a formátumban, amit a C64-es, a 68k-s és a JavaScript-es kitömörítő is olvas), minden sprite-blokkot külön folyamként, úgy, ahogy a 68k-s változat resource-ként fogja betölteni őket.

Honnan tudjuk, hogy a játék ugyanaz maradt? Egy regressziós tesztkörnyezetből: a tesztváltozat minden képkockában kiírja a képernyő, az ablakba küldött kép, a teljes játékállapot és a hangok hash-ét. 1384 forgatókönyv (véletlenszerű játék mind a 301 szobában, teljes játékok a menütől, mentés és visszatöltés, minden szoba képe, a rajzolási sorrend tesztjei) fut le a régi és az új változattal, 5 beállításban (30 és 25 fps, 512x342, trainer nélkül, és a kiadott változat optimalizálásával), és ezeket képkockáról képkockára összevetjük: 0 eltérés.

A macOS-es csomagból most kettő van, egy Apple Silicon-os és egy Intel-es, mindkettő fele akkora, mint a régi univerzális, és az Apple hitelesítette (notarization) őket: dupla kattintással megnyílnak.

Az adatok fájlokban, szerkesztők a böngészőben

A játék adatai most egyszerű fájlokból jönnek: a sprite-lap és a betűkészlet PNG képként, a hangadat binárisként, a bevezető szöveg szövegként; egy fordítási lépés becsomagolja, ami változott. Most már a hangadat is tömörítve van: 3263 helyett 1953 bájt a futtatható állományban. És két szerkesztő is készült hozzájuk, egyszerű HTML és JavaScript, ami a lemezről is megnyílik:

A pályaszerkesztő Head kiinduló szobájával, a 8A4-essel
A pályaszerkesztő
A zeneszerkesztő a főcímzene három szólamával
A zeneszerkesztő: a főcímzene

Két kisebb dolog: a főmenü előtt egy bevezető oldal mondja el, miről szól a projekt, a Cattail Scroller terminál-betűkészletével írva; trainer módban pedig a felső sarkokban látszik a szoba, a pontszám, a bejárt szobák száma és a megmaradt hal-mentések (F1-gyel elrejthető).

Head over Heels Playdate-en

Ugyanaz a játék Playdate-en: 400x240-es, 1 bites képernyő és egy 168 MHz-es Cortex-M7 processzor. A játék kódja ugyanaz, csak a platformréteg új, a harmadik képernyő-elrendezés pedig asztali gépre is lefordítható, így a szokásos eszközökkel tesztelhető. A szobák nagyobbak a képernyőnél: a nézet ott indul, ahol az eredeti játék mutatja a szobát, és csak annyit görget, hogy a szereplő a képen maradjon. A kurblival körül lehet nézni: fel és le, és ha egy szélén tovább tekerjük, a nézet oldalra megy tovább, aztán megint fel vagy le, kígyóvonalban az egész szobán. Az állapotsor a szoba fölött van, minden eleme 2 pixeles fekete körvonallal. Az A ugrik, a B visz vagy lő, az A és a B együtt vált a két szereplő között. A posztert és a főmenüt én rajzoltam.

Head és Heels egymáson a Playdate képernyőjén
Head és Heels egymáson, Playdate-en

A hang igényelte a legtöbb munkát. A SID-emuláció (Hermit cSID-je) minden mintánál dupla pontosságú exp()-et, pow()-t és osztásokat számol, a Playdate processzora pedig a dupla pontosságú műveleteket szoftveresen végzi. Ezek viszont mind csak a SID regisztereitől függenek, így a Playdate-es másolat csak akkor számolja ki őket, amikor egy regiszter megváltozik, vagy az eredeti kód által előre legenerált táblázatokból veszi — minden kifejezést megtartva, így a hang bitre ugyanaz: 8040 másodpercnyi zenét és effektet hasonlítottunk össze. A készüléken ez is sok volt még, ezért alapból a szólamok 22 050 Hz-en futnak, és interpolálva jutnak fel 44 100 Hz-re, ahol a szűrő már pontos együtthatókkal dolgozik. A letöltés 99 KB.

A Playdate-en játszva két olyan hiba is előkerült, ami minden változatban benne volt. A 420-as szobából a 410-esbe sétálva Head átesett a padlón, és zuhant a végtelenségig: a remake fél lépései olyan helyre tették, ahová az eredetiben sosem jut el, ahol már az ajtónyílásban volt, de a másik, megemelt ajtó magasságát kapta. Együtt teleportálás után pedig Head ott maradt, Heels meg elsétált: az érkezés animációja törli Heels „magas” jelzőjét (a C64-es és a Spectrum-os kód is így tesz), így Heels egyedül mozgott tovább; a remake most az animáció végén visszaállítja. Mindkettő javítva van minden változatban, és az oldal alján lévő csomagok is újra elkészültek.

A 68k-s Mac felé: a SID assemblyben

2017-ben az volt a kérdés, hogyan tudná egy 16 MHz-es 68030 a játék mellett a zenét is lejátszani. Most van rá válasz: Hermit cSID-je, 68k assemblyre átírva (kb. 5 KB), a Sound Manager dupla pufferelésén keresztül szólva, a játék zenelejátszójának általam készített 68k-s átiratával meghajtva. Először a Mac-en teszteltük: a valódi 68k kód egy 68040 emulátorban futott, a lejátszó mind a 15 dallam minden lépésében ugyanazokat a szólamregisztereket állította be, mint a C-s lejátszó, a hang pedig egyezett a cSID-ével. A teszt azt is kiderítette, hogy a 68k-s lejátszómban nem működött az arpeggio (a főcímzenében és a játék közbeni zenében); javítva.

Utána Fantasm / Anvil alatt is lefordult egy emulált LC 475-ön és Classic II-n — a két gép saját emulátorán, ami erre a munkára készült, és amit Claude is vezérelni tud: fordít az Anvillal, futtat, ciklusokat mér, és elkap minden bájtot, ami a hangchipbe megy. Az LC 475-ön a hang bitre azonos a Mac-en futó teszttel. A Classic II-n a 22 kHz az egész processzort elviszi (23 ms hang előállítása 22,9 ms), 11 kHz-en viszont kiesés nélkül szól, szintén bitre azonosan:

Classic II, 11 kHzprocesszoridő
főcímzene48%
játék közbeni zene34%
hangeffektek21–30%
rövid dallam (szoba)7%
A főcímzene spektrogramja: fent a 68k emulátor, alatta a cSID; legalul a két hullámforma
A főcímzene: 68k emulátor (fent) és cSID

A Classic II-n tehát a játéknak a processzor kb. fele jut, amíg a főcímzene szól; van még hova javítani (néhány effekt után a szűrő a csenden is tovább dolgozik). Az igazi gépek még hátravannak.

Mi jön most? Maga a 68k-s változat. A váza kész: a C változat végigjárva az assembly változathoz, minden struktúra, változó és függvény a bemeneteivel, kimeneteivel és a szerepével, a 640x480-as és az 512x342-es elrendezés egymás mellett, és a megírandó Mac-es platformréteg. A többit nekem kell kitöltenem 68k kóddal.

Letöltés

Legfrissebb változat: 2026 Október 13.

A remake macOS-re, Windows-ra és Linuxra; minden csomagban benne van az SDL2 is, telepíteni nem kell semmit:

macOS 11-től Apple Silicon-on, 10.13-tól Intel-en; alá van írva, és az Apple hitelesítette (notarization), így dupla kattintással megnyílik. Windows 7-től (64 bit). Linuxon Debian 11 / Ubuntu 20.04-től (x86_64 és arm64, X11 vagy Wayland).

A Playdate-es változatnak saját oldala van, a letöltéssel és a telepítés leírásával: