Aufgabenstellung: Qix

Dieses Mal ist ein Klon des 80er-Jahre Arcade-Spiels "Qix" von Euch zu erstellen. Zur Veranschaulichung stellen wir Euch auch ein Beispielprogramm zur Verfügung - bindend ist aber in jedem Fall die Aufgabenstellung!

Auf einem quadratischen und anfänglich leeren Spielfeld muß der Spieler dabei 75% der Fläche durch Abtrennen einzelner Bereiche für sich beanspruchen. Dabei wird er von drei verschiedenen Arten von Gegnern verfolgt und dabei behindert, sein Ziel zu erreichen.


Die Regeln im Einzelnen

Das Programm startet mit einem leeren Spielfeld, das lediglich einen komplett um die Fläche laufenden Rand sowie den Spieler (grün) und den Hauptgegner - Qix (rot) - enthält. Der Spieler befindet sich auf einer zufälligen Position auf dem Rand, Qix auf einer zufälligen Position im Spielfeld.

Der Spieler kann sich mit den Pfeiltasten auf dem Rand entlangbewegen. Möchte er in das Spielfeld vorstoßen und eine neue Linie (einen sog. Stix) beginnen, so muß er zusätzlich zur Pfeiltaste die ganze Zeit über x oder y gedrückt halten. x sorgt dabei für eine schnellere Bewegung des Spielers und somit für ein geringeres Risiko, von einem der Gegner getroffen zu werden. Eine mit x abgeschlossene Fläche gibt aber auch weniger Punkte. Benutzt der Spieler hingegen y, um eine langsamere Bewegung zu erreichen, erhöht er sein Risiko, bekommt dafür aber auch mehr Punkte. Solange ein Stix noch nicht abgeschlossen ist (der Stix also nicht wieder an einer schon fertigen Linie ankam), kann der Spieler immer von langsam (y) zu schnell (x) wechseln - aber nie andersherum! Ein einmal schnell gezeichneter Stix (egal, ob am Anfang, mittendrin oder am Ende) bringt also immer nur so viele Punkte, als wäre er die gesamte Zeit über schnell gezeichnet worden.

Trifft ein Stix auf eine schon fertige Linie (wozu auch der Rand zählt), so wird die eingegrenzte Fläche farbig markiert. Eine schnell gezeichnete Fläche soll dabei eine andere Farbe haben als eine langsam gezeichnete (s. Einstellungen). Ebenso soll ein Stix andersfarbig dargestellt werden als eine fertige Linie. Ein Stix darf sich selbst nie berühren! Es muß also immer mindestens ein "Pixel" Abstand zwischen verschiedenen Teilen des Stix sein. Dasselbe gilt für den Abstand zu schon fertigen Linien und zum Rand: Wenn sich der Stix soweit annähert, daß er sich direkt neben einer fertigen Linie oder dem Rand befindet, dann wird er automatisch auf die benachbarte Linien-/Randposition "gezogen" und die Fläche wird somit abgeschlossen. Es kann somit auch nie zwei Linien oder eine Linie und den Stix geben, die direkt nebeneinander parallel verlaufen.

Sobald eine Fläche abgeschlossen wird, erfolgt eine Berechnung, wie viel Prozent der Fläche der Spieler schon für sich gewonnen hat. Fertige Linien zählen dabei ebenfalls für den Spieler, der Rand allerdings wird nicht mit gewertet (weder für, noch gegen den Spieler). Weiterhin werden beim Beenden eines Stix Punkte vergeben. Dabei gibt eine langsam gezeichnete Fläche wie oben geschrieben mehr Punkte als eine schnell gezeichnete. Zudem soll auch eine große Fläche mehr Punkte bringen als zwei kleine, gleich große Einzelflächen. Die erreichenten Prozente, die Punkte und die restlichen Leben des Spielers sollen als Textausgabe sichtbar sein.

Qix wird als einfacher Kreis dargestellt. Er bewegt sich anhand eines mehr oder weniger komplexen Ablaufes über die noch freie Fläche des Spielfeldes. Der Qix im Beispielprogramm sucht sich z.B. eine noch freie Stelle des Spielfeldes, die er auf geradem Weg erreichen kann und bewegt sich dann dorthin, wobei er am Anfang und Ende dieser Strecke langsamer als in der Mitte ist. Ihr könnt den genauen Algorithmus für Qix aber gerne auch selbst und anders festlegen!
Sobald Qix einen Stix berührt, verliert der Spieler ein Leben, der Stix verfällt (wird also entfernt) und der Spieler wird an einer zufälligen Stelle auf dem Spielfeldrand neu platziert.

