4. Grafik

 

Bis jetzt können Sie Grafik nur mit BASIC und vielen, vielen POKE-Befehlen programmieren. Sprites müssen Sie aus DATA-Zeilen auslesen, genauso wie Zeichensätze. Mit dem „Saver“ können Sie Ihre Maschinenroutinen und Grafiken auch in einer schnellen Weise nachladen, allerdings zerfällt dann Ihre Anwendung oder Ihr Spiel in viele kleine Dateien, und kann unter Umständen nur als ganze Diskette weitergegeben werden (moderne Spiele verfahren genau so, aber meist ist dies vor Allem eine Sache des Kopierschutzes). Irgendwann wird die Sache trotzdem zu langsam, besonders, wenn Sie Ihren Bildschirm scrollen wollen. Aber auch die Steuerung von Sprites ist mit BASIC zu langsam. Ich möchte Ihnen an dieser Stelle einige einfache Techniken vorstellen, mit denen Sie Ihrem BASIC unter die Arme greifen können.

 

4.1 Scrolling

 

Scrolling ist eine der wichtigsten Dinge auf dem C64. Schon wenn Sie den BASIC-Befehl LIST benutzen, sehen Sie, dass Sie ohne Scrolling nicht weit kommen. Scrolling bezeichnet das Verschieben des Bildschirminhalts um eine bestimmte Anzahl Zeilen oder Spalten nach oben und unten (bzw. nach links und rechts). Was ist jedoch am Scrolling so schwer? Im Endeffekt können Sie mit Assembler den Bildschirmspeicher in einer beliebigen Weise verschieben, und da Assembler schnell ist, sehen Sie auch die Auffrischung nicht, da Ihre Augen zu träge sind. Sehen Sie sich dazu das nächste Listing DOWNSCROLL an.

 

07-DOWNSCROLL

10                      .BA 49152

20                      LDA #$BF

30                      STA 248

40                      LDA #$07

50                      STA 249

60    MOVEBYTE          LDY #0

70                      LDA (248),Y

80                      LDY #40

90                      STA (248),Y

100                     SEC

110                     LDA 248

120                     SBC #1

130                     STA 248

140                     LDA 249

150                     SBC #0

160                     STA 249

170                     LDA 249

180                     CMP #3

190                     BNE MOVEBYTE

200                     LDX #0

210                     LDA #32

220   CLEARLINE         STA 1024,X

230                     INX

240                     CPX #40

250                     BNE CLEARLINE

260                     RTS

 

DOWNSCROLL initialisiert einen Zeiger in den Adressen 248 und 249, der am Anfang auf das letzte Zeichen der vorletzten Zeile des Bildschirmspeichers (Adresse $07BF) zeigt. Das Scrolling selbst ist sehr simpel und wird in den Zeilen 60 - 90 innerhalb der Schleife MOVEBYTE realisiert. Mittels Y-indizierter indirekter Adressierung wird das Zeichen an der Adresse (248),Y mit Y=0 eingelesen und um 40 Bytes nach hinten kopiert (mit Y=40). Dies ist ein Verschieben um eine Zeile nach unten. Beim Scrolling nach unten muss danach vom Zeiger in den Adressen 248 und 249 1 abgezogen werden (Zeile 100 - 160), und zwar so lange, bis in der Adresse 249 der Wert 3 steht (dies bedeutet, dass der Zeiger in den Adressen 248 und 249 die gültigen Bildschirmspeicherseiten verlassen hat). Ist dies noch nicht der Fall, springt das Programm zurück zum Label MOVEBYTE. Nun bleibt beim Scrolling nach unten die erste Zeile stehen, wird also am Ende doppelt angezeigt. Deshalb müssen durch eine zweite Schleife zusätzlich die Adressen 1024-1063 mit dem Wert 32 beschrieben werden.

Geben Sie nun SYS 49152 ein, sehen Sie, dass die Sache wirklich funktioniert: Der Bildschirm scrollt nach unten, und Sie können an der oberen Stelle neue Zeichen einfügen. Dies geht sogar relativ einfach durch PRINT-Befehle in BASIC, die hier nur mit der Kombination „[CLR/HOME]“ beginnen müssen. In der Tat nutzen viele einfache Autorennspiele das simple Scrolling. Zusammen mit BASIC-Compilern wie Austro Comp, die Ihre BASIC-Programme noch einmal um den Faktor 10 bis 20 beschleunigen können, können Sie in der Tat rasante Autorennspiele programmieren. Das bei Retro-Fans sehr bekannte Spiel Burnin‘ Rubber, das auch heute noch Spaß macht, ist so ein Spiel. Und da das simple Scrolling sogar zusammen mit selbst erstellten Zeichen und Sprites funktioniert, sind Ihrer Fantasie hier kaum Grenzen gesetzt.

 

4.2 Weiches Scrolling

 

Die obige Aussage gilt aber nicht mehr, sobald Ihnen das simple Scrolling (das Sie ja mit der richtigen Kopierschleife durchaus auch in sämtlichen Richtungen programmieren können) zu ruckelig ist. Sie kommen nun vielleicht auf die Idee, zusätzlich den Bildschirm eng zu stellen (z.B. den 24-Zeilen-Modus zu benutzen), und Ihre erste Zeile erst einmal pixelweise nach unten zu scrollen, z.B. in der folgenden Weise:

 

10 POKE 53272,16:REM 24-Zeilen-Modus

20 FOR I=16 TO 23

30 POKE 53272,I

40 NEXT I

50 POKE 53272,16:SYS 49152

… Auffrischen der ersten Zeile, die man ja nun nicht mehr sieht …

 

Wie Sie es auch drehen und wenden (selbst, wenn Sie das Zurücksetzen der Scrolling-Register in Assembler realisieren), Sie bekommen kein flimmerfreies Scrolling hin. Und selbst, wenn Sie am Anfang Ihres Maschinenprogramms darauf warten, dass Ihr Rasterstrahl (Adresse 53266) außerhalb des sichtbaren Bildschirms ist, bevor Sie Ihr Scrolling beginnen, wird sich an dieser Tatsache nichts ändern. Ich kann Ihnen sogar versichern, dass nicht einmal folgende schnelle Kopierschleife etwas ändert, in der Sie zwei separate Zeiger für Quelle und Ziel benutzen:

 

                  LDY #255

MOVEBYTE          LDA(248),Y

                  STA(250),Y

                  DEY

                  BPL MOVEBYTE

 

Selbst diese schnelle, speicherseitenweise arbeitende, kompakte Kopierschleife benötigt pro Durchlauf 20 Prozessortakte inklusive der Zeit, die Sie für den Rücksprung zu MOVEBYTE benötigen. Für die Verschiebung von 1000 Zeichen benötigen Sie also 20.000 Prozessortakte. Da der 6510 mit etwa 1000.000 Hz läuft, benötigen Sie also 0.02 Sekunden für Ihre Scrolling-Routine. Dies hört sich erst einmal sehr wenig an, wenn Sie aber bedenken, dass Ihr Bildschirm mit einer Rate von 50 Hz aktualisiert wird, dann braucht Ihr Rasterstrahl genau so lange, das Bild aufzubauen, wie Ihr Programm läuft. Beim Scrolling nach oben ist dies auch kein Problem, wenn Sie zunächst mit der folgenden Schleife darauf warten, dass der Rasterstrahl den unteren Rahmen erreicht hat:

 

RASTWAIT          LDA 53266

                  CMP #250

                  BNE RASTWAIT

 

Beim Scrolling nach unten müssen Sie den Bildschirm jedoch von unten nach oben aktualisieren. Deshalb läuft Ihnen beim Scrolling nach unten der Rasterstrahl entgegen, und überholt Sie auch irgendwann. Genau an dieser Stelle (etwa in der Mitte) flimmert dann Ihr Bildschirm. Es gibt nun mehrere Möglichkeiten, ein Flimmern beim weichen Scrolling zu verhindern.

 

4.2.1 Benutzen von weniger als 25 Scrollzeilen

 

Wenn Sie diese Variante benutzen, müssen Sie an dem simplen Scrolling (zur Not noch beschleunigt durch optimierte Schleifen) nichts ändern. Sie scrollen eben einfach z.B. 16 statt 25 Bildschirmzeilen. Zusammen mit der Technik eines geteilten Bildschirms können Sie sehr beeindruckende Effekte erzeugen. Das beste Beispiel hierfür ist das Adventure „journey to the centre of the earth“. Im Oberen Teil des Bildschirms erscheint weich gescrollt Ihr Labyrinth, im unteren Teil erscheint ein Panel, in dem sämtliche Gegenstände erscheinen, die Sie gerade bei sich tragen. Aber auch andere Spiele wie z.B. die beliebten „Ballerspiele“ Zaxxon oder Uridium benutzen geteilte Bildschirme, um z.B. am oberen Rand den Punktestand anzuzeigen. Leider funktioniert die Sache nicht immer- „journey“ flimmert z.B. zeitweise doch sehr stark, und stürzt sogar zuweilen ab- was fatal ist, wenn man schon fast alle Schätze geholt hat, und auch den Spielstand nicht speichern kann.

