• 14Minuten
blank

Snake ist ein sehr altes und heute noch beliebtes Spiel. Um es umzusetzen, gibt es viele Möglichkeiten. Natürlich ist es auch kein Problem, es in GameMaker Studio 2 zu realisieren. Wie das geht, zeigt dieses Tutorial.

Zielgruppe des Tutorials

Der Code ist vergleichsweise umfangreich und etwas anspruchsvoller. Anfänger können sich gerne daran versuchen, aber die Zielgruppe liegt eindeutig bei erfahreneren Programmieren, die sich bereits in GameMaker auskennen. Ich werde also nicht mehr, wie in vielen anderen Tutorials dieser Seite, auf jede Kleinigkeit eingehen.

Was ist Snake?

Wer wissen will, was Snake ist oder einfach gleich spielen möchte, kann sich an der Version austoben, die ich auf itch.io hochgeladen habe. Und wer sich für den Zweispielermodus interessiert, aber nur alleine spielen kann, darf sich gerne folgendes Video anschauen.

Und ja, Kenner haben es sicher gleich erkannt: Es ist die QBasic-Version von 1990/1991 bzw. mein Nachbau in GameMaker Studio. Das Spiel hieß damals eigentlich Nibbles.

Bei Snake geht es darum, dass ein oder zwei Schlangen durch mehrere Level kriechen und Früchte, im Spiel als Zahlen zwischen 1 und 9 dargestellt, einsammeln. Die Schlangen werden dabei immer länger. Eine Schlange darf nicht mit sich selbst, einer anderen Schlange oder den Wänden kollidieren. Tut sie das, stirbt sie und das Level beginnt von vorne. Am Anfang können wir die Anzahl Spieler und die Geschwindigkeit bestimmen. Für das ganze Spiel hat jede Schlange fünf Leben. Wenn eine Schlange kein Leben mehr hat, ist das Spiel vorbei.

In meiner Version habe ich auch die 10 Levels des Originals nachgebaut. Wer in Level 10 immer noch lebt, muss dieses so oft wiederholen, bis alle Leben verbraucht wurden.

Abweichungen

Es ist kein 100%iger Nachbau. Ich weiche hier und da bewusst ab. Im Tutorial geht es ohnehin nur um die Spielmechanik. Das Menü am Anfang und ein paar der Effekte, die zu sehen sind, kommen hier und in der Downloadversion (siehe ganz unten) nicht vor. Abweichungen gibt es in folgenden Punkten:

  • der Text am Anfang des Menüs ist etwas anders
  • die roten Sterne drehen sich mit, nicht gegen den Uhrzeigersinn und umfassen den ganzen Bildschirmrand
  • die Geschwindigkeit lässt sich nur von 1 bis 10, nicht von 1 bis 100 einstellen
  • es gibt keinen Monochrom-Modus
  • alle Textfenster wurden etwas größer gestaltet
  • im Original gibt es keinen Erdbeben-Effekt, wenn eine Schlange stirbt
  • wenn man stirbt verschwinden bei mir nicht nur die Schlangen, sondern auch die Wände
  • die Wände färben sich immer, wenn sie von einer Schlange berührt werden
  • die Schlangen starten in den Leveln anders als im Original
  • dem ganzen Spiel liegt ein 32×32 Pixel-Raster zur Grunde, das Original hat ein 80×25 Zeichen im EGA-Textmodus.

Außerdem gibt es kleinere und größere Unterschiede in der Code-Logik. Es ist also nicht eine simple QBasic zu GML Portierung. Das hat u. a. den Grund, dass eine 1:1 Portierung der Logik für Anfänger zu schwierig wäre. Wahrscheinlich auch für Gelegenheitsprogrammierer.

Die Farben, Sounds, Steuerung und Levels sind aber identisch und es kommt wirklich das Gefühl des alten Nibbles auf.

Logik der Spielmechanik

Es ist immer ratsam, sich erst einmal Gedanken zu machen, bevor man beginnt, Code zu schreiben. Im Prinzip läuft das Spiel so ab, dass sich die Schlange innerhalb einer festen Welt auf eine bestimmte Art bewegt. Die Welt ist immer gleich groß. Die Schlange hat einen Kopf, mit dem sie fressen und kollidieren kann, und einen Körper, der ihr schrittweise folgt und länger wird, wenn sie frisst.

