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 dem eigentlichen Programm gar nicht erlauben, ein Sprite so zu platzieren, dass es zu mehr als 8 Sprites nebeneinander 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.
-
In Zeitlupe kann man sehen, dass immer nur ein Laserstrahl zeitgleich auf dem Bildschirm zu sehen ist.
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 werden, 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.