Mehr muss ich zum Punkt 4.2.1 nicht schreiben, denn wenn Sie z.B. Ihre Maschinenprogramme die Scrolling-Register des VIC aktualisieren lassen, und diese Aufgabe eben nicht dem langsamen BASIC überlassen, können Sie ein Bildschirmflimmern fast immer verhindern.

 

4.2.2 Verwenden eines Offscreen Buffers

 

Der Königsweg ist sicherlich diese Methode: Sie speichern Ihr eigentliches Bild in einem Speicherbereich, den man zunächst nicht sehen kann, einem sogenannten Offscreen Buffer. Erst nach Auffrischen der gepufferten Bilddaten lösen Sie eine schnelle Kopierschleife aus, die den externen Puffer auf den sichtbaren Bildschirm kopiert - und zwar in der richtigen Weise von oben nach unten. Es gibt nun mehrere Möglichkeiten, Scrolling mit einem Offscreen-Buffer zu realisieren. Sie können z.B. zunächst den Offscreen Buffer scrollen, danach die freiwerdende Bildschirmzeile oder -Spalte löschen, und anschließend den entsprechenden freiwerdenden Bereich auffrischen. Anschließend kopieren Sie dann nach dem Warten auf den Rasterstrahl den Offscreen Buffer in den sichtbaren Bereich. Hier gilt es vor Allem, Prozessortakte einzusparen. So sollten Sie z.B. die folgende Kopierschleife für eine Bildschirmzeile bevorzugen

 

                  LDY #39

LOOP              LDA (250),Y

                  STA (248),Y

                  DEY

                  BPL LOOP

 

und folgende Kopierschleife vermeiden

 

                  LDY #0

LOOP              LDA (250),Y

                  STA (248),Y

                  INY

                  CPY #40

                  BPL LOOP

 

Die erste Schleifenstruktur spart gegenüber der zweiten Schleifenstruktur pro Durchlauf 5 Prozessortakte ein. Ganz langsam (und deshalb zu meiden, vor Allem beim Bildschirmauffrischen) ist natürlich die Variante, die beim simplen Scrolling um eine Zeile nach unten benutzt wird. An dieser Stelle können Sie bereits genug Assembler, um ein solches Projekt in die Tat umzusetzen, das den Offscreen Buffer scrollt, anstatt den sichtbaren Bildschirm. Sie müssen nur etwas Geduld haben, denn Sie müssen sehr viele Schleifen und Subroutinen programmieren, und auch immer wieder die Laufzeit testen. Schon ein paar eingesparte Prozessortakte können darüber entscheiden, ob Ihr Scrolling flimmert, oder nicht. Es gibt aber ein noch viel besseres Verfahren, das Ihnen auch die Mühe mit den ganzen Scrolling-Routinen für jede einzelne Richtung erspart, und das auch relativ einfach umzusetzen ist. Genau dieser Königsweg aller Königswege ist das Arbeiten mit Labyrinth-Bausteinen.

 

4.2.3 Das Arbeiten mit Baustein-Blöcken

 

Wie gesagt, füllt das Scrolling (vor Allem das weiche Smooth Scrolling) ganze Zeitschriften und Bücher, und es gab unter den Spieleentwicklern in den 80-ern jahrelange Diskussionen (und sogar Anfeindungen) über die richtige Scrolling-Technik. Hier wurden wirklich sehr trickreiche Algorithmen erfunden. Man frischte z.B. den Bildschirm stückweise auf (z.B. erst die untere, dann die obere Hälfte), sortierte die Zeichen schnell um, oder benutzte sogar illegale OP-Codes, um auch noch das letzte Quäntchen Leistung aus dem Prozessor herauszukitzeln. Illegale OP-Codes sind OP-Codes, die eigentlich nicht vorgesehen sind, trotzdem aber eine Wirkung haben. Ein Beispiel ist z.B. der LAX-Befehl, der einen Wert in den Akkumulator und gleichzeitig in das X-Register einliest.

Ich möchte Sie aber nicht mit solchem Schabernack quälen, denn diesen braucht man eigentlich nicht, wenn man einfach hergeht, und einen Spiele-Level aus Bausteinen aufbaut. Diese Bausteine sind mehrere Zeichen groß, quadratisch aufgebaut (meistens aus 4x4 oder 8x8 Zeichen) und werden durch eine einfache Kopierroutine ausgewählt und in einen Offscreen Buffer kopiert. Dieser Offscreen Buffer kann anschließend mit einer schnellen Kopierschleife in den sichtbaren Bereich kopiert werden. Der Trick ist nun, dass der Offscreen Buffer größer ist, als der Bildschirm, und man deswegen einen komplizierten Scrolling-Algorithmus gar nicht benötigt. Wenn z.B. ein Baustein 8x8 Zeichen groß ist, dann definierten Sie einfach einen Offscreen Buffer, der 48 Zeichen breit ist, geben aber immer nur Zeilen mit 40 Zeichen Breite auf dem sichtbaren Bildschirm aus. Auf diese Weise können Sie in X-Richtung innerhalb Ihres Offscreen Buffers hin und her wandern. Wenn Sie dann zusätzlich Ihr Offscreen Buffer z.B. noch 33 statt 25 Zeilen hoch ist, dann können Sie auch noch in Y-Richtung hin und her wandern. Das Einzige, dass Sie hier beachten müssen, ist, dass Sie stets einen Block mehr in den Offscreen Buffer schreiben müssen, als Ihr sichtbarer Bildschirm in der entsprechenden Richtung anzeigen kann.

Für das Arbeiten mit Bausteinen gibt es wahrscheinlich genauso viele Techniken, wie es Programmierer gibt - Sie müssen also wohl oder übel Ihren eigenen Stil finden. Lassen Sie sich dabei aber bitte nicht in die Irre führen, und z.B. von einigen selbsternannten Experten dazu verleiten, eine ganz bestimmte Technik, einen ganz bestimmten Assembler oder sogar einen ganz bestimmten Level-Editor zu verwenden. Ich selbst benutze z.B. die hier erwähnte Technik mit dem Offscreen Buffer, der in jeder Richtung genau um einen Block größer ist, als der sichtbare Bildschirm. Ferner scrolle ich auch sehr oft nicht den ganzen Bildschirm, weil ich in meinen Spielen fast immer eine Score- oder Infoline anzeigen lassen muss. Meine Leveleditoren und Kopierschleifen programmiere ich stets selbst, weil ich dies quasi schon im Schlaf beherrsche. Vielleicht haben Sie aber auch eine ganz andere Möglichkeit, die Ihnen besser zusagt, oder sogar schon Ihr Traum-Toolkit gefunden. Vielleicht verwenden Sie sogar einen C-Compiler wie CC65, und müssen sich deshalb gar nicht mehr so oft mit Assembler rumschlagen. Tun Sie mir an dieser Stelle bitte den Gefallen, und verwenden weiterhin Ihr Traumprogramm, anstatt sich von wem auch immer (nicht einmal von mir) Methoden abzuschauen, die Ihnen nicht zusagen. Auch, wenn das Internet immer wieder dazu verleidet, schlichtes Copy-And-Paste auszuführen, so lernt man durch Ausprobieren immer noch am meisten.

 

4.3 Sprites durch Interrupt-Routinen steuern

 

Sprites sind nicht nur in Spielen ein Hingucker. Leider sind vor allem mehrere Sprites nicht leicht mit BASIC zu steuern. Aber auch Assemblerprogramme können hier an ihre Grenzen kommen, wenn diese zusätzlich Scrolling durchführen müssen- und dadurch riesige „Spaghettiprogramme“ entstehen. Eine Möglichkeit, die z.B. auch der Game Maker verwendet, ist, die Steuerung von Sprites von einer Interrupt-Routine durchführen zu lassen. Eine Interrupt-Routine ist ein normales Unterprogramm, das aber nicht vom Programmierer per Hand, sondern automatisch bei Eintritt eines bestimmten Ereignisses aufgerufen wird. Im Endeffekt ist ein Interrupt eine Unterbrechung des laufenden Programms. Diese Unterbrechung ist aber immer an eine bestimmte Quelle gebunden. Diese Quelle kann z.B. die Computeruhr sein, aber z.B. auch eine Kollision von Sprites, oder eine bestimmte Position, an der sich der Rasterstrahl gerade befindet (dies wäre dann ein sogenannter Raster-Interrupt).

