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

350 RASTWAIT      LDA 53266

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

330 COPYBLOCK       LDA (248),Y

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

230 LINEOUT        LDA (250),Y

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.