C-Kurs/Pong: Unterschied zwischen den Versionen
(→Kommentare) |
|||
(20 dazwischenliegende Versionen von 5 Benutzern werden nicht angezeigt) | |||
Zeile 1: | Zeile 1: | ||
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: | 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: | ||
− | <pre> | + | <pre> 4 2 |
+----------------------------------------+ | +----------------------------------------+ | ||
| | | | | | ||
Zeile 19: | Zeile 19: | ||
+----------------------------------------+</pre> | +----------------------------------------+</pre> | ||
− | Leider ist es nicht ganz einfach, einzelne Tasten einzulesen, wie sie gebraucht werden, wenn der Benutzer den Schläger bewegen möchte. | + | Leider ist es nicht ganz einfach, einzelne Tasten einzulesen, wie sie gebraucht werden, wenn der Benutzer den Schläger bewegen möchte. <tt>scanf</tt> 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 == | == Aufgabe == | ||
Zeile 27: | Zeile 27: | ||
Schreibe jetzt eine Funktion ''void init()'', die für das Initialisieren von curses zuständig wird. Sie enthält folgende Befehle: | Schreibe jetzt eine Funktion ''void init()'', die für das Initialisieren von curses zuständig wird. Sie enthält folgende Befehle: | ||
− | * < | + | * <tt>initscr();</tt> aktiviert curses |
− | * < | + | * <tt>noecho();</tt> damit Buchstaben der getippten Tasten nicht im Terminal erscheinen |
− | * < | + | * <tt>cbreak();</tt> deaktiviert das Puffern der Terminalzeile |
− | * < | + | * <tt>keypad(stdscr, TRUE);</tt> aktiviert spezielle Tasten, wie die Pfeiltasten |
− | * < | + | * <tt>nodelay(stdscr, TRUE);</tt> damit die Funktion, die später die Tastendrücke entgegennimmt, nicht blockiert, bis eine Taste gedrückt wird |
− | Jetzt folgt die | + | Jetzt folgt die <tt>main</tt>-Funktion. Sie enthält als ersten Aufruf unsere <tt>init</tt>-Funktion. Unser Programm sollte beim Beenden curses vernünftig deaktivieren, weswegen als letzter Aufruf in der <tt>main</tt>-Funktion folgender sein sollte: |
<pre> endwin(); </pre> | <pre> endwin(); </pre> | ||
− | Wenn die | + | Wenn die Quelltextdatei ''pong.c'' heißt, kann sie ab jetzt mit folgendem Befehl kompiliert werden: |
− | <pre> gcc -lcurses pong.c </pre> | + | <pre> gcc -o pong -lcurses pong.c </pre> |
=== Variablen für den Zustand des Spiels === | === 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 | + | 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 | + | 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 <tt>int</tt>. Damit wir wissen, in welche Richtung sich der Ball gerade bewegt, brauchen wir x- und -y-Richtung. Dafür eignet sich auch <tt>int</tt>; 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 | + | Die Funktion <tt>void place_paddles()</tt> soll die Schläger mittig platzieren. Die Funktion <tt>void place_ball()</tt> 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 <tt>main</tt> aufgerufen werden. |
=== Ausgeben des Spielfelds === | === Ausgeben des Spielfelds === | ||
− | Jetzt soll die Funktion | + | Jetzt soll die Funktion <tt>void print_field()</tt> 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 | + | In der Funktion sollte zuerst <tt>clear()</tt> von curses aufgerufen werden, was den bisherigen Terminalinhalt löscht. Wichtig ist, dass du <tt>printw()</tt> von curses statt <tt>printf()</tt> verwendest. Die Art und Weise der Verwendung ist aber identisch. Zum Schluss muss noch <tt>refresh()</tt> von curses aufgerufen werden, damit das eben ausgegebene erscheint. |
− | Teste nun dein | + | Teste nun dein <tt>print_field()</tt>, indem du es in <tt>main</tt> aufrufst. Sollte das Bild gleich nach Starten des Programms wieder verschwinden, baue am Ende noch ein <tt>sleep(10)</tt> ein, damit das Programm noch 10 Sekunden läuft. |
=== Verarbeiten der Tastendrücke === | === Verarbeiten der Tastendrücke === | ||
+ | Als nächstes kommt die Funktion <tt>void key_processing()</tt> dran. Darin fragen wir nun folgendermaßen einen Tastedruck ab: | ||
+ | <pre>int ch = getch();</pre> | ||
+ | Wir können das Ergebnis in ''ch'' nun z.B. mit der Konstante <tt>KEY_UP</tt> und <tt>KEY_DOWN</tt> bzw. den Buchstaben <tt>'w'</tt> und <tt>'s'</tt> 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 <tt>print_field()</tt> und einer 100-Millisekunden-Pause in eine Endlosschleife gepackt werden. Für Pausen eignen sich die Funktionen <tt>sleep</tt> und <tt>usleep</tt> aus ''stdlib.h''. Erstere nimmt Sekunden, letztere Mikrosekunden an. (1 Sekunde = 1.000 Millisekunden = 1.000.000 Mikrosekunden) | ||
+ | |||
+ | === Ball bewegen === | ||
+ | Schreibe jetzt die Funktion <tt>void move_ball()</tt>, 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 === | === Kollisionen des Balls erkennen === | ||
+ | Die jetzt zu schreibende Funktion <tt>void detect_collision()</tt> 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 === | === Erkennen, wenn der Ball nicht vom Schläger getroffen wurde === | ||
+ | Dazu schreiben wir jetzt die Funktion <tt>int detect_ball_out()</tt>. 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 <tt>1</tt> bzw. für die linke Seite <tt>2</tt> zurückgeben. Ansonsten gibt die Funktion <tt>0</tt> 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 === | === alles in ein rundenbasiertes Spiel stecken === | ||
+ | Wir lagern nun die bisherige Testschleife in die neue Funktion <tt>int play_round()</tt> um. Der Rückgabewert soll der Wert sein, den <tt>detect_ball_out()</tt> 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 <tt>move_ball</tt> nur jeden fünften Schleifendurchlauf aufgerufen werden. | ||
+ | |||
+ | In der <tt>main</tt>-Funktion erstellen wir nun eine Schleife mit der Bedingung, dass beide Spieler noch weniger als 10 Punkte haben. In den Schleifenkörper kommt unser <tt>play_round()</tt>; danach wird abhängig von dessen Ergebnis der Punktestand von Spieler 1 bzw. Spieler 2 inkrementiert. Dann wird der Ball mit <tt>place_ball</tt> 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 <tt>place_ball</tt> übergeben werden). | ||
+ | |||
+ | Wenn das Spiel zu Ende ist, soll noch ausgegeben werden, welcher Spieler gewonnen hat (wieder <tt>printw()</tt> und <tt>refresh()</tt> 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 <tt>ai_opponent</tt>. Nun bewegt sich der Schläger genauso schnell, wie der Ball, womit die die KI unbesiegbar ist. Führe deswegen eine Funktion <tt>can_move</tt> 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 <tt>float</tt> speichern, um andere Winkel zu erreichen. | ||
== Kommentare == | == Kommentare == | ||
Zeile 71: | Zeile 96: | ||
Als kleine Starthilfe folgt ein Beispiel, wie so ein Kommentar formatiert sein könnte. Mit "Vorschau zeigen" kannst du dir ansehen, was deine Änderung bewirken würde, ohne wirklich etwas zu ändern. | Als kleine Starthilfe folgt ein Beispiel, wie so ein Kommentar formatiert sein könnte. Mit "Vorschau zeigen" kannst du dir ansehen, was deine Änderung bewirken würde, ohne wirklich etwas zu ändern. | ||
Du musst übrigens außerhalb dieses auskommentieren Bereichs schreiben ;) | Du musst übrigens außerhalb dieses auskommentieren Bereichs schreiben ;) | ||
+ | --> | ||
+ | |||
+ | ---- | ||
+ | |||
+ | 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 :/ |
Aktuelle Version vom 19. September 2014, 12:54 Uhr
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).
Inhaltsverzeichnis
- 1 Aufgabe
- 1.1 curses initialisieren
- 1.2 Variablen für den Zustand des Spiels
- 1.3 Ausgeben des Spielfelds
- 1.4 Verarbeiten der Tastendrücke
- 1.5 Ball bewegen
- 1.6 Kollisionen des Balls erkennen
- 1.7 Erkennen, wenn der Ball nicht vom Schläger getroffen wurde
- 1.8 alles in ein rundenbasiertes Spiel stecken
- 1.9 Zusatzaufgabe: KI
- 1.10 Zusatzaufgabe: andere Winkel des Balls
- 2 Kommentare
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 :/