Für die Sprite-Steuerung eignet sich natürlich am besten der Timer-Interrupt, der periodisch alle 1/60 Sekunde ausgelöst wird. Ein Timer-Interrupt führt normalerweise dazu, dass der Computer einige Arbeiten erledigt, wie z.B. zu schauen, ob gerade eine Taste gedrückt wird, ober ob die Floppy gerade ein Byte über den Bus senden will. Aber auch die Aktualisierung des Cursors wird periodisch erledigt. Wenn all diese Arbeiten erledigt sind, muss natürlich anschließend aufgeräumt werden (sonst entsteht irgendwann Chaos, das ist nicht nur in Ihrem Bastelkeller so). Aber wo steht die Adresse der Aufräum-Routine? Die Antwort: An der Adresse $EA31. Diese Adresse wird aber nicht direkt angesprungen, sondern dem Zeiger in den Adressen 788 und 789 in Form eines Lo- und Hi-Bytes entnommen. Im Endeffekt wird hier nur der Befehl

 

JMP (788)

 

ausgeführt, aber Sie können natürlich auch durchaus den direkten Sprungbefehl

 

JMP $EA31

 

benutzen. Was Sie hiervon haben, ist, dass Sie den Zeiger in den Adressen 788 und 789 auch auf Ihre eigene Routine umbiegen können. Dies funktioniert in der Tat ganz hervorragend, wenn Sie nur Ihr eigenes Unterprogramm nicht mit RTS, sondern mit JMP $EA31 beenden. Auf diese Weise wird Ihr eigenes Unterprogramm alle 1/60 Sekunde aufgerufen, und Sie können sich auch darauf verlassen, dass hier das Timing stimmt. Wenn Sie z.B. ein Sprite 60-mal in der Sekunde um einen Pixel nach rechts bewegen, dann bewegt Ihre Interrupt-Routine ganz automatisch im Hintergrund Ihr Sprite um 60 Pixel pro Sekunde nach rechts. Und diese Geschwindigkeit wird auch beibehalten, so lange Ihre Routine läuft. Aber wie sieht Ihre Steuer-Routine nun aus? Im Endeffekt müssen Sie hierfür nur das folgende einfache Grundgerüst benutzen:

 

.BA 49152

SEI

LDA #<(MOVE)

STA 788

LDA #>(MOVE)

STA 789

CLI

RTS

MOVE …

… Ihr Sprite-Steuerprogramm

JMP $EA31

 

Das Maschinenprogramm an Adresse 49152 macht nun nichts anderes, als das Lo- und Hi-Byte, an der Ihr Unterprogramm MOVE steht, in den Zeiger an den Adressen 788 und 789 einzutragen. Das Einzige, dass Sie hier beachten müssen, ist, dass Sie den Inhalt der Adressen 788 und 789 nur bei abgeschalteten Interrupts verändern dürfen. Wenn Sie dies tun, dann kehrt Ihr Programm, das Sie mit SYS 49152 aufrufen, auch sofort zurück. Anschließend wird MOVE alle 1/60 Sekunde automatisch aufgerufen. MOVE könnte nun z.B. so aussehen:

 

MOVE                  LDA #100

                      STA 53249

                      CLC

                      LDA 53248

                      ADC #1

                      STA 53248

                      BCC CONT

                      LDA #0

                      STA 53248

                      LDA 53264

                      EOR #1

                      STA 53264

CONT                  JMP $EA31

 

MOVE bewegt Sprite Nr. 0 60-Mal in der Sekunde um 1 Pixel nach rechts, die Y-Position ist konstant 100. Zu beachten ist hier allerdings, dass in Adresse 53264 (VIC-Register Nr. 16) das 9. Bit der X-Position steht. MOVE führt an dieser Stelle einfach eine Addition des Werts 1 zu dem Wert in Adresse 53248 durch. Wenn nach der Addition das Carry-Flag gesetzt ist, dann wird das 9. Bit auch wirklich benutzt, und Bit 0 im VIC-Register Nr. 16 muss dann verändert werden. MOVE benutzt hierfür eine XOR-Operation (Befehl EOR), die bewirkt, dass Bit Nr. 0 in VIC-Register 16 nur dann gesetzt wird, wenn dieses Bit vorher tatsächlich 0 war. Ansonsten wird dieses Bit wieder gelöscht, und Sprite Nr. 0 erscheint an X-Position 0. Die Tatsache, dass die X-Position von Sprite 0 auf diese Weise von 0 bis 511 reicht, stört in diesem einfachen Beispiel nicht.

 

4.3.1 Acht Sprites gleichzeitig steuern

 

Zugegeben: Jetzt wird es anspruchsvoll, denn nun sollen 8 Sprites in einer professionellen Weise gesteuert werden. Professionell heißt: Etwa in der Manier vom Game Maker: Jedes der 8 Sprites wird automatisch durch eine Interrupt-Routine gesteuert und bewegt sich auch unabhängig z.B. vom BASIC-Programm. Damit die Bewegung eindeutig definiert ist, bekommt jedes Sprite de folgenden Eigenschaften:

 

·       Eine Bewegungs-Richtung (direction)

·       Eine Bewegungs-Geschwindigkeit (movement speed)

·       Eine Animations-Geschwindigkeit (animation speed), die angibt, wie schnell die einzelnen Animations-Frames abgespielt werden (dies ändert die Sprite-Zeiger in den Adressen 2040-2047)

·       Ein Start-Frame, das angibt, ab welchem Sprite-Zeiger-Wert die Animation startet

·       Ein End-Frame: Wenn dieser Wert erreicht ist, dann wird das Frame nicht angezeigt, sondern es wird stattdessen wieder der Start-Frame ausgewählt

 

Es wird nun wieder davon ausgegangen, dass die Interrupt-Routine für die Sprite-Steuerung 60-mal in der Sekunde aufgerufen wird. Jeder Aufruf soll nun bestimmte Zähler aktualisieren, die für die Sprite-Steuerung benötigt werden. Die wichtigsten Zähler sind hier XTimeCount und YTimeCount. Bei der langsamsten Bewegung geschieht die Veränderung von XTimeCount und YTimeCount in Einer-Schritten. Allerdings wäre diese Veränderung der X- bzw. Y-Position um 1 pro 1/60 Sekunde zu schnell, denn in diesem Fall würde sich ein Sprite mit 60 Pixeln pro Sekunde bewegen. Deshalb werden für die tatsächliche Sprite-Position auf dem Bildschirm nur die obersten Bits eines 16-Bit-Zählers verwendet, für die X-Position muss hier natürlich ein zusätzliches 17. Bit reserviert werden. Die Positionen und aktuellen Zählerstände müssen natürlich irgendwo gespeichert werden. Zu diesem Zweck wird im Programmbereich des Maschinenprogramms ein separates Datensegment angelegt, in dem ab Adresse 49530 achtmal für die Sprites 0-7 der folgende 10 Bytes große Datenblock abgelegt wird:

 

Byte 0: XTimeCount Lo-Byte (Zeit-Zähler für die X-Position LSBs)

Byte 1: XTimeCount Hi-Byte (Zeit-Zähler für die X-Position MSBs)

Byte 2: XLo: tatsächliche X-Position auf dem Bildschirm Bit 0-7

Byte 3: XHi: tatsächliche X-Position MSB (Bit 8)

Byte 4: YTimeCount Lo-Byte (Zeit-Zähler für die Y-Position LSBs)

Byte 5: YTimeCount Hi-Byte (Zeit-Zähler für die Y-Position MSBs)

Byte 6: YLo (die Y-Position überschreitet den Wert 255 nicht, also hier nur ein Lo-Byte)

Byte 7: AnimTimeCount (Zeit-Zähler für die Animationsgeschwindigkeit)

Byte 8: CurrentFrame (Aktueller Animations-Frame)

Byte 9: Nicht benutzt

 

Die jeweiligen Zähler XTimeCount, YTimeCount und AnimTimeCount werden nun pro Aufruf der Interrupt-Routine um einen bestimmten Wert verändert. So können Sie z.B. in jedem Schritt den Wert 10 zu XTimeCount addieren. Wenn dann XTimeCount überläuft und >255 wird, dann wird das überlaufende Bit auch zu XLo addiert. Wenn XLo ebenfalls überläuft, dann wird das 9. Bit von XLo in XHi übernommen. Genauso ist es mit YTimeCount und AnimTimeCount. Allerdings benutzen die Zähler für die Sprite-Y-Position und das aktuelle Animations-Frame kein 3. Byte.

Nun müssen Sie natürlich noch festlegen können, wie groß die Zähler-Änderungen in jedem Schritt wirklich sein sollen. Hierzu benötigen Sie ein weiteres Datensegment, das ich an die Adresse 900 gelegt habe, und das ebenfalls 10*8 Bytes umfasst. Auch hier sind die jeweiligen Datenblöcke für jedes Sprite identisch und beinhalten jeweils 8-mal die folgenden 10 Bytes:

 

Byte 0: X-Differenz-Flag (0=negativ/links, 1=positiv/rechts)

