…und warum diese Bezeichnung beim C64 eigentlich falsch ist.
Color-Cycling wurde in der „guten alten Zeit“ verwendet, um durch einfaches Kopieren von Farbwerten den Eindruck von Bewegung zu erzeugen. Rechenzeit war knapp und die Grafikkarten weit weg von ihrer heutigen Leistung. Wen der geschichtliche Hintergrund nicht interressiert, der kann den gräulichen Text überspringen.
Wie funktioniert das klassische Color-Cycling?
Schauen wir dazu mal auf den VGA-Modus, wie er vor über 20 Jahren am PC zum Standard für Spiele wurde. Die standard VGA-Auflösung betrug, wie beim C64, 320*200 Bildschirmpunkte. Der PC kann aber 256 Farben gleichzeitig, aus einer gesamt Palette von 262144 Farben, darstellen. Dabei wurde für jeden Bildpunkt ein Byte verwendet. Dieses Byte zeigte dann wiederum auf eine Liste, in der für jeden Byte-Wert eine Farbe hinterlegt wurde.
Um nun den Eindruck einer Bewegung, z. B. fließendes Wasser, zu erzeugen, brauchte man nicht alle Bildpunkte zu ändern, was sehr viel Rechenzeit benötigt hätte. Wenn man die Liste mit den 256-Farben geschickt aufgebaut hat und die Grafik entsprechend entworfen wurde, dann konnte man die Bewegung durch einfaches Umkopieren der Farben in der Liste erreichen. Dabei brauchte man meistens nur wenige Kopiervorgänge.
Wenn ihr auf das folgende Bild klickt, könnt ihr euch dies einmal genauer anschauen. Ihr findet dort verschiedene Color-Cycling-Bilder unter HTML 5. Wenn ihr dort dann noch auf den Button Show Options rechts oben klickt, wird euch die Liste der Farben angezeigt und ihr könnt nachverfolgen, was kopiert wird.
Wer sich schon etwas mit der Grafikprogrammierung auf dem C64 beschäftigt hat, dem wird nun auffallen, dass es dort kaum solche Farblisten gibt, die umkopiert werden können. Mit viel gutem Willen, kann man die MultiColor-Werte (z. B. vom Sprite) als micro Farbpalette ansehen, aber das sind ja auch nur zwei Farben. Wir verwenden das Color-Cycling gleich im Textmodus. Da werden wir aber die Farben im Farb-RAM ändern müssen, um einen solchen Effekt zu erzielen und somit (nach meinem Verständnis) kein klassisches Color-Cycling verwenden. Dies ist der Grund, weshalb ich zu Beginn behauptet habe, dass die Bezeichnung eigentlich falsch ist.
Wie soll der Effekt auf dem C64 funktionieren?
Um im Textmodus des C64 einen solchen Effekt zu erzielen, gehen wir nun zweistufig vor. Im Speicher legen wir eine Tabelle mit 40 Bytes an. Dort stehen die von uns sorgsam ausgewählten Farbwerte drin. Diese Tabelle wird zyklisch umkopiert und dann muss noch die Farbe im Farb-RAM aktualisiert werden.
Text ausgeben
Als erstes wird nur dafür gesorgt, dass die Rahmen- und Hintergrundfarbe schwarz ist und der Text auf dem Bildschirm erscheint.
;*** Startadresse *=$0801 ;** BASIC-Zeile: 2018 SYS 2062 !word main-2, 2018 !byte $9e !text " 2062" !byte $00,$00,$00 main lda #$00 ;schwarz sta $d020 ;für Rahmen sta $d021 ;und Hintergrund jsr clrScreen ;BS löschen jsr showText ;Text ausgeben jmp * ;Endlosschleife ;*** Bildschirm löschen clrScreen ldx #$00 ;Schleifenzähler (256 Zeichen je Page) lda #" " ;alles mit Leerzeichen füllen clrLoop sta $0400,X ;1. Page des BS-Speichers sta $0500,X ;2. Page sta $0600,X ;3. Page sta $0700-24,X ;4. Page (-24 wegen der Sprite-Register) dex ;Schleifenzähler verringern bne clrLoop ;Solange größer 0 -> clrLoop rts ;sonst, zurück ;*** Text ausgeben showText ldx #$27 ;Schleife für 40 Zeichen nextChar lda infotext,X ;Zeichen holen sta $0400,X ;und ausgeben dex ;Schleifenzähler verringern bpl nextChar ;solange positiv -> nextChar rts ;sonst, zurück infotext !scr "* (c) 2018 by www.retro-programming.de *"
Na, das ist doch mal unspektakulär, oder? Lasst uns diesen Text jetzt einfärben, aber nicht einfach in einer Farbe, sondern laut einer Tabelle, in der für jedes Zeichen der Zeile eine Farbe steht.
Text einfärben
Dazu fügen wir zunächst den Aufruf der neuen Funktion jsr setColor hinter die Textausgabe jsr showText ein.
Dann wird natürlich die Funktion setColor benötigt, also diese am Besten hinter die vorhandenen Funktion showText einfügen.
;*** Farb-RAM laut Tabelle setzen setColor ldx #$27 ;Schleifenzähler für 40 Zeichen nextColor lda colors,X ;Farbe holen sta $d800,X ;und ins Farb-RAM dex ;Schleifenzähler verringern bpl nextColor ;solange positiv -> nextColor rts ;sonst, zurück
Die Funktion macht nichts Anderes, als einfach die Farbwerte aus der Tabelle colors ins Farb-RAM für die erste Bildschirmzeile zu kopieren.
Ganz am Ende des Programms legen wir jetzt noch unsere Farbtabelle ab:
colors !byte $01,$01,$03,$03,$0E,$0E,$06,$06 !byte $06,$06,$0E,$0E,$03,$03,$01,$01 !byte $03,$03,$0E,$06,$06,$0E,$03,$03 !byte $01,$01,$03,$03,$0E,$0E,$06,$06 !byte $06,$06,$0E,$0E,$03,$03,$01,$01
Hier können nun die gewünschten Farben abgelegt werden. Man sollte darauf achten, dass diese gut harmonieren, sonst wirkt der Effekt nachher zu fahrig. Ich habe mich hier für blau Töne und weiß entschieden.
Ein neuer Start sieht zwar schon bunter, aber immer noch uninteressant aus.
Hier kann man jetzt schön sehen, dass ich die Farben so gewählt habe, dass diese erst von hell (weiß) immer dunkler werden und dann wieder aufsteigend heller. Das sorgt gleich für einen relativ ansehnlichen Effekt.
Farben rotieren lassen
Jetzt wird etwas benötigt, um den Effekt zu timen. Ich begnüge mich hier mit dem bekannten Interrupt, der bei $ea31 verarbeitet wird. Also direkt vor dem jmp * den Interrupt umbiegen.
;*** IRQ einrichten sei ;IRQs sperren lda #<irq ;LSB sta $0314 lda #>irq ;MSB sta $0315 ;in den RAM-Vektor cli ;Interrupts wieder erlauben
Bei der Gelegenheit, können wir auch gleich wieder die Zeile jsr setColor löschen, die Funktion wird nun im Interrupt aufgerufen.
Fügt anschließend die Interrupt-Routine (wie wäre es direkt hinter dem jmp *?) ein:
;*** Eigene Interrupt-Routine irq dec cycleCnt ;Zähler verringern bpl irqExit ;solange positiv -> EXIT lda #$02 ;sonst Zähler sta cycleCnt ;wieder auf Anfang setzen jsr doColorCycle ;Farben umkopieren jsr setColor ;und ins Farb-RAM kopieren irqExit jmp $ea31 ;weiter zur System-Routine
Da 60 Veränderungen pro Sekunde evtl. etwas viel sind, benutzen wir einen kleinen Zähler cycleCnt. Bei jedem Interrupt wird der um eins verringert. Sobald er negativ wird, setzen wir ihn auf den Startwert zurück, kopieren die Farben in doColorCycle um, setzen dann wieder die Farben im Farb-RAM (hier ist nun das jsr setColor von eben gelandet) und springen dann (wie beim positiven Zähler) zur System-Routine nach $ea31.
Die Variable cycleCnt kann einfach am Programmende eingefügt werden.
cycleCnt !byte 2
Kommen wir zur Kernfunktion, dem Umkopieren der Farben. Fügt diese doch einfach hinter setColors ein.
;*** Farben umkopieren von links nach rechts doColorCycle ldy colors+$27 ;letzte Farbe 'retten' ldx #$27 ;Schleifenzähler (40 Farbwerte) loopColor lda colors-1,X ;Farbe 'links' von X holen sta colors,X ;und nach X kopieren dex ;Schleife verringern bne loopColor ;solange größer null -> loopColor sty colors ;sonst, gerettete Farbe an die erste Position rts ;zurück
Die Funktion ist auch ganz simpel. Es werden die Farben von links nach rechts kopiert. Das X-Register zeigt dabei auf das aktuelle Zielfeld, hier hin wird also die links stehende Farbe kopiert. Da wir die Farben wieder rückwärts abarbeiten, retten wir zu Beginn die letzte Farbe erstmal im Y-Register. Dann wird der Schleifenzähler (X-Reg.) wieder mit #$27 (dez. 39) initialisiert. Die Schleife holt sich erstmal die Farbe links vom X-Register in den Akku und kopiert diese an die Position, auf die X zeigt. Dann wird X verringert und das ganze wiederholt sich, solange X größer 0 ist. Somit wurden alle Farben kopiert, nur die Farbe für die erste Stelle fehlt noch. Den benötigten Wert haben wir uns zum Glück ganz zu Anfang im Y-Reg. gemerkt. Der kann nun direkt an die erste Position der Farbtabelle geschrieben werden. Schon sind wir fertig und können zurück zum Aufrufer.
Das Programm ist nun fertig und bereit von euch assembliert und gestartet zu werden.
;*** Startadresse *=$0801 ;** BASIC-Zeile: 2018 SYS 2062 !word main-2, 2018 !byte $9e !text " 2062" !byte $00,$00,$00 main lda #$00 ;schwarz sta $d020 ;für Rahmen sta $d021 ;und Hintergrund jsr clrScreen ;BS löschen jsr showText ;Text ausgeben ;*** IRQ einrichten sei ;IRQs sperren lda #<irq ;LSB sta $0314 lda #>irq ;MSB sta $0315 ;in den RAM-Vektor cli ;Interrupts wieder erlauben jmp * ;Endlosschleife ;*** Eigene Interrupt-Routine irq dec cycleCnt ;Zähler verringern bpl irqExit ;solange positiv -> EXIT lda #$02 ;sonst Zähler sta cycleCnt ;wieder auf Anfang setzen jsr doColorCycle ;Farben umkopieren jsr setColor ;und ins Farb-RAM kopieren irqExit jmp $ea31 ;weiter zur System-Routine ;*** Bildschirm löschen clrScreen ldx #$00 ;Schleifenzähler (256 Zeichen je Page) lda #" " ;alles mit Leerzeichen füllen clrLoop sta $0400,X ;1. Page des BS-Speichers sta $0500,X ;2. Page sta $0600,X ;3. Page sta $0700-24,X ;4. Page (-24 wegen der Sprite-Register) dex ;Schleifenzähler verringern bne clrLoop ;Solange größer 0 -> clrLoop rts ;sonst, zurück ;*** Text ausgeben showText ldx #$27 ;Schleife für 40 Zeichen nextChar lda infotext,X ;Zeichen holen sta $0400,X ;und ausgeben dex ;Schleifenzähler verringern bpl nextChar ;solange positiv -> nextChar rts ;sonst, zurück ;*** Farb-RAM laut Tabelle setzen setColor ldx #$27 ;Schleifenzähler für 40 Zeichen nextColor lda colors,X ;Farbe holen sta $d800,X ;und ins Farb-RAM dex ;Schleifenzähler verringern bpl nextColor ;solange positiv -> nextColor rts ;sonst, zurück ;*** Farben umkopieren von links nach rechts doColorCycle ldy colors+$27 ;letzte Farbe 'retten' ldx #$27 ;Schleifenzähler (40 Farbwerte) loopColor lda colors-1,X ;Farbe 'links' von X holen sta colors,X ;und nach X kopieren dex ;Schleife verringern bne loopColor ;solange größer null -> loopColor sty colors ;sonst, gerettete Farbe an die erste Position rts ;zurück infotext !scr "* (c) 2018 by www.retro-programming.de *" colors !byte $01,$01,$03,$03,$0e,$0e,$06,$06 !byte $06,$06,$0e,$0e,$03,$03,$01,$01 !byte $03,$03,$0e,$06,$06,$0e,$03,$03 !byte $01,$01,$03,$03,$0e,$0e,$06,$06 !byte $06,$06,$0e,$0e,$03,$03,$01,$01 cycleCnt !byte 2
Wie ihr seht, könnt ihr auch ganz ohne Rastertricks und VIC-Manipulationen schon einen netten kleinen Effekt erzielen. OK, einen Preis wird man damit nicht gewinnen, aber ein Anfang ist gemacht.
Nun ist es an euch, weiter zu experimentieren.
Sollen sich die Farben z. B. in die andere Richtung bewegen, dann müsst ihr diese nur von rechts nach links kopieren. Testet auch mal andere Farben in der Farbtabelle, gebt mehrere Texte aus, bei denen die Farben in unterschiedliche Richtungen laufen oder ändert CycleCnt.
Auch sind natürlich noch Optimierungen möglich, warum das Umkopieren und Setzen des Farb-RAMs trennen? Dies könnte in einem Rutsch geschehen bzw. warum nicht gleich nur das Farb-RAM umkopieren?
Man kann den Effekt zum Beispiel noch verstärken, wenn man mehrere Zeilen direkt untereinander ausgibt und die Farben in jeder Zeile um eine Position verschiebt. Ihr könnt das Color-Cycling auch mit einem Textscroller verbinden.
Im folgenden Image findet ihr eine etwas aufgebohrte Version, bei der auch schon eine Laufschrift eingesetzt wird (es ist auch noch ein Rasterzeileninterrupt im Spiel).
Color Cycling
Hi Jörn,
auch dieses Tutorial konnte ich wunderbar nachvollziehen, habe es auch geschafft mehrere Textzeilen ‘sinnvoll’ einzufärben 🙂
Bei weiterer Recherche im Web habe ich gesehen, dass man auch auf die Clear Screen Routine Kernel Routine bei $E544 zugreifen kann um den Bildschirm zu löschen. Also einfach -> JSR $E544
Das macht den Code dann nochmal schlanker 🙂
Du kannst z. B. auch das bereits an anderer Stelle erwähnte:
lda #147
jsr $FFD2
verwenden.
Denk aber immer daran, dass die Kernalfunktionen nicht sonderlich schnell sind! Hier spielt es zwar keine Rolle, aber bei anderen Projekten, kann es notwendig sein, den BS schnell zu löschen. Außerdem muss hier nur auf den Sprung nach
$EA31
verzichtet werden, dann kann der Code auch ohne Kernal zum Laufen gebracht werden. (Achtung: Dabei an die IRQ-Vektoren denken.)Stimmt… Ist eigentlich kein Color Cycling.
Ich habe aber auch schon öfter den Begriff “Color Washer” oder “Color Washing” für diesen Effekt auf dem C64 gelesen… 🙂