Im Folgenden sollte man sich über den prinzipiellen Ablauf des Spieles klar werden. Alle bereits erstellten Methoden und auch diejenigen, die noch zu erstellen sind, werden im Rahmen des Spielablaufes, der in der Main()-Methode implementiert wird, aufgerufen.

Man muss sich also grob darüber klar werden in welchen Einzelschritten solch ein Spiel abläuft. Da gibt es sicherlich mehrere Möglichkeiten hinsichtlich der einzelnen Schritte und deren Reihenfolge.

In dem dargestellten Struktogramm ist eine dieser Möglichkeiten realisiert. Auch werden die bereits implementierten Methoden noch zu erweitern sein. Aber für einen ersten groben Überblick mag das Struktogramm ganz nützlich sein.

Struktogramm zum Spielablauf

Realisierung der Schlange

Die Schlange besteht aus einem Schlangenkopf und einer Vielzahl von Körperteilen. Zu Beginn des Spiels hat die Schlange bereits eine vorgegebene Länge. Jedes Teil der Schlange hat eine Position im Spielfeld, die durch die jeweilige Kombination aus Zeile und Spalte bestimmt ist.

Das bedeutet also, dass für den Schlangenkopf und für jedes Körperteil dessen Position zu speichern ist. Da sich die Schlange im Spielfeld bewegt, ändern sich entsprechend die Positionen dieser Schlangenelemente. Abgesehen davon, ändert sich auch die Anzahl der Körperteile der Schlange. Jedes Mal, wenn sie einen Futterhappen frisst, wird ein Körperteil am Ende der Schlange hinzugefügt.

Wie speichert man nun die Positionen (Zeile/Spalte) der einzelnen Schlangenelemente (Kopf und Körper)? Sicherlich kann man sich verschiedene Möglichkeiten der Speicherung überlegen. Die hier verwendete Möglichkeit ist eine Tabelle mit zwei Spalten für die Position des betreffenden Schlangenelementes (Zeile und Spalte) und einer ausreichenden Anzahl an Zeilen, um die wachsende Schlange zu realisieren. Da die Positionswerte immer größer Null sind, bedeutet dies, dass alle Zeilen mit den Wertepaaren 0/0 kein Schlangenelement sind.

Zu Beginn des Spiels wird die Schlangenposition vorgegeben. Somit wird die Tabelle mit Startwerten versehen. Im Beispiel rechts befindet sich der Schlangenkopf zu Beginn des Spiels an der Position Zeile = 30 und Spalte = 40. Das erste Körperteil befindet sich an der Position Zeile = 30 und Spalte = 41. Das zweite Körperteil bei 30/42 und das dritte Körperteil bei 30/43. Die Gesamtlänge der Schlange ist also 4 Körperelemente. Die verbleibenden sechs Tabelleneinträge haben die Werte 0/0. Somit kann die Schlange maximal aus 10 Körperelementen bestehen. Der erste Eintrag bezeichnet immer den Schlangenkopf.

Wir werden noch sehen, dass sich mit dieser zweispaltigen Tabelle dann auch die Bewegung der Schlange relativ leicht realisieren lässt!

Tabelle für Positionen des Schlangenkörpers

Die Anzahl der Zeilen, die ja die maximale Länge der Schlange bestimmt, wird durch eine Konstante namens maxLengthOfSnake festgelegt, so dass diese an einer zentralen Stelle im Programmcode verändert werden kann.
Das Zeichen für die Schlange, soll wie auch schon beim Spielfeldrand, durch eine Character-Konstante definiert werden. Gewählt wurde der Bezeichner snakeBodyCharacter mit dem Wert ‚S‘. Dieses Zeichen wird dann verwendet, wenn die Schlange in die aktuelle Spielsituation im Array gameArea eingetragen wird.
Beide Konstanten sind außerhalb von Main(), dort wo bereits die schon existierenden Konstanten erzeugt wurden, anzulegen. Veränderliche Informationen zur Schlange werden in Variablen innerhalb von Main() gespeichert. Dazu gehören die aktuelle Position der Schlange und die momentane Länge der Schlange.
Die momentane Länge der Schlange darf natürlich nie den Wert für die maximale Länge überschreiten. Das wird später wichtig, wenn die Schlange durch das Fressen von Futterhappen länger wird. Die momentane Länge der Schlange wird in der Variablen lengthOfSnake gespeichert. Die Länge sind immer ganzzahlige Werte, daher wird der Datentyp int verwendet.
Die Datenstruktur zur Speicherung der Schlangenposition wurde ja schon besprochen. Diese ist das zweidimensonale Array mit dem Bezeichner positionsOfSnake.

