Billard
Dieses Programm simuliert die Bewegungen von bis zu sieben Billardkugeln in Realzeit physikalisch richtig und im Rahmen der möglichen Bildschirmauflösung sehr genau.
Der C 64 verfügt mit den Sprites über hervorragende Möglichkeiten, grafische Figuren besonders durch Maschinenbefehle sehr schnell auf dem Bildschirm zu bewegen. Es stellte sich aber heraus, daß die CPU auch nach Ausschöpfung aller programmtechnischen Möglichkeiten mit Bewegung und Stoßauswertung so ausgelastet ist, daß eine realistische Effet-Behandlung in Realzeit nicht mehr möglich ist (für Interessierte, die auf einem anderen Rechner ähnliches realisieren möchten, sei darauf hingewiesen, daß aber noch genügend Zeitreserven für eine Programmlösung auch ohne Sprites vorhanden sind!).
Trotz dieser Einschränkung läßt sich aber ein Billardspiel gut simulieren.
Vor dem eigentlichen Programm jedoch noch einige prinzipielle Bemerkungen zum wichtigsten Teilproblem, der Stoßberechnung.
Da sich alle Kugeln zu jeder Zeit, also insbesondere auch, wenn ein Stoß auszuwerten ist, gleichmäßig bewegen sollen, wird durch diese Rechnung entscheidend die Schnelligkeit der Kugelbewegungen bestimmt. Wie so oft liegt hier die Schwierigkeit weniger in der programmtechnischen Ausführung als in der rechnergerechten Formulierung des Problems.
Ein kleiner Ausflug in die Physik
Eine Geschwindigkeit ist bekanntlich ein Vektor, das heißt, sie wird bestimmt durch einen Betrag und durch eine Richtung. Ein Vektor läßt sich grafisch gut als Pfeil darstellen, dessen Länge hier für die Größe der Geschwindigkeit steht, während die Spitze beziehungsweise der Winkel mit der Horizontalen die Richtung angibt (siehe Bild 1).

Für grafische Betrachtungen dürfen wir in diesem Zusammenhang Vektoren beliebig verschieben, nicht aber drehen.
Wie in Bild 1 b) gezeigt, kann eine Geschwindigkeit V in ihre Komponenten in x-Richtung (VX) und in y-Richtung (VY) zerlegt werden und ist durch diese auch eindeutig bestimmt.
Bild 1 c) verdeutlicht, wie sich aus den Komponenten VX und VY wieder die Geschwindigkeit V konstruieren läßt. Dazu braucht lediglich VY bis zur Spitze von VX verschoben zu werden, und die Verbindung des Fußpunktes von VX mit der Spitze des verschobenen VY (VY’) stellt V dar. Ein solches Aufspalten und Zusammenfügen von Vektorkomponenten bezüglich zweier Richtungen ist immer möglich, wenn die Richtungen aufeinander senkrecht stehen. Bei der Stoßbetrachtung werden uns später zwei andere Richtungen interessieren, wo die Zerlegung dann nach dem gleichen, hier in Bild 1 beschriebenen Prinzip, durchgeführt wird.
Die Aufspaltung in x- und y-Komponenten paßt sehr gut zur Art der Spritebewegung, so daß es naheliegt, alle vorkommenden Geschwindigkeiten so zu zerlegen und VX und VY getrennt zu behandeln. Damit wird bei der Bewegung das Rechnen mit Winkeln vermieden.
Zur Stoßberechnung selbst bieten sich zunächst die Energie- und Impuls-Erhaltungssätze an (Mancher wird sich jetzt wohl an den Physikunterricht erinnern). Diese allein liefern aber noch keine eindeutige Lösung, es ist noch eine zusätzliche Annahme erforderlich.
In Bild 2 sind zwei sich berührende Kugeln dargestellt, die gleiche Größe und gleiches Gewicht haben sollen. Außerdem sind zwei ausgezeichnete Richtungen angedeutet, die senkrecht aufeinander stehen: die sogenannte Tangential- und die Normalrichtung. Denken wir uns nun die Kugelgeschwindigkeiten ähnlich wie oben in einen Anteil von Tangential- (VT) und einen Anteil in Normal- (VN) Richtung zerlegt, dann gilt folgende Regel:

Bei einem Stoß bleibt für jede Kugel deren Geschwindigkeitsanteil in Tangentialrichtung nach Betrag und Richtung erhalten, während jede Kugel die Normalkomponente der anderen Kugel übernimmt.
Mit dieser Annahme lassen sich nun die Kugelgeschwindigkeiten nach einem Stoß durch Zerlegen der vorherigen Geschwindigkeiten in VT und VN und entsprechendes Zusammenfügen dieser Komponenten zeichnerisch einwandfrei bestimmen.
Für das Programm sind aber mathematische Gleichungen erforderlich. Dazu müssen die Tangential- und Normal-Geschwindigkeiten jede für sich noch einmal in einen x-Anteil (VXT, VXN) und einen y-Anteil (VYT, VYN) zerlegt werden:
VXT = (VX * SIN(W) - VY * COS(W)) * SIN(W)
VXN = (VY * SIN(W) + VX * COS(W)) * COS(W)
VYT =-(VX * SIN(W) - VY * COS(W)) * COS(W)
VYN = (VY * SIN(W) + VX + COS(W)) * SIN(W)
Der Winkel W ist der Winkel zwischen Normalenrichtung und der Horizontalen (siehe Bild 2); er läßt sich aus den Koordinatendifferenzen der Kugelpositionen bestimmen.
Dann ergeben sich die resultierenden Kugelgeschwindigkeiten nach dem Stoß beziehungsweise deren x- und y-Komponenten:
VX1 = vxn2 + vxtl }
} für Kugel 1
VY1 = vyn2 + vyt1 }
VX2 = vxn1 + vxt2 }
} für Kugel 2
VY2 = vyn1 + vyt2 }
Für die sich stoßenden Kugeln 1 und 2 sind die Geschwindigkeiten vor dem Stoß klein, nach dem Stoß groß geschrieben.
Mit einiger Knobelei habe ich die Gleichungen auf diese Form gebracht, die den Vorteil hat, daß alle vorkommenden Summanden, Klammerausdrücke und Zwischenergebnisse nicht größer werden können als die Ausgangsgröße V.
Werden für V dann nur Werte zwischen 0 und 12 7 zugelassen, so können die Rechnungen mit positiven und negativen Ein-Byte-Zahlen durchgeführt werden, was, wie Sie aus dem laufenden Assemblerkurs wissen, besonders einfach und schnell geht.
Geschwindigkeit durch Tabellen
Bei diesen schönen Ein-Byte-Rechnungen stören aber noch die vielen » * SIN« und » * COS«, die viel zuviel Rechenzeit beanspruchen. Es bleibt nur die Möglichkeit, mit Tabellen zu arbeiten, wobei aber nicht alle benötigten Werte gespeichert werden müssen. Zunächst ergeben sich in jedem Viertelkreis immer die gleichen SIN- und COS-Werte, nur mit unterschiedlichem Vorzeichen. Außerdem ist der COS symmetrisch zum SIN:
COS(W) = SIN(W+90 Grad). Ich unterteile also den Viertelkreis in 23 Winkelteile und lege die zugehörigen SIN-Werte im Fließkommaformat in einer Tabelle ab.
Nun stört nur noch die Multiplikation. Da wie oben beschrieben nur Faktoren von 0 bis 127 vorkommen, ist auch hier eine Tabelle noch vertretbar. So werden nach Start des Programmes zu jedem SIN-Wert 128 Ein-Byte-Zahlen in eine Multiplikationstabelle geschrieben: (Byte 0 bis 127) * SIN.
Das sind jetzt zwar schon eine ganze Menge Bytes, dafür wird das Programm aber auch kürzer und sehr schnell.
Durch geeignetes Setzen von Zeigern können alle Werte »Byte * SIN« und »Byte * COS« nun ohne weitere Rechnung direkt abgerufen werden. Es bleibt nur noch die schnelle Ein-Byte-Addition und -Subtraktion. Die gewählte Winkel- und Geschwindigkeitsauflösung ist im Rahmen der Bildschirmauflösung voll ausreichend.
Da hier Winkel durch das Verhältnis der Geschwindigkeiten VX und VY dargestellt werden, ist klar, daß die Genauigkeit bei sehr kleinen Geschwindigkeiten geringer wird. Bei einer Gesamtgeschwindigkeit von beispielsweise 2 kann der Winkel eben nicht mehr so genau dargestellt werden wie mit höheren Werten, die viel mehr Kombinationen VY/VX zulassen.
Aus diesem Grund dürfen die Kugeln auch nicht rollen bis ihre Geschwindigkeit Null geworden ist, sondern die Bewegung wird schon etwas früher abgebrochen.
Am Schluß dieser Betrachtungen noch zwei Beispiele, an denen Sie bei Billardprogrammen einmal die Richtigkeit der Stoßberechnung überprüfen können:
Die Verhältnisse sind besonders einfach, wenn eine bewegte Kugel eine ruhende stößt. Erfolgt der Stoß zentral, so muß die stoßende Kugel anschließend stillstehen, während die andere Kugel die Geschwindigkeit voll aufgenommen hat. Bei nichtzentralen Stößen müssen die Richtungen, in die sich die zwei Kugeln nach dem Stoß bewegen, einen rechten Winkel einschließen.
Tips zum Eintippen
Es besteht aus zwei Teilen, dem eigentlichen Billardprogramm, das in Maschinencode geschrieben ist und am Basic-Anfang bei Adresse 2048 beginnt, und einem Basic-Teil im Speicher ab 4973, in dem die Spielregeln definiert werden.
Die ersten Bytes des Maschinenprogrammes entsprechen einer Basic-Zeile: »1 SYS2061«. Beim Start des Programmes findet der Basic-Interpreter diesen SYS-Befehl und startet das Maschinen-Programm bei 2061. Dort wird der Basic-Anfang auf 4973 hochgesetzt, die oben beschriebene Multiplikationstabelle erstellt und zum Basic-Teil gesprungen, von dem aus dann die weiteren Maschinen-Routinen aufgerufen werden.
Auf diese Weise ist das Maschinen-Programm so eingebunden, daß Sie das gesamte Programm später wie ein gewöhnliches Basic-Programm ohne Zeigerumstellungen handhaben können.
Da der MSE die gleiche Methode benutzt, also auch ab 2048 im Speicher liegt, wird die Eingabe des Programmes etwas umständlich.
Verfahren Sie daher am besten folgendermaßen:
- Mit dem MSE (siehe Seite 8) den Maschinenteil ab $C000 eingeben und unter »BILL(MP)« abspeichern.
- Mit dem Checksummer (Seite 6) das Basic-Programm eingeben und unter »BILL(BASIC)« abspeichern.
- Beide Programmteile binden:
a) »BILL(MP)« absolut laden (»LOAD"BILL(MP)",8,1« oder »…,1,1«)
b) Basic-Anfang auf 4973 setzen mit Eingabe im Direktmodus »POKE 43,109: POKE 44,19: POKE 4972,0: NEW«
c) Maschinen-Programm im Direktmodus übertragen »FOR 1=49152 TO 52076: POKE 1-47104, PEEK(I): NEXT«
d) Basic-Programm laden mit »LOAD "BILL(BASIC)",8« (beziehungsweise »…,1« für Datasette)
e) Basic-Anfang zurückstellen und Gesamtprogramm speichern mit »POKE 43,1: POKE 44,8: SAVE "BILLARD",8« (beziehungsweise »…,1«)
Im Programm »BILLARD« sind nun beide Teile enthalten. Es läßt sich wie ein übliches Basic-Programm nach Einschalten des C 64 laden und starten.
Die Bedienung des Programms erfolgt bei der Spielwahl über die Tastatur; Schlagwinkel und Schlagstärke werden über Joystick an Port 2 eingegeben. Die Spielregeln sollen hier nicht näher erklärt werden; Sie werden sie während des Spiels schnell herausfinden.
Änderungen erwünscht!
Betrachten Sie den Basic-Teil von »BILLARD« lediglich als Vorschlag und Anregung zu einem eigenen Programm.
Das Maschinenprogramm ist so modular und variabel aufgebaut, daß Sie leicht ein Billardspiel nach eigenem Geschmack und mit eigenen Regeln realisieren können (zum Beispiel ein Lochbillard mit fünf Kugeln unter Berücksichtigung von Stößen).
Zur Erkennung eines Stoßes oder einer Bandenberührung werden die entsprechenden Sprite-Register nicht benutzt, wodurch Sie auch in der grafischen Gestaltung völlig frei sind.
Die drei Tongeneratoren werden vom Programm vor jedem Stoß auf die benötigten Werte eingestellt. Sie müssen nur von Basic aus das Register 54296 für die Lautstärke einstellen (=15).
Generator 1: Bandenberührung
Generator 2: Stoßgeräusch
Generator 3: Ton, wenn eine Kugel in ein Loch fällt
Alle drei Generatoren werden mit der Dreieck-Wellenform »16/17« geschaltet.
1. Veränderbare Programmparameter
Der Bewegungsablauf kann durch drei Parameter beeinflußt werden.
a) Byte 3687: Dämpfung (hier = 10)
Ein größerer Wert bedeutet hier schwächere Dämpfung; die Kugeln rollen dann bei gleicher Schlagstärke weiter. Ein kleinerer Wert bewirkt stärkere Dämpfung.
b) Byte 4200: Ende der Kugelbewegung (hier = 2) Wie schon oben bei der Stoßauswertung beschrieben, ist es sinnvoll, die Kugelbewegung abzubrechen, bevor die Geschwindigkeit auf 0 abgesunken ist. Ein zu kleiner Wert an dieser Stelle hat vor allem bei schwacher Dämpfung ein Abknicken der Bewegungsrichtung kurz vor Stillstand zur Folge.
c) Byte 4248: Verzögerung des Bewegungsablaufes (hier = 10)
Eine automatische Geschwindigkeitsanpassung abhängig von der Anzahl der rollenden Kugeln ist an anderer Stelle eingebaut; sie sorgt für gleichmäßige Bewegung, gleichgültig, ob sich gerade nur eine oder sieben Kugeln bewegen.
An dieser Stelle bewirkt ein größerer Wert zusätzliche Verzögerung. Sie können sich beispielsweise mit »255« einen Stoß in Zeitlupe ansehen. Während mit der Dämpfung die zurückzulegende Strecke bestimmt wird, läßt sich hier die Geschwindigkeit der Bewegung festlegen.
Experimentieren Sie ruhig etwas mit diesen Parametern. Nach einem entsprechenden POKE sehen Sie die Wirkung gleich beim nächsten Stoß.
Wenn Sie nach Änderungen hier das Programm wieder neu abspeichern, werden die neuen Werte mitgespeichert. Sie können selbstverständlich aber auch während des Programmlaufes die Parameter mehrfach ändern.
2. Parameter für Feld- und Lochgrößen
Sie sind in Bild 3 mit den hier verwendeten Bezeichnungen dargestellt.
a) Byte 3204: Linker Feldrand (hier = 20)
b) Byte 3205: Rechter Feldrand (hier = 56)
c) Byte 3206: Oberer Feldrand (hier = 73)
d) Byte 3207: Unterer Feldrand (hier = 73)
e) Byte 3208: (hier = 3) Dieser Wert legt fest, um wieviel Grafikpunkte sich der Cursor (zur Einstellung der Stoßrichtung) über den Feldrand hinaus bewegen lassen soll.
Dieser Cursor — hier ein Zielkreuz (Sprite 0) — ist übrigens in Sprite-Block 39 definiert und kann durch Eingabe entsprechender Daten in 2496 bis 2559 geändert werden.
f) Byte 3209: Größe der Ecklöcher (hier = 6)
g) Byte 3210: Größe der mittleren Löcher (hier = 10)
Es gilt hier die Einschränkung, daß der rechte Feldrand sowie die rechten Ecklöcher im Bildschirmbereich des V+16-Registers (53264) liegen müssen, während die anderen Werte nicht in diesem Bereich liegen dürfen (für den rechten Feldrand ist unter b natürlich das Low-Byte einzugeben).
Als kleine Hilfe: Die Kugeln bleiben voll sichtbar im Bereich X zwischen 12 und 64, Y zwischen 41 und 229.
Auch diese Parameter stehen im Programm und werden bei erneutem Abspeichern mit übernommen. Allerdings sehen Sie Änderungen hier nicht gleich beim Programmlauf, da diese Werte erst beim Programmstart abgerufen werden. Ein Unterprogramm errechnet daraus die im Programm benötigten Daten und schreibt sie an die richtige Stelle. Wollen Sie diese Parameter während des Programmes ändern, rufen Sie nach den entsprechenden POKEs das Maschinen-Programm »SE« (siehe später) auf.

