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.