Byte 1: X-Pixel-Differenz-Betrag XDiff (dieser Wert wird zu XTimeCount addiert bzw. von diesem Wert subtrahiert)

Byte 2: X-Pixel-Differenz-Betrag XDiff Hi-Byte

Byte 3: Y-Differenz-Flag (0=negativ/oben, 1=positiv/unten)

Byte 4: Y-Pixel-Differenz-Betrag YDiff (dieser Wert wird zu YTimeCount addiert bzw. von diesem Wert subtrahiert)

Byte 5: Y-Pixel-Differenz-Betrag YDiff Hi-Byte

Byte 6: Animation Speed (dieser Wert wird zu AnimTimeCount addiert)

Byte 7: Animation Frame Min.

Byte 8: Animation Frame Max.

 

Die Geschwindigkeit einer Bewegung wird also durch die Änderung der X- bzw. Y-Position pro 1/60 Sekunde um einen bestimmten Wert ausgedrückt, und dieser Wert wird Schritten von 1/256=0,004 angegeben. Wenn Sie also für die X-Pixel-Differenz den Wert 256 angeben (natürlich in Form von Lo- und Hi-Byte), dann ändert sich die entsprechende Position um 1 Pixel pro 1/60 Sekunde. Wenn Sie hier allerdings nur den Wert 1 angeben, dann benötigt Ihr Sprite 256/60=4,26 Sekunden, um sich um 1 Pixel zu bewegen.

Kommen wir nun zum eigentlichen Mover-Programm. Dies ist so angelegt, dass Sie es mit SYS 49152 starten können. Läuft die Interrupt-Routine schon, wird sie gestoppt. Wenn Sie in Adresse 1000 und 1001 einen Zeiger eintragen, bei dem das Hi-Byte nicht 0 ist, dann wird zusätzlich zur IRQ-Routine Ihr Unterprogramm ausgeführt, auf das Ihr Zeiger in den Adressen 1000 und 1001 zeigt. Kommen wir nun zum eigentlichen Listing, das wie gesagt viel umfangreicher ist, als das vorige Beispiel.

 

08-MOVER

10                      .BA 49152

20                      .EQ V=53248

30                      SEI

40                      LDA 999

50                      CMP #0

60                      BEQ INIT

70                      CMP #1

80                      BEQ DEINIT

90    INIT              LDA #<(MOVER)

100                     STA 788

110                     LDA #>(MOVER)

120                     STA 789

130                     LDA #1

140                     STA 999

150                     JMP IEXIT

160   DEINIT            LDA #49

170                     STA 788

180                     LDA #234

190                     STA 789

200                     LDA #0

210                     STA 999

220   IEXIT             CLI

230                     RTS

240   MOVER             LDA #0

250                     STA 994

260                     STA 995

270                     STA 996

280                     LDA #1

290                     STA 997

300   SPRITE            LDY 995

310                     LDX SPOFFSET,Y

320                     LDA 900,X

330                     CMP #0

340                     BEQ LEFT

350                     CMP #1

360                     BEQ RIGHT

370   LEFT              SEC

380                     LDA SPDATA,X

390                     SBC 901,X

400                     STA SPDATA,X

410                     LDA SPDATA+1,X

420                     SBC 902,X

430                     STA SPDATA+1,X

440                     LDY 994

450                     STA V,Y

460                     LDA SPDATA+2,X

470                     SBC #0

480                     AND #1

490                     STA SPDATA+2,X

500                     CMP #1

510                     BNE NOLMSB

520                     JSR SETMSB

530                     JMP LMSB

540   NOLMSB            JSR CLEARMSB

550   LMSB              JMP YMOVER

560   RIGHT             CLC

570                     LDA SPDATA,X

580                     ADC 901,X

590                     STA SPDATA,X

600                     LDA SPDATA+1,X

610                     ADC 902,X

620                     STA SPDATA+1,X

630                     LDY 994

640                     STA V,Y

650                     LDA SPDATA+2,X

660                     ADC #0

670                     AND #1

680                     STA SPDATA+2,X

690                     CMP #1

700                     BNE NORMSB

710                     JSR SETMSB

720                     JMP RMSB

730   NORMSB            JSR CLEARMSB

740   RMSB              JMP YMOVER

750   YMOVER            LDA 903,X

760                     CMP #0

770                     BEQ UP

780                     CMP #1

790                     BEQ DOWN

800   UP                SEC

810                     LDA SPDATA+4,X

820                     SBC 904,X

830                     STA SPDATA+4,X

840                     LDA SPDATA+5,X

850                     SBC 905,X

860                     STA SPDATA+5,X

870                     LDY 994

880                     STA V+1,Y

890                     JMP ANIMATOR

900   DOWN              CLC

910                     LDA SPDATA+4,X

920                     ADC 904,X

930                     STA SPDATA+4,X

940                     LDA SPDATA+5,X

950                     ADC 905,X

960                     STA SPDATA+5,X

970                     LDY 994

980                     STA V+1,Y

990   ANIMATOR          CLC

1000                    LDA SPDATA+6,X

1010                    ADC 906,X

1020                    STA SPDATA+6,X

1030                    LDA SPDATA+7,X

1040                    ADC #0

1050                    STA SPDATA+7,X

1060                    LDA SPDATA+7,X

1070                    CMP 908,X

1080                    BNE CONT

1090                    LDA 907,X

1100                    STA SPDATA+7,X

1110                    LDA #0

1120                    STA SPDATA+6,X

1130  CONT              LDA SPDATA+7,X

1140                    LDY 995

1150                    STA 2040,Y

1160                    INC 994

1170                    INC 994

1180                    INC 995

1190                    ASL 997

1200                    LDA 995

1210                    CMP #8

1220                    BEQ EXIT

1230                    JMP SPRITE

1240  EXIT              LDA 1001

1250                    CMP #0

1260                    BEQ IRQEND

1270                    LDA #>((IRQEND)-1)

1280                    PHA

1290                    LDA #<((IRQEND)-1)

1300                    PHA

1310                    JMP (1000)

1320  IRQEND            JMP $EA31

1330  SETMSB            PHP

1340                    PHA

1350                    LDA V+16

1360                    ORA 997

1370                    STA V+16

1380                    PLA

1390                    PLP

1400                    RTS

1410  CLEARMSB          PHP

1420                    PHA

1430                    LDA 997

1440                    EOR #$FF

1450                    STA 996

1460                    LDA V+16

1470                    AND 996

1480                    STA V+16

1490                    PLA

1500                    PLP

1510                    RTS

1520                    .BA49530

1530  SPDATA            .BY0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

1540                    .BY0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

1550                    .BY0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

1560                    .BY0,0,0,0,0,0,0,0

1570  SPOFFSET          .BY0,10,20,30,40,50,60,70

 

Zeile 10 - 230 entspricht fast genau dem vorigen Listing: Wenn in Adresse 999 der Wert 0 steht (Standard), dann wird dieser Wert zu 1 und die Interrupt-Routine MOVER wird an den Timer-Interrupt gebunden. Wenn bereits der Wert 1 in der Adresse 999 steht, dann wird dieser wieder zu 0 gesetzt, und die Standard-Interrupt-Routine wird reaktiviert. Auf diese Weise können Sie den Sprite-Mover beliebig stoppen und neu starten. Beachten Sie allerdings, dass das Neustarten nur die Animation der Sprites neu startet, und die Koordinaten nicht auf 0 zurücksetzt. Dies müssen Sie in einem separaten (BASIC-) Programm per Hand erledigen, indem Sie die richtigen Byte-Werte in den entsprechenden Datenstrukturen eintragen.

Kommen wir nun zum Hauptprogramm MOVER. MOVER initialisiert in Zeile 240 - 290 erst einmal ein paar Adressen mit Standardwerten. Dies ist zunächst Adresse 996, die das MSB (also das 9. Bit) der X-Koordinate des aktuellen Sprites zwischenspeichert (Standardwert=0), sowie die Adressen 994 und 995, die einmal die aktuelle Sprite-Nummer und einmal einen Offset auf die vom aktuellen Sprite zu benutzenden VIC-Register beinhalten. Wie dies genau funktioniert, wird an gegebener Stelle noch ausführlich erklärt.

In Zeile 300 (Label SPRITE) beginnt nun die Sprite-Steuerung. Um das aktuelle Sprite zu steuern, wird erst einmal die aktuelle Sprite-Nummer aus Adresse 995 in das Y-Register geladen. In Zeile 310 wird dann der entsprechende Zeiger auf den Parameter-Datenblock für das entsprechende Sprite in das X-Register geladen. Der Befehl

 

LDX SPOFFSET,Y

 

