Tipps
und Tricks
In diesem Bereich
erfahren Sie nichts Spezielles über Assembler oder BASIC. Jedoch benötigen Sie
fast immer noch Tipps, die Ihnen das Leben erleichtern können. Viele Tipps
passen natürlich nicht wirklich in den Kursbereich, z.B. zum Thema Scrolling,
Aufbau von Spiele-Leveln, Dateiverwaltung und Musikprogrammierung. Es gibt
einfach so viele Tipps und Tricks, dass diese einfach nicht in einem
Standardkurs untergebracht werden können. Genau deswegen habe ich auch das
Extrakapitel „Tipps und Tricks“ geschaffen. Betrachten Sie diesen Bereich
einfach als Programm-Sammlung, die im Laufe der Zeit immer größer wird, wenn
ich mal die Zeit finde, Dinge am C64 auszuprobieren. Ich habe die Tipps und
Tricks in einen BASIC- und einen Assembler-Bereich unterteilt, wobei der
Assembler-Bereich umfangreicher ist.
1. Tipps und Tricks für
BASIC
1.1 PRINT-Ausgaben
umleiten
Das C64-BASIC ist sehr
schlicht und einfach gestrickt. Deshalb liegen auch sämtliche Zeiger und
Zusatzinformationen, die irgendwie von BASIC benutzt werden, innerhalb eines
bestimmten Adressbereichs- den Adressen 2-1023. Da BASIC-Programme erst nach
dem Bildschirmspeicher (an Adresse 2049) beginnen, können Sie auch die
Konfiguration von BASIC selbst verändern, indem Sie den Inhalt bestimmter
Adressen ändern. Der erste BASIC-Tipp ändert nun die Adresse, an der der PRINT-Befehl
die als nächstes auszugebenden Zeichen ausgibt.
Damit PRINT
feststellen kann, wohin mit den Zeichen, die als nächstes auf dem Bildschirm
erscheinen sollen, muss ein Zeiger benutzt werden. Dieser Zeiger zeigt in Form
eines Lo- und Hi-Bytes auf den Bereich im Bildschirmspeicher, in den als
nächstes geschrieben wird. Dieser Zeiger ist in den Adressen 209 und 210
abgelegt. In der Adresse 209 steht das Lo- Byte, und in der Adresse 210 das
Hi-Byte. Um also die nächste PRINT-Ausgabe „umzubiegen“, muss der Inhalt
der Adressen 209 und 210 verändert werden. Angenommen, Sie wollen mit PRINT
den Text „HALLO“ in einen Offscreen-Buffer schreiben,
der an der Adresse 4096 liegt. Das folgende
BASIC-Listing leistet dies:
10 AD=4096
20
HI=INT(AD/256): LO=AD-256*HI
30
POKE 209,LO: POKE 210,HI
40 PRINT“HALLO“
Der letzte Tipp hat
leider einen Nachteil: In den Adressen 209 und 210 befinden sich nur temporäre
Werte. Das heißt: Sobald in der PRINT-Ausgabe irgendein Steuereichen
auftaucht, um z.B. den Cursor zu versetzen, schreibt PRINT sofort wieder
in den sichtbaren Bildschirmbereich. Dies liegt einfach daran, dass PRINT
Kernal-Funktionen aufruft, um die einzelnen Zeichen
auszugeben, und hier ist der Bildschirmspeicher fest mit der Adresse 1024
verbunden. Dieses Verhalten können Sie nur dann ändern, wenn Sie die Kernal-Funktionen für die Zeichenausgabe selbst neu
programmieren. Ansonsten müssen Sie z.B. einen Offscreen-Buffer
quasi „hintereinander weg“ ohne Steuerzeichen mit PRINT ausgeben, z.B.
so:
10 AD=4096
20 HI=INT(AD/256): LO=AD-256*HI
30
POKE 209,LO: POKE 210,HI
40
PRINT”DIES IST DIE ERSTE ZEILE DES PUFFERS
“;
50
PRINT“UND DIES IST DIE ZWEITE ZEILE
“;
60
PRINT“UND DIES DIE DRITTE
“
Der Offscreen-Buffer
kann angezeigt werden, indem man das VIC-Register Nr. 24 ändert (Adresse 53272).
Der Standardwert in VIC-Register Nr. 24 ist der Wert 21, was bedeutet, dass der
Bildschirmspeicher an Adresse 1024-2023 liegt. Die Position des
Bildschirmspeichers kann nun in Kilobyte-Schritten geändert werden. Wenn der Offscreen-Buffer nun an der Adresse 4096 beginnt, kann er
durch die folgenden BASIC-Zeilen angezeigt werden:
70
BU=4096: REM ADRESSE PUFFER
80
BK=INT(BU/1024): REM BANKNUMMER
90 POKE 53272,PEEK(53272)
AND 15
100 POKE 53272,PEEK(53272)
OR (16*BK)
Der Standardbildschirm
wird mit POKE 53272,21 wiederhergestellt.
Der VIC sieht also
seinen Speicher als einzelne Speicherbänke mit jeweils 1024 Bytes. Die
Banknummer des zu nutzenden Bildschirm-Zeichen-Speichers steht dabei in den
oberen 4 Bits von VIC-Register Nr. 24, kann also nur zwischen der Adresse 0 und
16383 liegen. Eine weitere Einschränkung ist, dass die Lage des Speichers für
die Zeichenfarben nicht verschoben werden kann, es sei denn, Sie verwenden
VIC-Versionen oberhalb von 3. Diese relativ neuen VIC-Bausteine sind aber
teilweise inkompatibel zu dem C64-Standard-BASIC 2.0 und benötigen deshalb
andere BASIC-ROMs. Dies ist z.B. beim Plus 4 der Fall, aber auch schon bei der
Plus-Version des C 128.
1.2 INPUT-Eingaben
umleiten
Mit dem INPUT-Befehl
können beliebige Zeichenfolgen von der Tastatur eingelesen werden, mit INPUT#
funktioniert die Sache auch mit Dateien. So können z.B. Tastatureingaben auch
in Dateien geschrieben werden, z.B. auf die folgende Weise:
10
OPEN 1,8,0,“LOGFILE“
20
INPUT#1,A$: REM NUR BEI KERNAL 1 UND 2 LANDET A$ AUCH
IN DER DATEI!
30 IF A$=“E“ THEN GOTO
50
40
GOTO 20
50
CLOSE 1
Der Sekundärkanal 0 ist
für INPUT# immer die Tastatur, und deshalb landen die im letzten
Beispiel mit INPUT# eigegebenen Daten auch in der Logdatei, und nicht im
String A$. A$ wird hier im Endeffekt nur dazu benutzt, um
festzustellen, ob das Programm (bei Eingabe von E) beendet werden soll.
Allerdings funktioniert dies nur bei den älteren Kernal-Versionen
1 und 2. Offenbar haben die Kernal-Entwickler das
ursprüngliche Verhalten des obigen Listings (also, dass A$ direkt in der
Datei landet) als Bug gesehen.
Wenn als Quellgerät
nicht die Floppy oder der Drucker angegeben werden soll, sondern die Tastatur,
muss OPEN mit den Parametern
OPEN
1,0
aufgerufen werden. INPUT#
kann nun direkt auf die Tastatur zugreifen, und die Quelldaten später auch in
einem String ablegen. Was Sie davon haben, ist, dass Sie das oft störende
Fragezeichen umgehen können, das der INPUT-Befehl stets anzeigt. Dazu
genügt das folgende kleine Programm:
10
OPEN 1,0: REM ALLE KERNAL-VERSIONEN
20 PRINT“READ
FILE:“;:INPUT#1,F$
30
CLOSE 1
40
OPEN 1,8,1,F$
50 …
Die Dinge, die Sie mit der Datei anstellen wollen …
Natürlich können Sie
das obenstehende Listing auch für andere Eingaben anpassen.
1.3 Erweiterte
Tastaturabfrage
Die Tastatur lässt sich
bekanntlich mit GET abfragen, z.B. mit der folgenden BASIC-Zeile:
10 GET A$: IF A$=““
THEN 10
Leider lässt sich so
z.B. nicht explizit feststellen, ob SHIFT oder COMMODORE gedrückt
wurde. Nur die Cursor- und Funktionstasten erscheinen in A$ als
ASCII-Zeichen. Für wirklich professionelle Anwendungen reicht also GET
nicht aus. Um z.B. festzustellen, ob SHIFT gedrückt wurde, muss das
Tastatur-Status-Byte ausgelesen werden. Dieses Byte liegt an Adresse 653
und wird laufend über den Timer-Interrupt
(bzw. die Steuerroutine, die an diesen Interrupt normalerweise gebunden ist)
aktualisiert. Um also festzustellen, ob SHIFT gedrückt wurde, muss ein BASIC-Programm
das Byte an Adresse 653 abfragen, z.B. so:
10 A=PEEK(653)
20 IF ((A AND 1)=1)
GOTO [SHIFT-Routine]
Für jede der drei
Tasten SHIFT, CTRL und COMMODORE gibt es nun in Adresse
653 ein Flag. Ein Flag wird
immer dann gesetzt, wenn die entsprechende Taste aktiv ist, und gelöscht, wenn
die entsprechende Taste inaktiv (also nicht gedrückt) ist. Die folgenden Bits
enthalten nun folgende Flags:
Bit 0: SHIFT ist
gedrückt
Bit 1: COMMODORE
ist gedrückt
Bit 2: CTRL ist
gedrückt
Die einzelnen Flags
sind auch kombinierbar, wenn also SHIFT+COMMODORE gleichzeitig
gedrückt sind, dann ist PEEK(653)=3.
Der Tastaturcode selbst
wird aber in Adresse 197 (zuletzt gedrückte Taste) und 203
(gerade aktive Taste) festgehalten, wobei der Wert 0 in Adresse 197 bedeutet,
dass gerade keine Taste gedrückt ist. Die Tastaturcodes sind leider keine
ASCII-Zeichen, sondern im Endeffekt nur Zwischenspeicher, die von der
Tastatur-Dekodier-Tabelle dazu benutzt werden, um daraus ASCII-Codes zu machen.
Die Tastaturcodes sind wie folgt definiert:
00 INST/DEL 01 RETURN 02 CSR LEFT/RIGHT 03 F7 04 F1 05 F3 06 F5 07 CSR UP/DOWN 08 3 09 W 10 A 11 4 12 Z 13 S 14 E 15 --- |
16 5 17 R 18 D 19 6 20 C 21 F 22 T 23 X 24 7 25 Y 26 G 27 8 28 B 29 H 30 U 31 V |
32 9 33 I 34 J 35 0 36 M 37 K 38 O 39 N 40 + 41 P 42 L 43 – 44 . 45 : 46 @ 47 , |
48 Pfund-Zeichen 49 * 50
; 51 CLR/HOME 52 --- 53 = 54 Pfeil nach oben 55 ? 56 1 57 Pfeil nach
links 58 --- 59 2 60 SPACE-Taste 61 --- 62 Q 63 RUN/STOP |
Die Tastatur-Dekodier-Tabellen
(also die Tabellen, die einer bestimmten Taste ein bestimmtes Zeichen zuordnen)
werden durch den Zeiger in den Adressen 245 (Lo) und 246 (Hi)
adressiert. Allerdings geschieht dies durch die Routine, die normalerweise
standardmäßig an den Timer-Interrupt
gebunden ist. Das heißt, es genügt nicht, den Zeiger in den Adressen 245 und
246 umzubiegen, um so auf eine einfache Weise eine eigene Tastaturtabelle z.B.
für deutsche Tastaturen zu erstellen. Denn der Wert, der in die Adressen 245
und 246 geschrieben wird, ist auf die folgende Weise fest vorgegeben:
Ohne gerade gedrückte
Steuertaste: Adresse $EB81
Mit gedrückter SHIFT-Taste:
Adresse $EBC2
Mit gedrückter COMMODORE-Taste:
$EC03 (SHIFT wird hier ignoriert)
Mit gedrückter CTRL-Taste$EC78
(SHIFT und COMMODORE werden hier ignoriert)
Wie Sie sehen, liegen
sämtliche Bereiche, die der Kernal-Tastaturtreiber
anspricht, im ROM, und so kann auch der Bereich nicht verändert werden, den die
Standard-Interrupt-Routine für die Tastaturtabellen verwendet. Deshalb benutzen
auch professionelle Schreibprogramme wie Viza Write
eigene Tastaturtreiber, die dann natürlich in Assembler programmiert werden,
und nicht in BASIC. Dies ist nicht besonders schwer, aber zeitaufwendig: Die
Adressen 56320 und 56321 müssen entsprechend ausgewertet werden. Dies geschieht
auf die folgende Weise: CIA-Port A an der Adresse 56320 wird nacheinander mit
den einzelnen Bits von 0 bis 7 beschrieben (also mit den Zahlen
1,2,4,8,16,32,64 und128). Dieses Vorgehen aktiviert nacheinander die 7
Ausgangspins am CIA-Baustein. Die C64-Tastatur leitet dann diese Ausgangspins
auf einen der Eingangspins des CIA weiter (Adresse 56321), je nachdem, welche
Taste gerade gedrückt ist. Ein Pin, an dem Strom anliegt, setzt das
entsprechende Bit in Adresse 56321 auf 1, ein Pin, an dem kein Strom anliegt,
setzt das entsprechende Bit auf 0. Leider sind die Tastatur-Scan-Codes, die aus
den einzelnen Kombinationen aus Aus- und Eingangspins
abgeleitet werden, nicht so geordnet, wie die Tasten an der C64-Tastatur- und
auch, ob gerade z.B. eine Steuertaste oder SHIFT gedrückt ist, lässt
sich nicht so einfach feststellen. Allein mit dem langsamen BASIC ist die
Programmierung eines Tastaturtreibers auf jeden Fall nicht machbar.
1.4 Strings in einen
Puffer umleiten
In 1.1 wurde
schon gezeigt, wie die Ausgaben von PRINT in einen Puffer umgeleitet
werden können. Dieses Vorgehen ändert in einfacher Weise den Zeiger in den
Adressen 209 und 210, hat aber auch viele Nachteile. Der größte Nachteil ist
sicherlich, dass sämtliche Steuerzeichen in der PRINT-Anweisung den
alten Zustand wieder herstellen. Es geht aber besser: Sie können z.B. auf die
folgende Weise eine String-Definition an eine beliebige Adresse schreiben, z.B.
an die Adresse 32768:
10 AD=32768
20 AD=AD-20: REM
20=LAENGE DES STRINGS+1
30 HI=INT(AD/256):LO=AD-(256*HI)
40
POKE 52,LO:POKE 53,HI
50
A$=“DIES IST EIN STRING“
Der Zeiger, der auf den
String-Puffer zeigt, liegt also in Form von Lo- und Hi- Byte an den Adressen 52
und 53 in der Zeropage. Allerdings ist der
String-Puffer eher ein String-Stack, und deshalb wächst der String-Puffer auch
nach unten, und nicht nach oben. Deshalb muss auch zu der Adresse AD=32768
zunächst die Länge des Strings addiert werden und zusätzlich ein Byte, das den
Namen des Strings enthält, der als nächstes im String-Puffer abgelegt wird.
Erst diese Adresse (also AD+19+1) kann dann in den Zeiger an Adresse 52 und 53
geschrieben werden. Wenn Sie direkt danach A$ neu definieren, dann wird A$
nicht in den Standardpuffer, sondern in Ihren eigenen Puffer geschrieben. Aber
Achtung: Die hier benutzten Zeigeränderungen sind dauerhaft wirksam, und werden
erst wieder rückgängig gemacht, wenn Sie entweder den CLR-Befehl benutzen,
oder Ihr Programm erneut mit RUN starten. Leider wird dadurch der Inhalt
sämtlicher Variablen gelöscht.
1.5 Automatische
Zeichenwiederholung auf der Tastatur
Normalerweise
wiederholt der C64 das zuletzt auf der Tastatur eingegebene Zeichen nicht, auch
dann nicht, wenn Sie die entsprechende Taste lange gedrückt halten. Zum Glück
ist die automatische Tastenwiederholung nur standardmäßig deaktiviert, aber
nicht grundsätzlich nicht vorhanden. Pech ist nur, dass sich das Flag für die Tastenwiederholung nicht im
Tastatur-Status-Byte befindet, wie in manchen Internet-Foren fälschlicherweise
angegeben, sondern an Adresse 650 im höchsten Bit. Deshalb schaltet
POKE 650,PEEK(650) OR 128
die Tastenwiederholung
ein und
POKE 650,PEEK(650) AND 127
Die Tastenwiederholung
wieder aus. Allerdings sollten Sie bedenken, dass Joysticks ebenfalls über die
Tastatur eingeschliffen werden, was vor allem in Spielen zusammen mit der
Tastenwiederholung schwer auffindbare Fehler nach sich ziehen kann. Die
sicherste Variante ist deshalb sicherlich, die Joystickroutine an den Timer-Interrupt und die
Sprite-Steuerung an den Raster-Interrupt zu binden. Dies ist aber mit reinem
BASIC nicht möglich.
1.6 BASIC-Programme
beschleunigen
BASIC ist sehr langsam,
deshalb werden vor allem Spiele oft vollständig in Assembler entwickelt.
Meistens gibt es aber für Spiele Software-Teams, in denen jeder Programmierer
nur einen ganz kleinen Teil, z.B. ein bestimmtes Modul, entwickelt. Die einzelnen
Module werden dann am Ende zu einer Einheit zusammengefügt (man spricht hier
auch von Linken) und natürlich anschließend auch ausführlich getestet. Oft
erfinden die Spiele-Entwickler-Teams an einigen Stellen das Rad immer wieder
neu, z.B. bei komplexen Berechnungen, die in BASIC innerhalb nur einer Zeile
abgearbeitet werden- leider eben zu langsam. Diesen Weg kann man als einzelner
Programmierer oft nicht gehen, denn man möchte Berechnungen eben übersichtlich
in BASIC programmieren und z.B. nur Puffer-Auffrischungen, Scrolling oder
Speicherverschiebungen in Assembler umsetzen, sprich: Alles in Assembler
programmieren, das in BASIC nicht direkt in der gewünschten Geschwindigkeit
machbar ist. Aber vielleicht kann man zusätzlich auch BASIC beschleunigen? Dies
geht in der Tat mit einem Compiler: Ein Compiler ist ein Programm, das
Hochsprachen-Code (z.B. ein BASIC-Programm) in Maschinensprache übersetzt. Es
gibt zwei BASIC-Compiler, die auch im Internet als D64-Diskettenabbilder
verfügbar sind. Der bekannteste Compiler ist sicherlich der Austro-Compiler
(meistens in der Version A1, B1 oder E1). Dies ist ein echtes
Rundrum-Sorglos-Paket und sehr einfach zu bedienen:
1. LOAD“AUSTRO-COMPILER“,8
eingeben
2. Den Compiler mit RUN
starten
3. Den Programmnamen der
BASIC-Datei eingeben
4. Warten, bis der
Compiler fertig ist (etwa 5-10 Minuten)
Wenn nichts
Schwerwiegendes geschieht, dann wird das Compilat
unter dem Dateinamen
C/[BASIC-Programmname]
abgelegt. Wenn Fehler
auftreten, dann wird keine endgültige Compilat-Datei
erstellt, sondern die Datei
P/[BASIC-Programmname]
verbleibt auf der
Diskette. Wenn allerdings keine Fehler auftreten, dann startet der
Austro-Compiler einen zweiten Durchlauf (Pass 2) und erzeugt zusätzlich die
Datei
Z/[BASIC-Programmname]
In der zusätzlichen Datei
werden sämtliche Zeilennummern und die dazugehörigen vom Compiler benutzten
Speicheradressen abgelegt. Auf diese Weise können Sie genau erkennen, wie viel
Speicher Ihr Programm genau belegt.
Alternativ zum
Austro-Compiler wird auch oft der BASIC-Compiler von Data Becker benutzt.
Allerdings bietet dieser Compiler weit mehr Optionen an, als der
Austro-Compiler, der nur den Standard-Adressraum von BASIC benutzt- und so kann
man auch eine Menge falsch machen. Vor allem die Option, das BASIC- und das Kernal-ROM vollständig auszublenden, kann Ihre Programme
sowohl beschleunigen, als auch immens aufblähen. Das beste Negativ-Beispiel,
das mit dem Data-Becker-Compiler erstellt wurde, stammt sicherlich von Data
Becker selbst. Das Datenbankprogramm „Superbase“ hat 230 Blocks und
funktioniert nur mit einer Speichererweiterung optimal. Natürlich musste diese
Speichererweiterung früher explizit von Data Becker erworben werden, es kann
also sein, dass bei Superbase auch Marketing im Spiel war.
Wer also nichts falsch
machen will, und ein bisschen darauf achtet, dass das Compilat
nicht z.B. mit den Sprites kollidiert, der verwendet
am besten den Austro-Compiler. Vor allem die String-Zugriffe, die PRINT-Anweisungen
und aufwendige Berechnungen werden um den Faktor 10-20 beschleunigt, was z.B.
bei einer Spiele-Idee schon den Unterschied zwischen „spielbar“ und
„unbrauchbar“ ausmachen kann.
1.7
BASIC-Programm-Zeiger verbiegen
Wenn Sie Tipp 1.6
angewendet haben, um z.B. mit dem Austro-Compiler ein brauchbares
Autorenn-Spiel mit einfachem Down-Scrolling zu programmieren, kann es sein,
dass Sie einige Dinge, wie z.B. den Hintergrundsong (den Sie z.B. in der Hi-Voltage-SID-Collection gefunden haben), die
Sprites (die Sie mit einem professionellen Tool erstellt haben), den
Zeichensatz (den Sie mit einem speziellen BASIC-Algorithmus erstellt haben) und
Ihre Rennstrecke in verschiedene Module ausgelagert haben. Sie müssen also Ihr
Spiel z.B. immer auf die folgende Weise ausführen:
LOAD“STRECKEN“,8,1
(Adressen 8192-10239)
NEW
LOAD“SPRITES“,8,1
(Adressen 12288-14335)
NEW
LOAD“ZEICHEN“,8,1 (Adressen
14336-16383)
NEW
LOAD“SCOREPANEL“,8,1 (Adressen
16384-16999)
NEW
LOAD“MUSIK”.O“,8,1 (Adressen
17000-20480)
NEW
SYS 64768 (bzw.
Reset-Taster)
LOAD”C/RACER”,8
(Adressen 2049-7566)
RUN
Direkt in eine neue
Datei als vollständiges Paket abspeichern können Sie das Autorennen jedoch
nicht, denn
SAVE“RACER“,8
sichert nur das Compilat an den Adressen 2049-7556, und nicht die
zusätzlichen Module, die das Spiel benötigt, um zu funktionieren. Sie müssen
also einen Trick anwenden, der Ihnen gestattet, dem SAVE-Befehl zu
sagen, dass er Ihr Programm im Adressbereich 2049-18999 abspeichert, natürlich
so, dass das nächste LOAD Ihre Module auch genauso vollständig wieder
einliest. Dieser Trick ist wieder: Verändern bestimmter Zeiger-Bytes in der Zeropage, die BASIC dazu benutzt, den Speicherbereich zu
bestimmen, in dem das aktuelle Programm (bzw. Austro-Compiler-Compilat) abgelegt wurde. Um den Anfang des aktuellen
BASIC-Programms zu bestimmen, werden die Adressen 43 und 43 benutzt, um das
Ende des aktuellen BASIC-Programms zu bestimmen, werden die Adressen 44 und 45
benutzt. Diese Zeiger werden dann auch von SAVE benutzt und von LOAD
wiederhergestellt, deshalb ist es auch relativ einfach, das Autorennen
vollständig mit SAVE abzuspeichern:
POKE 44,0:POKE 45,80 (Adresse
20480=$5000=$00 Lo, $50 Hi)
SAVE”RACER”,8
Noch einfacher geht es
mit dem Saver, der schon im BASIC-Kurs
vorgestellt wurde:
SYS 53176“RACER“,2049,20480
Da hier als
Anfangsadresse 2049 angegeben wurde, kann der Racer anschließend mit
LOAD“RACER“,8
geladen
werden.
2. Assembler-Tipps
2.1 Score-Panels
Ein Score Panel
ist ein Bereich, in dem z.B. Ihr Punktestand angezeigt wird. Ein einfaches
Score Panel wird z.B. mit dem folgenden BASIC-Befehl erzeugt:
PRINT“[CLR/HOME]SCORE”;SC;”LIVES“;LI
Wenn Sie aber
zusätzlich die Scrolling-Register des VIC dazu benutzen, um den Bildschirm zu
verschieben oder enger zu stellen (Smooth Scrolling), dann wird Ihr Score Panel
nicht immer an der gleichen Stelle angezeigt, sondern scrollt quasi mit.
Natürlich stört dies ungemein, und wenn Sie dieses Problem nicht lösen, hat Ihr
Spiel zumindest kein Score Panel. BASIC bietet für dieses Problem keine direkte
an Lösung, deshalb müssen Sie ein Assembler-Programm erstellen und dieses
später aus DATA-Zeilen auslesen:
10 FOR I=49400 TO 49514: READ APOKE I,A: NEXT I
10000 REM *** SCORE PANEL ***
10010 DATA
120,169,0,141,18,208,173,17,208,41,127,141,17,208,173,26,208
10020 DATA
9,1,141,26,208,169,26,141,20,3,169,193,141,21,3,88,96,173,25
10030 DATA
208,48,7,173,13,220,88,76,49,234,141,25,208,173,134,3,201,0
10040 DATA 240,48,173,17,208,72,173,22,208,72,173,33,208,72,173,132,3
10050 DATA
141,33,208,169,27,141,17,208,169,200,141,22,208,173,18,208
10060 DATA
205,133,3,208,248,104,141,33,208,104,141,22,208,104,141,17
10070 DATA
208,169,0,141,18,208,104,168,104,170,104,64
Das entsprechende
Assembler-Pendant benutzt hier den Raster-Interrupt des VIC in der folgenden
Weise: Immer, wenn der Rasterstrahl an Position 0 ist, wird ein Interrupt
ausgelöst. In diesem Interrupt werden folgende POKE-Befehle durch
Assembler simuliert:
POKE
53265,27:POKE 53270,200:POKE 53272,21
Während des Interrupts
ist also der Bildschirm nicht eng gestellt und die Standard-Zeichen sind
ausgewählt. Dieser Zustand wird aber nur aufrechterhalten, bis der Rasterstrahl
bei einer bestimmten Position angekommen ist, die Sie vorher in der Adresse 901
abgelegt haben. Die Hintergrundfarbe für das Panel wird in Adresse 900
abgelegt. Der Wert in Adresse 902 entscheidet dann darüber, ob das Score Panel
eingeschaltet ist (nicht 0) oder ausgeblendet wird (=0). Wenn das Score Panel
eingeschaltet ist, wartet die Interrupt-Routine auf eine bestimmte
Rasterstrahl-Position. Nachdem der Rasterstrahl die Zielposition erreicht hat,
wird der alte Zustand der VIC-Register wieder hergestellt und die
Interrupt-Routine wird beendet. Ein neuer Interrupt
wird dann erst wieder ausgelöst, wenn der Rasterstrahl erneut bei Position 0
ankommt. Die Konsequenz daraus ist, dass z.B. die ersten drei Bildschirmzeilen
stets an der Standardposition anfangen, egal, wie Sie die Scrolling-Register
des VIC eingestellt haben. Sie können also Ihr Score Panel nun ganz normal mit PRINT
aktualisieren, wann immer Sie wollen. Um nun ein Score-Panel zu initialisieren,
das 3 Zeilen umfasst und bei schwarzer Rahmenfarbe einen grauen Hintergrund
aufweist, benutzen Sie die folgende BASIC-Zeile:
POKE
900,11: POKE 901,81: POKE 902,1:SYS 49400
Das vollständige
Assembler-Listing des Score-Panels sieht so aus:
10 .BA 49400
20 SEI
30
LDA #0
40
STA $D012
50
LDA $D011
60
AND #$7F
70
STA $D011
80 LDA $D01A
90 ORA #$01
100 STA $D01A
Die Zeilen 0-100
dienen dazu, zunächst die Interrupts mit SEI abzuschalten, damit Sie
auch die entsprechenden Zeiger auf Ihre eigene Routine umbiegen können. Sie
können aber für das Score-Panel nicht den Timer-Interrupt
benutzen, da dieser nicht bei einer bestimmten Rasterstrahl-Position ausgelöst
werden kann, sondern vom CIA1 gesteuert wird. Für den VIC-Interrupt müssen Sie
stattdessen zunächst dem VIC mitteilen, an welcher Position (nämlich 0) der
nächste Raster-Interrupt ausgelöst wird, indem Sie diese Position zunächst in
das Rasterstrahl-Register des VIC (Adresse $D012) schreiben.
Anschließend müssen Sie den Raster-Interrupt erst bestätigen und danach
aktivieren, indem Sie das oberste Bit in Adresse $D011 löschen
(Interrupt bestätigen) und danach das unterste Bit in Adresse $D01A
setzen (Raster-Interrupt einschalten). Als nächstes muss natürlich noch
festgelegt werden, welches Unterprogramm bei Auftreten eines Raster-Interrupts
aufgerufen wird. Im Endeffekt gibt es nur einen einzigen Sprungvektor für den
VIC, nämlich in den Adressen 314 und 315. Deshalb
entsprechen auch die folgenden Zeilen dem Vorgehen beim Umbiegen des Timer-Interrupts:
110
LDA #<(PANEL)
120
STA $314
130
LDA #>(PANEL)
140 STA $315
150 CLI
160 RTS
In Zeile 110-160
wird die Interrupt-Routine, die für den VIC zuständig ist, auf die Routine PANEL
umgebogen. PANEL zeigt hier das eigentliche Score Panel an. Leider
besteht nun das Problem, dass im Endeffekt der CIA1-Baustein, der für den Takt
der C64-Uhr zuständig ist, und der VIC-Interrupt denselben Sprung-Vektor
benutzen, zumindest in der Standard-Speicherbelegung. CIA1 löst also exakt
60-Mal in der Sekunde einen Interrupt aus, der VIC
dagegen frischt den Bildschirm nur ungefähr 60-mal in der Sekunde auf (dies
entspricht der Frequenz des NTSC-Fernsehsignals), und eben nicht immer genau an
der gleichen Rasterstrahl-Position. Das heißt also: Es müssen vor die
eigentliche Anzeigeroutine des Score Panels folgende Zeilen vorgelagert werden:
170 PANEL
LDA $D019
180
BMI VICIRQ
190
LDA $DC0D
200 CLI
210 JMP $EA31
In Adresse $D019 steht
also, ob der letzte Interrupt wirklich ein
Raster-Interrupt (oft auch einfach als VIC-IRQ bezeichnet) war, oder doch nur
ein Timer-Interrupt, ausgelöst durch den
CIA1-Baustein. In diesem Fall (also bei einem Timer-Interrupt)
ist das oberste Bit in Adresse $D019 gelöscht, beim VIC-IRQ ist es dagegen gesetzt. Da das oberste Bit auch als negatives
Vorzeichen interpretiert wird, springt deshalb BMI immer dann zur
Anzeigeroutine für das Score Panel, wenn der Interrupt
wirklich ein Raster-Interrupt war. Ansonsten wird nur der Timer-Interrupt in Zeile 190 bestätigt, und
die Standard-Routine für den Timer-Inerrupt (Adresse
$EA31) angesprungen. Die eigentliche Einsprung-Adresse für das Score Panel
ist also das Label VICIRQ:
220 VICIRQ
STA $D019
221
LDA 902
222
CMP #0
223
BEQ EXIT
230
LDA 53265
240
PHA
250
LDA 53270
260
PHA
270 LDA 53281
280 PHA
290 LDA 900
300 STA 53281
310 LDA #27
320 STA 53265
330 LDA #200
340 STA 53270
360 CMP 901
370 BNE RASTWAIT
380 PLA
390 STA 53281
400 PLA
410 STA 53270
420 PLA
430 STA 53265
440
EXIT LDA #0
450
STA $D012
460
PLA
470
TAY
480 PLA
490 TAX
500 PLA
510 RTI
Das Score Panel selbst
(Startpunkt VICIRQ) macht eigentlich nicht viel: Zunächst werden die
Inhalte der Adressen 53265, 53270, 53272 und 53281 auf dem Stack gesichert.
Anschließend wird der Adresse 900 die Farbe entnommen, den der Rahmen bekommen
soll. Diese Farbe wird gesetzt und der Bildschirm wird zusätzlich weit und auf
die Standardschrift eingestellt. Anschließend wartet RASTWAIT darauf,
dass der Rasterstrahl an die Position gelangt, die in Adresse 901 steht. Danach
ist das Score Panel vollständig auf dem Bildschirm sichtbar, deshalb können nun
dem Stack die alten Werte für die Adressen 53265, 53270, 53272 und 53281
entnommen werden. Wenn allerdings in Adresse 902 der Wert 0 steht, dann wird RASTWAIT
übersprungen, und es wird stattdessen EXIT ausgeführt. EXIT
schreibt im Endeffekt nur die Position in die Adresse $D012, an der der nächste
Raster-Interrupt auftreten soll (also 0), und beendet dann die
Interrupt-Routine. Allerdings muss dies immer geordnet geschehen, was nichts
anderes bedeutet, als dass die Register A, X und Y, die
vor dem Eintritt in die Interrupt-Routine in genau dieser Reihenfolge auf den Stack gelegt wurden, wieder hergestellt werden
müssen. Ferner werden Interrupt-Routinen mittels RTI und nicht mittels RTS
beendet.
2.2 Split Screen
(geteilter Bildschirm)
Viele Spiele teilen den
Bildschirm in mindestens zwei Teile, manchmal sogar (mit Score Panels und 2
Spielern, die gleichzeitig steuern) in 3 Teile. Wer das Score-Panel-Beispiel 2.1
und den Raster-Interrupt verstanden hat, für den ist aber ein Split Screen nicht
weiter schwierig umzusetzen, denn im Endeffekt ist ein Split Screen nur eine
erweiterte Version des Score Panels, die mehrere Positionen des Rasterstrahls
zulässt, an dem ein Interrupt ausgelöst wird. Natürlich muss zu diesem Zweck
bei der Initialisierung zunächst eine Position (z.B. 0) für den ersten
Interrupt festgelegt werden. RASTWAIT wird danach so angepasst, dass es
mehrere Positionen auswertet:
350 RASTWAIT
LDA 53266
360
CMP #100
370
BMI PART1
380
CMP #200
390 BMI PART2
400 JMP PART3
Nun müssen allerdings PART1,
PART2 und PART3 die entsprechenden Aktionen ausführen und in
korrekter Weise auf die Position des Rasterstrahls warten. Z.B. kann PART2
nicht darauf warten, dass der Rasterstrahl die Position 100 erreicht, dies muss
schon vorher PART1 leisten. Außerdem muss PART1 vor dem Beenden
des Interrupts den Wert 100 in die Adresse $D012 schreiben, denn sonst wird PART2
niemals aufgerufen. PART2 dagegen muss vor dem Beenden des Interrupts
den Wert 200 in die Adresse $D012 schreiben, sonst wird PART3 nicht
aufgerufen. In der Programmiersprache C sähe das Programm übrigens so aus:
if (R<=100) { Part1();
}
else if (R<=200) { Part
2(); }
else { Part3(); }
Wie Sie sicherlich
erkennen können, führt die Verwaltung von zu vielen Bildschirmteilen zu
langsamen Programmen, weil der Hauptprozessor dann ständig auf den Rasterstrahl
wartet, ohne noch genug Zeit für andere Aufgaben zu haben. Als grober Richtwert
gilt, dass mehr als 3 Bildschirmteile die Ausführung des (BASIC-)
Hauptprogramms schon merklich verzögern.
2.3 Level-Bausteine
Wenn man sich nicht mit
Scrolling-Routinen herumschlagen möchte, die versuchen, den Bildschirmspeicher
mit allerhand Tricks, Zeichen-Umsortierungs-Algorithmen und illegalen OP-Codes
so zu verschieben, dass der Rasterstrahl dem Bildaufbau „hinterherhinkt“ (und
man dadurch kein Flimmern sehen kann), dann verwendet man oft Bausteine für
seine Spiele-Levels. Diese Bausteine bestehen aus mehreren Zeichen und werden
zunächst anhand einer Level-Map in einen Offscreen-Buffer kopiert. Dieser Offscreen-Buffer
(oder Teile davon) werden dann später in den sichtbaren Bildschirmspeicher
kopiert, und zwar mit einer schnellen Kopierroutine, die vom Rasterstrahl nicht
beeinflusst wird. Meistens enthält dann die Puffer-Ausgabe-Routine (kurz Buffout) auch zusätzlich noch Optionen, um z.B. die
Scrolling-Register des VIC in den Adressen 53265 und 53270 zu verändern,
eventuell auch, ohne zusätzlich den Puffer auszugeben.
2.3.1 16x16 Zeichen
große Blöcke definieren
Die einfachste Variante
ist, für die Blöcke eine Größe von 16x16=256 Zeichen zu wählen und die Blockmap, in dem die einzelnen Zeichen der Blöcke
gespeichert werden, an einer bestimmten Speicherseite auszurichten (sog. Page-Allignment). Auf diese Weise kann die Speicheradresse eines
bestimmten Blocks auf die folgende einfache Weise berechnet werden (im
Akkumulator A steht hier die Blocknummer, und im Y-Register die
Speicherseite, an der die Blockmap beginnt):
STA 249
CLC
TYA
ADC 249
STA 249
LDA #0
STA
248
Die Adresse, an der der
Block mit der Nummer beginnt, die im Akkumulator übergeben wurde, steht nun als
Zeiger in den Adressen 248 und 249, und Sie können in einfacher Weise auf die
Blockdaten zugreifen:
LDY
[Zeichenposition innerhalb des Blocks]
LDA
(248),Y
Beachten Sie hier
bitte, dass die Blöcke selbst als Array mit 16 Spalten und 16 Zeilen gesehen
werden, und Sie diese Blocks auch genau so auf dem
Bildschirm ausgeben bzw. in den Offscreen-Buffer
schreiben müssen. Wenn z.B. der normale Bildschirmspeicher als Ziel gesehen
wird (40 Spalten pro Zeile), dann muss die Ausgaberoutine eines Blocks, auf die
der Zeiger in den Adressen 248/249 zeigt, wie folgt aussehen:
Blockout
LDA #0
STA 250
LDA #4
STA 251
LDX #0
DOALL
LDY #0
DOLINE
LDA (248),Y
STA (250),Y
INY
CPY #16
BNE DOLINE
CLC
LDA 248
ADC #16
STA 248
LDA 249
ADC #0
STA 249
CLC
LDA 250
ADC #40
STA 250
LDA 251
ADC #0
STA 251
INX
CPX #16
BNE DOALL
RTS
Leider ist die obige
Ausgaberoutine sehr unflexibel, weil sie nur einen einzigen Block in den
sichtbaren Bereich des Bildschirmspeichers schreiben kann, und nicht in einen
beliebigen Offscreen-Buffer an eine beliebige
Position bzw. Adresse. Genau deshalb muss dann auch die Routine BUFFOUT
(Ausgabe des Offscreen-Buffers) und die Routine BO16
(Blockout in der Variante 16x16) separat programmiert
werden. Ferner benötigen Sie auch einen Blockeditor, mit dem man in einfacher
Weise Blöcke erstellen kann, inklusive der Möglichkeit, umdefinierte Zeichen im
Mehrfarbmodus nutzen zu können. Zum Glück sind die dazugehörigen Programme
nicht weiter schwierig umzusetzen, weil sie nur eine Weiterentwicklung des
ersten Blockout -Listings sind.
Tun wir nun diesen
Schritt und setzen BO16 auf die folgende Weise um: BO16 bekommt
im Akkumulator A die Blocknummer (von BASIC aus ist dies die Adresse
780), und im Y-Register (von BASIC aus ist dies die Adresse 782) die
Startseite der Blockmap übergeben. Zusätzlich kann
das X-Register (von BASIC aus ist dies die Adresse 781) einen X-Offset
enthalten, der die X-Verschiebung in Zeichen nach rechts angibt. Der Offscfeen-Buffer wird also als nxm-Array
mit betrachtet (n Spalten, m Zeilen). Nun muss natürlich auch die Größe des Offscreen-Buffers bekannt sein, deshalb werden BO16
in den Adressen 1000-1002 dann auch die folgenden Dinge zusätzlich übergeben:
Die Adresse 1000 enthält die Größe einer Puffer-Zeile in Zeichen
(also Bytes) und die Adressen 1001 und 1002 enthalten einen Zeiger
auf die Startadresse des Offscreen-Buffers.
Angenommen, Sie haben nun einen Offscreen-Buffer, der
64x16 Zeichen umfasst, an Adresse 16384 abgelegt und Ihre Blockmap
an Adresse 32768. In diesem Fall kann BO16 mit den folgenden
BASIC-Zeilen aufgerufen werden, um Block Nr. 0 an die Position (0,0) im Offscreen-Buffer zu schreiben:
10 POKE 1000,64:POKE 1001,0:POKE 1002,64
20 POKE 780,0:POKE 781,0:POKE 782,128: SYS
49152
BO16 Assembler-Listing
10 .BA 49152
20 STA 249
30 PHP
40 PHA
50 TXA
60 PHA
70 TYA
80 PHA
90 TYA
100 CLC
110 ADC 249
120 STA 249
130 LDA #0
140 STA 248
150 LDA 1001
160 STA 250
170 LDA 1002
180 STA 251
190 CLC
200 TXA
210 ADC 250
220 STA 250
230 LDA 251
240 ADC #0
250 STA 251
260 LDY #0
270 SEC
280 LDA 1000
290 SBC #16
300 STA 1003
310 LDX #0
320 LDY #0
340 STA (250),Y
350 INY
360 INX
370 CPX#16
380 BNE CONT
390 LDX #0
400 CLC
410 LDA 250
420 ADC 1003
430 STA 250
440 LDA 251
450 ADC #0
460 STA 251
470 CONT CPY #0
480 BNE COPYBLOCK
490 PLA
500 TAY
510 PLA
520 TAX
530 PLA
540 PLP
550 RTS
BO16 schreibt statt direkt
in den Bildschirmspeicher in einen Puffer, die Zeilengröße in Zeichen wird in
Adresse 1000 angegeben. Der zu schreibende Block wird im Akkumulator übergeben
(vom BASIC aus also in Adresse 780), zunächst muss dieser Wert jedoch als Basis
für einen Adresszeiger in den Adressen 248/249 dienen. Dazu wird der
Akkumulator zunächst in Zeile 20 in die Adresse 249 übertragen- was
genügt, da ja sämtliche Blöcke genau eine Speicherseite belegen. Bevor jedoch
der Zeiger auf den Blockanfang weiter aufgebaut werden kann, müssen in Zeile
30-80 noch sämtliche Prozessor-Register auf dem Stack gesichert werden.
Dies ist nötig, da BO16 später auch als Routine dienen soll, die Sie als
Unterprogramm mittels JSR 49152 aufrufen können, ohne dabei ihre alten
Registerinhalte zu zerstören. Deshalb wird erst ab Zeile 90 der Zeiger
für den Blockanfang weiter aufgebaut, und zwar so: Erst die Speicherseite, und
dann der Offset. Der Offset ist bei BO16 jedoch 0, deshalb genügt es
hier, die Blocknummer in Adresse 249 abzulegen, und anschießend in Zeile
90-120 den Inhalt des Y-Registers hinzuzuzählen, das die
Speicherseite enthält, an der Ihre Blockmap anfängt.
In Adresse 248 wird dann in Zeile 130 und 140 nur der Wert 0 eingetragen
(Page-Alignment). In Adresse 250 und 251 wird dann der Zeiger eingetragen, den
Sie in Adresse 1001/1002 übergeben haben (Anfang des Offscreen-Buffers).
Dies hat den einfachen Grund, dass Sie nur Zeiger in der Zeropage
für die Y-indizierte indirekte Adressierungsform benutzen können. Wenn Sie an
dieser Stelle wollen, dass sich auch die Inhalte der Adressen 248-251 nicht
ändern, dann müssen Sie auch diese vorher auf dem Stack sichern und vor
Rückkehr aus dem Unterprogramm auch wieder vom Stack entfernen. An dieser
Stelle gehe ich jedoch davon aus, dass die Adressen 248-251 nur temporäre
Zeiger enthalten dürfen. Die Zeilengröße des Puffers steht wie gesagt in
Adresse 1000, um jedoch den Kopiervorgang der Blockdaten noch etwas zu
beschleunigen, wird in Adresse 1003 der Wert [Zeilengröße-16] eingetragen. Dies
leisten die Zeilen 270-300. Der eigentliche Kopiervorgang beginnt in Zeile
330 beim Label COPYBLOCK, mit X=0 und Y=0. Das eigentliche
Kopieren der Block-Daten ist nun so konzipiert, dass dieser in die folgenden
zwei Zeilen passt:
COPYBLOCK LDA (248),Y
STA (250),Y
Der Kopiervorgang selbst
benötigt nur 8 Prozessortakte, und die Erhöhung der zwei Schleifenzähler im X-
und Y-Register nur 4 Takte. Zusammen mit CPY und BNE
kommen Sie auf diese Weise auf nur 21 Takte. Dies ist so, weil Sie die Daten
niemals seitenübergreifend kopieren, und auch BNE keine Seiten
überschreitet, und Sie auf diese Weise 3 Takte einsparen. Allerdings dürfen Sie
zunächst nur eine Zeile mit 16 Zeichen in den Zielpuffer kopieren, denn Sie
können die nächste Zeile mit Blockdaten nicht direkt an die vorige Zeile anhängen.
Deshalb enthält das X-Register auch den Zähler für die X-Koordinate:
Wenn nach INX der Wert im X-Register noch nicht 16 ist, dann
springt BNE in Zeile 380 zum Label CONT. Wenn im X-Register
allerdings der Wert 16 steht, dann wurde eine vollständige Zeile kopiert. X
wird nun auf 0 gesetzt, und zum Zeiger in Adresse 250/251 wird der Wert in
Adresse 1003 addiert (Zeile 390-460). Dieser Wert ist [Zeilenlänge-16],
das heißt, der Zeiger in Adresse 250/251 landet auf diese Weise wieder an der
Ausgangsposition (vor dem Zeichnen der Zeile), nur eben genau eine Zeile
tiefer. Und genau dies soll hier auch erreicht werden: Das Y-Register
enthält auf diese Weise stets den Zeiger auf das nächste Block-Zeichen,
unabhängig davon, wo Sie sich im Offscreen-Buffer
befinden, und wie oft Sie in die nächste Zeile gesprungen sind. Allerdings
kopieren Sie nur 256 Zeichen aus dem Quellblock, danach springt der BNE-Befehl
in Zeile 480 nicht mehr zurück zu COPYBLOCK, weil dann das Y-Register
vom Wert 255 zum Wert 0 umschlägt (INY verendet das Carry-Flag nicht). In Zeile 490-550 werden dann die alten
Registerinhalte wiederhergestellt (inklusive des Prozessor-Status-Registers)
und BO16 wird beendet.
Nun können Sie Ihre
Daten unter Umständen mehrerer Blöcke, die Sie eventuell auch versetzt im Offscreen-Buffer ausgegeben haben, natürlich noch nicht
sehen. Deshalb benötigen Sie noch die Routine BUFFOUT, die den Offscreen-Buffer (oder Teile davon) in den Bildschirmspeicher
kopiert. An dieser Stelle gehe ich davon aus, dass Sie den Bildschirmspeicher
nicht durch Pokes verschoben haben und die
Standardadressen 1024-2023 benutzen. BUFFOUT muss also so schnell
arbeiten, dass Sie weder ein eventuelles Zurücksetzen der VIC-Scrolling-Register
sehen (wenn Sie denn wünschen, dass BUFFOUT dies leistet), noch ein
Flimmern bei der eigentlichen Ausgabe des Offscreen-Buffers
wahrnehmen (denn sonst hätten Sie sich die Sache mit dem Double-Buffering
sparen können). Außerdem muss es möglich sein, einen Versatz im Offscreen-Buffer in X-Richtung (X-Offset) zu benutzen, um
eventuell innerhalb des Offscreen-Buffers hin und her
scrollen zu können. BUFFOUT benutzt folgende Adressen und Register für
die Konfigurations-Parameter:
A: Breite einer
Puffer-Zeile (bei BASIC in Adresse 780)
X: X-Offset in der
Zeile (bei BASIC in Adresse 781)
Y: Speicherseite (also
Hi-Byte) des Puffer-Starts (bei BASIC in Adresse 782)
Adresse 1000: Steuer-Flag (1=Buffer Output, 0=no
Output)
Adresse 1001: Wird nach
53265 übertragen
Adresse 1002: Wird nach
53270 übertragen
Adresse 1003: Wird nach
53272 übertragen
Adresse 1004: Anzahl an
ausgegebenen Zeilen auf dem sichtbaren Bildschirm
BUFFOUT
10 .BA 49250
20 STA 1005
30 RASTWAIT LDA 53266
40 CMP #250
50 BNE RASTWAIT
60 LDA 1001
70 STA 53265
80 LDA 1002
90 STA 53270
100 LDA 1003
110 STA 53272
120 LDA 1000
130 BNE CONT
140 RTS
150 CONT LDA #0
160 STA 248
170 LDA #4
180 STA 249
190 STX 250
200 STY 251
210 LDX 1004
220 BUFFOUT LDY #39
240 STA (248),Y
250 DEY
260 BPL LINEOUT
270 CLC
280 LDA 248
290 ADC #40
300 STA 248
310
LDA 249
320 ADC #0
330 STA 249
340 CLC
350 LDA 250
360 ADC 1005
370 STA 250
380 LDA 251
390 ADC #0
400 STA 251
410 DEX
420 BNE BUFFOUT
430 RTS
BUFFOUT ist also relativ
einfach gestrickt: Zunächst wird darauf gewartet, dass die
Rasterstrahl-Position (Adresse 53266) den Wert 250 erreicht, was bedeutet, dass
der Rasterstrahl gerade den unteren sichtbaren Bereich verlassen hat. Direkt
danach werden die Adressen 53265, 53270 und 53272 mit den Werten aus Adresse
1001-1003 beschrieben, was dazu führt, dass die Änderungen z.B. der
VIC-Scrolling-Register erst beim nächsten Bildaufbau sichtbar werden. Wenn in
Adresse 1000 der Wert 0 steht, dann führt der BNE-Befehl in Zeile130
nicht dazu, dass das Programm zum Label CONT springt (und danach den Offscreen-Buffer ausgibt), sondern dazu, dass sich das
Programm mit RTS beendet. Wenn Sie an dieser Stelle dafür sorgen wollen,
dass auch BUFFOUT von jeder beliebigen Stelle aus als Unterprogramm
aufgerufen werden kann, dann müssen Sie vorher (wie auch bei BO16)
sämtliche Register auf dem Stack ablegen und auch vor Rückkehr zum Hauptprogramm
wiederherstellen. Am besten ist es dann sicherlich, in Zeile 140 nicht RTS,
sondern z.B. JMP EXIT zu benutzen- EXIT stellt dann die alten
Registerinhalte wieder her und führt anschließend RTS aus.
Die Kopierschleife
selbst für den Offscreen-Buffer ist nicht sehr schwer
zu verstehen und braucht deshalb auch nicht ausführlich erklärt zu werden: Der Offscreen-Buffer wird Zeile für Zeile in den sichtbaren
Bildschirmspeicher kopiert, der an Adresse 1024 beginnt. Der Zähler für die
aktuelle Zeile des Offscreen-Buffers wird hier jedoch
im X-Register abgelegt, wodurch die Kopierschleife für eine
Bildschirmeile nur vier Zeilen umfasst (im Y-Register steht der
Spalten-Offset beim Kopiervorgang):
LINEOUT
LDA (250),Y
STA (248),Y
DEY
BPL LINEOUT
Der Rumpf der
Kopierschleife benötigt nur 17 Takte, also für einen 40-Zeichen-Ausschnitt aus
dem Offscreen-Buffer nur (inkl. Wartezyklen nach
Sprüngen) 700 Takte. Für die Zeiger-Updates nach jeder Zeile (Adressen 248-251)
benötigt man weitere 50 Takte, man kommt also auf 750 Takte pro
kopierter Zeile im Offscreen-Buffer. Bei einem
vollständigen Bildschirm macht dies 25*750=18.750 Takte, das sind beim 1 MHz
Prozessor-Taktfrequenz 0,01875 Sekunden. Dies reicht eben gerade aus, um dem
Rasterstrahl vorauszueilen und den Offscreen-Buffer
(oder einen Teil davon) flimmerfrei anzuzeigen.
Viele C64-Spiele, vor
Allem die sogenannten Sidescroller, die das
Bild nur nach links bewegen, benutzen nur einen Offscreen-Buffer
mit 16 Zeilen. Die Level selbst sind also nur einen Baustein hoch, und können
relativ einfach und schnell aufgefrischt werden, ohne zu flimmern. Manchmal
gibt es bei der Ausgabe noch einen Offset nach unten, um z.B. eine Score Line
anzuzeigen. Der berühmteste Sidescroller seiner Art
ist sicherlich das Spiel Giana Sisters, das genau so vorgeht: Die Level sind alle 16 Zeichen hoch, und
im oberen Bereich kann man den Punktestand und die noch verbliebenen Leben
sehen. Der Offscreen-Buffer ist 16*64 Zeichen groß,
und wird immer dann mit neuen Bausteinen aufgefrischt, wenn die X-Position des
Spielers durch 16 teilbar ist. Leider wurde das Spiel an manchen Stellen nicht
korrekt programmiert, deshalb kann man z.B. in Level 5,6 und 12 einen Teil
überbrücken, indem man quasi oben aussteigt über somit das eigentliche
Labyrinth läuft. Aber auch bei anderen Sidecrollern,
wie z.B. Super Mario, gibt es solche als „Cheats“ bezeichnete
Programmierfehler, die sich bewusst zum eigenen Vorteil ausnutzen lassen.
2.4 Songs aus dem Internet
auf dem C64 abspielen
Das Internet ist
bekanntlich voll mit Videos und Musik, und sicherlich hat jeder von uns schon
mindestens ein Album seiner Lieblingsband heruntergeladen. Musik gibt es aber
auch direkt für den C64, und da die Lizenzen für die meisten Songs und Tunes
(so nannte man früher die Intros und Hintergrund-Songs für Spiele) inzwischen
erloschen sind, gibt es die meisten alten Titel dann auch als Downloads. Die
bekannteste „C64-Mediathek“, wenn man es so nennen kann, ist sicherlich die High-Voltage-Collection.
Hier gibt es in der Tat fast sämtliche alten Titel in gezippter Form als
Sammlung sogenannter SID-Dateien. SID-Dateien sind Dateien, die direkt auf
den C64 übertragen werden können, und die dann auch auf diesem direkt
abspielbar sind. Dies funktioniert deshalb, weil SID-Dateien den Player gleich
mit integriert haben. Die Player benutzen dabei meistens entweder den Timer-Interrupt des CIA1, oder
aber den Raster-Interrupt. Um die Interrupt-Routinen bzw. den Song zu
initialisieren, besitzen die SID-Dateien einen Load-Bereich und einen Init-Bereich. Der Load-Bereich ist der
Adressbereich, in der der Song mit ,8,1 geladen wird. Der Init-Bereich ist der Bereich, der die
Initialisierungsroutinen beinhaltet, für die es auch immer einen bestimmten SYS-Befehl
gibt, der INIT direkt aufruft. Für den Song selbst gibt es dann noch
einen Player-Bereich, für den es dann auch einen SYS-Befehl gibt,
der den Player aufruft. Allerdings funktioniert der Player-SYS-Befehl in
vielen Fällen nicht zusammen mit BASIC und muss z.B. durch eine Timer-Interrupt-Routine ersetzt werden. Dies ist so, weil
viele Player in Schritten von 1/60 Sekunden rechnen und die Player-Routine dann
bei einen Aufruf auch nur immer 1/60 Sekunde des Songs
abspielt. Oft muss also der Player selbst innerhalb eines Interrupts
aufgerufen, z.B. so:
TIMERIRQ
JSR PLAY_ROUTINE
JMP $EA31
JMP $EA31 statt RTS sorgt
hier dafür, dass der Timer-Interrupt
zusätzlich zum Song-Player die Routinen ausführt, die auch der Kernal normalerweise ausführt, um z.B. die Uhr und den
Cursor aufzufrischen, oder auch den BASIC-Interpreter zu bedienen. Durch JMP
$EA31 kann also ein Song quasi im Hintergrund abgespielt werden. Eine
weitere Möglichkeit ist, den Song-Player an den Raster-Interrupt zu binden und
quasi auf dieselbe Weise zu bedienen, wie Sie dies schon beim Score Panel
gesehen haben. Natürlich können Sie den Song-Player auch direkt von Ihrem Score
Panel aufrufen lassen, allerdings ist hier das Timing kritischer. Wie man nun
vorgeht, ist reine Geschmackssache, denn die Geschwindigkeit, in der der Song
abgespielt wird, ist zumindest bei den Standardeinstellungen sowohl beim
CIA-Interrupt, als auch beim Raster-Interrupt gleich. Allerdings gilt dies nur,
wenn im Hintergrund ein BASIC-Programm bzw. BASIC-Compilat
läuft, das man vorher mit dem Austro-Compiler erstellt
hat. Spiele dagegen, die oft zeitkritische Routinen enthalten, verwenden
manchmal ein eigenes Timing und trennen auch die Grafikausgabe (VIC-Interrupt)
streng vom Timer-Interrupt. Manchmal wird auch die
Uhr des CIA1 auf eine andere Taktfrequenz eingestellt, z.B. auf 1/100 Sekunden.
In diesem Fall laufen natürlich auch die Timer der
Songs in Schritten von 1/100 statt 1/60.
2.4.1 Assembler-Listing
für das Abspielen eines High-Voltage-Songs
Nehmen wir nun die
Datei jumpman.sid (der Dateiname sollte klein
geschrieben werden). jumpman.sid enthält sämtliche
Tonsequenzen des legendären Spiels Jumpman als
einzelne Tunes. Zunächst muss jumpman.sid auf
den C64 übertragen werden. Dazu kann z.B. das Programm Trans64 dienen,
das im Download-Bereich auf dieser Homepage zur Verfügung steht. Trans64
benötigt allerdings einen Arduino Uno als Vermittler zwischen Windows-PC (die
Linux-Version ist in Arbeit) und C64, wer keinen Arduino besitzt, kann
alternativ auch das DOS-Programm Star Commander benutzen, muss allerdings
zusätzlich ein extra Transferkabel für die 1541 im Internet bestellen (Preis
etwa 15 Euro). Wer nichts von beiden besitzt, kann auch den C64-Emulator VICE
herunterladen. Das freie Programm SIDEdit für
Windows oder Linux dagegen benötigt man auf jeden Fall, weil man mit SIDEdit die Ladeadresse und die Init-Adresse
ermitteln muss, um eine SID-Datei korrekt auf dem C64 wiedergeben zu können.
SIDEdit ist leider etwas
gewöhnungsbedürftig, denn es funktioniert nicht, wie ein normaler
Standard-Dateibrowser, sondern eher, wie der alte Norton-Commander: Auf der
linken Seite muss man zunächst das Verzeichnis auswählen, das die anzuzeigende
SID-Datei enthält, also z.B. den Dokumente-Ordner oder unter Linux /home/[Benutzername]. Direkt rechts neben der Liste der
Unterordner des aktuell ausgewählten Ordners erscheint nun die Datei jumpman.sid. Es genügt hier, den entsprechenden
Dateinamen mit der Maus anzuklicken, um die Daten der SID-Datei anzuzeigen. Im
Fall von jumpman.sid sind die folgenden
Einträge wichtig:
initAdress:6C80
playAddress:6D40
songs:23
Abb. 1: Mit SIDEdit lassen sich sämtliche Eigenschaften einer SID-Datei
anzeigen
die Variable initAdress ist wichtig für die Initialisierung des Songs
durch Ihre Player-Routine. Im Fall von jumpman.sid
benutzt der Player den CIA-Timer-Interrupt. Da keine
Speed-Bits gesetzt sind, ist auch die Geschwindigkeit so eingestellt, dass es
genügt, alle 1/60 Sekunden an die Adresse playAddress
zu springen. Dies muss natürlich Ihr Player erledigen, der hier durch SYS
49152 aufgerufen werden soll: Zunächst wird die Init-Routine
aufgerufen, anschließend wird der Timer-Interrupt
so umgebogen, dass danach 60-mal pro Sekunde playAddress
mit JSR angesprungen wird. Im Endeffekt ist dies nicht sehr schwer und
durch das folgende kleine Programm machbar:
10
- .BA 49152
20
- JSR $6C80
30
- SEI
40
- LDA #<(PLAYER)
50 - STA $314
60 - LDA #>(PLAYER)
70 - STA $315
80 - CLI
90 -PLAYER JSR $6D40
100
- JMP $EA31
Wenn Sie nun jumpman.sid mit
LOAD“JUMPMAN.SID“,8,1
In den Speicher geladen
und Ihr Player-Programm vorher an Adresse 49152 abgelegt haben, dann genügt
normalerweise der Befehl
SYS
49152
um einen hörbaren Song
abzuspielen. Im Fall von jumpman.sid gibt es
aber mehrere Tunes, man muss also bestimmen können, welcher Tune genau abgespielt
wird. Deswegen muss stets die Init-Routine die
entsprechende Nummer eines Tunes im X-Register übergeben bekommen. Wird
dies nicht beachtet, dann wird bei einer ungültigen Nummer (hier <0 und
>23) Tune Nr. 0 abgespielt. Wollen Sie stattdessen Tune 1 statt Tune 0
abspielen, benutzen Sie die folgende Anweisung:
POKE
781,1:SYS 49152
Leider wird der
ausgewählte Song nun direkt abgespielt, und man kann den Player selbst auch
nicht mehr stoppen. Bei manchen SID-Dateien stoppt die Init-Routine
beim wiederholten Aufruf zunächst den Player (JSR $6D40 hätte also dann
keinen Effekt), bei manchen SID-Dateien ist dies aber nicht der Fall- es gibt
hier einfach keinen einheitlichen Standard. An dieser Stelle müssen Sie also
wohl oder übel das Heft selbst in die Hand nehmen, und zumindest dafür sorgen,
dass es eine Adresse gibt (hier 900), in der Sie Parameter eintragen können.
Das nächste Listing ändert das vorige Listing dementsprechend ab: Wenn Adresse
900 den Wert 0 enthält, durchläuft das Programm die komplette Initialisierung,
und verwendet auch die Song-Nummer im X-Register für die
Initialisierung. Wenn in Adresse 900 nicht 0 steht, dann wird der
Standard-Interrupt wieder hergestellt und der Player stoppt. Der Wert in
Adresse 901 entscheidet dann darüber, ob der Player selbst ausgeführt wird (=1)
oder aber so lange anhält, wie in Adresse 901 der Wert 0 steht.
10 - .BA 49152
20 - LDA 900
30 - CMP #0
40 - BEQ DOINIT
50 - SEI
60
- LDA #47
70
- STA $314
80
- LDA #234
90
- STA $315
100
- CLI
110
- RTS
120 -DOINIT JSR $6C80
130 - SEI
140 - LDA #<(PLAYER)
150 -
STA $314
160 - LDA #>(PLAYER)
170 - STA $315
180 - CLI
190 - RTS
200 -PLAYER LDA 901
210 - CMP #0
220 - BEQ EXIT
230 - JSR $6D40
240 -EXIT
JMP $EA31
Vielleicht denken Sie
nun, dass sein so simples Player-Programm, wie es hier in Assembler vorgestellt
wurde, auch in BASIC realisierbar ist- man benötigt ja quasi nur ein
Unterprogramm, das immer wieder den SYS-Befehl für den Player aufruft. Bei
geschickter Programmierung und zusätzlicher Verwendung des Austro-Compilers
dürfte das ja auch alles kein Problem sein. Leider kommen sich auf diese Weise
die Routine playAddress und BASIC in die
Quere, weil BASIC den Inhalt der Prozessor-Register immer wieder ändert. Genau
deswegen muss die Routine playAddress auch
stets aus einem Interrupt heraus aufgerufen werden, denn dies garantiert -
entweder durch JMP $EA31, oder durch eine eigene Routine für den
Raster-Interrupt - dass der Inhalt der Prozessor-Register stets wieder
hergestellt wird, nachdem Sie playAddress
aufgerufen haben. Auf diese Weise (und nur auf diese Weise) kann erreicht
werden, dass Ihr Player z.B. parallel zu Ihrem BASIC-Programm läuft.
2.4.2 Einen eigenen
Player erstellen
Vielleicht denken Sie
nun daran, das Heft komplett selbst in die Hand zu nehmen, und einfach Ihren
eigenen Musik-Player zu programmieren. Einige Hinweise dafür haben Sie ja schon
im Kapitel über Musikprogrammierung mit Assembler erhalten, und diese Hinweise
deuteten ja schon darauf hin, dass die Sache so schwer auch wieder nicht ist.
Und Sie haben noch einen weiteren großen Vorteil: Sie können Ihren Player (da
Sie ja den Quellcode haben) und Ihre Songs an beliebige Speicheradressen legen,
so, wie es Ihnen gerade passt. In der Tat sind auch viele Programmierer früher
so vorgegangen, da sie ihre Musik sowieso komplett in ein Spiel integrieren
wollten, und deshalb auch die volle Kontrolle über den Player brauchten. Diese
Kontrolle gibt es bei SID-Dateien (vor allem bei denen, die den Player
integriert haben) in der Tat nicht, denn auch hier kochte jeder Künstler, der
Musik mit dem C64 machte, immer schon sein eigenes Süppchen.
Das Problem ist aber
auch oft nicht der Player, denn dieser muss im Endeffekt nur die folgenden Schritte
immer wieder ausführen:
-
Aktualisierung
der internen Player-Uhr z.B. durch den CIA-Interrupt
-
Feststellen,
ob bei der aktuellen Stimme eine neue Note eingelesen werden muss, und
eventuell Einlesen und Abspielen dieser Note für die aktuelle Stimme
-
Wiederholen
des letzten Schrittes für sämtliche Stimmen des SID
-
Aktualisierung
der Noten-Positionszeiger
Das wirkliche Problem
liegt ganz woanders, nämlich beim Setzen der Noten. Wenn Sie dies z.B. per Hand
machen wollen, und z.B. die einzelnen Song-Bytes einfach in DATA-Zeilen
ablegen, dann kommen Sie über einfache Lieder wie „O Tannenbaum“ oder „Hänschen
klein“ nicht hinaus. Dies liegt einfach daran, dass das Erstellen von wirklich
komplexen Musikstücken ganz andere Fertigkeiten benötigt, als die Assembler-
oder BASIC-Programmierung. Im Endeffekt benötigen Sie zumindest für
professionelle Demos und Intros einen separaten Noten-Editor, der dann am besten auch gleich zahlreiche Instrumente unterstützt.
Eine Alternative ist ein Sequencer, also ein
Programm, das die Noten der Stimmen als Balken anzeigt- je länger der Balken,
desto länger ist die Note, und desto weiter oben der Balken, desto höher ist
die Note. Sequencer vereinfachen also das
Notensetzen, und beinhalten auch zusätzlich Funktionen wie Beats (Rhythmus-Spuren),
Loops (Wiederholungen) und Samples (Abspielen vorher
digitalisierter Klänge). Mit Sequencern arbeitet vor
allem die Techno- oder Rave- Szene, da es hier besonders auf Beats und Loops
ankommt. Ein hier oft benutzter Sequencer ist der Master
Composer, mit dem sogar Interrupt-Songs erstellt werden können. Leider ist
hier die Speicherbelegung des Players (der durchaus auch autonom als eigenes
Programm laufen kann) und der Songdaten fest vorgegeben, und Sie müssen dann
quasi Ihr Hauptprogramm um den Player (Init-Adresse
50000) und die Songdaten (Startadresse 16384) herum anordnen.
Eine andere Alternative
ist der Musik-Editor von Garry Kitchen’s Game Maker.
Der Game Maker wird im nächsten Tipp noch ausführlich behandelt. Den Game Maker
kann man sich inzwischen übrigens frei als D64-Datei im Internet herunterladen
(es reicht hier aus, einfach nach „Garry Kitchen Game Maker“ zu googeln).
3. Tipps für Garry Kitchen‘s
Game-Maker
Der Game Maker war in den
80-ern ein beliebtes Tool, um eigene Spiele zu erstellen. Trotz des fehlenden
Scrollings, der proprietären Dateiformate, und einiger Beschränkungen bei der
Speichergröße, wurden einige bekannte Spiele mit dem Game Maker gemacht. Vor
allem die bereits integrierten zahlreichen Songs, Demos und Sprites
vereinfachten das Spiele-Design ungemein. Leider findet man die Formate
Dateien, die z.B. der Sprite Maker oder der Music Maker benutzt, nicht mehr mit
Google oder einer vergleichbaren Suchmaschine. Dies war dann auch der Grund
dafür, den Game Maker wieder auszugraben und ausführlich zu analysieren.
Herausgekommen ist Tipp 3, der genau erklärt, wie man z.B. Game Maker Songs,
Hintergründe und Sprites in eigenen Spielen benutzen kann.
3.1 Song-Dateien des
Music Makers (/SNG, maximal 6 Zeichen im Dateinamen)
Der Music Maker hat
einige Beschränkungen z.B. in der Anzahl der Instrumente oder der Songsteuerung
(man kann z.B. innerhalb des Songs nicht die Instrumente wechseln oder einen
Teil der Noten wiederholen). Dennoch kann man mit dem Music Maker brauchbare
Intros oder Hintergrundmelodien erstellen.
3.1.1 Aufbau der
Song-Dateien
1. Song-Header (34
Bytes)
Offset
00: Die Bytes $E0/$0B=3040 (2 Bytes Load Address,
aber nur für interne Zwecke)
Offset
02: Die 3 Buchstaben GEK (3 Bytes)
Offset
05: Der Dateiname ohne /SNG, der aber auf 6 Zeichen begrenzt ist (6 Bytes)
Offset
11: $03=Anzahl der Stimmen, ist hier immer 3 (1 Byte)
Offset 12: Song-Länge
in Bytes (2 Bytes Lo/Hi)
Offset 14: $00/$00 (2 Bytes)
Offset 16: Position Stimme
2 als Offset nach
Song-Header (2 Bytes Lo/Hi)
Offset 18: Position Stimme
3 als Offset nach
Song-Header (2 Bytes Lo/Hi)
Offset
20: Tempo (1 Byte)
Offset
21: Instrument Stimme 1 (1 Byte, 0=Stimme aus)
Offset
22: Instrument Stimme 2 (1 Byte, 0=Stimme aus)
Offset
23: Instrument Stimme 3 (1 Byte, 0=Stimme aus)
Offset
24-33: Füllbytes (Zufallswerte, meistens jedoch 255 oder 127)
Dem Header folgt zunächst
ein Song-Start-Marker-Byte mit dem Wert $AD, danach kommen die Daten für
einzelnen Stimmen als 2-Byte-Pakete.
Notenbytes der
einzelnen Stimmen
Notenformat:[Notelänge][Höhe]
Notenlänge:0-31
in 1/32-Schritten, z.B.:
00=1/32
15=1/2
31=1/1
Gerade Werte gibt es
nicht, da es diese Noten einfach nicht gibt. Die längste Note, die der Editor
darstellen kann, ist die ganze Note bzw. ganze Pause. Es gibt keine ganze Note
mit Punkt (1 1/2) und keine doppelte Note (2/1). Die Notenhöhe ist auf C0
geeicht, die tiefste Note ist also C0 mit dem Wert 0. Der Song Maker
unterstützt 64 Notenwerte, der Notenwert 63 ($3F) ist also das Maximum.
Flags im
Noten-Längen-Byte
Bit 7:
Bit
7=1 im Längen-Byte bedeutet, dass die aktuelle Stimme zu Ende ist- alle Stimmen
werden nacheinander im Speicher abgelegt. Wenn Bit 7 gesetzt ist, sind auch
stets alle weiteren Bits gesetzt, das heißt, wenn Bit 7 gesetzt ist, ist der
Byte-Wert 255 ($FF). Bit 7=1 im Noten-Höhen-Byte bedeutet, dass die Note
ungültig ist und nicht angespielt wird. Auch hier sind stets alle Bits gesetzt,
wenn Bit 7 gesetzt ist. Da die Noten stets als Byte-Paare abgelegt werden, wird
das Ende einer Stimme durch
[255][255]
gekennzeichnet.
Bit 6:
Ein
gesetztes Bit Nr. 6 zeigt eine Pause an, in der Form
Pause=[Pausenlänge OR 64], z.B.
[67]=67-64 (67 AND 63)=Pause Länge 3 (1/8 Pause)
[71]=71-64 (71 AND 63)=Pause Länge 7 (1/4 Pause)
Bit 5:
Ein
gesetztes Bit Nr. 5 zeigt an, dass die aktuelle Note mit der nachfolgenden Note
gebunden wird. Da Notenlängen nicht über 31 sein können, ist dies OK. Der
Editor lässt es zwar zu, dass unterschiedlich Noten gebunden werden können,
lässt aber kein Decendo/Increndo
zu, das die Tonhöhen langsam nach unten sinken oder
nach oben steigen lassen kann. Stattdessen kommt es zu einem Neuanschlag der
folgenden Note, wenn sich die Tonhöhen unterscheiden (Game Maker Bug oder
bewusste Beschränkung des Game Makers).
3.1.2 Game-Maker-Instrumente
0: Off (Stimme aus)
1: Bass (Bass)
2: Cow bell (Kuhglocke)
3: Cymbal (Ride-Becken)
4: Flute (C-Flöte)
5: Guitar (akkustische Gitarre)
6: Hapsycrd (Hapsy-Chord-Synthesizer)
7: Piano (Klavier)
8: Saxophone (Saxofon)
9: Snare (Scare-Drum)
10: Synthesiz
(elektronischer Synthesizer)
11: Trumpet
(Trompete)
12: Violin
(Violine)
13: Xylophone (Xylofon)
14: Nicht belegt
15: Nicht belegt
Es gibt leider keine
Möglichkeit, im Game Maker neue Instrumenten-Klänge zu erstellen, spätere
Erweiterungen gibt es nicht. Die Tonleiter-Skalierung ist fest vorgegeben
(G-Schlüssel). Ferner sind die SID-Einstellungen für die einzelnen Instrumente
wie folgt fest vorgegeben:
W=Waveform, A=Attack, D=Decay, H=Noten-Halte-Wert
Eine
Note wird angespielt, indem das SID-Gate der entsprechenden Stimme zunächst auf
0, und anschließend auf 1 gesetzt wird. Da S und R normalerweise 0 sind,
entspricht dies einem sofortigen Neuanschlag beim Spielen der nächsten Note.
Einen Stacatto unterstützt der Game Maker nicht, wohl
aber das Halten einer Note für manche Instrumente. Sustain und Release sind
also normalerweise stets 0, es sei denn, ein Instrument unterstüzt
das Halte-Flag. In diesem Fall wird beim Noten-Halten
S auf 14, R auf 0 gesetzt, was einem Wert von $E0 in der Adresse SI+(7*Stimme)+6 entspricht. Die Instrumente, die die
Rechteck-Wellenform $40 auswählen, nutzen zusätzlich eine feste Pulsweite von
25%, das Lo-Byte der Pulsbreite wird also nicht benutzt, während das Hi-Byte
(hier die Variable P) stets 4 ist. Der Songplayer ist an den Timer-Interrupt gebunden, also entsprechen 10 ms 6 Player-Takten. Die Sprites
steuert der Game Maker dagegen über den Raster-Interrupt.
Instrument
0 (off): W=$20,A=$00,D=$09,H=$00,P=$00
Instrument
1 (bass): W=$20,A=$00,D=$09,H=$00,P=$00
Instrument
2 (cowbell):
W=$10,A=$00,D=$08,H=$00,P=$00
Instrument
3 (cymbal):
W=$80,A=$00,D=$0A,H=$00,P=$00
Instrument
4 (flute): W=$10,A=$04,D=$0A,H=$E0,P=$00
Instrument
5 (guitar):
W=$20,A=$00,D=$09,H=$00,P=$00
Instrument
6 (hapsychord):
W=$40,A=$00,D=$0A,H=$00,P=$04
Instrument
7 (piano): W=$20,A=$00,D=$0A,H=$00,P=$00
Instrument
8 (saxophone):
W=$20,A=$06,D=$0B,H=$00,P=$00
Instrument
9 (snare):
W=$80,A=$00,D=$08,H=$00,P=$00
Instrument
10 (synthesizer): W=$40,A=$00,D=$0C,H=$00,P=$04
Instrument
11 (violin):
W=$20,A=$04,D=$08,H=$E0,P=$00
Instrument
12 (trumpet):
W=$20,A=$04,D=$0A,H=$00,P=$00
Instrument
13 (xylophone):
W=$10,D=$00,D=$09,H=$00,P=$00
Instrument
12 (trumpet) verhält sich leider nicht stabil im
Original
Game Maker (Originaldiskette) und es kann nicht
herausgefunden
werden, ob auch die Trompete gehalten werden kann.
Das
gleiche gilt für den Synthesizer. Im Song SPHERE und WILTEL
gibt
es jedenfalls Bindezeichen für Noten, aber die Instrumente
synthezizer und trumpet werden nicht
gehalten, sondern klingen aus.
Der
Song SPHERE wird darüber hinaus nur auf dem Original-SID
korrekt
abgespielt, nicht aber von Emulatoren (Emulation zu langsam?)
Meine durch
Reverse Engineering, Debugging und viel Gehirnschmalz entwickelte Version
Des
Song Players für Game Maker Songs sieht in Assembler nun so aus (Kommentare,
die
ich in C-Manier hinter den Code geschrieben habe, bitte nicht mit eingeben):
Assembler-Routine
für den IRQ-Player
(mit
dem SAVER zu speichern mit SYS 53176,“GMTOOLS.BIN“,49500,49999)
10
- .EQ SI=54272 // Startadresse SID
(Datensegment ab Adr. 900)
20
- .EQ MCLK=996 // Master-Clock des IRQ
(8 Bit)
30
- .EQ SCLK=997 // Uhr des Songs
(Song-Clock)
40
- .EQ REPEAT=998 // Song Repeat-Flag (>0=Repeat)
50
- .EQ BASELO=950 // Basisadresse Song
Lo-Byte
60
- .EQ BASEHI=951 // Basisadresse Song
Hi-Byte
70
- .EQ VRDY=952 // Ready-Flags der
Stimmen (1=alles gespielt)
// Die
Daten und Flags der einzelnen Stimmen werden in 7 Bytes großen Blöcken
abgelegt.
//
Dies ist so, weil der SID ebenfalls 3*7 Bytes benutzt und so die Verwaltung
einfacher wird.
// Der
Offset ab Adresse 900 ist 7*(Stimme), Stimmennummern gehen hier von 0-2
80
- .EQ VPOS=900 // Positionszeiger auf
Noten
90
- .EQ VCNT=902 // Zeitzähler in Form von
2 Bytes {aktuell,max}
100
- .EQ TIE=904 // Halteflag
(halten=$E0, nicht halten=$00)
110
- .EQ WAVE=905 // Wellenformwert für das
Instrument
120
- .EQ AD=906 // AD-Wert im SID-Register
für das Instrument
130
- .EQ VITAB=925 // temporäre
Wertetabelle für das Instrument
140
- .EQ V2POSLO=16 // Startzeiger auf
Stimme 2 im Songheader Lo-Byte
150
- .EQ V2POSHI=17 //
Startzeiger auf Stimme 2 im Songheader Hi-Byte
160
- .EQ V3POSLO=18 // Startzeiger auf
Stimme 3 im Songheader Lo-Byte
170
- .EQ V3POSHI=19 // Startzeiger auf
Stimme 3 im Songheader Hi-Byte
180
- .EQ TEMPO=20 // Tempoeintrag im
Songheader (bpm)
190
- .EQ INSTR=21 // Offset für Instrumentnummern im Songheader
//
Initialisierungssequenz für den IRQ-handler (Init=49500,
Load=32768)
200
- .BA 49500 // Init=49500
210
- CMP#0 // wird in A 0
übergeben, wird der Handler gestoppt,
220
- BNEINIT // andernfalls
neu initialisiert
230
- JMPDEINIT // Dient
hier der Verhinderung von BRANCH TOO FAR
240
-INIT STA SCLK // A>0
bestimmt, wie viele Player-Takte 1/32 Note sind
250
- STX REPEAT // X>0 setzt das Repeat-Flag
260
- LDA #0
270
- STA MCLK //
Master-Clock zurücksetzen
280
- STA BASELO //
Basis-Song-Adresse an Seitengrenzen ausrichten
290
- STA 248 // temporärer
Zeiger Lo
300
- STY BASEHI //
Startseite des Songs im Y-Register
310
- STY 249 // temporärer
Zeiger Hi
320
-INIT2 LDA#0 // Die nächsten
Zeilen setzen die Ready-Flags zurück
330
- STA VRDY
340
- STA VRDY+7
350
- STA VRDY+14
360
- STA VPOS // Dies
nächsten Zeilen lesen die Noten-Positionszeiger
370
- STA MCLK
380
- LDA #0 // 1. Stimme
hat immer den Offset 0 nach dem Songheader
390
- STA VPOS+1
400
- LDY #V2POSLO //
Notenzeiger der 2. Und 3. Stimme ist im Songheader
410
- LDA (248),Y // 248/249=Basisadresse des Songs (inkl. Header)
420
- STA VPOS+7 // 1
Parameterblock=7 Bytes!
430
- LDY #V2POSHI
440
- LDA (248),Y
450
- STA VPOS+8
460
- LDY #V3POSLO
470
- LDA(248),Y
480
- STA VPOS+14
490
- LDY #V3POSHI
500
- LDA (248),Y
510
- STA VPOS+15
// Die
Instrumentendaten müssen in einer Schleife ermittelt werden
// Die
Instrumentennummern im Songheader diesen dabei als Referenz auf INSTTAB
520
- LDX #0 // Offset zunächst 0 (im
X-Register)
530
- LDA #INSTR
540
- STA 999 // Offset für
Instrumentenauswahl im Songheader->999 (Temp)
550
-INSTSETUP LDY 999 // Y=Zeiger
auf die aktuelle Instrumentennummer
560
- LDA (248),Y // In A steht nun die aktuelle Instrumentennummer
570
- ASL // A=A*2
580
- ASL // A=A*2 INSTTAB
hat 4 Einträge für je ein GM-Instrument
590
- TAY // Y zeigt nun auf
den entsprechenden Eintrag in INSTTAB
600
- LDA INSTTAB,Y
610
- STA VITAB,X
// Nun die Einträgen auslesen und kopieren (nach VITAB)
620
- LDA INSTTAB+1,Y
630
- STA VITAB+1,X
640
- LDA INSTTAB+2,Y
650
- STA VITAB+2,X
660
- LDA INSTTAB+3,Y
670
- STA VITAB+3,X
680
- CLC // Nun zu X 7
addieren (nächster Block)
690
- TXA // Dies geht nur
per Umweg über den Akkumulator
700
- ADC #7
710
- TAX
//
Liegt das an der Speicheradresse 999, die z.B. auch von Hypra-Ass
benutzt wird,
// oder
übersehe ich was? Jedenfalls beeinflusst die Adresse 999 komischerweise das V-Flag,
//
aber nur dann, wenn man INC oder DEC darauf anwendet. Und das nächste CMP
greift dann auch
//
komischerweise nicht mehr, und der C64 friert ein (ich habe Tage gebraucht, um
festzustellen, dass
//
dies so ist (und zwar nur bei Hypra-Ass), und war
schon fast davor, VERRÜCKT zu werden).
720
- INC 999 // 999 um 1
erhöhen (komischerweise kann dies hier das V-Flag
setzen)
730
- LDA 999 // V-Flag kann hier gesetzt sein, und ein LDA löscht es wieder
740
- CPX #24 // Ende des
Kopiervorgangs?
750
- BNE INSTSETUP // Nein,
dann nächsten Block kopieren
760
- LDA #0 // Die nächsten
Befehle setzen die Stimmenuhren zurück
770
- STA VCNT // Zugegeben:
Dies hätte man auch in der Schleife machen können
780
- STA VCNT+7
790
- STA VCNT+14
800
- STA VCNT+1
810
- STA VCNT+8
820
- STA VCNT+15
//
Zuletzt wird die IRQ-Routine eingerichtet (Zeiger in 788/789 umbiegen)
830
- LDA #<(PLAYER)// Hypra-Ass-Schreibweise, ggf. also anpassen
840
- STA 788
850
- LDA #>(PLAYER)
//
Hypra-Ass-Schreibweise, ggf. also anpassen
860
- STA 789
870
- RTS // Zurück zu BASIC
880
-DEINIT SEI // Beim
Zurückbiegen dürfen keine zusätzlichen IRQs auftreten!
890
- LDA #49
900
- STA 788
910
- LDA #234
920
- STA 789
930
- CLI // IRQs wieder
zulassen, sonst friert der C64 ein!
940
- RTS // zurück zu BASIC
//
Player-IRQ-Routine
950
- PLAYER INC MCLK // Master-Clock aktualisieren
(1/60 Ticks)
960
- LDA MCLK
970
- CMP SCLK //
Master-Clock=SongClock?
980
- BEQ PLAY // Wenn ja,
nächsten Player-Schritt aufrufen
990
- JMP $EA31 // Sonst auf
den nächsten IRQ warten
1000
-PLAY LDX #0 //
Player-Hauptroutine starten
1010
-PLAYLOOP LDA VCNT,X
// X=Offset in den Stimmen-Datenblöcken
// Die
Uhr für jede Stimme besteht aus 2 Komponenten: Offset VCNT und Offset VCNT+1
//
VCNT wird in jedem Schritt um 1 erhöht, in VCNT+1 steht der Maximalwert.
//
Dieser Maximalwert wird aus der Notenlänge abgeleitet und ist am Anfang genau
wie VCNT=0
1020
- CMP #0 // VCNT ist
(noch) 0?
1030
- BEQ CONT2 // Dann die
nächste Note einlesen
1040
- JMP DONTPLAY //
Ansonsten die Note nicht (nochmal) abspielen
1050
- CONT2 CLC // Note
einlesen (VNCT=0)
1060
- LDA BASELO,X
// BASELO/BASEHI=Load-Adresse des Songs (Standard 32768)
1070
- ADC VPOS,X
// In VPOS+X steht der Offset der Noten ab BASE
1080
- STA 248 //
248/249=temporärer Zeropage-Zeiger auf Noten der Stimme
1090
- LDA BASEHI
1100
- ADC VPOS+1,X
1110
- STA249
1120
- LDY#34 // Songheader
(34 Bytes) zählt nicht zum Offset
1130
- CLC // Carry der
letzten Addition muss an dieser Stelle gelöscht werden
1140
- LDA (248),Y // Nächste Note einlesen
1150
- CMP #255 //
Notenlänge=255?
1160
- BNE NOTREADY // Nein?
Dann fortfahren (sonst Stimme beenden)
1170
- LDA #1
1180
- STA VRDY,X
// VRDY=1:Stimme beendet, 0:nicht beendet
1190
-NOTREADY LDA (248),Y // Hier Note neu laden (A hat sich verändert)
1200
- AND #31 // Gültige
Längenwerte sind 0 (1/32) bis 31 (1/1)
1210
- ADC #1 // 1 addieren,
damit 1/32 nicht 0, sondern 1 Takt lang ist
1220
- STA VCNT+1,X // VCNT max. aus Notenlänge ableiten
1230
- LDA (248),Y // Notenlänge noch mal laden (A wurde verändert)
1240
- AND #$20 // Binde-Flag (engl. TIE) extrahieren (Bit 5)
1250
- STA TIE,X
1260
- LDA (248),Y // Notenlänge noch mal laden (A wurde verändert)
1270
- AND #$40 // Pause-Flag extrahieren (Bit 6)
1280
- BNE SKIPSID // Pause?
Dann SID-Setup überspringen
1290
- INY
1300
- LDA (248),Y // Notenhöhe einlesen
1310
- ASL // ->dies sind 2
Bytes pro Notenwert für die SID-Register
1320
- TAY // Zeiger auf die
Frequenztabelle->Y
1330 -
LDA 50000,Y
// Beginn der Tabelle=50000
1340
- STA SI,X
// FreqLo
1350
- LDA 50001,Y
1360
- STA SI+1,X // Freq Hi
1370
- LDA TIE,X
1380
- AND #$20 // Binde-Flag TIE gesetzt?
1390
- BNE DOTIE // Nein? Dann
Sustain-Init überspringen
1400
- BEQ NOTIE // Sonst,
ohne Halten fortfahren (S=0, R=0)
1410
-DOTIE LDA VITAB+2,X // S und R aus den Instrument-Daten auslesen
1420
- STA SI+6,X
// kann hier nur $00 (S=0, R=0) oder $E0 (S=14, R=0) sein
1430
- JMP DOWAVE
1440
-NOTIE LDA #0 // Ohne
gesetztes Bilde-Flag ist stets S=0 und R=0
1450
- STA SI+6,X
1460
-DOWAVE LDA VITAB+1,X // 1. Teil der Wellenform: A und D
1470
- STASI+5,X // A und D stehen an Offset 1 de Instrument-Daten
1480
- LDA#0
1490
- STA SI+2,X // Lo-Byte der Pulsbreite ist immer 0
1500
- LDA VITAB+3,X // Hi-Byte steht an Offset 3 der Instrument-Daten
1510
- STA SI+3,X
1520
- LDA VITAB,X
1530
- STASI+4,X // Wellenform auswählen (Offset 4) und Gate auf 0 setzen
1540
- ORA #1 // Gate=1
startet die Wiedergabe (nur AD-Zyklus benutzt)
1550
- STA SI+4,X // Ohne Halte-Flag ist S=0,
und er on schwillt ab
1560
-SKIPSID CLC // Direkt Hier
fährt die Routine bei einer Pause fort
1570
- LDA VPOS,X
// Die nächsten Zeilen erhöhen den Notenzeiger um 2
1580
- ADC #2
1590
- STA VPOS,X
1600
- LDA VPOS+1,X
1610
- ADC #0
1620
- STA VPOS+1,X
//
Eine neue Note wird nur ausgewählt, wenn VCNT vorher auf 0 zurückgesetzt wurde.
//
Dies geschieht aber nur dann, wenn die vorige Note komplett abgespielt wurde.
//
Ansonsten wartet die Player-Routine nur und erhöht VCNT um 1
1630
-DONTPLAY INC VCNT,X
// Keine neue Note ausgewählt? Dann nur Uhr aktualisieren
1640
- LDA VCNT+1,X // Nun schauen, ober der Maximalwert erreicht ist
1650
- AND #$3F // Nur die
untersten 6 Bits enthalten die aktuelle Notenlänge
1660
- CMP VCNT,X
1670
- BNE CONT // Maximalwert noch nicht
erreicht? Dann zu CONT
1680
- LDA #0 // Maximalwert
erreicht?
1690
- STA VCNT,X
// Dann VCNT für aktuelle Stimme auf 0 zurücksetzen
1700
-CONT TXA // Am Ende wird X
auf jeden Fall um 7 erhöht
1710
- CLC
1720
- ADC#7
1730
- TAX
1740
- CPX #21
1750
- BEQ EXIT // alle 3
Stimmen abgearbeitet->IRQ beenden
1760
- JMP PLAYLOOP // ansonsten PLAYLOOP erneut
ausführen
1770
-EXIT LDA #0
1780
- STA MCLK // Am Ende
wird MCLOCK stets zurückgesetzt
1790
- LDA VRDY // Nun muss
geprüft werden, ob alle Stimmen fertig sind
1800
- CMP #1
1810
- BNE NOREPLAY
1820
- LDA VRDY+7
1830
- CMP #1
1840
- BNE NOREPLAY
1850
- LDA VRDY+7
1860
- CMP #1
1870
- BNE NOREPLAY
1880
- LDA REPEAT
1890
- CMP #0
1900
- BNE DONTSTOP
1910
- LDA #49 // Wenn das
Programm hier landet, wird die IRQ-Routine zurückgesetzt
1920
- STA 788
1930
- LDA #234
1940
- STA 789
1950
- JMP $EA31 // Timer-IRQ mit JMP $EA31 beenden, nicht mit RTI
1960
-DONTSTOP LDA BASELO // Wenn das
Programm hier landet, wird der Song neu gestartet…
1970
- STA 248 //
BASE->248/249
1980
- LDA BASEHI
1990
- STA 249
2000
- JSR INIT2 // Dies
geschieht genau hier bei INIT2
2010
-NOREPLAY JMP $EA31 // Timer-IRQ mit JMP $EA31 beenden, nicht mit RTI
2020
-INSTTAB .BY$00,$00,$00,$00
// Instrumententabelle mit 4 Bytes pro Instrument
2030
- .BY$20,$09,$00,$00
2040
- .BY$10,$08,$00,$00
2050
- .BY$80,$0A,$00,$00
2060
- .BY$10,$4A,$E0,$04
2070 -
.BY$20,$09,$00,$00
2080
- .BY$40,$0A,$00,$04
2090
- .BY$20,$0A,$00,$00
2100
- .BY$20,$6B,$00,$00
2110
- .BY$80,$08,$00,$00
2120
- .BY$40,$0C,$00,$04
2130
- .BY$20,$48,$E0,$00
2140
- .BY$20,$4A,$00,$00
2150
- .BY$10,$09,$00,$00
BASIC-Programm,
dass die /SNG-Dateien an die Adresse 32768 lädt und danach abspielt
(bitte
vorher GMTOOLS.BIN erstellen). Das Tempo ist hier die Länge einer 1/32-Note
in 1/60-Sekunden-Schritten,
Standard ist 4. Wenn ich herausfinde, wie der Game Maker
wirklich
das Tempo eines Songs verwaltet, werde ich diese Infos nachreichen.
10
PRINT"[SHIFT+CLR/HOME]BITTE WARTEN..."
15 FOR
I=53176 TO 53247:READ A:POKE I,A:NEXT I
20 FOR
I=49152 TO 49173:READ A:POKE I,A:NEXT I
30 FOR
I=49200 TO 49223:READ A:POKE I,A:NEXT I
35 SYS
53176"GMTOOLS.BIN"
36
GOSUB 11000
37
PRINT"[SHIFT+CLR/HOME]DATEINAME OHNE /SNG";:INPUT
N$
38
GOSUB 10000
40
OPEN 1,8,2,N$
50 SYS
49152:CLOSE 1:BA=16384
60
FL=PEEK(BA+12)+256*PEEK(BA+13)+35
70
BL=INT(FL/256):IF (FL>256*BL) OR (BL=0) THEN
BL=BL+1
80
PRINT"[SHIFT+CLR/HOME]LADE";BL;"SPEICHERSEITEN..."
90
OPEN 1,8,2,N$
100
FOR I=1 TO BL
110
SYS 49152:POKE 780,0:POKE 781,0:POKE 782,127+I:SYS
49200
120
NEXT I
125
CLOSE 1
130
PRINT"[SHIFT+CLR/HOME]TEMPO";:INPUT T:REM
STANDRAD=4
150
SI=54272:POKE SI+24,15
160
POKE 780,T:POKE 781,1:POKE 782,128:SYS 49500
170
END
10000
REM *** DATEINAME ANGLEICHEN ***
10010
T$=""
10020
FOR I=1 TO 6
10030
IF LEN(MID$(N$,I,1))=0 THEN T$=T$+" "
10040
IF LEN(MID$(N$,I,1))=1 THEN T$=T$+MID$(N$,I,1)
10050
NEXT I
10060
N$=T$+"/SNG"
10070
RETURN
11000
REM *** GM NOTETAB INIT ***
11010
FOR I=0 TO 75
11020
W=INT(16*(32.7*2^(I/12)))
11030
HI=INT(W/256):LO=W-(256*HI)
11040
POKE 50000+(2*I),LO:POKE 50001+(2*I),HI
11050
NEXT I
11060
RETURN
30000
REM *** SAVE/LOAD ***
30010
DATA 32,87,226,162,8,134,186,32,121
30020
DATA 0,240,44,32,253,174,32,138,173
30030
DATA 32,247,183,72,32,121,0,240,21,104,132,193,133,194,32,253,174,32
30040
DATA 138,173,32,247,183,132,174,133,175,76,237,245,104,132,195,133
30050
DATA 196,160,0,44,160,1,132,185,169
30060
DATA 0,76,165,244,60,54,52,39,69,82,62
40000
REM *** LOAD FILE BLOCK ***
40010
DATA 162,1,32,198,255,160,0,162,2,32,207,255,153,0,64,200,208,245,32
40020
DATA 204,255,96
50000
REM *** BUFFER TO MEM ***
50010
DATA 134,250,132,251,170,169,0,133,248,169,64,133,249,160,0,177,248
50020
DATA 145,250,200,202,208,248,96
3.2 Grafik-Dateien
(/SPR, /PIC, max. 6 Zeichen im Dateinamen)
3.2.1 Sprite-Maker
Das Dateiformat des
Sprite-Makers ist sehr proprietär und leider inkompatibel zu allen anderen
Sprite-Editoren, die z.B. für jedes Sprite einfach einen Header mit der Kopie
sämtlicher VIC-Register vor die eigentlichen Bilddaten schreiben. Auch die
Header-Länge ist nicht genormt und unterscheidet sich sogar Game-Maker-intern
z.B. von dem Dateiheader des Music- und Scene-Makers.
Die Header-Länge ist 37
Bytes, und der Header ist wie folgt aufgebaut:
Offset 0: $1D,$60 (2
Bytes)
Offset 2: GEK (3 Bytes)
Offset
5: Dateiname ohne /SPR (max. 6 Zeichen)
Offset
11: Aktuelles Animations-Frame im Editor (1 Byte, max. 32 Frames pro Animation)
Offset
12: Sprite-Farbe 2 bei Multicolor, bei Monocolor ignoriert (1 Byte)
Offset 13: Sprite-Farbe
3 bei Multicolor, bei Monocolor ignoriert (1 Byte)
Offset 14: Multicolor-Flag (1=Multicolor, 0=Singlecolor)
Offset 15: Letztes
Animations-Frames (#Frames-1)
Offset
16: X-Expand-Flag (1=an,
0=aus)
Offset
17: Y-Expand-Flag (1=an,
0=aus)
Offset
18: Editor-Hintergrundfarbe (1 Byte)
Offset
19: Anzahl der zusätzlichen Sprites (0=Nur 1 Sprite,
3=4 Sprites)
Offset
20-21: $A0,$00
Offset
22-23: Temporärer Zeiger des Editors z.B. für die Flip-Funktionen
Offset 24: Sprite-X-Position im
Editor (1 Byte)
Offset 25: Sprite-Y-Position im
Editor (1 Byte)
Offset
26: Flags: Bit 7=9.Bit der Sprite X-Position, Bit 6=1.Sprite im Frame bei
gesetztem Bit
Offset
27: Nummer des aktuellen Sprites
Offset
28: Sprite-Hauptfarbe (1 Byte)
Offset
29-36: Füll-Bytes ($00), leider nicht auf die gleiche Länge, wie der Music
Maker
Ab Offset 37 kommen die
Sprite-Daten eines vollständigen Sprite-Blocks (64 Bytes). Bei
mehreren Sprites (maximal jedoch 4) werden zunächst die Bilddaten
sämtlicher Animations-Frames ohne Header nacheinander abgelegt. Danach folgt
den Bilddaten jeweils wieder ein Header für das nächste Sprite, und danach
kommen dann wieder die Bilddaten für sämtliche Animations-Frames. Der Header wird
stets für jedes Sprite vollständig kopiert, und es werden nur die Farben und
die X/Y-Koordinaten aktualisiert. Dem Dateinamen geht bei zusätzlichen
Sprite-Headern immer ein Bindestrich voran, deshalb kann man mehrere
Sprites gut erkennen (den Bilddaten folgt ein Bindestrich). Wenn der
Dateiname inkl. Bindestrich über 6 Zeichen lang ist, dann wird der Dateiname
auf 6 Zeichen gekürzt. Bei mehreren Animations-Frames wird nur dann ein
zusätzlicher Header für ein neues Animations-Frame angelegt, wenn ein Frame aus
mehr als einem Sprite besteht. Ansonsten werden die
Bilddaten hintereinander weg als 64-Byte-Blöcke abgelegt. Mit dem nächsten
Listing können Sie /SPR-Dateien des Game Makers laden und die
Sprite-Frames neu als Rohdaten abspeichern.
0 V=53248:SP=192:CS=0:AF=0:GOSUB
2000
5 FOR I=53176 TO 53247:READ
A:POKE I,A:NEXT I
10 PRINT"[SHIFT+CLR/HOME]FILE";:INPUT
F$
20 OPEN 1,8,2,F$
25 PRINT"[SHIFT+CLR/HOME]";
30 FOR I=1 TO 5
40 GET#1,A$
50 NEXT I
51 PRINT"[SHIFT+CLR/HOME]";
53 FOR I=1 TO 6
54 GOSUB 1000
55 IF BY>=32 THEN PRINT CHR$(BY);:GOTO 57
56 PRINT CHR$(BY+64);
57 NEXT I
58 GET#1,A$:PRINT
60 GOSUB 1000:CB=BY:PRINT"SPRITE
COLOR 2 IS";CB
70 GOSUB 1000:CC=BY:PRINT"SPRITE COLOR 3 IS";CC
80 GOSUB 1000:MC=BY:PRINT"MULTI COLOR FLAG
IS";MC
85 IF MC=1 THEN POKE V+28,1
90 GOSUB 1000:FR=BY:PRINT"NUMBER OF FRAMES
ARE";FR+1
100 GOSUB 1000:XX=BY:PRINT"X-EXPAND-FLAG
IS";XX
110 GOSUB 1000:XY=BY:PRINT"Y-EXPAND-FLAG
IS";XY
120 GOSUB 1000:BC=BY:PRINT"EDITOR
BACKGROUND IS";BC
130 GOSUB 1000:SP=BY:PRINT"SPRITES
PER FRAME ARE";SP+1
140 FOR I=1 TO 7:GOSUB
1000:NEXT I
150 GOSUB 1000:PRINT"SPRITE NUMBER IS";BY
160 GOSUB 1000:PRINT"SPRITE MAIN COLOR IS";CA
170 FOR I=1 TO 8:GOSUB
1000:NEXT I
176 FOR CF=0 TO FR
180 FOR I=0 TO 63:GOSUB
1000:POKE 12288+I+(64*AF),BY:NEXT I
185 AF=AF+1:GOSUB 2000
186 NEXT CF
200 CS=CS+1
210 IF (CS<=SP) AND (SP>0) THEN GOSUB 2000:GOTO 51
220 CLOSE 1
226 AE=(192+AF)*64:AE=AE-1
230 PRINT"ANIM END:2040 IS
";192+AF;" -";AE
235 GET A$:IF
A$="" THEN 235
240 PRINT"SAVE AS";:INPUT
F$
250 OPEN 15,8,15,"S:"+F$:CLOSE 15
260 SYS 53176 F$,12288,AE
270 END
1000 REM *** GET FILE BYTE ***
1010 GET#1,A$
1020 IF A$="" THEN BY=0:GOTO 1040
1030 BY=ASC(A$)
1040 RETURN
2000 REM *** SHOW NEXT SPRITE FRAME ***
2010 POKE V+21,1:POKE
V,24:POKE V+1,140:POKE 2040,192+AF
2020 POKE V+39,1
2030 RETURN
10000 REM *** SAVER ***
10010 DATA 32,87,226,162,8,134,186,32,121
10020 DATA 0,240,44,32,253,174,32,138,173
10030 DATA
32,247,183,72,32,121,0,240,21,104,132,193,133,194,32,253,174,32
10040 DATA 138,173,32,247,183,132,174,133,175,76,237,245,104,132,195,133
10050 DATA 196,160,0,44,160,1,132,185,169
10060 DATA 0,76,165,244,60,54,52,39,69,82,62
3.2.2 Scene-Maker
Bitmap-Daten des Scene-Makers
werden- anders, als Sprite-Daten- als einzelne Bytes abgelegt. Die Bilder
(Dateiendung /PIC) werden zeilenweise von oben nach unten als Bytes
gespeichert. Eine primitive Kompression und das Weglassen der meisten
Datei-Header-Bytes (der Header besteht nur aus den Bytes $02,$A0,
dem Text „GEK“ und dem Byte $FF) soll Speicherplatz sparen. Das
Kompressionsverfahren besteht einfach daraus, dass vor den eigentlichen
Bilddaten-Bytes ein Längenbyte steht, das angibt, wie oft das entsprechende
Byte wiederholt wird. Trotz der Kompression kann der Game-Maker sehr komplexe
Grafiken kaum bewältigen, denn das Fehlen von Wiederholungen z.B. in
digitalisierten Fotos bläht die Daten sogar auf, anstatt sie zu komprimieren.
Dies sieht man schon z.B. an dem Bild BRTHDY/PIC, das trotz Comic-Stil
(viele einfarbig gefüllte Flächen) 19 Blocks belegt. Ein 1:1-Speicher-Dump z.B.
der Adressen 8192-16383 würde hier 33 Blocks belegen und kann Bilder beliebiger
Komplexität enthalten. Der Vorteil ist aber, dass mit dem folgenden einfachen
BASIC-Listings /PIC-Dateien an die Adressen 8192-16383 geladen werden
können:
PICLOADER
10 PRINT"[SHIFT+CLR/HOME]DATEINAME";:INPUT
F$:OPEN 1,8,2,F$
20 POKE 53272,27:POKE
53265,54:POKE 53270,216:PRINT"[SHIFT+CLR/HOME]"
30 FOR I=1 TO 6
40 GET#1,A$
50 NEXT I
60 AD=8192
70 GET#1,A$
80 IF A$="" THEN L=0:GOTO
100
90 L=ASC(A$)
100 GET#1,A$
110 IF A$="" THEN B=0:GOTO
130
120 B=ASC(A$)
130 FOR I=0 TO L
140 POKE AD,B:AD=AD+1
150 IF AD>16383 THEN CLOSE 1:GOTO
180
160
NEXT I
170 GOTO
70
180
END
Im Game Maker gibt es
zwei Bilder (auch als Scenes bezeichnet) die auch stets vollständig in den
Speicher geladen werden (da der Loader in Assembler
programmiert wurde, geht dies natürlich schneller, als mit einem
BASIC-Programm). Um nun Scene 2 statt Scene 1 anzuzeigen, wird einfach die
Startadresse des Bildschirmspeichers umgeschaltet. Der Game Maker unterstützt
keinen reinen Textmodus, sondern erledigt alles auf Bitmap-Ebene. Deshalb kann
man auch keine Zeichen umdefinieren, sondern muss den vorgegebenen
Bitmap-Zeichensatz benutzen, bei dem jedes Zeichen 8x16 Farb-Pixel groß ist.
3.3 Sound-Dateien des Sound Makers (/SND)
Der Sound Maker erzeugt
die komplexesten Dateien, und es dürfte nicht so einfach sein, einen Reader
hierfür zu programmieren. Die Standard-Header-Länge ist 29 Bytes bei folgender Header-Struktur:
Offset 0: $00,$A0 (2
Bytes)
Offset 2: GEK (3 Bytes)
Offset
5: Dateiname ohne /SND (max. 6 Zeichen)
Offset
11: $02 (1 Byte)
Offset
12: Repeats im Editor (Anzahl Wiederholungen des
Sounds, 1 Byte)
Offset
13: Repeat Delay im Editor (Verzögerung zwischen den Wiederholungen, 1 Byte)
Offset
14/15: Datenzeiger für interner Zwecke (2 Bytes)
Offset
16: Aktueller Frame im Editor
Offset
17: Anzahl der Frames im Sound (1 Byte)
Offset
18: Wellenform ohne Gate-Bit (1 Byte)
Offset
19: Attack/Decay (Kopie des entsprechenden
SID-Registers, 1 Byte)
Offset
20: Sustain/Release (Kopie des entsprechenden SID-Registers, 1 Byte)
Offset
21-22: Tonfrequenz (Kopie der entsprechenden SID-Register, 2 Bytes)
Offset 23: Speed-Wert im
Editor (1 Byte)
Offset
24/25: Pulsbreite bei Rechteckwelle (Kopie der entsprechenden SID-Register, 2
Bytes)
Offset
26: Dur-Wert im Editor (Länge des Sounds im Frame)
Offset
27: 1. EQ-Byte (1. Wert im unteren Nibble, 1 Byte)
Offset
28: 2. EQ-Wert (2. und 3. Byte als Nibbles Hi/Lo, 1
Byte)
Die einzelnen
Sound-Frames werden hintereinander abgelegt, ohne den Dateinamen noch einmal
neu zu schreiben: Der Header von Frame 2 beginnt bei Offset 13. Allerdings
werden hier die ersten 5 Bytes (Offset 13-17) nicht benutzt. Meistens enthalten
diese Bytes den Wert 0, aber manchmal auch den Wert $FF oder $80 (Bug, oder
einfach nur Speichermüll?) Ein Sound kann nur eine einzige Stimme benutzen,
nämlich die 3. SID-Stimme. Zwei Sounds können nicht gleichzeitig abgespielt
werden. Ein Sound kann auch ein Musikstück überlagern, wenn dies ebenfalls die
3. SID-Stimme benutzt. Sämtliche Timing-Werte werden in Einheiten von 1/60
Sekunden gemessen (Intervall des Timer-Interrupts).
Der Speed-Wert, der bei
Offset 23 gespeichert wird, ist [Editorwert+128]. Das bedeutet: Werte zwischen
129 und 255 erniedrigen den Ton langsam (zu beachten ist hier: 255=-1,
129=-128), Werte zwischen 1 und 127 erhöhen den Ton langsam. Ein Wert von 0 bedeutet,
dass der Ton konstant ist. Leider sind auch hier die Timing-Werte nicht
intuitiv, denn es ist so, dass die im Editor angegebene anfängliche Tonfrequenz
immer nur eine Kopie der entsprechenden SID-Register ist. Der Speed-Wert (bzw.
die ersten 7 Bits davon) wird nun bei jedem Timer-Interrupt
zu den 2 Frequenz-Bytes in den SID-Registern hinzuaddiert (wenn Bit 7 nicht
gesetzt ist) oder abgezogen (wenn Bit 7 gesetzt ist). Die 16-Bit-Grenzen werden
hier nicht beachtet, was bedeutet, dass z.B. ein Ton, der zu lange steigt,
irgendwann bei den tiefen Frequenzen wieder ankommt. Das dazugehörige
Assembler-Programm ist sehr primitiv (per Debugger, Gehirnschmalz und sehr viel
Ausdauer herausgefunden):
.EQ
TEMP=$80D2 (Datensegment anscheinend bei $8000=32768)
LDA SPEED
AND #$80
BNE FALL
BEQ RISE
FALL
LDA SPEED
AND #$7F
STA TEMP
SEC
LDA FREQLO
SBC TEMP
STA FREQLO
LDA FREQHI
SBC #0
STA FREQHI
JMP EXIT
RISE
LDA SPEED
AND #$7F
STA TEMP
CLC
LDA FREQLO
ADC TEMP
STA FREQLO
LDA FREQHI
ADC #0
STA FREQHI
EXIT LDY SOUNDFRAME (wird später durch
PLAYSOUND aktualisiert)
LDX #14 (nur Offset für 3.
SID-Stimme)
JSR PLAYSOUND
RTS
Der Sound-Player und
der Song-Player werden durch den Timer-Interrupt
gesteuert und nacheinander aufgerufen. Wahrscheinlich ist die Steuerschleife
für die Sounds deshalb so einfach gestrickt und der Song-Maker unterstützt nur
fest verdrahtete Instrumente, die in Lookup-Tabellen stehen: Die Laufzeit der
Sound-Steuerschleife und der Song-Steuerschleife dürfen zusammen genommen 1/60
Sekunde nicht überschreiten. Die Sprites werden jedoch
nicht durch den Timer-Interrupt, sondern durch den
Raster-Interrupt gesteuert, und dies kann bei sehr schnellen Werten für AnimationSpeed zusammen mit komplexen Sounds
durchaus zum Bildschirmflimmern führen. Vor allem die Tatsache, dass sich Sounds
und Songs eine Stimme teilen müssen, führt manchmal zu großen Beschränkungen.
Dies liegt aber eher daran, dass der SID zu wenig Stimmen hat, als am Game
Maker.