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
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
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:
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
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.