kommt daher, dass ein Datenblock, der die Bewegungsparameter für ein Sprite speichert, 10 Bytes groß ist, und diese Offset-Werte sind für alle 8 Sprites beim Label SPOFFSET als Byte-Werte abgelegt. Für die Bewegung selbst muss nun zunächst in Zeile 320 - 360 die Adresse 900,X ausgelesen werden (der erste Datenblock für Sprite 0 beginnt an Adresse 900). Steht in dieser Adresse der Wert 0, so wird der Sprite nach links bewegt (bedingter Sprung zum Label LEFT), steht in dieser Adresse der Wert 1, so wird der Sprite nach rechts bewegt (bedingter Sprung zum Label RIGHT). Die Bewegung selbst zu realisieren, ist im Endeffekt recht simpel: Wenn das Sprite nach links bewegt werden soll, dann wird der Wert, der in der Adresse 901,X und 902,X steht, von dem Zähler für die X-Position des aktuellen Sprites abgezogen (Zeile 370 – 550). Wenn das Sprite nach rechts bewegt werden soll, dann wird der Wert, der in der Adresse 901,X und 902,X steht, zu dem Zähler für die X-Position des aktuellen Sprites addiert (Zeile 560 - 740). Diese Mehr-Byte-Addition oder -Subtraktion erfordert natürlich den korrekten Umgang mit dem Carry-Flag. Allerdings ist dies nur die halbe Miete, denn die X-Position eines Sprites hat 9 Bits (die restlichen Bits 10-15 des Zählers werden stets durch AND ausmaskiert). Das 9. Bit muss hier sowohl im Zähler für die X-Position verbleiben, als auch in VIC-Register Nr. 16 korrekt gesetzt werden. Hierzu dienen die Unterroutinen SETMSB (Zeile 1330 - 1400) und CLEARMSB (Zeile 1410 - 1510). SETMSB setzt das MSB der X-Position für das aktuelle Sprite durch OR, CLEARMSB maskiert das entsprechende MSB durch eine AND-Maske aus. Die Aktualisierung der Y-Position (Zeile 750 - 980) arbeitet analog zu den Zählern für die Y-Position, allerdings muss hier kein MSB in ein entsprechendes VIC-Register übertragen werden.

Die Positionszähler und Zeitzähler für die 8 Sprites sind allerdings als separates Datensegment im Programmbereich untergebracht (Label SPDATA), und beginnen an Adresse 49530 direkt hinter dem Codesegment. Dies hat einen einfachen Grund: Die Adressen 900-979 belegen bereits den maximalen Bereich, der durch BASIC nicht verändert wird, und ab Adresse 1024 beginnt auch schon der Bildschirmspeicher. In diesen hatte ich dann am Anfang auch versehentlich die Zähler einiger Sprites hineingeschrieben, und wunderte mich anschließend, wieso dort so komische Zeichen erschienen, sobald ich mehr als 3 Sprites steuern wollte. Damit ich nicht mein ganzes Steuerprogramm neu schreiben musste, legte ich die Zähler, die man nur selten per Hand ändern muss, in das Segment direkt hinter meinem Code. Natürlich ist dies suboptimal, und ich werde den Mover auch später noch einmal überarbeiten.

Wie werden aber nun die Koordinaten aus den Positionszählern korrekt in die entsprechenden VIC-Register eingetragen? Bei der Sprite-X-Position sind dafür die folgenden Zeilen verantwortlich

 

440                     LDY 994

450                     STA V,Y

(die entsprechende Koordinate wurde zuvor in den Akkumulator geladen)

 

,sowie auch die Unterprogramme SETMSB und CLEARMSB. Bei der Sprite-Y-Position sind dafür die folgenden Zeilen verantwortlich:

 

970                     LDY 994

980                     STA V+1,Y

(die entsprechende Koordinate wurde zuvor in den Akkumulator geladen)

 

In Adresse 994 steht also stets der Offset, der zu der Adresse V (V=53248) addiert werden muss, um die entsprechenden Sprite-Positionsregister zu erreichen. Für das Sprite Nr. 0 ist dieser Wert also 0, und für das Sprite Nr. 1 ist dieser Wert 2. Da es 8 Sprites gibt, wird natürlich das Label SPRITE, an dem jeweils die Bewegung eines einzigen Sprites einleitet wird, 8-mal angesprungen, und natürlich wird auch der Wert in Adresse 994 8-mal um 2 erhöht.

An dieser Stelle war mein Programm ursprünglich zu Ende: Ich musste nur die Zähler in den Adressen 994-996 entsprechend aktualisieren, und meine Schleife in den Zeilen 300 - 1230 8-mal aufrufen, nämlich für jedes Sprites einmal. Leider lief die ursprüngliche Variante sehr holperig, stürzte manchmal sogar ab, und bot auch ein sehr langweiliges Bild: Ich hatte keine Animation, und konnte so z.B. kein laufendes Männchen oder einen sich drehenden Ball realisieren, wie ich dies mit dem Game Maker in vier Zeilen tun konnte:

 

SPRITE 0 IS [BALL  ]

SPRITE 0 DIR=[180° (RIGHT)]

SPRITE 0 MOVEMENT SPEED=[025]

SPRITE 0 ANIMATION SPEED=[025]

 

So fügte ich zusätzlich die Routine ANIMATOR ein. ANIMATOR ist die Routine, die die Sprite-Zeiger in den Adressen 2040-2047 periodisch ändert, und zwar so, dass eine sichtbare Animation entsteht. Die Zeiger in den Adressen 2040-2047 werden dabei als Animations-Frames bezeichnet. Ein Frame ist also das aktuelle Bild, das gerade für ein Sprite angezeigt wird. Die Animationssteuerung übernehmen hier die Adressen 906 (AninSpeed), 907 (MinFrame) und 908 (MaxFrame). AnimSpeed ist auch hier wieder nur ein Wert, der bei jedem Durchlauf der Interrupt-Routine zu dem Zähler AnimTimeCnt addiert wird. AnimTimeCnt ist ein Zähler, deshalb wird dieser wieder in dem Datensegment ab Adresse 49530 abgelegt, und zwar in Byte Nr. 6 in dem 10 Bytes großen Datenblock für jedes Sprite. Das nächste Animations-Frame, das angezeigt wird, wird auf die folgende Weise bestimmt: Immer, wenn der 8-Bit-Zähler AnimTimeCnt überläuft, wird das darauffolgende Byte (Byte Nr. 7 im Datenblock) um 1 erhöht. Dies geschieht auch hier wieder einfach durch eine Addition mittels ADC (also durch eine Addition mit Übertrag in Zeile 990 - 1050). In Zeile 1060 – 1120 wird anschließend geprüft, ob das aktuelle Animations-Frame schon mit dem Wert MaxFrame übereinstimmt. Diesen Wert können Sie wieder in den Datenstrukturen ab Adresse 900 für jedes Sprite separat festlegen (Byte Nr. 8 in dem entsprechenden 10 Bytes großen Datenbock für ein bestimmtes Sprite). Wenn das aktuelle Animations-Frame mit dem Wert MaxFrame nicht übereinstimmt, dann springt der BNE-Befehl in Zeile1080 direkt zum Label CONT (=continue). Ist jedoch CurrentFrame=MaxFrame, dann wird CurrentFrame zu MinFrame gesetzt, und zusätzlich der Zähler AnimTimeCount auf 0 zurückgesetzt. Anschließend wird das aktuelle Animations-Frame in den entsprechenden Sprite-Zeiger an der Adresse (2040+[aktuelle Sprite-Nummer]) eingetragen. Die Zeilen 1160 - 1230 beenden anschließend die Aktualisierungsschleife für das aktuelle Sprite, und wählen den Datenblock für das nächste Sprite in dem Datensegment ab Adresse 49530 aus. Anschließend erfolgt ein Sprung zurück zum Label SPRITE, aber nur so lange, wie die aktuelle Sprite-Nummer in Adresse 995 noch nicht den Wert 8 erreicht hat. Ansonsten wird die Interrupt-Routine durch IRQEND beendet.

IRQEND hatte am Anfang nur die Aufgabe, den Befehl JSR $EA31 auszuführen, und dadurch korrekt zu BASIC zurückzukehren. Dies war mir aber zu wenig, denn der Game Maker kann z.B. auch Songs im Hintergrund abspielen. Dies ist aber nur eine Option, die ich anschließend in die Routine IRQEND übernommen habe: In die Adressen 1000 und 1001 können Sie einen Zeiger auf ein Unterprogramm eintragen, das stets zusätzlich ausgeführt wird, wenn in Adresse 1001 nicht der Wert 0 steht. Da der 6510 Prozessor leider nur den JMP-Befehl zusammen mit indirekten Adressen ausführen kann, muss ein indirekter JSR-Befehl durch die folgenden Programmzeilen simuliert werden:

 

1270                    LDA #>((IRQEND)-1)

1280                    PHA

1290                    LDA #<((IRQEND)-1)

1300                    PHA

1310                    JMP (1000)

1320   IRQEND           JMP$EA31

 

