Sitzung: Jeden Freitag ab 14:30 s.t. online. Falls ihr den Link haben wollt, schreibt uns.

C-Kurs/Pong

Hier soll das Spiel Pong Schritt für Schritt implementiert werden. Die Anzeige des Spielfelds soll durch ASCII-Zeichen realisiert werden, sodass das Spiel in einem Terminal laufen kann. Ein Beispiel-Screenshot könnte so aussehen:

         4                     2
+----------------------------------------+
|                                        |
|                                        |
|#                                       |
|#                                       |
|#                                       |
|            *                           |
|                                        |
|                                       #|
|                                       #|
|                                       #|
|                                        |
|                                        |
|                                        |
|                                        |
|                                        |
+----------------------------------------+

Leider ist es nicht ganz einfach, einzelne Tasten einzulesen, wie sie gebraucht werden, wenn der Benutzer den Schläger bewegen möchte. scanf arbeitet gepuffert und gibt die Eingabe erst nach Betätigung der Enter-Taste an das Programm. Wir verwenden deswegen die C-Programmbibliothek curses, die uns entsprechende Funktionen zum Abfragen eines Tastendrucks liefert. im CS-Netz ist schon alles nötige installiert. Solltest du Linux benutzen, musst du das Entwicklerpacket von libncurses installieren (unter Debian/Ubuntu libncurses-dev).

Aufgabe

curses initialisieren

Erstmal müssen wir das Headerfile für curses includen:

 #include <curses.h> 

Schreibe jetzt eine Funktion void init(), die für das Initialisieren von curses zuständig wird. Sie enthält folgende Befehle:

  • initscr(); aktiviert curses
  • noecho(); damit Buchstaben der getippten Tasten nicht im Terminal erscheinen
  • cbreak(); deaktiviert das Puffern der Terminalzeile
  • keypad(stdscr, TRUE); aktiviert spezielle Tasten, wie die Pfeiltasten
  • nodelay(stdscr, TRUE); damit die Funktion, die später die Tastendrücke entgegennimmt, nicht blockiert, bis eine Taste gedrückt wird

Jetzt folgt die main-Funktion. Sie enthält als ersten Aufruf unsere init-Funktion. Unser Programm sollte beim Beenden curses vernünftig deaktivieren, weswegen als letzter Aufruf in der main-Funktion folgender sein sollte:

 endwin(); 

Wenn die Quelltextdatei pong.c heißt, kann sie ab jetzt mit folgendem Befehl kompiliert werden:

 gcc -o pong -lcurses pong.c 

Variablen für den Zustand des Spiels

Zuerst überlegen wir uns, wie groß unser Spielfeld und die Schläger sein sollen. Diese Werte sollten als Define-Makros angegeben werden. Ein kleines, aber feines Feld bekommt man z.B. mit einer Breite von 40, einer Höhe von 15 und einer Schlägerhöhe von 3.

Jetzt führen wir die Variablen ein, die den Spielzustand repräsentieren sollen. Wir brauchen je eine y-Positionen für die beiden Schläger. Dazu kommt x- und y-Position des Balls. Diese Positionsvariablen sind vom Typ int. Damit wir wissen, in welche Richtung sich der Ball gerade bewegt, brauchen wir x- und -y-Richtung. Dafür eignet sich auch int; welche die Werte 1 und -1 annehmen können sollen, je nachdem, ob es nach rechts/links bzw. nach unten/oben gehen soll. Jetzt brauchen wir noch zwei Variablen für den Punktestand, die wir auch gleich mit 0 initialisieren können.

Die Funktion void place_paddles() soll die Schläger mittig platzieren. Die Funktion void place_ball() soll den Ball in der Mitte platzieren. Die x- und y-Richtung können wir erstmal mit 1 belegen. Beide Funktionen sollten jetzt in der main aufgerufen werden.

Ausgeben des Spielfelds

Jetzt soll die Funktion void print_field() geschrieben werden, die das komplette Spielfeld und den Punktestand anzeigen soll. Schaue dir dazu den obigen Screenshot an und überlege, wie du diesen zeilenweise aus den Positionsvariablen zusammenbauen kannst.

In der Funktion sollte zuerst clear() von curses aufgerufen werden, was den bisherigen Terminalinhalt löscht. Wichtig ist, dass du printw() von curses statt printf() verwendest. Die Art und Weise der Verwendung ist aber identisch. Zum Schluss muss noch refresh() von curses aufgerufen werden, damit das eben ausgegebene erscheint.

Teste nun dein print_field(), indem du es in main aufrufst. Sollte das Bild gleich nach Starten des Programms wieder verschwinden, baue am Ende noch ein sleep(10) ein, damit das Programm noch 10 Sekunden läuft.

Verarbeiten der Tastendrücke

Als nächstes kommt die Funktion void key_processing() dran. Darin fragen wir nun folgendermaßen einen Tastedruck ab:

int ch = getch();

Wir können das Ergebnis in ch nun z.B. mit der Konstante KEY_UP und KEY_DOWN bzw. den Buchstaben 'w' und 's' vergleichen und entsprechend die Positionsvariable der Schläger erhöhen oder veringern. Dabei sollte abgefangen werden, dass der Schläger außerhalb des Spielfelds landet. Wurde keine oder eine ungültige Taste gedrückt, können wir das hier stillschweigend ignorieren.

