Az előző cikkben azt ígértem most a mozgatható elemek – alig meglepő módon – mozgatásáról lesz szó. Ez lesz az ami biztosítja nekünk, hogy a kicsit szegény és limitál grafikai tárházunkat meghökkentő trükkökkel, lélegzet elállító látványossággá alakítsuk. Legalább is értékelhetővé tegyük, hogy ne akadjon fel az elő-zsűrin. :) Aki persze játékgyáros ambíciókkal rendelkezik ezt a részt áhítattal kell olvassa! Hiszen félig ettől kelnek életre a kis – ám nagy gonddal megalkotott – játék elemek.
Vágjunk is bele!
Vertikális mozgatás
Az előző részben kiderült, minden akkor történik amikor mondjuk – mintegy state-machine jelleggel. Tudjuk, hogy nincs egy buffer amibe a hardver render-elget így sejthetjük a vertikális pozíciót sem egy regiszterben kell beállítanunk. Volt szó viszont az elemek, mint a labda és a lövedékek bekapcsolásáról és a játékos sprite-ok grafikájának beállításáról. Bizony itt ezt fogjuk használni.
Egy játékos sprite a beállított horizontális pozícióban folyamatosan rajzolódik minden scanline-on. Kérdés hogy mi van beállítva grafikának. Ha minden bit 0, akkor nem rajzol semmit, tehát kvázi ki van kapcsolva. Ez azt jelenti hogy a megfelelő scanline-on mielőtt kirajzolódna a sprite, be kell állítani a GRP0 és GRP1 címeken a sprite adott scanline-hoz tartozó grafikáját. Izgi mi? :)
A többi mozgatható elemnél a ki- bekapcsolást egy bit beállításával tehetjük meg. Ez a labdánál az ENABL, a két lövedéknél az ENAM0 és ENAM1 első bitje – 0 alapú számozás természetesen, programozók vagyunk vagy mi.. ez jutott. :D
Már érezhető ha időben nem volt egyszerű a színek és a grafika beállítása, akkor ez, hogy csekkolni is kell melyik scanline-on vagyunk mindenféle hardveres segítség nélkül, tovább nehezíti a dolgunk. Hogyan is férhetünk el a scanline-onkénti 76 ciklusba? Ügyes kóddal természetesen. Ez mindig adott feladattól függ, de azért próbálok adni pár mérsékelten gondolatébresztő példát a későbbiekben.
Kezdetnek rögtön vegyünk is egy olyat amit még az előző részben ígértem. A feladat egyszerű lesz: tegyünk ki két sprite-ot egymás mellé és ezeket lehetőleg async mozgassuk fel és le.
Ehhez kell két változó amiben a vertikális pozíciót fogjuk tárolni a két sprite-hoz. Most azt hinnénk annyi, hogy csak ellenőrizzük elértük-e az adott scanline-t. Ez kérem, tévedés! Valójában nincs elég időnk arra, hogy ugyanazon scanline ellenőrizzünk és be is állítsuk a grafikát – arról nem is beszélve, hogy mindezt kétszer kell megtennünk.
Ok, tehát egy scanline-al előbb kell ezt megtenni és a kívánt scanline-on már csak a színt és a grafikát kell beállítani – lehetőleg mielőtt rajzolnánk bármit is, tehát mondjuk HBlank alatt. Ámde van itt egy olyan gond is, hogy a grafikák éppen rajzolandó sorait is követni kell. Választhatunk megengedhetünk-e magunknak olyat hogy az X és Y regisztereket is bevonjuk a buliba, vagy kénytelenek leszünk még némi memóriát használni, ami persze több ciklust igénylő utasításokkal jár.
Remélem ez elég elgondolkodtató bevezetés volt. Az alábbiakban megnézhettek egy kernelt ami egy a sok megoldás közül és biztosan nem optimális, de talán érthető.
LEFT_SHIP_BUFF ds 1 LEFT_SHIP_GPOS ds 1 LEFT_SHIP_YPOS ds 1 LEFT_SHIP_DIR ds 1
Először is szükség lesz néhány változóra. Azt mondtuk a scanline legelején állítjuk be a rajzolandó grafikát, erre használjuk a LEFT_SHIP_BUFF változót. Követnünk kell továbbá az aktuális magasságot – LEFT_SHIP_YPOS – és irányt – LEFT_SHIP_DIR. És végül még tudnunk kell hogy melyik sort rajzoljuk a grafikából – LEFT_SHIP_GPOS.
Mindez kétszer a két sprite-hoz természetesen – az már 8 bájt a 128-ból.. :)
Lássuk a rajzoló kernel-t:
- Kirajzoljuk a buffer-ből a betöltött grafikát. Gondoskodnunk kell róla hogy #%00000000 legyen amikor nem akarunk rajzolni. A trükk a grafikában rejlik. ;)
- Ezután jöhet a logika. Kezdve azzal, hogy megnézzük nem-e épp ezen a scanline-on kell elkezdenünk rajzolni. Amennyiben igen, beállítjuk a kirajzolandó sort a grafikából.
- Most hogy már tudjuk honnan kell rajzolni, töltsük is be a buffer-be.
- És elölről…
Vertical_Mover_DrawLine ; draw from buffer STA WSYNC LDA LEFT_SHIP_BUFF STA GRP0 LDA RIGHT_SHIP_BUFF STA GRP1 ; check if we need to start drawing - first ; remember Y counts the picture scanlines LDX #PLAYER_SHIP_HEIGHT CPY LEFT_SHIP_YPOS ; is this the line? BNE SkipLeftShipDraw STX LEFT_SHIP_GPOS SkipLeftShipDraw LDX #ENEMY_SHIP_HEIGHT CPY RIGHT_SHIP_YPOS ; is this the line? BNE SkipRightShipDraw STX RIGHT_SHIP_GPOS SkipRightShipDraw ; check if we need to draw LDX LEFT_SHIP_GPOS BEQ SkipLeftShipGfx LDA PlayerShip-1,X ; load STA LEFT_SHIP_BUFF DEX ; set next gfx line STX LEFT_SHIP_GPOS SkipLeftShipGfx LDX RIGHT_SHIP_GPOS BEQ SkipRightShipGfx LDA AlienShip-1,X ; load STA RIGHT_SHIP_BUFF DEX ; set next gfx line STX RIGHT_SHIP_GPOS SkipRightShipGfx INY CPY #VERTICAL_MOVER_HEIGHT BNE Vertical_Mover_DrawLineHorizontális mozgatás
A mozgatás pedig imigyen történik:
LDA LEFT_SHIP_DIR BNE LeftShipMoveOtherWay INC LEFT_SHIP_YPOS ; move down LDA LEFT_SHIP_YPOS ; test picture bottom CMP #VERTICAL_MOVER_HEIGHT-PLAYER_SHIP_HEIGHT BEQ LeftShipToggleDir JMP RightShipMove LeftShipMoveOtherWay DEC LEFT_SHIP_YPOS ; move up LDA LEFT_SHIP_YPOS CMP #HORIZONTAL_MOVER_HEIGHT BNE RightShipMove ; test picture top LeftShipToggleDir LDA #1 EOR LEFT_SHIP_DIR STA LEFT_SHIP_DIR RightShipMove LDA RIGHT_SHIP_DIR BNE RightShipMoveOtherWay INC RIGHT_SHIP_YPOS ; move down LDA RIGHT_SHIP_YPOS ; test picture bottom CMP #VERTICAL_MOVER_HEIGHT-ENEMY_SHIP_HEIGHT BEQ RightShipToggleDir JMP EndShipMove RightShipMoveOtherWay DEC RIGHT_SHIP_YPOS ; move up LDA RIGHT_SHIP_YPOS CMP #HORIZONTAL_MOVER_HEIGHT BNE EndShipMove ; test picture top RightShipToggleDir LDA #1 EOR RIGHT_SHIP_DIR STA RIGHT_SHIP_DIR EndShipMove
Horizontális mozgatás
Az előzőekhez képest itt egy kicsit egyszerűbb dolgunk van, mert csak annyit kell tennünk, hogy megstrobe-oljuk a megfelelő regisztert és már be is állítottuk a kívánt pozíciót, ami természetesen úgy marad amíg át nem állítjuk. Jól hangzik ugye? Nem! :) Aki eddig figyelt az tudja hogy minden akkor történik amikor csináljuk. Ez jelen esetben azt jelenti, hogy amikor kiadjuk az STA utasítást nekünk kell gondoskodnunk arról, hogy azt a megfelelő időben tegyük. És aki egy kicsit kódolt már assembly-ben az tudja, hogy az utasításoknak van ciklusideje is, tehát a felbontás nem túl jó ilyen téren sem. Hiszen 1 ciklus az 3 pixelt jelent és egy STA bármelyik strobe regiszterbe – mivel zero page-en vannak – 3 ciklus, azaz 12 pixel. Tehát leghamarabb 12 pixel távolságra tudunk bármit is pozicionálni ezzel a hardveres segítséggel. A regiszterek pedig a RESP0, RESP1 a játékosoknak, RESM0, RESM1 a lövedékeknek és RESBL a labdának. Mindegyik elvileg csak egyszer használható egy scanline-on. Elvileg..
Az előzőekben tárgyalt rutinhoz például közvetlen a rajzolás előtt felhasználunk egy scanline-t.
NOP NOP STA $FF ; 3 cycles ; clear playfield LDA #0 STA PF2 ; set player colors - missile color too comes from this LDA #LEFT_SHIP_COLOR STA COLUP0 LDA #RIGHT_SHIP_COLOR STA COLUP1 ; color bg for separator - line number color :) STY COLUBK NOP STA RESP0 NOP NOP NOP NOP STA RESP1 STA $FF ; 3 cycles NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP LDA #0 STA COLUBK ; reset bg color
Ez itt oké, de képzeljük el ha ugyanazt a sprite-ot később máshova kellene pozicionálni – netán még mozog is..
Természetesen sejthetjük hogy itt nincs vége a történetnek, hiszen elég használhatatlan lenne ha csak 12 pixelenként mozgathatnánk bármit is. Már csak azért is mert egy player sprite 8 pixel széles és ha egymás mellé szeretnénk pozicionálni..
Van egy finom mozgatásra alkalmas mechanizmus is. Ez abból áll, hogy a megfelelő regiszterek felső nibble-jébe – 4 bit – be tudunk állítani egy -8-tól +7-ig terjedő értéket. Ezek nem azonnal aktiválódnak, hanem amikor megstrobe-oljuk a HMOVE regisztert.
Az értékek ilyenkor nem törlődnek így a következő strobe hatására újra annyit mozdulnak.
A regiszterek pedig: HMP0, HMP1 a player-ekhez, HMM0, HMM1 a lövedékekhez és HMBL a labdához.
Most azt hisszük ezzel könnyebb az életünk, de mint eddig mindig, itt is megvan a kis kellemetlenségük. Egész konkrétan a platform specifikáció szerint a HMOVE-ot csak a scanline első utasításaként adhatjuk ki, jellemzően egy WSYNC után. Ehhez még hozzájárul az hogy működésből adódóan ez a művelet eltart egy ideig, ami alatt a gép nem tud rajzolni. Innen a sok fekete csík a bal oldalon amit már korábban is láttatok. Mind pozicionálásból adódik.
A működésből adódóan annyit jelent, hogy van a gépnek egy időzítője amit beállít és ehhez idő kell. Persze azt is mondtam, ezek a pozicionálásból adódó csíkok csak a korai játékokra jellemzőek. Az történt ugyanis hogy mivel ki lehet adni az utasítást tulajdonképpen bármikor, elkezdtek kísérletezni az emberek, hogy mégis mi történik ha nem a scanline elején mozgatunk. Rájöttek, hogy a mozgatás mindig végbemegy csak nem feltétlenül a kívánt eredménnyel. Ezek közül nem feltétlenül mindegyik pozíció viselkedik konzisztensen a különböző VCS-ek között, de úgy találták, hogy a 73. ciklus – az STA a 71-en történik – szintén annyira stabil mint az 1. ezért elkezdték használni, ami már nem adta az említett jellegzetes csíkokat. :)
Táblázat, ami segít kitalálni mi fog történni. Persze javaslom az AtariAge fórum böngészését ahol rengeteg furcsa kísérletezés eredményeit lehet fellelni. ;)
Érdemes belegondolni, hogy amíg ez nem történt meg, vagy együtt éltek a csíkokkal, vagy kitrükközték fekete háttérrel, kerettel..
Ehhez a példához, mivel együtt jelenik meg az előző példával az egyik lövedéket használtam. Ha nincs ugyanis hozzázárva a játékoshoz, külön is mozgatható. Ezt a RESMP0 és RESMP1 regiszterek 1. bitjének beállításával vagy törlésével szabályozhatjuk.
Tehát hasonlóan az előzőekhez – annyi hogy itt nincs betöltendő grafika:
BALL_YPOS ds 1 BALL_YDIR ds 1 BALL_XPOS ds 1 BALL_XDIR ds 1 BALL_GFX_POS ds 1
A rajzoló kernel – ugye csak ki be kapcsolgatjuk a rajzolást:
; horizontal mover LDX #%00000010 ; for enabling missile Horizontal_Mover_DrawLine STA WSYNC CPY BALL_YPOS BNE SkipBallDraw STX ENAM0 LDA #BALL_HEIGHT STA BALL_GFX_POS SkipBallDraw DEC BALL_GFX_POS LDA BALL_GFX_POS BNE SkipBallHide STA ENAM0 SkipBallHide INY CPY #HORIZONTAL_MOVER_HEIGHT BNE Horizontal_Mover_DrawLine
És a mozgató kód – külön a horizontális és vertikális:
; move ball up-down LDA BALL_YDIR BNE BallMoveOtherWayY INC BALL_YPOS ; moving down JMP BallTestYPos BallMoveOtherWayY DEC BALL_YPOS ; moving up BallTestYPos LDA BALL_YPOS BEQ BallToggleYDir ; test up CMP #HORIZONTAL_MOVER_HEIGHT-BALL_HEIGHT BEQ BallToggleYDir ; test down JMP SkipBallToggleYDir BallToggleYDir LDA #1 EOR BALL_YDIR STA BALL_YDIR SkipBallToggleYDir ; move ball left-right LDX #%00100000 ; moves left LDY #%11100000 ; moves right LDA BALL_XDIR BNE BallMoveOtherWayX DEC BALL_XPOS ; moving left DEC BALL_XPOS STX HMM0 JMP BallTestXPos BallMoveOtherWayX INC BALL_XPOS ; moving up INC BALL_XPOS STY HMM0 BallTestXPos LDA BALL_XPOS CMP #BALL_LEFT_WALL_POS BEQ BallToggleXDir ; test up LDA BALL_XPOS CMP #BALL_RIGHT_WALL_POS BEQ BallToggleXDir ; test down JMP SkipBallToggleXDir BallToggleXDir LDA #1 EOR BALL_XDIR STA BALL_XDIR SkipBallToggleXDir STA WSYNC STA HMOVE
A fenti példa is mutatja hogy ez bizony megint egy melós ciklus számolgatós móka lesz. És tényleg minden feladathoz külön speciális kernelt lehet írogatni. Természetesen trükkök itt is vannak, pl készült egy algoritmus aminek megadva a kívánt pozíciót és a mozgatandó elemet kb 3 scanline alatt beállítja a megfelelő pozíciót. Ez nagyon pazarló, de vannak helyzetek amikor könnyen együtt élhetünk vele.
Fórum beszélgetés ahol találtam, és egy jobb megoldás közvetlen alatta. :)
Házi feladat kielemezni! ;)
Addig is mutatok egy szerény kis pozíció számláló rutint amit személyem alkotott. Lényege hogy körpályán mozgat egy sprite-ot. A körpálya 256 pontból áll és 8 részre van osztva. Ez azt jelenti hogy 32 pozíciót megadva tükrözgetve körbe mozgatja a választott grafikai elemet.
; moving in a circle along a curve ; A := Angle PositionByAngelSubrutine LDX #1 Div64 ; which section? CMP #64 BCC SelQuarter ; if < 64 INX ; X = Piece+1 SBC #64 JMP Div64 SelQuarter TAY ; Y = mod Angle TXA ; A = Piece AND #1 ; is odd? BNE Odd ; these (1,2,5,6) ; not (3,4,7,8) CPY #32 BCC SetOpposite ; if < 32 (3,7) ; (4,8) TYA ; A = mod Angle SBC #63 EOR #$FF TAY INY ; Y = Remain JMP SetStraight Odd CPY #32 BCC SetStraight ;if < 32 (1,5) ; (2,6) TYA ; A = mod Angle SBC #63 EOR #$FF TAY INY ; Y = Remain SetOpposite LDA CircleSectXLUT,Y ; DY STA PlayerYPos LDA CircleSectYLUT,Y ; DX LSR STA PlayerXPos JMP CorrectDir SetStraight LDA CircleSectYLUT,Y ; DY STA PlayerYPos LDA CircleSectXLUT,Y ; DX LSR STA PlayerXPos CorrectDir CPX #2 BCC RetPos ; 1. section LDA PlayerXPos ; A = DX EOR #$FF CLC ADC #1 STA PlayerXPos ; DX = -DX CPX #3 BCC RetPos ; 2. section LDA PlayerYPos ; A = DY EOR #$FF CLC ADC #1 STA PlayerYPos ; DY = -DY CPX #4 BCC RetPos ; 3. section ; 4. section LDA PlayerXPos ; A = DX EOR #$FF CLC ADC #1 STA PlayerXPos ; DX = -DX RetPos RTS
Ez lett
A fenti okosságok többé-kevésbé kikristályosított formában elérhetőek a következő linkeken:
Horizontális és Vertikális mozgatás.
Körpályán mozgatás.
Ha valakit jobban érdekel a dolog és érez magában elég lelki erőt, hogy olvasson és kísérletezzen, akkor feltétlen olvassa el ezt az irományt és keresgéljen a Stella levlista archívumban meg az AtariAge fórumon.
Sok hardverműködés illetve hibásan működést kihasználó rutint lehet találni. Pl a RESxx regiszterek akár többször is strobe-olhatók egy scanline-on belül némi kompromisszummal…
Röviden ennyi a történet. Izgalmas kísérletezést kívánok mindenkinek. :)
Legközelebb vagy hangot fogunk generálni, vagy az I/O eszközöket fogjuk hadra.
Addig is kitartás!
További cikkek a sorozatból:
Foly-ta-tást! Foly-ta-tást! Foly-ta-tást!
Csak hogy legyen visszajelzés is, én olvasom, és nagyon élvezem a soroztatot, köszi az energiát, amit beleölsz!