Startposition für die Schlange festlegen

Vor Beginn des Spiels muss die Startposition der Schlange festgelegt werden. Hierzu müssen die Werte in das Array positionsOfSnake eingetragen werden. Diese Aufgabe soll eine Methode mit dem Namen initializeSnakePosition() übernehmen.
Wie muss der Methodenkopf gestaltet werden? Wie schon bei den anderen Initialisierungsmethoden, ist nicht wirklich ein Rückgabewert notwendig. Daher kann der Rückgabetyp void lauten. Aber welche Informationen benötigt diese Methode, um ihre Arbeit zu erledigen?
Sie benötigt zumindest das Array positionsOfSnake, in dem ja die Position der Schlange gespeichert ist. Und sie benötigt die aktuelle Länge der Schlange, mittels lengthOfSnake. Neben im Kasten ist die Methode (unvollständig) implementiert. Natürlich kann man den Kopf und den Körper der Schlange mit festen Werten in dem Array positionsOfSnake eintragen. Bei dieser Variante soll der Kopf in
der Mitte des Bildschirms erscheinen und der Rest der Schlange soll waagerecht nach rechts gezeichnet werden. Entsprechend müssen die beiden Variablen row und column anhand der Größe des Spielfeldes initialisiert werden.
Das heißt, dass die Zeile gleich bleibt, aber die Spalte um 1 erhöht wird. Noch wird für Kopf und Körper das gleiche Zeichen verwendet. Das wird sich aber noch ändern!

Eintragen der Schlange in die aktuelle Spielsituation

Um die aktuelle Position der Schlange im Spielgeschehen darzustellen, wird diese Tabelle zeilenweise von oben nach unten durchlaufen und dann das Zeichen für die Schlange in das Array gameArea eingetragen. Wie viele Zeilen zu durchlaufen sind, wird durch die Länge der Schlange bestimmt. Diese Aufgabe soll eine Methode mit dem Bezeichner drawSnake() übernehmen!
Auch hier wieder die Überlegungen, wie der Methodenkopf auszusehen hat. In jedem Fall benötigt diese Methode zusätzliche Informationen: Sie muss die aktuellen Positionen der Schlange kennen, die in dem Array positionsOfSnake gespeichert sind. Sie muss die Länge der Schlange kennen, damit nur die betreffenden Zeilen des Arrays verwendet werden. Und Sie muss das Array kennen, in dem die Spielsituation einzutragen ist.
Um es kurz zu machen: Ein Rückgabewert ist wieder nicht unbedingt erforderlich, daher wird auch hier erneut der Datentyp void verwendet.
Noch ein Hinweis! Bei den Parametern von Methoden muss
ja deren Datentyp und ein Bezeichner angegeben werden. Der Datentyp ist immer der Datentyp derjenigen Information, die als Parameter übergeben wird. Für den Bezeichner kann man sich immer etwas ausdenken! In diesem Projekt wird allerdings durchgängig der Bezeichner der zu übergebenden Information verwendet! Dass muss aber nicht so sein!

Schlangenfutter