Wenn Sie das geänderte Programm abspeichern, vergessen Sie nicht, vorher den Basic-Anfang zurückzustellen mit:
»POKE 43,1: POKE 44,8«! Andernfalls wird möglicherweise nur das Basic-Programm gespeichert.
Wie das Zielkreuz (Sprite 0, Block 39) stehen auch die Sprite-Daten der Kugeln im Maschinenprogramm in den Sprite-Blöcken 40 bis 47, zwischen denen zur Simulation einer Rollbewegung hin und her geschaltet wird.
Jeder Kugel ist entsprechend ihrer Sprite-Nummer eine Zahl zwischen 1 und 7 zugeordnet. Ebenso sind die Löcher, wie in Bild 3 gezeigt, durchnummeriert von 2 bis 7.
Nach jedem Stoß stehen Ihnen folgende Informationen zur Auswertung zur Verfügung:
a) Stoßregister $FD (= Basicvariable SR = 253):
Hier sind die Kugeln angegeben, die mindestens einmal mit der Stoßkugel kollidiert sind, und zwar im Format »2 hoch Kugelnummer 1 bis 7«. Dieses Format entspricht dem, das Ihnen beispielsweise vom Sprite-Sprite Berührungsregister her bekannt ist.
b) Lochregister $FE (= Basicvariable LR = 254):
Hier finden Sie im gleichen Format wie beim Stoßregister die Kugeln, die in ein Loch gefallen sind.
c) Zur weiteren Auswertung ist fürjede Kugel ein zusätzliches Lochregister vorhanden: 52123 + Kugelnummer 1 bis 7.
Dieses enthält »2 hoch Lochnummer« des Loches, in das die entsprechende Kugel gefallen ist. Liegt sie noch auf dem Feld, steht hier 0.
Alle Register a) bis c) werden vor jedem Stoß automatisch auf 0 zurückgesetzt.
Zum besseren Verständnis folgen einige Beispiele:
Zu a): IF PEEK(SR) = 12 THEN »Stoßkugel kollidierte mit den Kugeln 2 und 3« (22 + 23 = 12)
Zu b): IF PEEK(LR) AND 8 THEN »Kugel 3 gefallen«
IF PEEK(LR) AND 84 THEN »Kugel mit gerader Nummer, also Kugel 2, 4 oder 6 gefallen«
Zu c): IF PEEK(52123+K) = 64 THEN »Kugel K in Loch 6«
IF PEEK(52123+K) AND 28 THEN »Kugel K in einem der oberen Löcher, also Loch 2, 3 oder 4«
Die Maschinensprache-Routinen
Es folgt nun eine Kurzbeschreibung der Maschinenroutinen: Hinter SYS steht die im Basic-Programm benutzte Variable, die zugehörigen Adressen sind später aufgeführt.
SYS SE: Setzt die Feld- und Lochparameter direkt in das Programm. Die Routine braucht nur aufgerufen zu werden, wenn diese Parameter während des Programmlaufes verändert worden sind.
SYS IN, Anzahl der Kugeln 1 bis 7 + 0 oder + 128: Diese Routine muß einmal vor Beginn eines neuen Spieles aufgerufen werden. Sie definiert für ein Spiel die Anzahl der Kugeln, wobei dann beispielsweise mit einer Kugelzahl von 3 die Kugeln 1, 2 und 3 im Programm berücksichtigt werden. Zusätzlich wird durch Bit 7 festgelegt, ob Lochbillard (Bit 7 = 1, also + 128) oder Bandenbillard (Bit 7 = 0) gespielt werden soll.
SYS AL, Kugelnummer, X-Koordinate (2 Byte 0 bis 511), Y-Koordinate: Hierdurch wird eine Kugel auf das Feld gelegt, wobei die Register V+21 und V+16 automatisch entsprechend eingestellt werden. Wird eine Kugel so gesetzt, daß sie eine andere berühren würde, wird sie so weit wie nötig zur Feldmitte hin verschoben. Für regelgerechte Bewegungen muß die Kugel selbstverständlich in das Spielfeld gesetzt werden.
SYS VL, Kugelnummer: Legt eine Kugel vor dem Loch wieder auf, wenn sie gefallen ist, andernfalls hat der Aufruf keine Wirkung. Im Fall einer Berührung mit anderen Kugeln wird wie bei »AL« verschoben. Sie können diese Routine beispielsweise benutzen, wenn eine Kugel in ein verbotenes Loch gefallen ist.
SYS KS, Nummer der Stoßkugel: Dieser Aufruf muß vor jedem Stoß erfolgen. Es wird hier die Stoßkugel festgelegt und außerdem das Kreuz auf diese Stoßkugel gesetzt.
SYS JS: Nach diesem Aufruf kann mit Hilfe des Joysticks an Port 2 das Zielkreuz bewegt werden. Nach »FIRE« erfolgt Rücksprung in Basic, wo nun die Geschwindigkeits-Eingabe folgen sollte.
SYS GS, Anfangsgeschwindigkeit 0 bis 127: Für die Anfangsgeschwindigkeit der Stoßkugel sind hier Werte zwischen 0 und 127 erlaubt. Nach Auswertung der Geschwindigkeits-Eingabe beginnt die Bewegung der Kugeln. Rücksprung in Basic erfolgt erst, wenn alle Kugeln wieder in Ruhe sind, vorher ist ein Aussprung nur mit »STOP / RESTORE« möglich.
SYS N1, Anfangsgeschwindigkeit: Wählen Sie statt »GS« diesen Einsprungspunkt, so werden vor der Bewegung alle Sprite-Positionen und die Nummer der Stoßkugel in einem Speicherbereich 1 abgelegt. Da damit alle wichtigen Parameter vor einem Stoß erhalten bleiben, wird eine Stoßwiederholung ermöglicht.
SYS SW: Wählen Sie diesen Einsprungspunkt an Stelle von »JS«, und haben Sie den vorherigen Stoß mit »N1« durchgeführt, werden alle Sprites auf die dort gespeicherten Werte zurückgesetzt, die ja den Zustand vor dem letzten Stoß wiedergeben.
Außerdem werden die jetzigen Positionen in einem anderen Speicherbereich 2 abgelegt. Das Programm fährt fort bei »JS« mit der Joystickabfrage.
Wählen Sie dann für die Geschwindigkeitseingabe bei der Schlagwiederholung den Einsprungspunkt »GS«, haben Sie nach diesem wiederholten Stoß die Wahl zwischen drei Möglichkeiten:
Einmal können Sie den vorherigen Stoß mit »SW« und »GS« noch einmal wiederholen. Sie können aber auch mit »JS« und »N1« vom jetzigen Zustand aus weiterspielen. Für die dritte Möglichkeit, die ich in meinem Programm gewählt habe, fehlt noch eine Routine:
SYS V2: Hierdurch werden die Kugeln auf die in Speicherbereich 2 abgelegten Werte gesetzt, die ja den Zustand nach dem ursprünglichen Stoß wiedergeben. Fährt man danach mit »JS« und »N1«fort, ist die Schlagwiederholung sozusagen »außer Konkurrenz« durchgeführt worden, da ja vom Zustand nach dem ursprünglichen Stoß aus weitergespielt wird.
SYS ZS, 2 hoch Kugelnummer (= 2 bis 254); Geschwindigkeit, Winkel: Diese letzte Routine benutze ich, um einen zufälligen Anfangszustand vor Beginn eines Spieles zu erzeugen. Es lassen sich eine bis sieben Kugeln gleichzeitig mit der gleichen Anfangsgeschwindigkeit zwischen 0 und 127 anstoßen. Die Bewegungsrichtung wird durch einen Winkel in Altgrad (ein rechter Winkel entsprechend 90 Grad) eingestellt. Auf diese Weise läßt sich auch ein berechneter Stoß mit der Stoßkugel durchführen.
Der Umgang mit diesen Routinen erscheint nach dem ersten Durchlesen vielleicht etwas kompliziert. Sie werden aber sicher nach Lektüre des Demonstrationsprogrammes (Listing 1) und schlimmstenfalls nach einigem Probieren feststellen, wie einfach sich das beschriebene Maschinenprogramm anwenden läßt.
(Bernhard Tertelmann/ev)