Auf dem Rand und auf fertigen Linien (nicht aber auf Stix) bewegen sich auch die zweiten Gegner des Spielers - die Sparks. Ihre genaue Anzahl läßt sich in den Einstellungen (s.u.) festlegen, rein "äußerlich" werden sie als sehr kleiner Kreis dargestellt - was dann in Delphi zu einem rautenartigen Objekt führt. Dieses soll außen eine andere Farbe als innen haben. Sparks werden beim Spielstart wie der Spieler auf zufälligen Positionen auf dem Rand platziert, wobei darauf zu achten ist, daß sie nicht zu nahe am Spieler starten und diesem somit keine Chance zum Ausweichen lassen. Selbiges gilt auch, wenn der Spieler ein Leben verliert und neu auf dem Rand platziert werden muß. Auch dann darf seine neue Position nicht zu dicht an einem der Sparks sein.
Sparks bewegen sich solange geradeaus auf dem Rand oder einer fertigen Linie, bis sie auf eine Verzweigung stoßen. Dort wird dann per Zufall eine der möglichen neuen Richtungen bestimmt und der Sparks bewegt sich dorthin weiter. Er wird an einer Verzweigung aber nie den Weg wählen, aus dem er gerade kam! Berührt ein Spark den Spieler, verliert dieser ein Leben und wird neu auf dem Rand platziert. Sparks können sich gegenseitig durchdringen.

Der dritte Gegner des Spielers - der Fuse - bewegt sich ausschließlich auf dem Stix. Er ist normalerweise nicht sichtbar und taucht erst auf, wenn der Spieler einen Stix begonnen hat, dann aber für mehr als 2 Sekunden stillsteht. Nach 2 Sekunden entsteht der Fuse am Beginn des Stix, wartet dort noch 1 Sekunde und läuft dann den Stix entlang. Trifft er auf den Spieler, verliert dieser ein Leben und wird neu am Rand platziert, der Stix verfällt und Fuse verschwindet wieder. Setzt der Spieler den Stix fort, verschwindet der Fuse ebenfalls sofort und erscheint ggf. nach den genannten 2 Sekunden Stillstand wieder am Anfang des Stix.

Sonstige Anforderungen

Mit der Taste p läßt sich das Spiel pausieren und wieder fortsetzen. Sämtliche Bewegungen der Gegner werden während der Pause eingefroren und auch der Spieler kann sich solange nicht vom Fleck rühren. Achtet insbesondere beim Fuse darauf, daß eine Pause die für den Fuse verstrichene Zeit nicht verändern darf!

Der Spieler soll über zwei Unterformulare die Möglichkeit haben, verschiedene Einstellungen vor oder während dem/des Spiels vorzunehmen. Dabei soll es möglich sein, folgendes zu beeinflussen:

  • den Namen des Spielers
  • die Zugweite in "Pixeln" für schnell und langsam erstellte Stix
  • die Anzahl der Sparks
  • die Geschwindigkeit des Fuse und der Sparks (identisch für beide)
  • die Anzahl Leben des Spielers

  • die Farbe des Spielers
  • die Farbe des Qix
  • die beiden Farben von Fuse und Sparks
  • die Hintergrundfarbe
  • die Farbe für einen Stix
  • die Farbe für fertige Linien und den Rand
  • die Farbe für langsame und schnelle Flächen

Bei beiden Einstellungsgruppen sind Defaulteinstellungen vorzusehen, die auch jederzeit während des Spiels wiederhergestellt werden können. Änderungen an den Einstellungen wirken sich sofort auf das Spiel aus (bereits fertige Flächen werden also z.B. neu eingefärbt, die Zahl der Sparks ändert sich etc.). Die Einstellungen werden in zwei getrennten typisierten Dateien abgelegt.

Der Spieler kann den aktuellen Spielstand jederzeit speichern und wieder laden. Ein bereits laufendes Spiel wird für das Speichern kurz pausiert und danach direkt fortgesetzt. Lädt der Spieler während eines laufenden Spiels einen Spielstand, so verfällt der bisherige Spielstand und der Spieler kann nach dem Laden direkt mit dem gespeicherten Spiel weitermachen. Das genaue Format für die Spielstandsdatei sollt Ihr Euch selbst überlegen. Es müssen aber mindestens folgende Werte in so einer Datei abgelegt und nach dem Einlesen auch im dann fortgesetzten Spiel benutzt werden:

  • Breite / Höhe des Spielfeldes (dieser Wert kann zwar vom Spieler nicht verändert werden; es soll aber trotzdem im Quellcode eine Konstante für die Breite / Höhe geben, die im gesamten Programm benutzt wird und die vom einem Programmierer mit Zugang zum Quellcode [also uns ;-)] geändert werden kann! Falls eine Datei geladen wird, deren Wert für Breite / Höhe NICHT der aktuellen Einstellung der Konstante entspricht, soll das Laden mit einer passenden Meldung abgebrochen werden. Das Spielfeld soll dabei mindestens 500x500 Pixel groß sein)
  • die Spielfelddaten selbst (also wo ist eine Linie / der Rand, wo ist ein Stix, wo ist eine schnelle / langsame Fläche, wo ist eine leere Fläche?)
  • die Position des Spielers
  • die Position, die Bewegungsrichtung und ggf. die Bewegungsgeschwindigkeit des Qix
  • die Position des Fuse
  • die Positonen der Sparks
  • der Punktestand
  • die restlichen Leben
  • ob das Spiel gerade pausiert ist oder nicht
  • ob der Spieler gerade einen schnell oder einen langsamen Stix (oder gar keinen) zeichnet