Wer genau hinschaut, stellt fest, dass die Schlange sich nicht pixelweise bewegt. Sie geht in 32-Pixel-Schritten voran. Das führt zur richtigen Annahme, dass das Spiel mit einem Raster arbeitet.

Der ganze Bildschirm bzw. Raum hat 1920×1056 Pixel. Die oberen 32 Pixel sind frei für die GUI. Das heißt, wir haben 1920×1024 Pixel für das eigentliche Level. Das macht, bei einem Raster mit 32×32 Pixeln, 60×32 Felder. Oder anders gesagt: Das Ganze lässt sich in ein 60×32 2D Array unterbringen. Und ja, da ich kürzlich noch Loblieder über das 1D-Array schrieb, habe ich auch damit begonnen und irgendwann festgestellt, dass den Code wohl kaum jemand verstehen wird. Somit haben wir hier ein wunderbares Beispiel, wo aufgrund von Codeverständnis ein 2D-Array wirklich besser ist.

Was wir im Tutorial tun, ist also folgendes: Alles, was der Spieler sieht, packen wir in ein 2D-Array. Insgesamt werden wir mit nur drei kleinen Sprites auskommen:

  • spr_player1
  • spr_player2
  • spr_wall

Außerdem haben wir vier Objekte:

  • obj_game (persistent)
  • obj_player1_start
  • obj_player2_start
  • obj_wall

Das ganze Spiel wird über obj_game ablaufen. Den Rest haben wir nur für den Leveleditor. In der Spiellogik werden sie nicht wirklich gebraucht, auch nicht für die Kollision. Alle Informationen schreiben wir in das Array und lesen es auch hier wieder aus. Außerdem brauchen wir noch ein paar Arrays zur Hilfe.

Die Tücken der Schlange

Wer die Schlange beobachtet, stellt fest, dass sie aus einem Nullpunkt heraus startet. Sie hat zwar einen Kopf und drei Körperteile, aber sie wächst schon zu beginn aus einem 32×32 Feld heraus, bis sie eine Länge von vier mal 32×32 Felder besitzt. Der Körper folgt dabei stets der Schlange. Wenn die Schlange eine Zahl einsammelt, passieren zwei Dinge:

  1. Die Schlange bekommt Punkte.
  2. Die Schlange wächst.

Punktzahl

Die Punktzahl ist einfach. Es ist lediglich die Zahl der Frucht mit 100 multipliziert. In einem Level kann eine Schlange somit bis zu 4500 Punkte einsammeln. Wenn sie stirbt, verliert sie immer 1000 Punkte. Das heißt, dass ein Spieler, der in Level 1 alle fünf Leben verliert, auf jeden Fall eine negative Punktzahl hat.

Länge

Wenn die Schlange eine Frucht aufnimmt, wächst sie um den Faktor 4. Also Anzahl Frucht mal vier. Das führt zu folgender Länge:

14
28
312
416
520
624
728
832
936
Summe:180

Dazu noch die vier vom Anfang, so dass eine Schlange zumindest theoretisch eine Länge von 184 Feldern erreichen kann. Aber nur Theoretisch, da nach dem Einsammeln der 9 sofort ein neues Level beginnt. Das bedeutet, dass eine Schlange in der Praxis nur auf eine Länge von 148 Feldern kommt. Im Zweispielermodus können damit beide Schlangen nur auf eine Länge von 152 Feldern kommen.

Wenn wir eine Frucht einsammeln, wächst also die Schlange und der ganze Körper sieht so aus, als würde er zunächst erstarren, während auf der Seite des Kopfes die Schlange immer länger wird.

Zu guter Letzt kommt hinzu, dass wir bei der Steuerung beachten müssen, dass die Schlange nicht rückwärts darf. Bewegt sie sich nach links, darf der Tastendruck nach rechts nicht funktionieren, und umgekehrt. Gleiches gibt selbstverständlich für oben und unten.

Zusammengefasst gibt es viele Dinge zu beachten, die wir nacheinander abarbeiten.

Kollision

