Sprite-Multiplexer
Ein Sprite-Multiplexer ist eine Anzeigetechnik, mit der man den Grafikchip des C64 dazu bringen kann, mehr als die offiziell vorgesehenen 8 Sprites anzuzeigen. Einzige Beschränkung ist hierbei, dass sich nie mehr als 8 Sprites in einer Zeile befinden.
Funktionsweise[Bearbeiten | Quelltext bearbeiten]
Um die Funktionsweise eines Sprite-Multiplexers zu verstehen, muss man wissen, dass das Bild beim C64 zeilenweise aufgebaut wird. Ein Sprite-Multiplexer basiert darauf, die Sprites, nachdem sie vollständig auf dem Bildschirm ausgegeben wurden, an eine neue, weiter unten gelegene, Stelle zu verschieben (und dabei gegebenenfalls auch zu verändern), sodass sie dort erneut angezeigt werden. Das kann man dann noch mehrmals so machen, bis der Bildschirmaufbau am unteren Ende angelangt ist. Danach wird das Sprite wieder an die ursprüngliche Stelle verschoben, damit es bei der nächsten Anzeige erneut oben erscheint und so fort.
Im Beispielbild rechts wird zuerst das Sprite als rotes Quadrat am oberen Rand postiert. Sobald der Rasterstrahl unterhalb dieses Quadrats angekommen ist, wird das Sprite an die mittlere Position verschoben, gelb eingefärbt und der Spritedatenzeiger auf einen Kreis gesetzt. Wenn der Rasterstrahl dort angekommen ist, zeigt er nun dieses (scheinbar) neue Sprite an. Wenn dann das Kreis-Sprite vollständig angezeigt wurden, werden die Spritedaten auf grünes Dreieck geändert und das Sprite erneut nach unten verschoben, wo es als (scheinbar) drittes Sprite angezeigt wird. Hat der Rasterstrahl dieses Sprite hinter sich gelassen, wird es wieder an die oberste Stelle verschoben und das Aussehen wird wieder zu einem roten Quadrat geändert. Und dann beginnt das Spiel von neuem.
Varianten[Bearbeiten | Quelltext bearbeiten]
Man unterscheidet meist zwischen zwei Varianten:
- In der einfachen Form wird der Bildschirm in mehrere horizontal getrennte Bereiche aufgeteilt, in denen sich jeweils bis zu 8 Sprites befinden, die nicht aus diesem Bereich herausragen. Dies ist im Grunde genommen einfach nur ein Split-Screen, bei dem jeder Split seine eigenen Sprites hat. Diese Variante wird vor allem dann eingesetzt, wenn die Sprites blockartig zu einer großen Grafik zusammengesetzt werden. Dabei kann es sich um ein Riesen-Sprite handeln, beispielsweise einen überdimensionierten Endgegner in einem Spiel, eine eigenständige größere Grafik, oder aber auch um Ergänzungen zu anderen Grafiken, um diese mit zusätzlichen Farben versehen zu können.
- Ein Sprite-Multiplexer kann aber die Sprites auch alle einzeln verwalten. Diese können dann nahezu beliebig auf dem Bildschirm angeordnet sein. Der Sprite-Multiplexer geht dabei in jedem Durchgang des Rasterstrahls alle Sprites von oben nach unten durch und aktiviert immer die Sprites, die sich gerade im Sichtbereich befinden. Dabei kann es allerdings passieren, dass sich mehr als acht Sprites in einer Zeile befinden. Je nach Implementation wird unterschiedlich damit umgegangen. Es gibt Sprite-Multiplexer, die dann einfach die überzähligen Sprites nicht mehr anzeigen. In Spielen wird dies manchmal so gehandhabt. Alternativ dazu gibt es Sprite-Multiplexer die es der eigentlichen Programm erst gar nicht erlauben ein Sprite so zu platzieren, dass es zu mehr als neun Sprites kommt. Es liegt dann in der Verantwortung des Programms, mit diesem Problem umzugehen.
Eine besondere Form des Sprite-Multiplexing liegt vor, wenn gar nicht mehr als acht Sprites auf dem Bildschirm zu sehen sind, dies aber so wirkt. Bei dem Spiel Impossible Mission, siehe Bild unten, werden drei Sprites für das Männchen benötigt, zudem vier weitere Sprites für die Roboter. Zudem scheint es noch bis zu vier Laserstrahlen zu geben, die die Roboter abfeuern. Dabei handelt es sich aber nur um ein Sprite, das bei jedem Bildschirmaufbau an einer anderen Stelle erscheint. Da die Laser ohnehin flackern sollen, fällt dies nicht weiter auf.
Eine besondere Form des Sprite-Multiplexing bei Impossible Mission: Obwohl nie mehr als acht Sprites auf dem Bildschirm zu sehen sind, wirkt es so, als wären es mehr.
Timing[Bearbeiten | Quelltext bearbeiten]
Es ist offensichtlich, dass für einen Sprite-Multiplexer gutes Timing erforderlich ist, weshalb diese in der Regel in Assembler programmiert werden. Um das Timing richtig hin zu bekommen, gibt es grundsätzlich zwei verschiedene Techniken:
- Man kann mit einer Busy-Wait-Schleife auf den richtigen Zeitpunkt für das Versetzen der Sprites warten. Das ist einfacher zu programmieren, hat aber den Nachteil, das der Prozessor fast vollständig für das Warten genutzt wird und deswegen nur noch wenige andere Dinge machen kann. Insbesondere bei komplexeren Spielen kommt diese Methode schnell an ihre Grenzen.
- Mit dem Rasterzeilen-Interrupt hat man die Möglichkeit, zum richtigen Zeitpunkt das Versetzen der Sprites durch eine Interrupt-Routine durchführen zu lassen. Dadurch kann der Prozessor in der verbleibenden Zeit für andere Dinge genutzt werden. Allerdings ist die Programmierung einer entsprechenden Interrupt-Routine anspruchsvoller.
Beispiele[Bearbeiten | Quelltext bearbeiten]
Mit Busy-Wait-Schleife[Bearbeiten | Quelltext bearbeiten]
Das nachfolgende Beispielprogramm nutzt eine Busy-Wait-Schleife für das Timing: Es liest das VIC-Register $D012 (Rasterzeile) so lange aus, bis die richtige Rasterzeile erreicht wurde und versetzt dann alle acht Sprites um 32 Pixel nach unten, sodass sie erneut erscheinen.
!to "spritemuxer1.prg", cbm *=$c000 lda #$00 ; Rahmen und Hintergrund schwarz. sta $d020 sta $d021 jsr init_sprites ; Sprites initialisieren. sei ; Interrupt unterdrücken, da der ; beim Timing stört und ein ; gelegentliches Flackern ; verursachen würde. busy_wait lda $D012 ; Auf Rasterzeile warten, bei der cmp .next ; alle acht Sprites angezeigt bne busy_wait ; wurden. ldy #00 ; Y-Koordinaten aller Sprites um - lda $D001,y ; 32 erhöhen, damit diese erneut clc ; angezeigt werden. adc #32 sta $D001,y iny iny cpy #16 bne - lda .next ; Rasterzeile, auf die gewartet clc ; wird, ebenfalls um 32 erhöhen. adc #32 sta .next jmp busy_wait .next !byte 78 ; Nächste Rasterzeile, s.o. init_sprites: lda #$C3 ; Aussehen des Sprites definieren. ldy #63 ; Der Einfachheit halber, sind es - sta 832,y ; hier nur senkrechte Streifen. dey bpl - lda #13 ; Alle Spritedatenzeiger auf ldy #7 ; Block 832 zeigen lassen. - sta 2040,y dey bpl - lda #24 ; X-Koordinaten der acht Sprites ldy #00 ; festlegen. Das erste ist am linken - sta $D000,y ; Rand, jedes weitere um 32 Pixel clc ; nach rechts versetzt. adc #32 iny iny cpy #16 bne - lda #50 ; Y-Koordinaten der acht obersten ldy #00 ; Sprites festlegen. Diese sind - sta $D001,y ; jeweils um ein Pixel nach unten clc ; verschoben. adc #01 iny iny cpy #16 bne - lda #$ff ; Alle acht Sprites anschalten. sta $D015 rts
Unter Nutzung des Rasterzeilen-Interrupts[Bearbeiten | Quelltext bearbeiten]
Das nachfolgende Beispielprogramm nutzt den Rasterzeilen-Interrupt des VIC, um die Y-Koordinaten der Sprites anzupassen. Dadurch ist es möglich, dass nebenher noch andere Dinge passieren, im Beispiel läuft der BASIC-Interpreter weiter. Man kann nach dem Start ganz normal Befehle eingeben und sogar BASIC-Programme schreiben, ohne dass dies die Darstellung der Sprites stört.
Beispielsweise kann man die Farben der Sprites ändern, die Spritedatenzeiger jeweils auf eigene Sprites zeigen lassen, die X-Koordinaten ändern, oder das Aussehen, indem man mit POKE-Befehlen in die Speicherzellen ab 832 geeignete Werte schreibt. Diese Änderungen betreffen dann allerdings immer alle fünf untereinander befindliche Sprites.
!to "spritemuxer2.prg", cbm *=$c000 lda #$00 ; Rahmen und Hintergrund schwarz sta $d020 sta $d021 jsr init_sprites ; Sprites initialisieren jmp start_irq ; IRQ einrichten init_sprites: lda #$C3 ; Aussehen des Sprites definieren ldy #63 ; Der Einfachheit halber, sind es - sta 832,y ; hier nur senkrechte Streifen dey bpl - lda #13 ; Alle Spritedatenzeiger auf ldy #7 ; Block 832 zeigen lassen - sta 2040,y dey bpl - lda #24 ; X-Koordinaten der acht Sprites ldy #00 ; festlegen. Das erste ist am linken - sta $D000,y ; Rand, jedes weitere um 32 Pixel clc ; nach rechts versetzt adc #32 iny iny cpy #16 bne - lda #$ff ; Alle acht Sprites anschalten sta $D015 rts start_irq: sei ; Den Interrupt-Vektor auf unsere lda #<irq ; eigene Routine umbiegen. sta $0314 lda #>irq sta $0315 lda #$80 ; Rasterzeilen-Interrupt auf Zeile sta $d012 ; $80 setzen lda $d011 ; Highbyte der Zeile löschen and #$7F sta $d011 lda $d01a ; Rasterzeilen-IRQ anschalten ora #$01 sta $d01a cli rts irq: lda $d019 ; Prüfen, ob es sich um einen bmi raster_irq ; Rasterzeilen-IRQ handelt lda $dc0d ; Wenn nicht, Timer-IRQ bestätigen cli ; Flackereffekte vermeiden jmp $ea31 ; System-Interrupt aufrufen raster_irq: sta $d019 ; Raster-IRQ bestätigen lda $d012 ; Rasterzeile prüfen cmp #$83 bcs + lda #$83 ; oberste Zeile ldy #$98 jmp finish_irq + cmp #$9B bcs + lda #$9B ; zweitoberste Zeile ldy #$B0 jmp finish_irq + cmp #$B3 bcs + lda #$B3 ; mittlere Zeile ldy #$C8 jmp finish_irq + cmp #$CB bcs + lda #$CB ; zweitunterste Zeile ldy #$E0 jmp finish_irq + lda #$6B ; unterste Zeile ldy #$80 finish_irq: sta $d001 ; Die neuen Y-Koordinaten sta $d003 ; aller Sprites schreiben. sta $d005 ; Dies kann nicht in einer sta $d007 ; Schleife passieren, da sta $d009 ; es dann zu langsam wäre sta $d00b ; und das letzte Sprite sta $d00d ; ab und zu flackern würde. sta $d00f sty $d012 ; Nächsten Rasterzeilen- ; Interrupt festlegen. pla ; Aufräumen und Interrupt tay ; beenden. pla tax pla rti
Beispiel mit Änderung der Spritedatenzeiger[Bearbeiten | Quelltext bearbeiten]
Die beiden obigen Beispiele verändern jeweils die Spritepositionen, um die Sprites mehrfach darzustellen. Wenn man auch das Aussehen der Sprites verändern möchte, muss man auch die Spritedatenzeiger dynamisch verändern. Dies erfordert mehr Aufwand, insbesondere wenn die Sprites vertikal nahtlos aneinandergefügt werden sollen, wie z.B. in der BASIC-Erweiterung Tegra.
Der VIC-Chip stellt ein Sprite immer komplett dar, wenn er angefangen hat, es anzuzeigen. Das hat zur Folge, dass man die Spriteposition bereits auf die nächste Position verändern kann, sobald die obere Position des Sprites erreicht wurde. Man muss also nicht auf eine Rasterzeile am Ende oder unterhalb des Sprites warten. Anders verhält es sich mit dem Spritedatenzeiger: Eine Veränderung des Spritedatenzeigers ist sofort wirksam (oder genau gesagt ab der nächsten Pixelzeile des Sprites), was zur Folge hat, dass man das Verändern des Spritedatenzeigers exakt auf das Ende des Sprites timen muss, wenn man das selbe Sprite mit anderem Inhalt nahtlos vertikal nochmal anzeigen möchte. Das gleiche gilt auch für die Spritefarbe, wenn man diese auch verändern möchte.
Um das Timing des Veränderns des Spritedatenzeigers nicht durch andere Aktivitäten zu verkomplizieren, teilt man am besten das Verändern der Spritepositionen und das Verändern der Spritedatenzeiger auf zwei unterschiedliche Rasterzeilen-Interrupts auf, die sich abwechseln. Den Rasterzeilen-Interrupt für das Verändern der Positionen lässt man einige Zeilen vor Ende des Sprites erfolgen, um dann genau am Ende des Sprites mit einem anderen Rasterzeilen-Interrupt die Spritedatenzeiger zu verändern.
Das folgende Beispielprogramm demonstriert dieses Schema der alternierenden Rasterzeilen-Interrupts. Es stellt horizontal sechs Sprites dar, die nachtlos vertikal zweimal untereinander mit unterschiedlichen Inhalten dargestellt werden.
.EQ VIC=$D000 .EQ IRQVEC=$0314 .EQ STDIRQ=$EA31 .EQ IRQEND=$EA81 .EQ SCREENMEM=$0400 .EQ SPRITEMEM=$3D00 SETIRQ SEI LDA #<(IRQEXEC1) ; Den Interrupt-Vektor auf eigene Routine umbiegen LDY #>(IRQEXEC1) STA IRQVEC STY IRQVEC+1 LDA #127 ; CIA-Interrupt ausschalten STA $DC0D LDA #1 ; Rasterzeilen-Interrupt einschalten STA VIC+26 LDA VIC+17 ; High-Bit der Rasterzeile löschen (Rasterzeilennummer kleiner 256) AND #127 STA VIC+17 LDA INTLINE1 ; Rasterzeile für ersten Interrupt setzen STA VIC+18 LDA #1 STA INTCOUNT ; Interrupt-Zähler initialisieren CLI RTS ; ; Interrupt-Routine für Verändern der Spritepositionen ; IRQEXEC1 LDA VIC+25 ; Interrupt-Anforderung löschen STA VIC+25 LDY INTCOUNT LDA YPOSTAB,Y ; nächste Spriteposition laden STA VIC+1 ; Spritepositionen verändern STA VIC+3 STA VIC+5 STA VIC+7 STA VIC+9 STA VIC+11 LDA INTLINE2,Y ; Rasterzeile für nächsten Interrupt STA VIC+18 LDA #<(IRQEXEC2) ; Switchen auf die andere Routine (siehe unten) LDY #>(IRQEXEC2) STA IRQVEC STY IRQVEC+1 JMP IRQEND ; ; Zweite Interrupt-Routine für Verändern der Spritedatenzeiger ; IRQEXEC2 NOP ; Genaus Timing LDY INTCOUNT LDX SMEMTAB,Y ; nächsten Spritedatenzeiger laden STX SCREENMEM+$03F8 ; Spritedatenzeiger verändern INX STX SCREENMEM+$03F9 INX STX SCREENMEM+$03FA INX STX SCREENMEM+$03FB INX STX SCREENMEM+$03FC INX STX SCREENMEM+$03FD LDA VIC+25 ; Interrupt-Anforderung löschen STA VIC+25 LDA #<(IRQEXEC1) ; Switchen auf die andere Routine (siehe oben) LDY #>(IRQEXEC1) STA IRQVEC STY IRQVEC+1 LDY INTCOUNT ; Interrupt-Zähler erhöhen INY CPY #2 ; In diesem Beispiel haben wir nur zwei Spritezeilen BEQ BINT1 STY INTCOUNT LDA INTLINE1,Y ; Rasterzeile für nächsten Interrupt STA VIC+18 JMP IRQEND BINT1 LDY #0 ; Einmal pro Bildschirm führen wir die reguläre System-Interruptroutine aus STY INTCOUNT LDA INTLINE1,Y STA VIC+18 JMP STDIRQ YPOSTAB .BY 50,71 ; Zu nutzende vertikale Spritepositionen SMEMTAB .BY 244,250 ; Zu nutzende Spritedatenzeiger INTLINE1 .BY 243,57 ; Rasterzeilen für erste Interrupt-Routine INTLINE2 .BY 255,70 ; Rasterzeilen für zweite Interrupt-Routine INTCOUNT .BY 0 ; Interrupt-Zähler
Alternative zum exakten Timing bei der Änderung der Spritedatenzeiger[Bearbeiten | Quelltext bearbeiten]
Alternativ dazu kann man die Spritedatenzeiger auch kurz vor dem Ende des vorigen Sprites bereits umstellen. Dann müssen allerdings die letzten Zeilen dieses neuen Sprites denen des ersten Sprites entsprechen. Am Ende des Sprites muss dann wieder genauso verfahren um diese Zeilen nicht anzuzeigen.
Dieses Verfahren ist dann notwendig, wenn die Grenze zwischen zwei Sprites auf eine Badline fällt. Dann stehen nicht genügend Taktzyklen zur Verfügung, um alle Datenzeiger gleichzeitig zu ändern. Bei Sprite-Multiplexern mit sehr vielen Sprite-Schichten kann man dies kaum verhindern.
Sprite-Multiplexer in BASIC[Bearbeiten | Quelltext bearbeiten]
Simon Stelling (S.E.S.) zeigte 2008 mit einem Demo-Programm, dass es auch in BASIC möglich ist, einen Sprite-Multiplexer zu programmieren. Das größte Problem ist die Langsamkeit von BASIC-Programmen: In der Zeit, in der ein kompletter Bildschirmaufbau stattfinden, können gerade mal zwei POKE-Befehle ausgeführt werden, was zu wenig ist, um die acht Y-Koordinaten der Sprites zu verändern.
Stelling benutzt hierfür einen Trick: Mit dem Befehl POKE 648,208
wird die Bildschirmausgabe in den Bereich $D000-$D3E7 verschoben. In diesem Bereich befinden sich die Register des VIC. Dadurch kann mit einem einzelnen PRINT-Befehl auf alle VIC-Register (fast) gleichzeitig zugegriffen werden. Für das richtige Timing benutzt er den WAIT-Befehl. Zudem schaltet er mit POKE 56334,0
den Timer der CIA 1 ab, der im BASIC-Modus für das Auslösen des Interrupts verwendet wird. Dadurch finden auch keine störenden Interrupts mehr statt.
Für das Multiplexen wird hier allerdings die Prozessor-Zeit fast vollständig aufgebraucht, weshalb es nicht möglich sein dürfte, dass der Computer außer der Anzeige der Sprites noch irgendwas anderes macht. Auch bei der Positionierung und der Gestaltung der Sprites sind arge Grenzen gesetzt.
Anwendungen[Bearbeiten | Quelltext bearbeiten]
Sprite-Multiplexer finden vor allem in zwei Bereichen Anwendung: In Spielen und bei der Ausgabe von Grafiken mit vielen Farben.
Spiele[Bearbeiten | Quelltext bearbeiten]
In Spielen werden Sprite-Multiplexer gerne dazu verwendet, die Anzahl der Agenten im Spiel zu erhöhen. Eine weitere Einsatzmöglichkeit in Spielen sind Riesensprites, bei denen zahlreiche Sprites in einem Raster zusammengesetzt werden, um damit beispielsweise einen gigantischen Endgegner zu haben.
14 Gegner in Ghosts'n Goblins.
Riesiger Endgegner in Katakis.
Grafiken[Bearbeiten | Quelltext bearbeiten]
Bei der Grafikausgabe werden Sprite-Multiplexer genutzt, um die Beschränkungen von HiRes- oder Charaktergrafik (zumindest teilweise) zu umgehen und diesen zusätzliche Farben hinzuzufügen. Je nach Anwendung wird hierbei ein festes Gittermuster an Sprites über die Grafik gelegt und dort einfach die gewünschten Farben angeschaltet, oder die Sprites werden einzeln an den Stellen platziert, wo zusätzliche Farben benötigt werden.
Geschichte[Bearbeiten | Quelltext bearbeiten]
- Der erste in Assembler geschrieben Sprite-Multiplexer wurde bereits in der 1980er-Jahren (todo: Recherchieren) vorgestellt.
- Der erste in BASIC geschriebene Sprite-Multiplexer wurde von 2008 von S.E.S vorgestellt.
Siehe auch Zeittafel der C64-Demos.