Nun soll an zufälligen Stellen innerhalb des Spielfeldes ein Futterhappen für die Schlange erscheinen. Aus meiner Sicht ist es sinnvoll, wie schon beim Zeichen für den Bildschirmrand oder für die Schlange, auch dieses Zeichen mittels einer globalen Konstante zu definieren. Dann muss man nur an dieser einen Stelle ein anderes Zeichen angeben, damit sich das auf das gesamte Spiel auswirkt. In meinem Projekt habe ich den Bezeichner snakeFoodCharacter mit dem Wert ‚O‘ verwendet.
Das Erzeugen eines Futterhappens für die Schlange soll ebenfalls in einer Methode erfolgen, die dann bei Bedarf aufgerufen wird. Bevor nun diese Methode entwickelt wird, muss man sich aber wieder überlegen, wie man die Position der Futterhappen speichert, um eine geeignete Datenstruktur zu verwenden.
Es liegt aber sicherlich nahe, dass man wie bereits bei der Schlange, eine Zeilen- und Spaltennummer speichern muss, bei der ein Futterhappen dann zu zeichnen ist. Wenn jeder Futterhappen den gleichen Wert haben soll, ist dies ausreichend. Um aber etwas mehr Abwechslung in das Spiel zu bringen, soll jeder Futterhappen einen anderen zufälligen Punktwert (innerhalb vorgegebener Grenzen) haben, um den die erreichten Spielpunkte des Spielers erhöht werden. Die Position (Zeile/Spalte) der Futterhappen soll ebenfalls zufällig sein. Somit sind pro Futterhappen drei Werte zu speichern: Zeilennummer, Spaltennummer und Wert.

Es wird also wieder eine Tabelle benötigt, die pro Tabellenzeile drei Werte enthält. Jede Zeile entspricht einem Futterhappen. Es wird also erneut ein zweidimensionales Array benötigt, wobei dieses Mal drei Spalten vorhanden sein sollen.
Wie auch schon bei der Schlange, ist es auch hier sinnvoll, die maximale Anzahl der Zeilen (maximale Anzahl der Futterhappen) mittels einer Konstanten festzulegen und dann nur noch mit dieser Konstanten zu arbeiten.
Des Weiteren soll in einer Variablen die Anzahl der bereits erzeugten Futterhappen gespeichert werden. Dieser Wert ändert sich ja im Verlauf des Spieles; es werden neue Futterhappen (bis zur Maximalanzahl) erzeugt und es werden auch Futterhappen von der Schlange gefressen, die dann aus dem Array gelöscht werden müssen.

Tabelle für Position der Futterhappen und deren Wert

Für die maximale Anzahl der Futterhappen soll eine Konstante mit der folgenden Bezeichnung verwendet werden: maxAmountOfSnakeFood. Und die Variable für die aktuell vorhandene Anzahl an Futterhappen soll den Bezeichner amountOfSnakeFood haben. Die Konstante maxAmountOfSnakeFood wird wiederum vor der Main()-Methode bei den anderen Konstanten definiert. Die Variable amountOfSnakeFood wird innerhalb der Main-Methode (zu Beginn) definiert.
Die Datenstruktur zur Speicherung der Futterhappen hat den Bezeichner positionsOfSnakeFood und wird wie neben dargestellt erzeugt.

Erzeugen der Futterhappen

Auch dieser Vorgang wird wieder in einer Methode definiert, die bei Bedarf aufgerufen wird. Diese Methode soll den Bezeichner createSnakeFood erhalten. Auch jetzt muss man sich wieder überlegen, wie der Methodenkopf aussehen soll, d.h. welche Informationen benötigt die Methode und soll sie einen Ergebniswert zurückliefern.
Okay…da ja diese Methode die soeben besprochene Datenstruktur verändern soll, muss zumindest dieses zweidimensionale Array positionsOfSnakeFood übermittelt werden. Dann kann die Methode dort einen neuen Futterhappen dort eintragen. Diese Methode darf aber nur dann einen neuen Futterhappen erzeugen, wenn die maximale Anzahl der Futterhappen noch nicht erreicht wurde. Also soll die Methode keinen neuen Futterhappen erzeugen, wenn diese maximale Anzahl bereits erreicht wurde.

Man kann dies auch anders realisieren! Zum Beispiel, dass man die Methode nur dann aufruft, wenn folgende Überprüfung wahr ist:

if (amountOfSnakeFood < maxAmountOfSnakeFood)
    createSnakeFood(…);

Aber so überprüft die Erzeugungsmethode selbst, ob dies zutrifft und somit muss die Methode createSnakeFood() die momentane Anzahl der Futterhappen kennen!
Was muss die Methode noch wissen? Futterhappen dürfen nicht an Stellen erzeugt werden, die gerade durch den Schlangenkörper belegt sind! Also muss diese Methode wissen, wo sich momentan die Schlange befindet (steht in positionsOfSnake) und wie lange die Schlange momentan ist (steht in lengthOfSnake). Das Gleiche gilt auch für bereits existierende Futterhappen.

Die Position und der Wert eines Futterhappens soll ja „zufällig“ sein. Hierzu gibt es in Programmiersprachen fertige kleine „Maschinen“, die Zufallswerte erzeugen können. Man erstellt eine solche Maschine wie neben dargestellt:

Random randomNumbersGenerator = new Random();

Der selbstgewählte Name der Maschine lautet randomNumbersGenerator. Da in diesem Projekt noch an anderen Stellen eine solche „Maschine“ benötigt wird, erstellen wir einmal diese Maschine in der Main()-Methode und nutzen diese an allen Stellen, wo man sie benötigt.
Somit benötigt unsere Methode createSnakeFood() insgesamt fünf Informationen:

  1. den Zufallszahlengenerator
  2. die Position der Schlange
  3. die Länge der Schlange
  4. die Position der bereits vorhandenen Futterhappen
  5. die Anzahl der bereits vorhandenen Futterhappen

Uff…das war jetzt eine ganze Menge an Information!
Einmal sacken lassen 🙂

Jetz soll noch überlegt werden, ob ein Rückgabewert erforderlich ist. Ja, in diesem Fall soll es einen Rückgabewert geben, nämlich die neue Anzahl von Futterhappen. Diese Information ist ja eine Ganzzahl, also ein Integer-Wert. Somit sieht der Methodenkopf wie neben dargestellt aus:

Methodenkopf

Wir haben also in unserer Methode Zugriff auf all diese Informationen und teilen dem Aufrufer am Ende der Methode die (neue) aktuelle Anzahl von Futterhappen mit.
Jetzt geht es an die Definition dieser Methode! Wie sieht deren Ablauf wohl aus?

  1. Direkt zu Beginn der Methode wird überprüft, ob bereits die maximale Anzahl von Futterhappen erreicht wurde. Falls ja, wird die Methode beendet. Hierzu wird die aktuelle Anzahl von Futterhappen zurückgeliefert.
  2. Es werden die für einen neuen Futterhappen benötigten Zahlen (Zeile, Spalte, Wert) per Zufallszahlengenerator gezogen.
  3. Nun wird überprüft, ob an dieser Stelle bereits ein Futterhappen existiert. Falls ja, soll die Methode ohne Erzeugung eines neuen Futterhappens wie zuvor enden
  4. Anschließend wird überprüft, ob sich an dieser Stelle ein Körperteil der Schlange befindet. Falls ja, wird wie auch zuvor die Methode ohne Erzeugung eines Futterhappens beendet.
  5. Jetzt kann der neu erstellte Futterhappen in dem Array positionsOfSnakeFood an der ersten freien Stelle eingetragen werden. Dann wird die aktuelle Anzahl von Futterhappen um Eins erhöht. Anschließend endet die Methode und liefert diese aktuelle Anzahl Futterhappen zurück.

Okay…hier folgt nun eine Beispiel-Implementation dieser Methode:

Eintragen der Futterhappen in das Array gameArea