Zum Testen kann die Funktion zusammen mit print_field() und einer 100-Millisekunden-Pause in eine Endlosschleife gepackt werden. Für Pausen eignen sich die Funktionen sleep und usleep aus stdlib.h. Erstere nimmt Sekunden, letztere Mikrosekunden an. (1 Sekunde = 1.000 Millisekunden = 1.000.000 Mikrosekunden)

Ball bewegen

Schreibe jetzt die Funktion void move_ball(), die die Positionsvariablen des Ball entsprechend der Werte der Richtungsvaraiblen inkrementiert bzw. dekrementiert.

Teste die Funktion, indem du sie in der Test-Endlosschleife aufrufst. Der Ball fliegt jetzt erstmal ziemlich schnell aus dem Spielfeld.

Kollisionen des Balls erkennen

Die jetzt zu schreibende Funktion void detect_collision() soll Kollisionen mit der oberen/unteren Kante und den Schlägern erkennen und den Ball entsprechend reflektieren.

Das lässt sich realisieren, indem man die y-Position mit der oberen und unteren Kantenposition vergleicht und bei Kollision die y-Richtung umkehrt. Bei den Schlägern geht das ähnlich mit der x-Position; hier muss aber noch geprüft werden, ob die y-Koordinate des Schlägers mit der y-Koordinate des Balls zusammenpasst (Schlägerhöhe beachten).

Zum Testen kann die Funktion jetzt wieder in der Endlosschleife aufgerufen werden. Jetzt fliegt der Ball nur noch aus dem Spielfeld, wenn ihn kein Schläger getroffen hat.

Erkennen, wenn der Ball nicht vom Schläger getroffen wurde

Dazu schreiben wir jetzt die Funktion int detect_ball_out(). Der Rückgabewert soll angeben, für welchen Spieler es im weiteren Programmablauf den Punkt geben soll. Die Funktion muss prüfen, ob der Ball in der x-Position auf der Höhe eines Schlägers ist und für die rechte Seite 1 bzw. für die linke Seite 2 zurückgeben. Ansonsten gibt die Funktion 0 zurück.

Getestet werden kann die Funktion jetzt, indem sie als negierte Bedingung für die bisherige Endlosschleife genutzt wird.

alles in ein rundenbasiertes Spiel stecken

Wir lagern nun die bisherige Testschleife in die neue Funktion int play_round() um. Der Rückgabewert soll der Wert sein, den detect_ball_out() im Fall des Nichttreffens liefert. Bisher reagieren die Schläger noch recht langsam, weshalb wir die Pause in der Schleife auf 20 Millisekunden herabsetzen. Da nun aber der Ball viel zu schnell wird, sollte move_ball nur jeden fünften Schleifendurchlauf aufgerufen werden.

In der main-Funktion erstellen wir nun eine Schleife mit der Bedingung, dass beide Spieler noch weniger als 10 Punkte haben. In den Schleifenkörper kommt unser play_round(); danach wird abhängig von dessen Ergebnis der Punktestand von Spieler 1 bzw. Spieler 2 inkrementiert. Dann wird der Ball mit place_ball neu platziert, bevor die nächste Runde beginnt. Hier ist es sinnvoll, die x-Richtung zum Spieler zu setzen, der zuletzt den Ball nicht getroffen hat (es muss also die Richtung noch an place_ball übergeben werden).

Wenn das Spiel zu Ende ist, soll noch ausgegeben werden, welcher Spieler gewonnen hat (wieder printw() und refresh() verwenden).

Zusatzaufgabe: KI

Statt des zweiten Spielers soll nun ein Computergegner spielen. Eine einfachste Lösung wäre, in jedem Schritt den die y-Position des Balls mit der des Schlägers zu vergleichen und ihn entsprechend nach oben/unten zu bewegen. Schreibe dazu eine entsprechende Funktion ai_opponent. Nun bewegt sich der Schläger genauso schnell, wie der Ball, womit die die KI unbesiegbar ist. Führe deswegen eine Funktion can_move ein, von der abhängt, ob sich der Schläger bewegt. Die Funktion sollte mit einer gewissen Wahrscheinlichkeit wahr zurückliefern. Die Schwierigkeit besteht darin, einen ausgewogenen Gegner zu programmieren. Eine gute Idee ist es z.B., die Position des Balls mit in die Wahrscheinlichkeit einfließen zu lassen.

Zusatzaufgabe: andere Winkel des Balls

Bisher fliegt der Ball immer im 45°-Winkel. Beim originalen Pong verändert sich der Winkel jedoch bei jedem Aufschlag auf den Schläger zufällig. Man könnte jetzt die Ballpositionen und -richtungen intern als float speichern, um andere Winkel zu erreichen.

Kommentare

Wenn du Anmerkungen zur Aufgabe hast oder Lob und Kritik loswerden möchtest, ist hier die richtige Stelle dafür. Klicke einfach ganz rechts auf "bearbeiten" und schreibe deinen Kommentar direkt ins Wiki. Keine Scheu, es geht nichts kaputt ;)



Der Befehl gcc -o pong -lcurses pong.c funktioniert nicht. So funktionierts: gcc -o pong pong.c -lcurses


Nach Aufruf der init() Funktion wird bei mir nichts mehr vor dem endwin() ausgeführt. 
Ich habe keine Ahnung warum :/