JSR arbeitet also auf die folgende Weise: Von der 16-Bit-Adresse des nächsten Befehls wird 1 subtrahiert, und anschließend wird dieser 16-Bit-Zeiger in Form von {Hi-Byte, Lo-Byte} auf dem Stack abgelegt. Beim Rücksprung mittels RTS wird dann die Rücksprungadresse von Stack geholt (in der korrekten Form {Lo-Byte, Hi-Byte}), PC wird um 1 erhöht, und anschließend wird mit der normalen Programmausführung fortgefahren. In diesem Fall ist die Adresse, zu der der 6510 zurückkehren soll, das Label IRQEND, der indirekte Sprung zu der Adresse, auf die der Zeiger in den Adressen 1000 und 1001 zeigt, wird in Zeile 1310 durch einen indirekten direkten Sprung ersetzt.

Wenn der Mover nun läuft, dann übersetzen Sie ihn mit Hypra-Ass und drücken anschließend den Reset-Schalter.  Anschließend erstellen Sie das folgende BASIC-Listing MOVER-TEST:

 

09-MOVER-TEST

10 PRINT"[SHIFT+CLR/HOME]":D=1:V=53248:POKE 999,0:GOSUB 1000

20 FOR I=0 TO 7

30 SX=INT(500*RND(1))+100

40 SY=INT(500*RND(1))+100

50 HI=INT(SX/256):LO=SX-(256*HI)

60 POKE 900+(10*I),D:POKE 901+(10*I),LO:POKE 902+(10*I),HI

70 HI=INT(SY/256):LO=SY-(256*HI)

80 POKE 903+(10*I),D:POKE 904+(10*I),LO:POKE 905+(10*I),HI

90 POKE 906+(10*I),10:POKE V+39+I,I

100 POKE 907+(10*I),192:POKE 908+(10*I),194:POKE 49537+(10*I),192

110 IF D=0 THEN D=1:GOTO 130

120 IF D=1 THEN D=0:GOTO 130

130 NEXT I

140 POKE V+21,255:POKE 1000,0:POKE 1001,0:SYS 49152

150 END

1000 REM *** INIT ***

1010 FOR I=12288 TO 12415:READ A:POKE I,A:NEXT I

1020 FOR I=53176 TO 53247:READ A:POKE I,A:NEXT I

1030 SYS 53176"SPRITE-MOVER"

1040 RETURN

10000 REM *** SPRITE-DATEN ***

10010 DATA 255,0,0,129,0,0,129,0,0,129,0,0,129,0,0,129,0,0,129,0,0,255,0,0

10020 DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

10030 DATA 0,0,0,0,0,0

10040 DATA 255,0,0,255,0,0,255,0,0,255,0,0,255,0,0,255,0,0,255,0,0,255,0,0

10050 DATA 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

10060 DATA 0,0,0,0,0,0

11000 REM *** SAVER ***

11010 DATA 32,87,226,162,8,134,186,32,121

11020 DATA 0,240,44,32,253,174,32,138,173

11030 DATA 32,247,183,72,32,121,0,240,21,104,132,193,133,194,32,253,174,32

11040 DATA 138,173,32,247,183,132,174,133,175,76,237,245,104,132,195,133

11050 DATA 196,160,0,44,160,1,132,185,169

11060 DATA 0,76,165,244,60,54,52,39,69,82,62

 

Das Programm MOVER-TEST liest zunächst durch das Unterprogramm an Zeile 1000 zwei Animations-Frames ein, die den Bereich 12288-12415 belegen (Sprite-Block 192 und 193). Außerdem wird das „Saver“-Programm initialisiert, um mit diesem das Maschinenprogramm SPRITE-MOVER nachladen zu können.

Nach Beenden des Initialisierungs-Unterprogramms (Zeile 1000 - 1040) werden Animationsdaten für alle 8 Sprites erzeugt, und in den Adressen 900-979 abgelegt. Bei den einzelnen Sprites ist das Start-Frame der Animationssequenz Sprite-Block Nr. 192, und das End-Frame 194- dies wird jedoch nicht mehr angezeigt, sondern auf Sprite-Block 193 folgt wieder Sprite-Block 192. Das Start-Frame ist bei jedem Sprite 192, und dies wird auch in Zeile 100 durch

 

POKE 49537+(10*I),192

 

am Anfang auf 192 gesetzt. Dies gilt für alle 8 Sprites, allerdings wird die Bewegungs-Richtung per Zufallsgenerator bestimmt. Dies geschieht durch Initialisierung der Zähler XDiff und YDiff mit Werten zwischen 100 und 599, das Vorzeichen, das z.B. bestimmt, ob sich ein Sprite nach rechts oder links bewegt, wechselt ständig (Zeile 110 und 120). Wenn sich sämtliche Sprites am Anfang an der Position (0,0) befinden, dann bewegen sich diese trotzdem in verschiedene Richtungen, und irgendwann sehen Sie dann auch alle 8 Sprites auf dem Bildschirm. Für die Sprite-Farben werden die Farbnummern 0-7 benutzt, Sprite 0 ist also schwarz, Sprite 7 gelb. Wenn Sie den Mover-Test mit RUN starten, beendet sich das BASIC-Programm nach einiger Zeit mit der Meldung READY. Der Cursor wird angezeigt, und Sie können neue BASIC-Befehlseilen eingeben. Allerdings werden sämtliche Sprites im Hintergrund unabhängig von BASIC gesteuert, und Sie können dieses Verhalten nur durch SYS 49152 abstellen.

Im nächsten Kapitel erfahren Sie, wie Sie durch eine ähnliche Technik, wie sie in den letzten zwei Listings dargestellt wurde, Songs im Hintergrund abspielen können.

 

4.4 Multicolor-Pixel mit Assembler setzen

 

Viele Spiele benutzen Bitmap-Bilder als Hintergrund für das Intro- meist zusammen mit einem Song. Manche Adventures laden auch teilweise Bitmap-Bilder nach, natürlich durch Assembler-Routinen. Aber auch die Grafik-Routinen z.B. von Simon’s BASIC wurden in Assembler erstellt. Wie setzt man aber nun einzelne Pixel mit Assembler, wenn auch Bitmaps im Endeffekt nur aus 8*8 Pixel großen Blöcken aufgebaut sind? Ist dies nicht eine Sache, die nur BASIC beherrscht? Zum Glück ist dies nicht so. Zunächst müssen Sie sich aber ein paar Tricks ausdenken, um Pixel in Assembler zu setzen. Da wäre z.B. die Tatsache, dass eine Zeile 320 Pixel umfasst, und dass z.B. die Adresse der 8. Bildschirmzeile (Zeile Nr. 7)

 

8192+(320*Y)

 

ist. Wie wollen Sie diese Multiplikation aber effizient auf dem 6510 ausführen, wenn er keine Multiplikationen ausführen kann? An dieser Stelle müssen Sie schon den ersten Trick anwenden: Sie schreiben die Zahl 320 als

 

320=Hi-Byte(320)+Lo-Byte(320)=256+64

 

64 ist nun eine Potenz von 2, und kann so durch Bitverschiebungen dargestellt werden. Nehmen wir nun an, Y sei die zu adressierende Bildschirmzeile, und YS entspricht dem ganzzahligen Ergebnis der Division von Y/8 ohne Rest. Dann können Sie erst einmal YS in das Hi-Byte des Zeigers kopieren, der am Ende auf den Beginn der Bildschirmzeile Y zeigen soll. Dies können Sie z.B. so erledigen (in Adresse 1000 sei die -Koordinate des zu setzenden Pixels abgelegt, in Adresse 1001 die Y-Koordiate):

 

LDA 1001

LSR

LSR

LSR

PHA

(die 3 LSR-Befehle ergeben eine Division durch 8 ohne Rest, und dieses Ergebnis wird anschließend auf dem Stack abgelegt)

STA 249

(der Zeiger auf die Zeile Y soll am Ende in den Adressen 248 und 249 abgelegt werden)

 

Wir haben nun das BASIC-Pendant

 

256*INT(Y/8)

 

ausgerechnet. Als nächstes müssen wir zu dem Zeiger in den Adressen 248 und 249 noch

 

64*INT(Y/8)

 

addieren. Diesen Wert wollen wir nun in den Adressen 250 und 251 ablegen, und zwar so:

 

                  LDA #0

                  STA 248

                  STA 251

                  PLA

                  STA 250

                  (stellt INT(Y/8) in Adresse 250 wieder her)

                  LDX #6

                  CLC

MULTIPLY          ROL 250

                  ROL 251

                  DEX

BNE MULTIPLY

 