In GameMaker Studio gibt es mehrere Wege, Kollisionen zu prüfen. Das Gute an der hier gezeigten Methode ist, dass wir weitestgehend darauf pfeifen können. Kollisionsabfragen sind recht rechenintensiv, weshalb wir das lieber bequem über das Array prüfen. Tatsächlich brauchen wir lediglich die position_meeting()-Funktion, wenn wir das Gitter erstellen, damit wir wissen, wo die Wände sind und wo sich die Schlangen zu Levelbeginn befinden.

Alarm statt Step

Wir arbeiten in obj_game nur mit vier Events. In Create legen wir die Variablen fest und initiieren die Arrays. Im Step-Event überprüfen wir nur die Steuerung. In Draw zeichnen wir die GUI und alles, was sichtbar ist. Dabei werden wir vor allem auf das Array zugreifen. Die ganze Spiellogik befindet sich im Alarm-Event. Das macht uns die Steuerung der Spielgeschwindigkeit sehr einfach. Hier ein Beispiel, wie das bei Schwierigkeitsgrad 4 aussieht:

Bei einer Spielgeschwindigkeit von 60 FPS wird der Alarm in diesem Fall alle 8 FPS aufgerufen. Bei einem Schwierigkeitsgrad von 10 ist es nach jedem Frame. Dadurch entsteht folgende Kurve:

Beziehung Schwierigkeitsgrad zu Geschwindigkeit
Beziehung Schwierigkeitsgrad zu Geschwindigkeit

Bei Schwierigkeitsgrad 1 liegt der Wert bei 30. Das heißt, dass sich die Schlange pro Sekunde um 2 Felder bewegt. Bei Schwierigkeitsgrad 2 haben wir den Wert 20, was drei Felder bedeutet usw. Wir sehen, dass die Kurve immer flacher wird. In der Praxis fühlt sich der Sprung jedoch umgekehrt ein. Von 8 zu 10 ist es, gefühlt, von „schaffbar” zu „unmöglich”, von 1 zu 3 eher „langweilig” bis „okay”. Das System habe ich sogar noch nach der Aufzeichnung des oben gezeigten Videos geändert. Davor war es eine simple Formel, die aber gleich mehrere Probleme mit sich brachte. Die switch sieht zwar nicht elegant aus, hat aber den Vorteil, dass man es sehr genau abstufen kann. So ergibt sich grob eine ganz gute Unterteilung: 1 und 2 ist für absolute Anfänger. Vor allem für kleine Kinder. Zwischen 3 und 8 liegen alle Gelegenheitsspieler, bei 9 und 10 die Profis. 

Doch kommen wir nun endlich zum Code.

obj_game

Create-Event

Zunächst einmal definieren wir viele Variablen. Die meisten sind selbstredend, weshalb ich nur auf wenige Punkte vertiefend eingehen möchte.

Hier definieren wir die Länge des Kopfes (1), die Ausgangslänge des Körpers und die Richtung, in welche sich die Schlange nach dem Start bewegt.

Hier rufen wir zwei Funktionen auf, in die wir rein schauen:

create_grid()

Zunächst erzeugen wir unser 2D-Array game_grid. In der ersten Schleife legen wir nur die Dimension fest, in der zweiten füllen wir es mit Informationen. Wir gehen dabei schrittweise durch das Level und tasten es ab. Finden wir eine Wand, schreiben wir _value = WALL; in das Feld. Ebenso verfahren wir mit den Startpositionen der Schlangen. Am Ende befindet sich der ganze Ausgangszustand des Levels in game_grid.

Und ja, wie so oft verzichte ich auch dieses Mal auf GML eigene Möglichkeiten wie ds_list() und ds_grid().

create_snakes()

Jetzt verfahren wir ganz ähnlich mit den Schlangen. Jede Schlange bekommt hier zwei eigene 1D-Arrays spendiert: player_1_snake_x und player_1_snake_y bzw. player_2_snake_x und player_2_snake_y. Die Variablen _player_1_start_x und _player_1_start_y erhalten wir bereits aus der Funktion create_grid().

Zurück zum Create-Event. Wir haben noch zwei wichtige Zeilen:

spawn_fruit()

