Billard (Pool)

Aus Informatik
Wechseln zu: Navigation, Suche

Billard Screenshot.gif

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

UML-Diagramm: 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

UML-Diagramm: 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

UML-Diagramm: 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

UML-Diagramm: 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

UML-Diagramm: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.