Der Trick ist hier, zunächst das Carry-Flag zu löschen. Anschließend wird der 16-Bit-Wert in den Adressen 250 und 251 sechsmal nach links verschoben, aber nicht mittels ASL, sondern mittels ROL. ROL bewirkt, dass das Bit, das links aus der Speicheradresse 250 herausgeschoben wird, im Carry-Flag landet, und anschließend (mit dem nächsten ROL-Befehl) in die Adresse 251 an der rechten Seite wieder hineingeschoben wird. Auf diese Weise gehen auch die zusätzlichen Bits, die z.B. bei der Multiplikation von 20 mit 64 entstehen. Setzen wir nun unseren Zeiger in den Adressen 248 und 249 so zusammen, dass dieser auf die Adresse

 

8192+320*INT(Y/8)

 

zeigt. Dies ist nun in sehr einfacher Weise durch Additionsbefehle zu erreichen:

 

CLC

LDA 248

ADC 250

STA 248

LDA 249

ADC 251

ORA #$20

STA 249

 

Beachten Sie an dieser Stelle, dass 819210=200016 ist, und dass es an dieser Stelle genügt (um den Anfang des Grafik-Bildschirmspeichers als Offset auszuwählen), das Hi-Byte in der Adresse 249 zusätzlich durch ORA mit dem Wert $20 verknüpfen. Überlaufen kann das Hi-Byte hier nicht, denn die höchst mögliche Zeigeradresse ist $FFFF. Nun fehlt natürlich noch die X-Position. Der Offset zu dem Zeiger, der bereits in den Adressen 248 und 249 steht, ist hier

 

8*INT (X/8)

 

8*INT(X/8) wird wie folgt ermittelt:

 

LDA #0

STA 251

LDA 1000

AND #$FC

STA 250

CLC

ROL 250

ROL 251

CLC

LDA 248

ADC 250

STA 248

LDA 249

ADC 251

STA 249

 

Ich habe an dieser Stelle die ursprüngliche X-Koordinate mit 2 multipliziert, weil ich vorhabe, Multicolor-Pixel zu setzen, und diese bestehen eben aus 2 Subpixeln. Auf diese Weise reicht eine einzige Speicheradresse aus (nämlich 1000), um sämtliche Pixelpositionen in einem Multicolor-Bitmap anzusprechen (Wert 0-159).

Der Zeiger in den Adressen 248 und 249 zeigt nun auf den 8*8 Bytes großen Bildblock, der auch das Pixel enthält, das Sie setzen wollen. Wir haben also unser Ziel fast erreicht, und müssen im Endeffekt nur noch den Rest der Division von

 

INT(Y/8)

 

zu dem Zeiger in den Adressen 248 und 249 addieren. Dies ist aber ganz einfach: Wenn Sie den Ursprungswert für die Bildschirmzeile Y, die sich nach wie vor in der Adresse 1000 befindet, durch 8 teilen, dann verwerfen Sie einfach den Divisionsrest, der vorher in Bit 0-2 gestanden hat. Diesen Divisionsrest können Sie nun in einfacher Weise wieder hinzufügen:

 

LDA 1001

AND #$07

ORA 248

STA 248

 

Dies geht deshalb gut, weil der Zeiger in den Adressen 248 und 249 bis jetzt nur Werte annehmen kann, die durch 8 teilbar sind. Kommen wir nun zum letzten Schritt, nämlich dem Setzen der korrekten Pixel-Bits. Ich nehme an dieser Stelle an, dass der Bildschirmspeicher nicht nur 0-Bytes enthält, sondern dass Sie gewissermaßen ein altes Pixel mit einer neuen Farbe überschreiben wollen. Auch hier müssen Sie einen Modulo bestimmen, nämlich

 

mod(2*(X/8))

 

Dies funktioniert auf die folgende Weise:

 

LDA 1000

ASL

AND #$07

 

Beachten Sie auch hier wieder, dass ich annehme, dass ein Pixel aus 2 Subpixeln besteht (deshalb das zusätzliche ASL), und dass die X-Position nur Werte zwischen 0 und 159 annehmen kann. Nehmen wir nun an, dass die Farbnummer des zu setzenden Pixels in Adresse 1002 in den obersten 2 Bits (also in Bit 6 und 7) steht. In diesem Fall ersetzen Sie das Multicolor-Pixel an der Position (X,Y) wie folgt durch ein neues Pixel:

 

                  LDA 1002

                  STA 1005

                  LDA #$C0

                  STA 1003

                  LDA 1000

                  ASL

                  AND #$07

                  TAX

BACK              CPX #0

                  BEQ CONT

                  LSR 1003

                  LSR 1005

                  DEX

                  JMP BACK

CONT              LDY #0

                  LDA 1003

                  EOR #$FF

                  STA 1004

                  LDA (248),Y

                  AND 1004

                  ORA 1005

                  STA (248),Y

 

Zugegeben: Das Setzen eines Pixels ist etwas tricky. Zunächst muss nämlich der Farbwert aus Adresse 1002 kopiert werden, in diesem Fall in Adresse 1005. In Adresse 1003 wird nun zunächst der Wert #$C0 (Bit 6 und 7 sind 1) gespeichert, und in den Akkumulator wird

 

mod(2*(X/8))

 

geladen. Dieser Wert wird nun mittels TAX in das X-Register geschrieben und als Schleifenzähler für die folgende Schleife verwendet: Bei jedem Schleifendurchlauf wird der Wert in den Adressen 1003 und 1005 um 1 Bit nach rechts verschoben. Der Rest der Division von (2*X)/8 wird also dazu benutzt, um die korrekte Pixelposition zu ermitteln, die bestimmten Bits in der Adresse entspricht, auf die der Zeiger in den Adressen 248 und 249 zeigt. Diese Bits werden nun erst ausmaskiert (mittels AND) und anschließend werden diese Bits wieder durch den Farbwert ersetzt, der in den obersten Bits in Adresse 1002 steht. Wie gesagt ist dies etwas tricky, und ich selbst habe die letzten Zeilen auch nur durch Probieren und lesen vieler alter 64-er-Hefte hinbekommen. Ich will Ihnen aber trotzdem ein vollständig lauffähiges Listing zum Setzen von Pixeln mit SYS 49152 nicht vorenthalten. Zusätzliche Funktion: Ein wert von 255 in der Adresse 1000 löscht den Grafikbildschirm, der Textbildschirm wird dann mit den Zeichen gefüllt, die in der Adresse 1001 stehen.

 

10-PSETV1

10                      .BA 49152

20                      LDA 1000

30                      CMP #$FF

40                      BNE PSET

50                      JSR CLRSCR

60                      RTS

70    PSET              LDA 1001

80                      LSR

90                      LSR

100                     LSR

110                     PHA

120                     STA 249

130                     LDA #0

140                     STA 248

150                     STA 251

160                     PLA

170                     STA 250

180                     LDX #6

190                     CLC

200   MULTIPLY          ROL 250

210                     ROL 251

220                     DEX

230                     BNE MULTIPLY

240                     CLC

250                     LDA 248

260                     ADC 250

270                     STA 248

280                     LDA 249

290                     ADC 251

300                     ORA #$20

310                     STA 249

320                     LDA #0

330                     STA 251

340                     LDA 1000

350                     AND #$FC

360                     STA 250

370                     CLC

380                     ROL 250

390                     ROL 251

400                     CLC

410                     LDA 248

420                     ADC 250

430                     STA 248

440                     LDA 249

450                     ADC 251

460                     STA 249

470                     LDA 1001

480                     AND #$07

490                     ORA 248

500                     STA 248

510                     LDA 1002

520                     STA 1005

530                     LDA #$C0

540                     STA 1003

550                     LDA 1000

560                     ASL

570                     AND #$07

580                     TAX

590   BACK              CPX #0

600                     BEQ CONT

610                     LSR 1003

620                     LSR 1005

630                     DEX

640                     JMP BACK

650   CONT              LDY#0

660                     LDA 1003

670                     EOR #$FF

680                     STA 1004

690                     LDA (248),Y

700                     AND 1004

710                     ORA 1005

720                     STA (248),Y

730                     RTS

740   CLRSCR            LDA #0

750                     STA 248

760                     LDA #$20

770                     STA 249

780                     LDY #0

790   CLEAR             LDA #0

800                     STA (248),Y

810                     CLC

820                     LDA 248

830                     ADC #1

840                     STA 248

850                     LDA 249

860                     ADC #0

870                     STA 249

880                     LDA 249

890                     CMP #$40

900                     BNE CLEAR

910                     LDA #0

920                     STA 248

930                     LDA #4

940                     STA 249

950                     LDY #0

960   CLEAR2            LDA 1001

970                     STA (248),Y

980                     CLC

990                     LDA 248

1000                    ADC #1

1010                    STA 248

1020                    LDA 249

1030                    ADC #0

1040                    STA 249

1050                    LDA 248

1060                    CMP #232

1070                    BNE CLEAR2

1080                    LDA 249

1090                    CMP #7

1100                    BNE CLEAR2

1110                    RTS

 

