Billard (Pool)
Inhaltsverzeichnis
Aufgabenstellung
Ausgehend vom im Unterricht bearbeiteten Problem TKugel soll ein Billardspiel implementiert werden.
Konkret habe ich mich für die Varianten Carambol (Freie Partie) und Pool (8-Ball) entschieden. Dabei entsprechen die grundlegenden Regeln den (laut Wikipedia) offiziellen Regeln mit einigen Vereinfachungen. Bei Carambol gibt es deshalb keine besonderen Eckbereiche. Bei Pool muss nicht angesagt werden, welche Kugel gelocht wird, und die 8 muss am Ende automatisch in das Loch gespielt werden, das dem gegenüberliegt, in das die letzte eigene Kugel gefallen ist.
Objekte
Im Unterricht wurden bereits die Grundlagen für die Objekte "TKugel" und "TTable" gelegt. Während diese dort allerdings noch von TPanel bzw. TShape abgeleitet waren, beruhen die aktuellen Objekte auf TImage, was einen deutlich größeren grafischen Spielraum bietet.
Im folgenden werden nun die einzelnen Objekte und ihre Eigenschaften/Methoden erklärt.
TBillard
Dieses Objekt dient im Wesentlichen zur Verwaltung der Objekte "Kugeln", "Tisch" und "Queue". So wird erreicht, dass das ganze Spielgeschehen von den Regeln abgegrenzt wird. Außerdem führt dieses Objekt anders als in der Vorlage aus dem Unterricht die Kollision zwischen den Kugeln aus, da es alle Kugeln kennt und so am effizientesten auf Kollision getestet werden kann.
FMainBallContacts: | enthält die Nummern der Kugeln, die von der gestoßenen berührt wurden |
FKugeln: | enthält die Kugeln |
FNoMovement: | wird die Variable auf true gesetzt, wird die Spielschleife beendet |
FMainBallNumber: | enthält die Nummer der gestoßenen Kugel |
FBallsInPocket: | enthält die Nummern der Kugeln die in einer Tasche und unsichtbar sind |
FContainer: | Assoziation des Steuerelements, auf das gezeichnet wird (Hauptformular) |
FOnMoveFinished: | ausgelöste Aktion, wenn alle Kugeln still stehen |
FNewInPocket: | Kugeln, die in diesem Zug in eine Tasche gegangen sind |
FQueue: | Queue |
FTable: | Tisch |
Ffps: | legt fest, wie oft pro Sekunde die Spielschleife durchlaufen wird |
Create: | erstellt das Billardspiel |
Free: | löscht das Billardspiel und gibt den entsprechenden Speicher frei |
GetBallPosition: | gibt die Position der gewünschten Kugel aus |
MoveOn: | ruft die Spielschleife auf |
OutOfPocket: | holt die gewünschte Kugel aus der Tasche |
SetBallPosition: | setzt die gewümschte Kugel auf eine neue Position |
ShootBall: | erzeugt einen Queue, um die gewünschte Kugel anzustoßen |
StopMovement: | hält die Spielschleife an |
FormToTable: | wandelt einen Punkt auf dem Formular in einen Punkt auf dem Tisch um |
CheckCollision: | auf Kollision zwischen Kugeln testen |
NewVectors: | berechnet bei Kollision zweier Kugeln deren neue Bewegung |
Pause: | pausiert die Schleife, sodass die FPS eingehalten werden |
ShotDone: | wird aufgerufen, nachdem eine Kugel angestoßen wurde |
Spielschleife: | bewegt die Kugeln, führt die Kollisionen aus |
Die Methoden SetFactor,GetFactor,SetReibung und GetReibung dienen nur zum Abrufen und Manipulieren des Faktors zwischen Ausholweite und Stoßkraft bzw. der Tischreibung.
TTable
Das Objekt entspricht dem Billardtisch. Es enthält die Eigenschaften Reibung, Löcher und die Abmessungen des Spielfelds. Die Löcher sind mit dem Loch oben links beginnend im Urzeigersinn durchnummeriert.
FTaschen: | gibt an, ob der Tisch Taschen hat |
FTableHeight: | Tischhöhe |
FTableWidth: | Tischbreite |
FReibung: | Reibung auf dem Tisch |
Create: | erstellt den Tisch |
GetTableDimensions: | gibt die Tischabmessungen aus |
SetReibung/GetReibung: | Reibung ausgeben/ neu setzen |
FormToTable/TableToForm: | wandelt einen Punkt auf dem Formular in einen Punkt auf dem Tisch um und umgekehrt |
HasPockets: | gibt an, ob der Tisch Taschen hat oder nicht |
InPocket: | gibt an, über welchem Loch sich eine Kugel befindet |
TKugel
Von dieser Klasse werden mehrere Objekte erzeugt. Sie entsprechen den Kugeln auf dem Tisch. In diesem Objekt wird die Bewegung der einzelnen Kugeln ausgeführt, sowie die Kollision mit der Bande.
FRadius: | Radius der Kugel |
FMX/FMY: | Position des Kugelmittelpunkts auf dem Tisch |
FVX/FVY: | Bewegungsvektor der Kugel |
FContainer: | Assoziation des Tisches |
Create: | erstellt die Kugel |
Move: | führt die Bewegung der Kugel, die Reibung und die Kollision mit der Bande aus |
OutOfPocket: | Kugel aus Tasche holen |
GetRadius: | Radius ausgeben |
DrawKugel: | Kugel neu zeichnen |
Die Methoden SetMovement,GetMovement,SetPosition und GetPosition dienen nur zum Abrufen und Manipulieren des Bewegungsvektors bzw. der Kugelposition.
TQueue
Dieses Objekt entspricht dem Billardqueue. Jedesmal, wenn eine Kugel gestoßen werden soll wird ein Queue für diese Kugel erstellt. Das ursprünliche Bitmap, das den Queue darstellt, wird entsprechend der Mausposition um die Kugel gedreht. Nach dem Stoßen wird der Queue wieder gelöscht.
FMouseDown: | gibt an, ob die linke Maustaste über dem Tisch gedrückt ist |
FAngle: | Winkel im Bogenmaß, in dem gestoßen wird (0 ist senkrecht nach unten) |
FFactor: | Faktor zwischen Ausholweite und Schlagkraft |
FMouseDownRadius: | Radius um Kugelmittelpunkt, an dem die Maustaste gedrückt wurde |
FPower: | Schlagkraft |
FSourceBitmap: | ungedrehtes Bild des Queues |
FKugel: | Assoziation der Kugel, die gestoßen werden soll |
FLastMousePoint: | letzte Mausposition |
FOnShot: | Aktion, die nach dem Anstoßen der Kugel ausgeführt wird |
FTable: | Assoziation des Tisches |
FTimer: | löst alle 10ms die Aktualisierung der Mausposition aus |
Create: | erzeugt den Queue |
Free: | Queue löschen |
SetFactor/GetFactor: | Faktor zwischen Ausholweite und Schlagkraft neu setzen/abrufen |
DrawQueue: | Queue neu zeichnen |
MouseDown/MouseUp: | ausgelöst, wenn Maustaste gedrückt/losgelassen wird |
UpdateMousePosition: | Mausposition aktualisieren |
Beziehungen
Das nebenstehende Bild zeigt die Beziehungen zwischen den einzelnen Objekten. Hierbei wurden nur die relevanten Eigenschaften berücksichtigt.
Wie bereits erwähnt sind die Objekte TTable, TKugel und TQueue von TImage abgeleitet. Besitzer und Verwalter dieser Objekte ist TBillard. TBillard hat immer nur jeweils ein Objekt TQueue und TTable, aber kann mehrere Objekte vom Typ TKugel haben. TKugel kennt den Tisch vom Typ TTable. TQueue kennt den Tisch und die Kugel, die jeweils gestoßen werden soll.
Tricks/Techniken
Events/Ereignisse
An mehreren Stellen war es nötig, dass ein untergeordnetes Objekt eine Methode des übergeordneten Objekts aufruft. Allerdings sollte dies geschehen, ohne das übergeordnete Objekt und die entsprechende Methode zu kennen. Hierfür können Events oder Ereignisse verwendet werden. Eine Anwendung hierfür ist, dass das Objekt BillardGame welches vom Typ TBillard ist, eine Funktion in der Unit1 aufruft, wenn ein Zug beendet ist, d.h. alle Kugeln nach einem Stoß wieder stillstehen.
Zunächst muss ein Ereignistyp vereinbart werden. Dies sieht folgendermaßen aus:
TMoveEvent = procedure(MainBallContacts: Array of byte; BallsInPocket: TPocketBalls) of object;
Nun kann in TBillard ein solches Ereignis definiert werden:
FOnMoveFinished: TMoveEvent;
Im Konstruktor wird das Ereignis festgelegt:
constructor TBillard.Create(...; OnMoveFinished: TMoveEvent); begin ... FOnMoveFinished := OnMoveFinished; ... end;
In der Unit1 existiert bereits folgende Prozedur, die dem Ereignistyp TMoveEvent entspricht:
procedure CarambolMoveFinished(MainBallContacts: Array of byte; BallsInPocket: TPocketBalls);
Beim Aufrufen des Konstruktors wird nun auch diese Funktion übergeben:
BillardGame := TBillard.Create(..., CarambolMoveFinished);
Immer wenn jetzt die Spielschleife beendet wird, weil sich keine Kugeln mehr bewegen, wird dieses Ereignis ausgelöst und somit die Funktion CarambolMoveFinished aufgerufen, ohne sie direkt zu kennen:
procedure TBillard.Spielschleife; begin ... if NoMovement then FOnMoveFinished(FMainBallContacts, FNewInPocket); ... end;
Diese Technik trägt erneut dazu bei das Spielgeschehen und die dazugehörigen Animationen von dem Reglement abzukapseln. Immer wenn ein Zug beendet ist, wird mit Hilfe dieses Ereignisses das Regelement aufgerufen. Hierfür werden die relevanten Informationen wie die Nummern der Kugeln, die von der gestoßenen Kugel berührt wurden, und die Nummern der in die Löcher gegangenen Kugeln übergeben.
Records
Records sind Datensätze, also Sammlungen von Daten verschiedenen Typs. So kann man verschiedene Variablen zu einer zusammenfassen. Genutzt habe ich dies beim Erstellen der Kugeln. In der Unit mTKugel ist folgender Datentyp definert:
type TKugelInfo = record PosX: real; PosY: real; Bitmap: Graphics.TBitmap; Name: string; Full: boolean; end;
Mehrere Datensätze können auf einmal erzeugt werden, wenn man Arrays verwendet. Auf die Daten kann dann wie auf Eigenschaften eines Objekts zugegriffen werden:
var KugelInfos: Array[0..15] of TKugelInfo; KugelInfos[15].PosX := 134.6; KugelInfos[15].PosY := 175; KugelInfos[15].Bitmap.LoadFromResourceName(HInstance, 'KugelSchwarz'); KugelInfos[15].Name := '8';
Spielschleife
Für das Bewegen der Kugeln und die Kollisionen mit Banden oder anderen Kugeln ist die Spielschleife verantwortlich. Diese Idee wurde bereits im Unterricht entwickelt. Es handelt sich hierbei um eine Schleife, die immer wieder durchlaufen wird und dabei in jedem Durchlauf die Kugeln bewegt, auf Kollisionen prüft, diese ggf. ausführt und die Ansicht aktualisiert. Sie kann entweder von außen abgebrochen werden, indem die Variable FNoMovement auf true gesetzt wird, oder sie bricht ab, wenn sich keine Kugel mehr bewegt. So können die Aktionen sehr schnell wiederholt ausgeführt werden.
Allerdings dürfen sie nicht zu schnell ausgeführt werden, weil sonst keine Bewegung zu sehen ist. Hierfür wird mittels der Variable Ffps festgelegt, wie oft die Schleife pro Sekunde durchlaufen wird. Hat der Durchlauf zu kurz gedauert, wird die restliche Zeit das Programm mit der Funktion Pause angehalten. Diese Prozedur ruft in einer Schleife jeweils sleep für 10ms und Application.ProcessMessages
solange auf, bis die die restliche Zeit um ist. So kann gewährleistet werden, dass das Programm nicht einfriert. Auf diese Weise wird die Schleife auf jedem Computer, der schnell genug ist, mit der gleichen Geschwindigkeit ausgeführt.
Programm
Beschreibung
Über den Menüpunkt Spiel kann ein neues Spiel gestartet werden. Zur Auswahl stehen Carambol oder Pool.
Gestoßen wird, indem man die linke Maustaste gedrückt hält, den Queue von der Kugel so wegzieht, wie wenn man mit dem Queue ausholen würde, und die Maustaste anschließend wieder loslässt. Ist die Maustaste gedrückt, lässt sich auch über das Fenster hinaus ausholen. Bei Pool kann nach einem Foul die weiße Kugel auf dem Tisch durch bewegen des Cursors frei positioniert werden. Sie kann gestoßen werden, sobald geklickt wird.
Im schwarzen Bereich oberhalb des Tisches wird angezeigt, welcher Spieler an der Reihe ist. Die Statusleiste unter dem Tisch zeigt, wie viel Punkte die Spieler haben.
Am Schluss wird der Gewinner eingeblendet. Es kann jederzeit ein neues Spiel gestartet werden.
Über den Menüpunkt Über... wird ein Infofenster aufgerufen, in dem auch der Entwicklungsmodus aktiviert werden kann. Der zusätzlich erscheinende, gleichnamige Menüpunkt bietet einige Funktionen zur Manipulation des Spiels. Die Änderungen bleiben allerdings nur für dieses Spiel erhalten.
Download
Code und Ressourcen: Datei:Billard.zip
kompiliertes Programm: Datei:Billard Exe.zip
Hinweis: Das kompilierte Programm "Billard.exe" läuft eigenständig und braucht die Bilder im Ordner resources nicht mehr.