Sprite-Multiplexer

Aus C64-Wiki
(Weitergeleitet von Multiplexing)
Zur Navigation springenZur Suche springen

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]

Funktionsprinzip eines Sprite-Multiplexers: Das Sprite wird an verschiedenen Stellen des Bildschirms erneut verwendet, wodurch der Eindruck entsteht, dass mehrere Sprites auf dem Bildschirm wären.

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.

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:

  1. 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.
  2. 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]

Bildschirmausgabe des Beispielprogramms mit Busy-Wait.

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]

Bildschirmausgabe des Beispielprogramms mit IRQ.

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 es 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

Sprite-Multiplexer in BASIC[Bearbeiten | Quelltext bearbeiten]

Bildschirmausgabe des BASIC-Sprite-Multiplexer von S.E.S. mit 16 Sprites.

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.

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.


Weblinks[Bearbeiten | Quelltext bearbeiten]