Das entsprechende BASIC-Pendant, das eine Multicolor-Ellipse mit der entsprechenden Maschinensprache-Routine zeichnet, sehen Sie in dem nächsten Listing.

 

11-PSET-DEMO1

10 PRINT"[SHIFT+CLR/HOME]":POKE 53282,0:POKE 53283,1:POKE 53284,5

20 FOR I=49152 TO 49370:READ A:POKE I,A:NEXT I

30 POKE 53265,54:POKE 53272,24:POKE 53270,216

40 POKE 1000,255:POKE 1001,1:SYS 49152:C=0

50 FOR I=-[PI] TO [PI] STEP 0.02

(bitte hier für PI das entsprechende Symbol der C64-Tastatur benutzen)

60 X=50+50*SIN(I):Y=100+50*COS(I)

70 POKE 1000,X:POKE 1001,Y:POKE 1002,64*C

80 SYS 49152:C=(C+1) AND 3

90 NEXT I

100 END

40000 REM *** PSET ***

40010 DATA 173,232,3,201,255,208,4,32,145,192,96,173,233,3,74,74,74,72

40020 DATA 133,249,169,0,133,248,133,251,104,133,250,162,6,24,38,250,38

40030 DATA 251,202,208,249,24,165,248,101,250,133,248,165,249,101,251

40040 DATA 9,32,133,249,169,0,133,251,173,232,3,41,252,133,250,24,38,250

40050 DATA 38,251,24,165,248,101,250,133,248,165,249,101,251,133,249,173

40060 DATA 233,3,41,7,5,248,133,248,173,234,3,141,237,3,169,192,141,235

40070 DATA 3,173,232,3,10,41,7,170,224,0,240,10,78,235,3,78,237,3,202

40080 DATA 76,110,192,160,0,173,235,3,73,255,141,236,3,177,248,45,236

40090 DATA 3,13,237,3,145,248,96,169,0,133,248,169,32,133,249,160,0,169

40100 DATA 0,145,248,24,165,248,105,1,133,248,165,249,105,0,133,249,165

40110 DATA 249,201,64,208,233,169,0,133,248,169,4,133,249,160,0,173,233

40120 DATA 3,145,248,24,165,248,105,1,133,248,165,249,105,0,133,249,165

40130 DATA 248,201,232,208,232,165,249,201,7,208,226,96

 

Wie Sie sehen, läuft auch das durch Maschinensprache unterstützte Programm nur um den Faktor 3-5 schneller, als das Pendant, das Sie schon im BASIC-Kurs kennengelernt haben. Dies liegt vor Allem an den BASIC-Funktionen SIN() und COS(), aber auch daran, dass BASIC für den SYS-Befehl viel Zeit benötigt. Auch Doppelschleifen, mit denen Sie z.B. ein ausgefülltes Rechteck zeichnen können, laufen eher behäbig. Sie sind also nun wirklich an die Grenzen des Machbaren gestoßen. Deshalb wurden wirklich professionelle Malprogramme wie Hi Eddi und Koala Painter auch komplett in Assembler geschrieben. Aber nicht nur das: Hier wurde auch vieles optimiert. Ich selbst habe noch einmal folgende Veränderungen vorgenommen, um das Setzen von Pixeln noch mehr zu beschleunigen:

 

·       Auslagern von umfangreichen Berechnungen wie 8192+(320*INT(Y/8)) oder 8*INT(X/8) in Tabellen, in denen Sie die entsprechenden Werte einfach nur nachschlagen müssen (sog. Lookup-Tabellen) - dies benötigt leider viel zusätzlichen Speicher

·       Vermeiden von Schleifen mit mehreren Durchläufen und ersetzen dieser Schleifen durch entsprechende Lookup-Tabellen - dies benötigt leider viel zusätzlichen Speicher

·       Vermeiden von ROL- und ROR-Befehlen, sowie von direkten Sprüngen (diese Befehle verschwenden Zeit durch zusätzliche Wartezyklen)

 

Leider ist es mir bis jetzt nicht gelungen, trotz Lookup-Tabellen einen Beschleunigungsfaktor von mehr als 10 zu erzielen. Mit dem Austro-Compiler habe ich immerhin einen Beschleunigungsfaktor von 20 erreicht. Diese Beschleunigung können Sie z.B. an dem Programm BOX sehen, das sich auch in dem Disketten-Image AssemblerKurs.d64 befindet. Aber trotzdem konnte ich immer noch zusehen, wie die einzelnen Pixel gesetzt wurden. Wenn ich irgendwann eine Lösung finde, mit der man Pixel in einem noch schnelleren Tempo setzen kann, werde ich diese an dieser Stelle einfügen. Deshalb mein Tipp: Um professionelle Bitmaps zu erstellen, sollten Sie ein Tool wie Hi Eddi oder Koala Painter benutzen. Auch der Game Maker kann gute Dienste leisten, da er die Bilder in einem einfachen Format abspeichert, nach dem Sie auch problemlos im Internet suchen können. Meine beschleunigte Programmversion zum Pixel-Setzen sieht nun so aus:

 

12-PSET2

10                      .BA 49152

20                      LDA 1000

30                      CMP #$FF

40                      BNE PSET

50                      JMP CLRSCR

60    PSET              LDA 1000

70                      LSR

80                      LSR

90                      ASL

100                     TAX

110                     LDA XPTR,X

120                     STA 249

130                     LDA XPTR+1,X

140                     STA 248

150                     LDA 1001

160                     LSR

170                     LSR

180                     LSR

190                     ASL

200                     TAX

210                     LDA YPTR,X

220                     STA 251

230                     LDA YPTR+1,X

240                     STA 250

250                     CLC

260                     LDA 248

270                     ADC 250

280                     STA 248

290                     LDA 249

300                     ADC 251

310                     STA 249

320                     LDA 1001

330                     AND #7

340                     ORA 248

350                     STA 248

360                     LDA 1002

370                     STA 1004

380                     LDA #$C0

390                     STA 1003

400                     LDA 1000

410                     ASL

420                     AND #$07

430                     TAX

440   BACK              CPX #0

450                     BEQ CONT

460                     LSR 1003

470                     LSR 1004

480                     DEX

490                     JMP BACK

500   CONT              LDY #0

510                     LDA 1003

520                     EOR #$FF

530                     STA 1003

540                     LDA (248),Y

550                     AND 1003

560                     ORA 1004

570                     STA (248),Y

580                     RTS

590   CLRSCR            LDA #$00

600                     STA 248

610                     LDA #$20

620                     STA 249

630   CLEAR             LDY #0

640                     LDA #0

650                     STA (248),Y

660                     CLC

670                     LDA 248

680                     ADC #1

690                     STA 248

700                     LDA 249

710                     ADC #0

720                     STA 249

730                     LDA 249

740                     CMP #$40

750                     BNE CLEAR

760                     LDA #232

770                     STA 248

780                     LDA #7

790                     STA 249

800   CLEAR2            LDY #0

810                     LDA 1001

820                     STA (248),Y

830                     SEC

840                     LDA 248

850                     SBC #1

860                     STA 248

870                     LDA 249

880                     SBC #0

890                     STA 249

900                     LDA 249

910                     CMP #3

920                     BNE CLEAR2

930                     RTS

940   XPTR .BY $00,$00,$00,$08,$00,$10,$00,$18,$00,$20,$00,$28,$00,$30

950        .BY $00,$38,$00,$40,$00,$48,$00,$50,$00,$58,$00,$60,$00,$68

960        .BY $00,$70,$00,$78,$00,$80,$00,$88,$00,$90,$00,$98,$00,$A0

970        .BY $00,$A8,$00,$B0,$00,$B8,$00,$C0,$00,$C8,$00,$D0,$00,$D8

980        .BY $00,$E0,$00,$E8,$00,$F0,$00,$F8,$01,$00,$01,$08,$01,$10

990        .BY $01,$18,$01,$20,$01,$28,$01,$30,$01,$38

1000  YPTR .BY $20,$00,$21,$40,$22,$80,$23,$C0,$25,$00,$26,$40,$27,$80

1010       .BY $28,$C0,$2A,$00,$2B,$40,$2C,$80,$2D,$C0,$2F,$00,$30,$40

1020       .BY $31,$80,$32,$C0,$34,$00,$35,$40,$36,$80,$37,$C0,$39,$00

1030       .BY $3A,$40,$3B,$80,$3C,$C0

 

Wie gesagt werden die aufwendigen Berechnungen in Lookup-Tabellen ausgelagert. So wird z.B. die Adresse für die Bildschirmzeile Y/8, sowie der Offset für die Koordinate 8*INT(X/8) vorberechneten Tabellen entnommen, und die einzige Schleife, die die PSET-Routine noch enthält, ist die Schleife, um die Pixelfarbe richtig in das entsprechende Byte zu schreiben. Trotzdem ist zumindest zusammen mit BASIC die Geschwindigkeit mäßig.