Mit dieser Funktion legen wir eine Frucht auf ein freies Feld im Spielfeld. Das heißt, wir wählen am Anfang eine zufällige x und y-Position. Dann starten wir eine while-Schleife und durchlaufen sie so lange, bis wir eine freie Position in game_grid finden. Sobald wir diese Position gefunden haben, schreiben wir sie in das Gitter und geben die Position der Frucht an die Variable fruit_pos zurück. Diese Variable ist wichtig zur Kollisionsprüfung und, um nicht jedes Mal das Gitter nach der Frucht durchsuchen zu müssen.

Step-Event

Die Steuerung ist ziemlich simpel. Da wir im Create-Event die Tasten festgelegt haben, kann man den Code auch recht gut lesen. Wir erkennen auch, dass wir im Spiel mehrere Zustände unterscheiden. pause ist so ein Zustand, ebenso game_over. Wer aufmerksam liest, wird feststellen, dass wir später bei Game Over eine Frage stellen, in dieser Version aber nur mit „Y” antworten können. Das ist ein Zugeständnis an das Original Nibbles. Bei „N” landet man wieder in QBasic.

Wichtig ist auch die Variable player_1_can_move bzw. player_2_can_move. Sobald der Spieler eine Taste drückt, wird die Variable player_1_dir bzw. player_2_dir geschrieben. Nun müssen wir die Steuerung sperren, weil es sonst sein kann, dass die Schlange rückwärts in die selbst fährt. Die Variablen werden am Ende von Alarm[0] wieder freigegeben.

Sehr weit unten gibt es noch eine Funktion, die wir uns genauer anschauen.

level_restart()

Wenn nach dem Tod eine der Schlangen die Leertaste gedrückt wird, führen wir diese Funktion aus. 

Alarm-Event

Das ist die eigentliche Spiellogik, die durchgeführt wird, wenn keine Pause ist. Im Fall einer Pause wird der Alarm natürlich weiter aufgerufen.

Der Code lässt sich in mehrere Abschnitte unterteilen. Da er recht ausführlich kommentiert ist, gehe ich lediglich die Logik abschnittweise durch.

Zuerst müssen wir die aktuelle Position des Kopfes zwischenspeichern. Die Information können wir später aus _player_1_old_x und _player_1_old_y abrufen. Anschließend bewegen wir den Kopf in die Richtung, die wir vom Step-Event erhalten. Danach bewegen wir den Körper. Dabei gibt es zwei Möglichkeiten: Entweder der Körper ist bereits voll auf dem Spielfeld, oder nicht.

Um das zu erfahren, zählen wir im Gitter, wie viele Teile bereits vorliegen. Danach verschieben wir die Körperteile in Richtung Kopf. Wenn wir noch Körperteile übrig haben, die nicht auf dem Spielfeld sind, setzen wir einen Teil an das Ende der Schlange. Wenn wir also eine Frucht eingesammelt haben, kommt hier mit jedem Aufruf des Alarms ein Körperteil hinzu, bis _player_1_snake_count-player_1_snake_head = player_1_snake_length ist.

Kollision

Nachdem alles verschoben wurde, prüfen wir die Kollision. Und ja, hier liegt der Hase im Pfeffer. Wir haben die Schlange verschoben und somit im Falle einer Kollision im Gitter etwas überschrieben.

Wirklich?

Nein, noch nicht. Die Position der Schlange befindet sich bisher nur im Schlangen-Array, nicht im Gitter.

Wir prüfen zunächst, ob eine der Schlangen mit der Frucht kollidiert. Wenn das der Fall ist, werden entsprechende Maßnahmen getroffen.

Danach schauen wir, ob wir mit einer Wand kollidieren. Das findet hier statt:

Danach geht es an die nächste Prüfung:

Hier prüfen wir, ob die Schlange mit der Wand, dem eigenen Körper, dem Kopf des anderen Spielers oder dem Körper des anderen Spielers kollidiert. Und wenn das so ist, ergreifen wir entsprechende Maßnahmen wie Punktabzug, Leben abziehen, Sound abspielen. Das Gleiche natürlich mit Spieler 2.

Und ja, die Kollisionsabfrage bei der Wand ist eigentlich doppelt gemoppelt, aber ich fand, dass man den Code so ein bisschen besser versteht.