Die Positionen und Werte aller Futterhappen befinden sich in dem zweidimensionalen Array mit dem Bezeichner positionsOfSnakeFood. Damit die Futterhappen dann auch tatsächlich angezeigt werden, muss anhand der Positionsangaben in dem Array gameArea nun das festgelegte Zeichen für einen Futterhappen gespeichert werden, damit diese bei Ausführung der Methode drawScreen() auch auf dem Bildschirm erscheinen.
Diese Methode soll den Bezeichner drawSnakeFood() erhalten. Wie schon zuvor ist zu überlegen, welche Informationen diese Methode in Form von Parametern benötigt und welcher Rückgabetyp nötig ist.
Im Wesentlichen benötigt diese Methode natürlich Informationen zu den Futterhappen. Konkret ist dies das Array mit den Positionsangaben positionsOfSnakeFood und den aktuellen Wert der Variable amountOfSnakeFood, also die aktuelle Anzahl an Futterhappen. Ebenso muss diese Methode auf das Array gameArea Zugriff haben, um das Zeichen für einen Futterhapppen dort eintragen zu können.

Da hier kein Ergebniswert zustande kommt, soll wieder der Rückgabetyp void zum Zuge kommen. Somit sieht der Methodenkopf wie neben dargestellt aus:

Was muss nun innerhalb der Methode passieren? Anhand der Anzahl der Futterhappen wird für jeden Futterhappen dessen Zeilen- und Spaltennummer ermittelt und dann genau an dieser Position im Array gameArea das per Konstante snakeFoodCharacter festgelegte Zeichen eingetragen. Dieser Vorgang lässt sich per for-Schleife (da ja die Anzahl der Futterhappen bekannt ist!) erledigen.
Wie dies zu programmieren ist, dürfen Sie sich überlegen 🙂 …oder im Sourcecode am Ende der Seite nachschauen.

Anpassen der Main()-Methode

Für den momentanen Stand des Projekts muss jetzt noch die Main()-Methode des Programms soweit ergänzt werden, dass das Spielfeld inklusive der Startposition der Schlange gezeichnet wird und zumindest in einer offenen Schleife permanent neue Futterhappen erzeugt werden, solange das Spiel aktiv ist.
Ob das Spiel aktiv oder nicht mehr aktiv ist, soll durch eine boolsche Variable mitgeteilt werden. Diese Variable hat einen Bezeichner, der als Frage formuliert ist, nämlich isGameActive. Diese Variable ist innerhalb von Main deklariert und erhält den Wert true. Die zuvor erwähnte und im Struktogramm dargestellte Schleife (eine do…while-Schleife) überprüft den Wert und solange dieser Wert true ist, wird die Schleife für den Spielablauf erneut durchlaufen.
Diese Variable sollte irgendwo zu Beginn der Main()-Methode, dort wo auch alle anderen Variablen bereits definiert sind, mit dem Wert false erzeugt werden. Nach den Aufrufen der verschiedenen Initialisierungsmethoden kommt nun die do…while-Schleife, in deren Schleifenrumpf nun der eigentlich Spielablauf stattfindet.
Und direkt vor der do…while-Schleife wird die Variable isGameActive auf true gesetzt. Nachfolgend ist die momentane Version der Main()-Methode dargestellt:

Momentaner Stand der Main()-Methode

Eine Anweisung zu Beginn der do…while-Schleife ist noch neu!
Die Anweisung Thread.Sleep(100); bewirkt, dass das Programm für 100 ms nichts macht, sich quasi „schlafen legt“. Dies wird gemacht, damit der Spielablauf etwas ausgebremst wird. Der Parameter in den runden Klammern gibt also die Zeit in Millisekunden an, die das Programm pausieren soll. Natürlich kann man den Wert vergrößern, dann läuft das Spielgeschehen langsamer oder den Wert verringern, dann läuft es eben schneller, oder ganz weglassen 🙂
Damit diese „Maschine“, die das Pausieren macht, auch gefunden wird, muss zu Beginn des Programms der Namensbereich System.Threading eingebunden werden.

Diese Anweisung ganz am Anfang der Datei, in der sich das Programm befindet (Program.cs) lautet also:

Mein Arbeitsblatt zum Thema:

Der momentane Stand des Projekts (Projekt erstellt mit Visual Studio 2015):

Weiter zu Snake – Teil 3: Die Schlange bewegt sich

Zurück zu Snake – Teil 1: Erzeugung des Spielfeldes