Die Auswirkung von Sprites auf das Timing
Ich denke, dies ist nun der letzte Beitrag zum Raster-IRQ und dem Timing. Hiernach sind alle Grundlagen einmal angesprochen worden. Bei den Demo-Effekten werde ich natürlich noch Spielereien vorstellen und es wird Themen (z. B. den FLIFlexible Line InterpretationIm 'Flexible Line Interpretation'-Modus können, im Gegensatz zum normalen Multicolor-Modus, sogar alle 16 Farben in jeder 4*8 Pixel großen 'Zelle' verwendet werden.-Modus) geben, die wieder eng mit den Rasterzeilen & IRQs verbunden sind. Aber alles Weitere setzt einfach nur auf die bisherigen Informationen auf.
…und wieder: Basis ist das standard PAL-System mit 63TZ!
Das Problem mit den Sprites
Wer den Beitrag L.O.V.E. – Level 2: Die Landung 004 gelesen hat, der ist bereits darüber gestolpert, dass Sprites das Timing beeinflussen. Um dies jetzt genauer zu untersuchen, erzeugen wir wieder einen stabilen Rasterzeileninterrupt, zeichnen eine durchgängige Linie in den Rahmen und fügen dann noch einige Sprites hinzu.
RASTER = $00 ;Hier beginnt die Linie SPRITESACTIVE = %11111111 ;welche Sprites sollen aktiviert werden SPRITESYPOS = $12 ;Y-Position für alle Sprites ;*** Startadresse *=$0801 ;** BASIC-Zeile: 2018 SYS 2064:NEW !word main-2, 2018 !byte $9e !text " 2064:" !byte $a2,$00,$00,$00 main sei ;IRQs sperren lda #<myIRQ ;Adresse unserer Routine in sta $0314 ;den RAM-Vektor lda #>myIRQ sta $0315 lda #%00000001 ;Raster-IRQs vom VIC-II aktivieren sta $d01a lda #RASTER ;Hier soll unsere Interrupt auftreten sta $d012 lda $d011 ;Zur Sicherheit höchstes BIT and #%01111111 ;für die Rasterzeile löschen sta $d011 lda #%0111111 ;Timer-IRQs abschalten sta $dc0d lda $dc0d ;zur Sicherheit bestätigen lda #$00 ;Muster für den 'offenen' Rahmen sta $3fff lda #%00000001 ;Sicherheitshalber auch den sta $d019 ;Raster-IRQ bestätigen lda #$0B sta $d021 lda #$19 ;X-Position sta $02 ;auf Zero-Page merken ldx #$07 ;alle 8 Sprites spriteLoop lda #spriteData/64 ;Spriteadresse berechnen sta $07f8,X ;Spritepointer txa sta $d027,X ;Farbe asl ;SpriteNr * 2 tay ;ins Y-Register lda $02 ;X-Pos in den Akku sta $d000,Y ;und fürs jeweilige Sprite setzen clc adc #$20 ;X-Pos für nächstes Sprite erhöhen sta $02 ;und auf der Zero-Page speichern lda #SPRITESYPOS ;Y-Position immer gleich sta $d001,Y dex ;Schleifenzähler verringern bpl spriteLoop ;solange positiv, nächstes Sprite lda #SPRITESACTIVE ;Sprites aktivieren sta $d015 cli ;Interrupts erlauben rts ;zurück zum BASIC *=$0c00 myIRQ ;*** Wenn wir hier landen, sind bereits 38-45 Taktzyklen in der ;*** aktuellen Rasterzeile (ab jetzt als STARTROW bezeichnet) vergangen! ;*** zweiten IRQ einrichten ;*** da die Zeit bei aktivierten ROM nicht reicht, können wir den ;*** 2. Raster-IRQ erst in der übernächsten Zeile (STARTROW+2) bekommen. lda #<doubleIRQ ;(2 TZ) 2. Raster-IRQ einrichten sta $0314 ;(4 TZ) lda #>doubleIRQ ;(2 TZ) sta $0315 ;(4 TZ) tsx ;(2 TZ) Stackpointer im X-Reg. retten stx doubleIRQ+1 ;(4 TZ) und fürs zurückholen sichern! nop ;(2 TZ) nop ;(2 TZ) nop ;(2 TZ) lda #%00000001 ;(2 TZ) 1. Raster-IRQ später bestätigen ;------ ;26 TZ ;*** Jetzt sind 64-71 Taktzyklen vergangen und wir sind ;*** aufjedenfall in nächsten Rasterzeile (STARTROW+1)! ;*** Verbraucht wurden dort 1-8 TZ inc $d012 ;(6 TZ) 2. IRQ in der übernächsten Zeile STARTROW+2 ; $D012 wurde bereits automatisch erhöht sta $d019 ;(4 TZ) IRQ bestätigen cli ;(2 TZ) Interrupts für den 2. Raster-IRQ ; wieder freigeben ;*** Wir befinden uns in Rasterzeile STARTROW+1 und ;*** haben bisher 13-20 Zyklen verbraucht ;*** etwas Zeit verschwenden... ldx #$08 ; 2 TZ dex ;8 * 2 TZ = 16 TZ bne *-1 ;7 * 3 TZ = 21 TZ ;1 * 2 TZ = 2 TZ ; ------ ; 41 TZ ;*** Bis hier sind 54-61 Taktzyklen vergangen, jetzt auf den IRQ warten... ;*** Der nächste Rasterinterrupt wird während dieser NOPs auftreten! nop ;2 TZ (56) nop ;2 TZ (58) nop ;2 TZ (60) nop ;2 TZ (62) nop ;2 TZ (64) nop ;2 TZ (66) doubleIRQ ;*** Wir sind nun in Rasterzeile STARTROW+2 und ;*** haben bisher genau 38 oder 39 Taktzyklen benötigt!! ;*** Wir können so sicher sein, da der IRQ während der NOPs auftrat. ;*** Jetzt exakt soviele Taktzyklen 'verschwenden', wie in ;*** dieser Zeile noch zu verarbeiten sind (also 24 oder 25). ldx #$00 ;(2 TZ) Platzhalter für 1. Stackpointer txs ;(2 TZ) Stackpointer vom 1. IRQ wiederherstellen lda #SPRITESACTIVE ;(2 TZ) sta $d015 ;(4 TZ) bit $01 ;(3 TZ) lda #$0e ;(2 TZ) ldy #$01 ;(2 TZ) ldx $d012 ;(4 TZ) cpx $d012 ;(4 TZ) sind wir noch in Rasterzeile STARTROW+2? ;====== ;25 TZ = 63 oder 64 TZ!!! beq myIRQMain ;(3 TZ) wenn JA einen letzten Takt 'verschwenden' ;(2 TZ) sonst einfach weiterlaufen... ;*** Wir beginnen also immer exakt nach 3 TZ in der dritten Rasterzeile (STARTROW+3) ;*** nach dem 1. Raster-IRQ (den hatten wir ja für Zeile STARTROW festgelegt) myIRQMain ; 3TZ (s. oben) ldx #$ae ;ein paar Leerzeilen einfügen dex bne *-1 ;Linie 'zeichnen' (Rahmen + Hintergrund!) sty $d021 sty $d020 ldx #$0b dex bne *-1 bit $02 sta $D020 lda #$0b sta $d021 lda $d011 ora #%00001000 ;Anzahl der Zeilen wieder auf 25 sta $d011 lda #<openBorderIRQ ;IRQ zum Öffnen des Rahmens einrichten sta $0314 lda #>openBorderIRQ sta $0315 lda #$f9 ;Zeile für IRQ sta $d012 lda #%00000001 ;IRQ bestätigen sta $d019 pla ;Register vom Stack holen tay pla tax pla rti ;IRQ verlassen openBorderIRQ lda $d011 and #%11110111 ;Anzahl der Zeilen auf 24 sta $d011 lda #$00 ;Sprites abschalten, sonst tauchen die sta $d015 ;im unteren Rahmen nochmal auf lda #<myIRQ ;Original IRQ-Vektor setzen sta $0314 lda #>myIRQ sta $0315 lda #RASTER ;ursprüngliche Rasterzeile zurücksetzen sta $d012 lda #%00000001 ;IRQ bestätigen sta $d019 jmp $ea31 ;zum Schluß zum 'Timer-Interrupt' springen *=$0fc0 spriteData ;Test-Sprite !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe !byte $fe,$fe,$fe
Das obige Programm stellt euch wohl vor keine größeren Probleme mehr, es erzeugt aktuell die von uns erwartete Ausgabe. Wir haben den oberen & unteren Rahmen geöffnet, sehen unsere weiße Linie und acht Testsprites. Wir werden das Programm gleich über die Konstanten SPRITESACTIVE und SPRITESYPOS steuern und kontrollieren, was genau passiert.
Wie gesagt, soweit, sogut.
Wir wollen nun nur Sprite-0 sehen und zwar eine Zeile höher. Ändert dazu die Konstanten:
SPRITESACTIVE = %00000001 ;welche Sprites sollen aktiviert werden SPRITESYPOS = $11 ;Y-Position für alle Sprites
Kaum verschieben wir das Sprite-0 um eine Zeile nach oben, ragt unsere Linie um 5 Taktzyklen in die nächste Zeile. Wir verbrauchen also 5 TZ mehr, sobald wir das Sprite aktivieren. Aber warum wirkt es sich auf die darüber befindliche Rasterzeile aus? Das Sprite wurde doch direkt unter unserer Linie positioniert?
Ändert erstmal die Spriteanzeige in SPRITESACTIVE = %00010000, um Sprite-4 (also das fünfte – die Zählung startet bei 0!) zu aktivieren und startet das Programm nochmal.
Komisch, oder? Sprite-4 hat keine Auswirkung auf unsere Linie, aktiviert jetzt zusätzlich noch Sprite-0 SPRITESACTIVE = %00010001 und schaut euch das folgende Ergebnis an:
Jetzt ragt unsere Linie sogar doppelt so weit, um ganze 10 Taktzyklen, in die nächste Zeile!
Was geht denn da ab?
Der VIC-II muss natürlich nicht nur die Daten für den Ausgabebereich (das führt bekanntlich zu den Bad Lines), sondern auch die Spritedaten irgendwann mal holen. Dies macht er für die Sprites 0 bis 2 am Ende einer Rasterzeile und für die restlichen zu deren Beginn. Daher hat das einzelne Sprite-0 unsere Linie im ersten Bild um 5 TZ verschoben, Sprite-4 allein zeigt keine Auswirkung, da sich in der Zeile keine sichtbaren Daten befanden. Dass Sprite-4 aber auch seine Zeit fordert, seht ihr im letzten Bild, sobald unsere Linie durch Sprite-0 in die nächste Zeile ragt, wird die Linie dank Sprite-4 nochmal auf insg. 10 TZ verlängert.
Für das Holen der Spritedaten gilt etwas Ähnliches wie für die Bad Lines (Daten für Ausgabebereich holen). Der VIC-II benötigt auch für die Sprites zusätzliche „Bus-Zeit“. Dafür hält er wieder den Prozessor an. Ihr erinnert euch bestimmt, 3TZ bevor er den Bus benötigt setzt der VIC das Signal BA auf LOW. Dadurch weiß die CPU, dass sie gleich erstmal Pause hat. Der VIC benötigt 2TZ für das Sprite, somit erklären sich unsere 5TZ aus dem erstem Beispiel mit dem schwarzen Sprite (3TZ für die Ankündigung + 2TZ für die eigentliche Arbeit).
Schauen wir uns mit SPRITESACTIVE = %00000011 mal an, was zwei benachbarte Sprites für eine Auswirkung haben.
Sprite-0 & 1 benötigen zusammen nur 7TZ. Das liegt daran, dass der VIC-II ja bereits den Bus übernommen hat und daher die 3TZ für die Übernahme entfallen, es kommen nur noch 2TZ für die zusätzliche Arbeit hinzu. Dies könnt ihr durch aktivieren weiterer Sprites z. B. mit SPRITESACTIVE = %00001111 überprüfen…
Sind die Sprites 0 bis 3 aktiv, dann werden insg. 11TZ benötigt (3TZ Übernahme + 4 * 2TZ für die Arbeit). Das passt ja zu unseren Beobachtungen von eben.
Um euch nun komplett zu verwirren, versucht mal SPRITESACTIVE = %00001011.
Wir verzichten auf Sprite-1, aber gewinnen dadurch keine TZ, es werden immer noch 11TZ benötigt!!! Die Reihenfolge der Sprites hat also auch noch eine Auswirkung auf die verbrauchte Zeit.
Vergleichen wir obgies Bild mal mit der Ausgabe nach SPRITESACTIVE = %00000111:
Auch hier sind drei Sprites aktiv, aber dieses Mal verlieren wir nur 9TZ.
Ihr solltet also auch darauf achten, welche Sprites, wann aktiv sind, wenn eure Rasterzeit knapp wird. Natürlich werden auch in Bad Lines die zusätzlichen Taktzyklen für die Sprites benötigt, dadurch habt ihr dort noch weniger Zeit.
Ein Blick aufs Timing
Nun folgen noch einige Tabellen, in denen ihr einen genaueren Blick aufs Timing werfen könnt. Diese Tabellen habe ich nicht selbst entwickelt, ich habe sie nur etwas vereinfacht.
Legende ------- P-1 VIC = Phase 1 hier gehört der Bus dem VIC P-2 CPU = Phase 2 der Bus sollte der CPU zur Verfügung stehen P-2 VIC = Phase 2 der VIC hat den Bus übernommen c = Videospeicher & Farb-RAM Zugriff g = Zugiff auf Zeichensatz oder Bitmap r = RAM-Refresh p = Spritepointer lesen s = Spritedaten lesen x = CPU Zugriff (lesen / schreiben) X = evtl. CPU Schreibzugriff (Lesezugriff stoppt den Prozessor)
normale Zeile (aktive Sprites: keine) 111111111122222222223333333333444444444455555555556666 123456789012345678901234567890123456789012345678901234567890123 Zyklen p p p p p rrrrrgggggggggggggggggggggggggggggggggggggggg p p p P-1 VIC P-2 VIC xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx P-2 CPU
Wie ihr seht werkeln die CPU und der VIC-II in einer „normalen“ Rasterzeile jeder für sich und kommen sich somit auch nicht ins Gehege. Der VIC muss nie in Phase-2 tätig werden, daher stehen uns hier die vollen 63TZ zur Verfügung.
normale Zeile (aktive Sprites: 0) 111111111122222222223333333333444444444455555555556666 123456789012345678901234567890123456789012345678901234567890123 p p p p p rrrrrgggggggggggggggggggggggggggggggggggggggg psp p P-1 VIC ss P-2 VIC xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxXXX xxxx P-2 CPU
Aktivieren wir jetzt (wie oben im Beispiel) nur das Sprite-0, dann ändert sich die Sachlage. Der VIC kündigt in Zyklus 55 die Bus-Übernahme an, d. h. die CPU kann in Taktzyklus 55 bis 57 nur noch Schreibzugriffe ausführen, beim ersten Lesen wird der Prozessor gestoppt. In Zyklus 58 liest der VIC zunächst wie gewohnt den Sprite-Pointer (hier gehen wir ja von Sprite-0 aus). Um die drei BYTEs mit den Spritedaten für diese Zeile zu laden, benötigt er aber auch drei weitere Bus-Zugiffe. Daher hat er die CPU gestoppt und kann nun in Zyklus 58 auch noch ein Byte holen, da ihm jetzt beide Phasen zur Verfügung stehen. Die beiden letzten Bytes holt er sich in Phase-1 und 2 vom Takt 59.
Bad Line (aktive Sprites: keine) 111111111122222222223333333333444444444455555555556666 123456789012345678901234567890123456789012345678901234567890123 Zyklen p p p p p rrrrrgggggggggggggggggggggggggggggggggggggggg p p p P-1 VIC cccccccccccccccccccccccccccccccccccccccc P-2 VIC xxxxxxxxxxxXXX xxxxxxxxx P-2 CPU
Hier das vertraute Timing in einer Bad Line. Der VIC kündigt in Zyklus 12 an, dass er den Bus haben muss. Damit kann die CPU in Zyklus 12-14 nur noch schreibend auf den Bus zugreifen. Dann benutzt der VIC die zusätzliche Bus-Zeit, um die 40 Zeichen für die Ausgabe zu holen. Anschließend steht der Bus wieder der CPU zur Verfügung. Zählt ihr die obigen „x“e, dann kommt ihr wieder auf die vertrauten 23TZ.
Bad Line (aktive Sprites: ALLE) 111111111122222222223333333333444444444455555555556666 123456789012345678901234567890123456789012345678901234567890123 Zyklen pspspspspsrrrrrgggggggggggggggggggggggggggggggggggggggg pspsps P-1 VIC ssssssssss cccccccccccccccccccccccccccccccccccccccc ssssss P-2 VIC xXXX XXX P-2 CPU
Na dass sieht ja mal übel aus. In einer Bad-Line mit allen acht Sprites können wir evtl. noch einen Befehl absetzen, dann ist unsere Zeit abgelaufen.
Wenn ihr jetzt Sehnsucht nach noch mehr Tabellen und technischen Details habt, empfehle ich euch den deutschsprachigen Text ‚Der MOS 6567/6569 Videocontroller (VIC-II) und seine Anwendung im Commodore 64‚ von Christian Bauer. Dieser Text wird in verschiedenen Foren als die Referenz zum VIC-II angepriesen und ich kann mich dieser Meinung nur anschließen. Daher möchte ich (eigentlich muss ich) jedem, der den VIC-II wirklich verstehen will, nahelegen sich diesen Text durchzulesen und beim Programmieren von Rastereffekten griffbereit zu haben. Ein weiterer Hinweis auf den referenz Status ist, dass ihr den Text auch bei den WinVice-Docs findet.
Zusammenfassung
Wir müssen also beachten, dass aktive Sprites uns auch Taktzyklen in der Rasterzeile stibitzen und zwar in jeder Zeile, in der das jeweilige Sprite aktiv ist. Wie ihr an den Diagrammen erkennen könnt, werden die Sprites 0-2 am Ende der Rasterzeile und die Spites 3-7 am Beginn der Zeile verarbeitet.
Solltet ihr bei obigen Tests darüber gestolbert sein, dass ein einzelnes Sprite-3 auch unsere Linie beeinflußt, dass liegt natürlich an den 3TZ, in denen der VIC die Bus-Übernahme ankündigt. Die Daten für Sprite-3 werden dann direkt mit dem Beginn der Rasterzeile gelesen.
Da zwischen zwei Sprites nur 2TZ liegen gewinnt man keine Zeit, wenn Sprite 1, 3 oder 5 deaktiviert, das Sprite davor und danach aber aktiviert ist. Die 2TZ reichen einfach nicht, um den Bus freizugeben und wieder zu belegen. Das könnt ihr mit unserem Beispiel auch direkt kontrollieren.
Ihr solltet also, sofern es möglich ist, in euren Programmen die Sprites immer aufsteigend verwenden, falls ihr Raster-Effekte einplant. Die max. benötigte Rasterzeit beträgt also 16-19TZ (3TZ „Übernahme“ + 8 * 2TZ „Arbeit“). Dies ist der Fall, wenn alle Sprites oder auch nur die Sprites 0, 2, 4, 6, 7 aktiviert sind.
Timing über Sprite-0
Abschließend möchte ich noch eine Möglichkeit aufzeigen, mit der man das Timing mit Hilfe von Sprite-0 erzielen kann. Dieses Vorgehen hat Pasi „Albert“ Ojala in C= Hacking Volume 1 Issue 3 beschrieben. Das Prinzip dahinter ist eigentlich ganz einfach. Durch aktivieren von Sprite-0 in einer normalen Rasterzeile, muss der VIC den Bus übernehmen. Dies kündigt er in TZ 55 an. Durch das Auslösen der richtigen Befehle zu dieser Zeit kann das Timing syncronisiert werden. Aber schauen wir uns dazu mal ein kleines Beispiel an.
RASTER = $20 ;Hier den IRQ auslösen ;*** Startadresse *=$0801 ;** BASIC-Zeile: 2018 SYS 2064:NEW !word main-2, 2018 !byte $9e !text " 2064:" !byte $a2,$00,$00,$00 main sei ;IRQs sperren lda #<myIRQ ;Adresse unserer Routine in sta $0314 ;den RAM-Vektor lda #>myIRQ sta $0315 lda #%00000001 ;Raster-IRQs vom VIC-II aktivieren sta $d01a lda #RASTER ;Hier soll unsere Interrupt auftreten sta $d012 lda $d011 ;Zur Sicherheit höchstes BIT and #%01111111 ;für die Rasterzeile löschen sta $d011 lda #%0111111 ;Timer-IRQs abschalten sta $dc0d lda $dc0d ;zur Sicherheit bestätigen lda #%00000001 ;Sicherheitshalber auch den sta $d019 ;Raster-IRQ bestätigen lda #RASTER-$14 ;Sprite-0 positionieren sta $d001 ;Y-Pos. setzen (X-Pos. ist egal) ldx #$00 stx $d017 ;Y-Vergrößerung zur Sicherheit abschalten inx stx $d015 ;Sprite-0 aktivieren cli ;Interrupts erlauben rts ;zurück zum BASIC myIRQ lda $d020 ;aktuelle Farbe 'merken' ldx #$01 ;Farbe für die Linie ins X-Reg nop nop inc $ffff ;hier geschieht das Timing inc $ffff ;hier geschieht das Timing stx $d020 ;Linie beginnen ldx #$0a ;etwas warten dex bne *-1 nop sta $d020 ;Linie beenden lda #%00000001 ;IRQ bestätigen sta $d019 jmp $ea31 ;zur System-Routine
Wir richten hier, wie gewohnt, unseren Raster-IRQ ein. Aber dieses Mal platzieren wir Sprite-0 so, dass die letzte Spritezeile genau mit der Zeile unseres Rasterzeileninterrupts übereinstimmt (s. lda #RASTER-$14 ;Sprite-0 positionieren). Dabei ist einzig die Y-Position wichtig! Außerdem sollten wir darauf achten, dass das Sprite nicht in der Y-Richtung verdoppelt wurde.
Bei myIRQ warten wir zunächst etwas. Die Zeit nutzen wir, um die Farbe für die Linie ins X-Register und die aktuelle Rahmenfarbe in den Akku zu holen.
Dann kommen die beiden wichtigen INC-Befehle:
Diese werden zum Zeitpunkt der Bus-Übernahme durch den VIC-II ausgeführt. Durch die interne Funktionsweise (ein DEC würde übrigens auch klappen) sorgen sie für das exakte Timing.
Anschließend schreiben wir den Inhalt des X-Registers in die Rahmenfarbe, warten die entsprechende Anzahl an Taktzyklen ab und setzten den Rahmen wieder auf die ursprüngliche Farbe zurück. Zum Schluß wird der IRQ bestätigt und zur System-Routine gesprungen.
Startet ihr das Programm nun, bekommt ihr wieder mal eine stabile Linie zu Gesicht:
Verzichtet ihr jetzt aber auf das Sprite-0, in dem ihr z. B. die Zeile stx $D015 ;Sprite-0 aktivieren auskommentiert und startet dann das Programm erneut. So fängt die Linie wieder zu Zittern an.
Diese Art des Timings kann unseren stabilen Raster-IRQ aber nicht wirklich ersetzen. Laßt ihr z. B. ein BASIC-Programm laufen, kommt wieder alles aus dem Tritt. Versucht ihr das mit dem Demo-Programm vom Anfang, dann seht ihr, dass dort alles stabil bleibt.
Damit sind wir am Ende unserer Reise durch die Geheimnisse des Raster-IRQs und VIC-Timings angelangt. Ich hoffe ihr habt euch nicht zu sehr gelangweilt und konntet den einen oder anderen Nutzen aus den Texten ziehen.
Weitere Beiträge, was man damit nun so alles anstellen kann, werden, wie eingangs bereits erwähnt, natürlich noch folgen.
Kurze Info; beim Einbauen von Sprite-Timing in meinen Emulator ist mir aufgefallen, dass in dieser Zeile eine Null fehlt: “Ändert erstmal die Spriteanzeige in SPRITESACTIVE = %0001000, um Sprite-4 (also das fünfte – die Zählung startet bei 0!)”
Vielen Dank für den Hinweis, ich habe es eben korrigiert.