Wenn alles geprüft wurde, aktualisieren wir endlich das Gitter. Ach ja, was ist eigentlich mit…

format_score()

Die Punkteanzeige wird auf eine ganz bestimmte Art formatiert. Sie soll so aussehen, wie im Original Nibbles. Ist der Wert kleiner als ein dreistelliger Wert, werden Nullen angeführt. Ab einem vierstelligen Wert gibt es Trennpunkte für die Tausender. Außerdem unterscheiden wir zwischen positiven und negativen Zahlen.

Draw-Event

Dafür, dass wir hier wirklich alles anzeigen, von Level, Schlangen, GUI bis hin zu den Meldungen, ist das wirklich sehr übersichtlich und wir haben es unter Kontrolle. 

Im ersten Abschnitt geht es um die GUI. Danach wird das Spielfeld angezeigt. Dabei gehen wir unser Array durch, fragen die Werte ab und zeichnen die entsprechenden Sprites. Einziger „Hack” ist dieser Abschnitt:

Hier schauen wir, ob sich die Schlange in der Nähe der Wand befindet. Das kostet auch die meiste Geschwindigkeit. Anschließend stellen wir unser Sprite-Index entsprechend ein:

Ganz unten wird die Anzeige aktiv, falls wir uns im Pause-Modus befinden. Dann wird genauer geprüft, was Sache ist. Ist einer der Spieler gestorben, das Spiel vorbei, Levelbeginn oder einfach nur Pause. Entsprechend wird der Text gewählt und in einer Box angezeigt.

Sonstige Skripte

Ab Level 2 gibt es immer einen Creation Code im Raum. Hier wird lediglich die Funktion level_next() aufgerufen. Sie ist der Funktion level_restart() sehr ähnlich.

level_next()

Das ist nötig, weil obj_game persistent ist und wir die entscheidenden Variablen im Create-Event zurücksetzen müssen.

player_status()

Außerdem haben wir noch diese kleine Funktion, um die Spieler auf den Ausgangszustand zurückzusetzen.

Optimierungen und Änderungsmöglichkeiten

Am Code selbst lassen sich natürlich noch einige Dinge optimieren. Auch die Länge des Codes ließe sich einschränken, indem man auf die separaten Variablen für Spieler 1 und 2 konsequent verzichtet und die mit NUM_PLAYERS über Schleifen bearbeitet. Ob sich das positiv auf die Geschwindigkeit auswirkt, sei dahingestellt.

Generell könnte man natürlich noch neue Levels erstellen und auch die Grafik moderner gestallten. Snake muss ja nicht zwingend nach 8 Bit aussehen. Auf der anderen Seite wäre natürlich auch ein weiteres Downgrade auf Monochrom möglich.

Und wenn wir schon bei den Möglichkeiten sind: Man könnte auch Objekte einbauen, die das Gameplay erweitern. Etwa ein Objekt um weitere Leben zu erhalten oder die Länge der Schlange zu verringern. Letzteres würde allerdings einen etwas tieferen Eingriff erfordern.

Dazu kommen modernere Soundeffekte, Musik, hübschere Grafikeffekte, womöglich noch Partikel. Wer an eine Highscore-Tabelle denkt, sollte die Schwierigkeitsgrade berücksichtigen. Bei Snake ließe es sich in einer Tabelle abbilden, indem man bei einem höheren Schwierigkeitsgrad auch mehr Punkte vergibt.

Und wer tiefgreifende Veränderungen will, kann gerne über folgende Punkte nachdenken:

  • Online-Mehrspielermodus
  • Modus für vier Schlangen
  • Spiel gegen die KI

Ich wünsche euch viel Spaß bei euren eigenen Kreationen!

Download

Das ganze Projekt könnt ihr hier downloaden und nach euren Wünschen anpassen.

Weiterführende Links

Project Snake auf itch.io
Projekt Tic-Tac-Toe – Teil 1
Mehrfache Sortierung von Arrays
Casino Würfel – Das Ein-Objekt-Spiel
Grid-Steuerung in GMS2

Autor

Abonnieren
Benachrichtige mich bei
guest

0 Comments
Inline Feedbacks
Alle Kommentare anzeigen