Achtet bei der Arbeit mit den Dateien auf einen sauberen Umgang mit schreibgeschützten Dateien, mit dem Überschreiben von Dateien, mit dem Öffnen von Dateien im falschen Format etc. Benutzt try..except und try..finally, um Euren Code abzusichern.

Wenn ein Spiel beendet wurde, weil der Spieler mehr als 75% der Fläche für sich gewonnen oder alle Leben verloren hat, wird eine Highscoreliste angezeigt, in die der Spieler mit seinem Namen und dem Punktestand automatisch eingetragen wird, wenn er ausreichend Punkte erlangt hat. Die Highscores werden in einer weiteren Datei abgelegt (insgesamt gibt es also 4 verschiedene Dateitypen: Einstellungen, Farbeinstellungen, Spielstand und Highscores). Auch für die Highscores sind Defaulteinträge mit Punkten und Namen vorzusehen.

Der Spieler kann jederzeit ein neues Spiel starten, ohne das Programm vorher zu beenden. Außerdem soll es eine Hilfeausgabe geben, in der die Spielregeln und die Tastenbelegungen erläutert werden. Die Verwendung von Sounds wie im Beispielprogramm ist nicht gefordert, aber natürlich auch nicht verboten!

 

Hilfe

Zur Auswahl einer Farbe bietet sich der ColorDialog an. Er erlaubt auch das Zusammenstellen und Auswählen einer ganz eigenen Farbe (zusätzlich zu den vordefinierten). Farben vom Typ TColor sind in Delphi wie folgt aufgebaut: Am Anfang steht immer ein $ und es folgen dann 3x zwei Hex-Zeichen für die Kanäle B, G und R (in dieser Reihenfolge). $0000FF ist also rot, $FF00FF magenta.

Zur Steuerung der Gegner ist der Timer sehr hilfreich. In seiner Eigenschaft interval kann man in Millisekunden einstellen, wie oft das Event onTimer ausgeführt werden soll.

Normalerweise werden in typisierten Dateien mehrere Datensätze eines Typs abgelegt. Bei der Datei für den Spielstand gibt es hingegen zwei große Bereiche: Die grundlegenden Werte und die Spielfelddaten selbst, wobei letzte durch die änderbare Breite / Höhe verschieden groß sein können. Hier gibt es mehrere Ansätze, um trotzdem alle Daten in einer Datei ablegen zu können. Eine besteht z.B. darin, die "Grunddaten" ganz normal in einem Record abzulegen und diesen in die Datei zu schreiben, sie dann zu schließen, als file of byte neu zu öffnen, mit seek(myfile, filesize(myfile)) zum Ende zu springen und die Daten (als byte!) in einer Schleife anzuhängen. Das Einlesen geschieht dann ähnlich, nur daß hier ein seek(myfile, sizeof(recordtype)) erforderlich ist.

Für das Füllen einer beliebig geformten Fläche bietet sich der FloodFill-Algorithmus an. Diesen solltet Ihr in der iterativen Variante implementieren, da es bei der geforderten Spielfeldgröße sonst sehr schnell zu Stacküberläufen kommt. Für die iterative Variante braucht Ihr einen einfachen Stapelspeicher zur Verwaltung von 2D-Positionen, der sich gut in einer separaten Unit erstellen läßt.

Das Spielfeld läßt sich gut in einem Image oder einer PaintBox darstellen. Achtet darauf, daß Ihr möglichst immer nur so wenige Pixel wie unbedingt erforderlich neu zeichnet, weil es sonst schnell flackert oder das Programm langsamer wird (wenn Qix sich weiterbewegt reicht es z.B., die Pixel an seiner vorherigen Position neu zu zeichnen, es ist kein Refresh des gesamten Images erforderlich!). Im onCreate-Ereignis des Formulars könnt Ihr mit FrmMain.DoubleBuffered := true schon einen Großteil des Flackerns abstellen.

Eine zufällig Zahl bekommt Ihr in Delphi mit der Funktion random. Zuvor muß einmal die Zufallszahlenberechnung mit der Funktion randomize gestartet werden.

Wie immer sind natürlich die generellen Richtlinien für die Seminaraufgabe Object-Pascal zu beachten. 

Der letzte Abgabetermin für Qix ist der 13.7.2012. Der letzte Termin für die Abgabe inklusiv einmaliger Nachbesserung (siehe Richtlinien) ist der 15.6.2012. 

Viel Erfolg!