Erstellen eines "Schlangenspiels" in MQL5
Einleitung
In diesem Beitrag beschäftigen wir uns mit einem Beispiel für die Programmierung eines Schlangenspiels in MQL5.
Ab der 5. Fassung von MQL ist da Programmieren von Spielen in erster Linie dank der Verarbeitungsroutinen für Ereignisse, einschließlich vom Benutzer selbst angelegter, möglich. Zudem erleichtert die Unterstützung der objektorientierten Programmierung (OOP) die Gestaltung derartiger Programme ganz wesentlich, macht den Programmcode nachvollziehbarer und verringert die Anzahl von Fehlern in ihm.
In diesem Artikel erfahren Sie etwas über die Besonderheiten der Verarbeitungsroutine für Ereignisse, die bei der Arbeit mit Diagrammen auftreten, und Sie lernen Beispiele für die Arbeit der Standardbibliothek in MQL5 sowie Möglichkeiten zum regelmäßig wiederkehrenden Aufrufen von Funktionen zur Ausführung bestimmter Berechnungen kennen.
Das Spiel
Die Wahl des Schlangenspiels „Snake“ als Beispiel ist in erster Linie auf die Einfachheit seiner Umsetzung zurückzuführen. Jeder, der sich etwas mit Programmierung auskennt, kann es schreiben.
In der deutschen Fassung von Wikipedia [Stand 29. 01. 2016] heißt es dazu:
„Snake (englisch für Schlange) ist ein Computerspielklassiker, bei dem man eine Schlange durch ein Spielfeld steuert und Futter (manchmal Apples genannt) aufnehmen soll.
Die Schlange wird mit jedem Futterhappen länger. Es können andere Schlangen auftauchen, die man nicht berühren darf. Auch können Plums auftauchen, die man nicht berühren darf (außer mit dem Schwanz). Auch die Berührung der Spielfeldränder oder des eigenen Schwanzes führen zum Tod der Schlange.“ Die Steuerung des Schlangenkopfs erfolgt mithilfe der Richtungspfeiltasten, der Körper der Schlange folgt auf demselben Weg. Der Spieler kann die Schlange weder stoppen noch rückgängig machen.
Unsere Umsetzung des Spiels in MQL5 wird eine Reihe von Beschränkungen und Besonderheiten aufweisen.
Es gibt 6 Stufen (0 bis 5). Auf jeder Stufe hat man 5 „Leben“. Sind alle Leben aufgebraucht oder alle Stufen durchlaufen, beginnt das Spiel von neuem. Es können eigene Stufen hinzugefügt werden. Die Geschwindigkeit sowie die maximale Länge der Schlange sind auf allen Stufen gleich.
Das Spielfeld besteht aus vier Elementen:
- der Kopfzeile des Spiels. Sie wird zur Platzierung des Spiels in dem Diagramm verwendet. Wird die Kopfzeile verschoben, wechseln auch alle Elemente des Spiels ihren Platz.
- dem Spielfeld. Dabei handelt es sich um ein Datenfeld (eine Tabelle) mit 20x20 Zellen. Jede Zelle wiederum hat die Größe von 20x20 Bildpunkten (Pixel). Auf dem Spielfeld befinden sich:
- Die Schlange (Snake). Sie besteht aus drei aufeinander folgenden Gliedern, Kopf, Körper und Schwanz. Der Kopf kann nach oben und unten sowie nach links und nach rechts bewegt werden. Die übrigen Glieder der Schlange folgen dem Kopf.
- Das Hindernis. Ein schwarzes Rechteck, bei dessen Berührung durch den Schlangenkopf die aktuelle Stufe von neuem beginnt, wobei sich die Anzahl der „Leben“ um 1 verringert.
- Das Futter. „Beeren“, bei deren Berührung die Schlange, um genau zu sein ihr Körper, länger wird. Nach dem „Verzehr“ von 12 Beeren erfolgt der Wechsel zur nächsten Stufe.
- der Infoleiste (Anzeige des Spielstands). Sie besteht aus drei Teilen:
- Level. Die aktuelle Stufe.
- Food left over. Verbleibende „Beeren“.
- Lives. Verbleibende „Leben“.
- dem Bedienfeld. Es umfasst drei Schaltflächen:
- Start. Zum Starten des Spiels auf der aktuellen Stufe.
- Pause. Zum Unterbrechen des Spiels mit Verbleib an der aktuellen Position.
- Stop. Zum Abbrechen des Spiels mit Rückkehr zur ersten Stufe.
Abbildung 1 zeigt alle aufgeführten Elemente:
Abbildung 1. Die Elemente des Schlangenspiels
Bei der Kopfzeile des Spiels handelt es sich um ein Objekt der Art „Schaltfläche“. Alle Elemente auf dem Spielfeld sind „grafische Objekte“ (BmpLabel). Die Infoleiste besteht aus drei Objekten vom Typ „Eingabefeld“, das Bedienfeld dagegen aus drei „Schaltflächen“. Alle Elemente werden in einem festgelegten Abstand von der oberen linken Ecke des Diagramms entlang der x- und der y-Achse angeordnet.
Es sei darauf hingewiesen, dass die Spielfeldränder für die Schlange kein Hindernis darstellen. Erreicht die Schlange beispielsweise den linken Rand, so taucht sie rechts wieder auf. Das zeigt Abbildung 2:
Abbildung 2. Überschreiten des Spielfeldrandes durch die Schlange
Anders als der Körper können der Kopf und der Schwanz der Schlange gedreht werden. Die Drehung des Kopfes folgt der Bewegungsrichtung der Schlange oder der Lage des nächst gelegenen Gegenstands bestimmt. Die Drehrichtung des Schwanzes wird nur durch Lage des nächst gelegenen Gegenstands beeinflusst.
Befindet sich der nächst gelegene Gegenstand beispielsweise links vom Schwanz, so dreht sich dieser nach links. Etwas anders sieht es beim Kopf aus. Er wird nach links gedreht, wenn sich der nächst gelegene Gegenstand rechts befindet. Beispiele für die Drehrichtungen von Kopf und Schwanz zeigen die folgenden Abbildungen. Achten Sie auf die Drehung des Kopfes und des Schwanzes in Bezug auf die jeweils nächstgelegenen Gegenstände.
Kopf und Schwanz weisen nach links | Kopf und Schwanz weisen nach rechts | Kopf und Schwanz weisen nach unten | Kopf und Schwanz weisen nach oben |
Die Bewegung der Schlange erfolgt in drei Schritten:
- Die Verlagerung des Kopfes um eine Zelle nach rechts, links, oben oder unten je nach Bewegungsrichtung.
- Die Verlagerung des letzten Elements des Schlangenkörpers auf die vorherige Position des Kopfes.
- Die Verlagerung des Schwanzes der Schlange auf die vorherige Position des letzten Elements ihres Körpers. Verlagerung des Schlangenschwanzes auf die vorherige Position des letzten Elements des Schlangenkörpers.
Frisst die Schlange einen Gegenstand, so wird der Schwanz nicht verlagert. Stattdessen kommt ein neues Körperelement hinzu und nimmt die Position des vorherigen letzten Elements des Körpers ein.
Die folgenden Zeichnungen geben ein Beispiel für die Bewegung der Schlange nach links wieder.
Ausgangsposition | Verlagerung des Kopfes um eine Zelle nach links |
Verlagerung des letzten Elements des Körpers auf die vorherige Position des Kopfes |
Verlagerung des Schwanzes auf die vorherige Position des letzten Körperelements |
Theoretischer Teil
Als Nächstes widmen wir uns den beim Schreiben von Programmen verwendeten Hilfsmitteln und Techniken.
Die Standardbibliothek
Für die Bearbeitung (Erstellung, Verlagerung, Löschung) gleichartiger Objekte des Spiels (zum Beispiel von Spielfeldzellen oder Schlangengliedern) empfiehlt sich die Verwendung von Datenfeldern (Arrays) für besagte Objekte. Die Organisation dieser Datenfelder und Objekte kann mithilfe der Klassen der Standardbibliothek von MQL5 erfolgen.
Die Standardbibliothek von MQL5 erleichtert die Programmierung. Für unser Spiel werden wir folgende Klassen aus dieser Bibliothek verwenden:
- CArrayObj, eine Klasse zur Organisation von Daten (ein dynamisches Datenfeld für Adressenverweise).
- CChartObjectEdit, CChartObjectButton und CChartObjectBmpLabel, Klassen mit Steuerelementen, Eingabefeldern (Edit), Schaltflächen (Button) bzw. grafischen Objekten (BmpLabel) entsprechend der jeweiligen Bezeichnung.
Um die Klassen der MQL5-Standardbibliothek nutzen zu können, müssen sie mithilfe folgender Include-Anweisung eingebunden werden:
#include <path_to_the_file_with_classes_description>
Zur Einbindung der Klasse CChartObjectButton muss beispielsweise geschrieben werden:
#include <ChartObjects\ChartObjectsTxtControls.mqh>
Die Dateipfade sind in der Dokumentation zu MQL5 zu finden.
Bei der Arbeit Klassen aus der MQL5-Standardbibliothek muss klar sein, dass einige von ihnen aufeinander aufbauen. Die Klasse CChartObjectButton zum Beispiel baut auf die Klasse CChartObjectEdit auf, im Gegenzug „beerbt“ die Klasse CChartObjectEdit die Klasse CChatObjectLabel usw. Das bedeutet, dass die „erbende“ oder abgeleitete Klasse über die Eigenschaften und Methoden der „Elternklasse“ verfügt.
Um die Vorzüge der Verwendung von Klassen aus der MQL5-Standardbibliothek in Programmen nachvollziehen zu können, sehen wir uns als Beispiel die Erstellung einer Schaltfläche auf zwei unterschiedlichen Wegen an (einmal ohne und einmal mit der Verwendung von Klassen).
Ein Beispiel ohne Verwendung von Klassen:
//Creating a button with name "button" ObjectCreate(0,"button",OBJ_BUTTON,0,0,0); //Specifying the text on the button ObjectSetString(0,"button",OBJPROP_TEXT,"Button text"); //Specifying the button size ObjectSetInteger(0,"button",OBJPROP_XSIZE,100); ObjectSetInteger(0,"button",OBJPROP_YSIZE,20); //Specifying the button position ObjectSetInteger(0,"button",OBJPROP_XDISTANCE,10); ObjectSetInteger(0,"button",OBJPROP_YDISTANCE,10);
Ein Beispiel unter Verwendung von Klassen:
CChartObjectButton *button; //Creating an object of CChartObjectButton class and assign a pointer to the button variable button=new CChartObjectButton; //Creating a button with properties (in pixels): (width=100, height=20, positions: X=10,Y=10) button.Create(0,"button",0,10,10,100,20); //Specifying the text on the button button.Description("Button text");
Unübersehbar ist es einfacher, mit Klassen zu arbeiten. Außerdem lassen sich die Objekte der Klassen in Datenfeldern speichern und mühelos bearbeiten.
Die Methoden und Eigenschaften der Klassen der Steuerelemente werden in der Dokumentation zu der Standardbibliothek gut und übersichtlich dargestellt.
Für die Organisation des Objektdatenfeldes verwenden wir die Klasse CArrayObj aus der Standardbibliothek, wodurch uns eine Menge Routinearbeiten erspart bleiben (wie die Anpassung der Größe des Datenfeldes, wenn ein neues Element hinzugefügt wird, oder das Löschen aller Objekte in dem Datenfeld u. ä.).
Die Besonderheiten der Klasse CArrayObj
Die Klasse CArrayObj ermöglicht die Einrichtung eines dynamischen Datenfeldes mit den Adressenverweisen für die Objekte der Klasse CObject. Bei der Klasse CObject handelt es sich um die Elternklasse aller Klassen der Standardbibliothek. Das heißt, dass wir für die Objekte jeder Klasse der Standardbibliothek ein dynamisches Datenfeld mit Adressenverweisen anlegen können. Wenn ein dynamisches Datenfeld für die Objekte einer eigenen Klasse angelegt werden muss, so muss dieses aus CObject abgeleitet bzw. „geerbt“ werden.
In dem folgenden Beispiel meldet das Kompilierungsprogramm keine Fehler, weil die benutzerdefinierte Klasse ein „Kind“ der Klasse CObject ist:
#include <Arrays\ArrayObj.mqh> class CMyClass:public CObject { //fields and methods }; //creating an object of CMyClass type and assign it to the value of the my_obj variable CMyClass *my_obj=new CMyClass; //declaring a dynamic array of object pointers CArrayObj array_obj; //adding the my_obj object pointer at the end of the array_obj array array_obj.Add(my_obj);
Im folgenden Fall dagegen meldet das Kompilierungsprogramm einen Fehler, da my_obj auf kein Objekt der Klasse CObject oder einer Kindklasse zu CObject verweist:
#include <Arrays\ArrayObj.mqh> class CMyClass { //fields and methods }; //creating an object of CMyClass type and assing it to the value of the my_obj variable CMyClass *my_obj=new CMyClass; //declaring a dynamic array of object pointers CArrayObj array_obj; //adding the my_obj object pointer at the end of the array_obj array array_obj.Add(my_obj);
Bei der Programmierung des Spiels kommen folgende Methoden der Klasse CArrayObj zum Einsatz:
- Add - Hinzufügen eines Elementes am Ende des Datenfeldes.
- Insert - Einfügen eines Elementes an einer angegebenen Stelle des Datenfeldes.
- Detach - Entfernen eines Elementes von einer angegebenen Stelle (das Element wird aus dem Datenfeld gelöscht).
- Total - Abrufen der Anzahl der Elemente in dem Datenfeld.
- At - Abrufen des Elementes einer angegebenen Stelle (das Element wird nicht aus dem Datenfeld gelöscht).
Es folgt ein Beispiel für die Arbeit mit der Klasse CArrayObj:
#include <Arrays\ArrayObj.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CMyClass:public CObject { public: char s; }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void MyPrint(CArrayObj *array_obj) { CMyClass *my_obj; for(int i=0;i<array_obj.Total();i++) { my_obj=array_obj.At(i); printf("%C",my_obj.s); } } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //creating the pointer to the object of CArrayObj class CArrayObj *array_obj=new CArrayObj(); //declaring the CMyClass object pointer CMyClass *my_obj; //filling the array_obj dynamic array for(int i='a';i<='c';i++) { //creating the CMyClass class object my_obj=new CMyClass(); my_obj.s=char(i); //adding an object of CMyClass class at the end of the array_obj dynamic array array_obj.Add(my_obj); } //printing result MyPrint(array_obj); //creating new object of CMyClass class my_obj=new CMyClass(); my_obj.s='d'; //inserting new element at the first position of the array array_obj.Insert(my_obj,1); //printing result MyPrint(array_obj); //detaching the element from the third position of the array my_obj=array_obj.Detach(2); //printing result MyPrint(array_obj); //deleting the dynamic array and all objects with pointers of the array delete array_obj; return(0); }
In diesem Beispiel wird in der Funktion OnInit eine dynamisches Datenfeld aus drei Bestandteilen angelegt. Die Ausgabe des Inhaltes des Datenfeldes erfolgt durch Aufrufen der Funktion MyPrint.
Nach dem Füllen des Datenfeldes mithilfe der Methode Add kann sein Inhalt in etwa folgendermaßen aussehen: {a, b, c}.
Nach Anwendung der Methode Insert kann der Inhalt des Datenfeldes folgendes Aussehen annehmen: {a, d, b, c}.
Nach Anwendung der Methode Detach wird das Datenfeld schließlich so aussehen {a, d, c}.
Bei Anwendung des Operators delete auf die Variable array_obj wird der Destruktor der Klasse CArrayObj aufgerufen, dieser löscht nicht nur das Datenfeld array_obj sondern auch die Objekte, deren Adressenverweise in ihm gespeichert sind. Damit das nicht geschieht, muss vor Anwendung des Befehls delete die Speicherplatzverwaltungsmarkierung der Klasse CArrayObj auf „false“ gesetzt werden. Diese Markierung wird mithilfe der Methode FreeMode eingestellt.
Somit muss also, wenn keine Notwendigkeit besteht, beim Löschen eines dynamischen Datenfeldes auch die Objekte zu löschen, deren Adressenverweise in ihm gespeichert sind, Folgendes programmiert werden:
array_obj.FreeMode(false);
delete array_obj;
Die Verarbeitung von Ereignissen
Bei Auftreten mehrerer Ereignisse werden diese der Reihe nach erfasst und anschließend nacheinander an die Ereignisverarbeitungsfunktion weitergegeben.
Für die Verarbeitung von Ereignissen, die bei der Arbeit mit dem Diagramm auftreten, sowie von benutzerdefinierten Ereignissen gibt es in MQL5 die Funktion OnChartEvent. Jedes Ereignis besitzt einen Bezeichner sowie Parameter, die an die Funktion OnChartEvent weitergegeben werden.
Die Funktion OnChartEvent wird nur dann aufgerufen, wenn sich der Ausführungsdurchlauf außerhalb aller übrigen Funktionen des Programms befindet. Auf diese Weise kann die Funktion OnChartEvent im nächsten Beispiel nie das Ruder übernehmen.
#include <ChartObjects\ChartObjectsTxtControls.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void MyFunction() { CChartObjectButton *button; button=new CChartObjectButton; button.Create(0,"button",0,10,10,100,20); button.Description("Button text"); while(true) { //The code, that should be called periodically } } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { MyFunction(); return(0); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id==CHARTEVENT_OBJECT_CLICK && sparam=="button") Alert("Button click"); }
Die Endlosschleife while entlässt den Programmausführungsdurchlauf nicht aus der Funktion MyFunction. Somit kann die Funktion OnChartEvent nicht das Kommando übernehmen. Folglich führt die Betätigung der angelegten Schaltfläche nicht zum Aufruf der Warnfunktion Alert.
Regelmäßig wiederkehrende Ausführung von Programmcode mit der Möglichkeit zur Verarbeitung von Ereignissen
Um die Schlange in bestimmten Zeitabständen zu verlagern benötigen wir im Spiel den regelmäßigen Aufruf der Verlagerungsfunktion mit der Möglichkeit zur Verarbeitung von Ereignissen. Wie oben gezeigt führt eine Endlosschleife jedoch dazu, dass die Funktion OnChartEvent nicht aufgerufen und die Ereignisverarbeitung somit unmöglich wird.
Es ist demnach nötig, sich eine andere Möglichkeit zur regelmäßig wiederkehrenden Code-Ausführung einfallen zu lassen.
Die Verwendung der Funktion OnTimer
MQL5 bietet die besondere Funktion OnTimer, die nach Ablauf einer vorher eingestellten Anzahl von Sekunden regelmäßig aufgerufen wird. Die Einstellung der Sekundenzahl erfolgt mithilfe der FunktionEventSetTimer.
Danach kann das vorige Beispiel wie folgt umgeschrieben werden:
#include <ChartObjects\ChartObjectsTxtControls.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void MyFunction() { //The code, that should be executed periodically } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { CChartObjectButton *button; button=new CChartObjectButton; button.Create(0,"button",0,10,10,100,20); button.Description("Button text"); EventSetTimer(1); return(0); } void OnTimer() { MyFunction(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id==CHARTEVENT_OBJECT_CLICK && sparam=="button") Alert("Button click"); }
In der Funktion OnInit werden eine Schaltfläche angelegt und ein Zeitraum im Umfang einer Sekunde bis zum Aufrufen der Funktion OnTimer festgelegt. Jede Sekunde erfolgt ein Aufruf der Funktion OnTimer, aus der wiederum ein regelmäßige Berechnungen erfordernder Code (die Funktion MyFunction) aufgerufen wird.
Dabei ist zu beachten, dass der Aufrufzeitraum OnTimer in Sekunden angegeben wird. Es ist ein anderes Vorgehen erforderlich, wenn die Funktion nach einer bestimmten Anzahl von Millisekunden aufgerufen werden soll. Bei diesem Vorgehen kommen benutzerdefinierte Ereignisse zum Einsatz.
Die Verwendung benutzerdefinierter Ereignisse
Benutzerdefinierte Ereignisse werden mit der Funktion EventChartCustom erzeugt, in deren Parametern der Bezeichner und die Parameter des Ereignisses festgelegt werden. Möglich ist die Festlegung von bis zu 65536 Bezeichnern für benutzerdefinierte Ereignisse, von 0 bis 65535. Bei Aufruf der Funktion EventChartCustom erweitert das Kompilierungsprogramm von MQL5 den Bezeichner automatisch um die Konstante CHARTEVENT_CUSTOM, um benutzerdefinierte Ereignisse von Ereignissen anderer Art zu unterscheiden. Somit erstreckt sich der tatsächliche Bereich für die Bezeichner benutzerdefinierter Ereignisse von CHARTEVENT_CUSTOM bis CHARTEVENT_CUSTOM + 65535 (CHARTEVENT_CUSTOM_LAST).
Weiter unten sehen wir ein Beispiel für den regelmäßigen Aufruf der Funktion MyFunction unter Verwendung benutzerdefinierter Ereignisse:
#include <ChartObjects\ChartObjectsTxtControls.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void MyFunction() { //The code, that should be executed periodically Sleep(200); EventChartCustom(0,0,0,0,""); } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { CChartObjectButton *button; button=new CChartObjectButton; button.Create(0,"button",0,10,10,100,20); button.Description("Button text"); MyFunction(); return(0); } //+------------------------------------------------------------------+ //| OnChartEvent processing function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id==CHARTEVENT_OBJECT_CLICK && sparam=="button") Alert("Button click"); if(id==CHARTEVENT_CUSTOM) MyFunction(); }
In diesem Beispiel erfolgt vor dem Verlassen der Funktion MyFunction eine Verzögerung um 200 ms (die Zeit bis zu ihrem turnusgemäßen Aufruf), und es wird ein benutzerdefiniertes Ereignis erzeugt. Die Funktion OnChartEvent verarbeitet die Ereignisreihe und ruft die Funktion MyFunction erneut auf, wenn sie auf ein benutzerdefiniertes Ereignis trifft. Auf diese Weise wird der regelmäßig wiederkehrende Aufruf der Funktion MyFunction umgesetzt, wobei die möglichen Aufrufintervalle in Millisekunden eingestellt werden können.
Praktischer Teil
Im Weiteren untersuchen wir sofort das Beispiel der Programmierung des Schlangenspiels.
Festlegung der Konstanten und der Stufenkarte
Die Stufenkarte (Map) befindet sich in der eigenen eingebetteten (Include) (Kopfzeilen-) Datei Snake.mqh und ist ein dreidimensionales Datenfeld namens game_level[6][20][20]. Die Stufenkarte (Map) befindet sich in der eigenen eingebetteten Kopfzeilendatei Snake.mqh und ist ein dreidimensionales Datenfeld namens game_level[6][20][20]. Jedes Datenfeldelement ist wiederum ein zweidimensionales Datenfeld mit der Beschreibung der jeweiligen Ebene. Hat das Datenfeldelement den Wert 9, so handelt es sich um ein Hindernis. Ist der Wert 1, 2 oder 3, so haben wir es entsprechend mit dem Kopf, dem Körper oder dem Schwanz der Schlange zu tun, deren Ausgangsposition auf dem Spielfeld festgelegt wird. Dem Datenfeld level können wir neue Stufen hinzufügen oder seine vorhandenen bearbeiten.
Außerdem werden in der Datei Snake.mqh die bei der Programmierung des Spiels verwendeten Konstanten festgelegt. Durch Änderung der Konstanten SPEED_SNAKE und MAX_LENGTH_SNAKE können beispielsweise die Geschwindigkeit der Schlange sowie ihre maximale Länge auf jeder Stufe herauf- bzw. herabgesetzt werden. Zu jeder Konstanten ist ein entsprechender Kommentar vorhanden.
//+------------------------------------------------------------------+ //| Snake.mqh | //| Copyright Roman Martynyuk | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Roman Martynyuk" #property link "http://www.mql5.com" #include <VirtualKeys.mqh> //File with keycodes #include <Arrays\ArrayObj.mqh> //File with CArrayObj class #include <ChartObjects\ChartObjectsBmpControls.mqh> //File with CChartObjectBmpLabel class #include <ChartObjects\ChartObjectsTxtControls.mqh> //File with CChartObjectButton and CChartObjectEdit classes #define CRASH_NO 0 //No crash #define CRASH_OBSTACLE_OR_SNAKE 1 //Crash with an "Obstacle" or snake body #define CRASH_FOOD 2 //Crash with a "Food"" #define DIRECTION_LEFT 0 //Left #define DIRECTION_UP 1 //Up #define DIRECTION_RIGHT 2 //Right #define DIRECTION_DOWN 3 //Down #define COUNT_COLUMNS ArrayRange(game_level,2) //Number of columns of playing field #define COUNT_ROWS ArrayRange(game_level,1) //Number of rows of playing field #define COUNT_LEVELS ArrayRange(game_level,0) //Number of levels #define START_POS_X 0 //Starting X position of the game #define START_POS_Y 0 //Starting Y position of the game #define SQUARE_WIDTH 20 //Square (cell) width (in pixels) #define SQUARE_HEIGHT 20 //Square (cell) height (in pixels) #define IMG_FILE_NAME_SQUARE "\\Images\\Games\\Snake\\square.bmp" //Path to the "Square" image #define IMG_FILE_NAME_OBSTACLE "\\Images\\Games\\Snake\\obstacle.bmp" //Path to the "Obstacle" image #define IMG_FILE_NAME_SNAKE_HEAD_LEFT "\\Images\\Games\\Snake\\head_left.bmp" //Path to the snake's head (left) image #define IMG_FILE_NAME_SNAKE_HEAD_UP "\\Images\\Games\\Snake\\head_up.bmp" //Path to the snake's head (up) image #define IMG_FILE_NAME_SNAKE_HEAD_RIGHT "\\Images\\Games\\Snake\\head_right.bmp" //Path to the snake's head (right) image #define IMG_FILE_NAME_SNAKE_HEAD_DOWN "\\Images\\Games\\Snake\\head_down.bmp" //Path to the snake's head (down) image #define IMG_FILE_NAME_SNAKE_BODY "\\Images\\Games\\Snake\\body.bmp" //Path to the snake's body image #define IMG_FILE_NAME_SNAKE_TAIL_LEFT "\\Images\\Games\\Snake\\tail_left.bmp" //Path to the snake's tail (left) image #define IMG_FILE_NAME_SNAKE_TAIL_UP "\\Images\\Games\\Snake\\tail_up.bmp" //Path to the snake's tail (up) image #define IMG_FILE_NAME_SNAKE_TAIL_RIGHT "\\Images\\Games\\Snake\\tail_right.bmp" //Path to the snake's tail (right) image #define IMG_FILE_NAME_SNAKE_TAIL_DOWN "Games\\Snake\\tail_down.bmp" //Path to the snake's tail (down) image #define IMG_FILE_NAME_FOOD "Games\\Snake\food.bmp" //Path to the "Food" image #define SQUARE_BMP_LABEL_NAME "snake_square_%u_%u" //Name of the "Square" graphic label #define OBSTACLE_BMP_LABEL_NAME "snake_obstacle_%u_%u" //Name of the "Obstacle" graphic label #define SNAKE_ELEMENT_BMP_LABEL_NAME "snake_element_%u" //Name of the "Snake" graphic label #define FOOD_BMP_LABEL_NAME "snake_food_%u" //Name of the "Food" graphic label #define LEVEL_EDIT_NAME "snake_level_edit" //Name of the "Level" edit #define LEVEL_EDIT_TEXT "Level: %u of %u" //Text of the "Level" edit #define FOOD_LEFT_OVER_EDIT_NAME "snake_food_available_edit" //Name of the "Food left" edit #define FOOD_LEFT_OVER_EDIT_TEXT "Food left over: %u" //Text of the "Food left" edit #define LIVES_EDIT_NAME "snake_lives_edit" //Name of the "Lives" edit #define LIVES_EDIT_TEXT "Lives: %u" //Text of the "Lives" edit #define START_GAME_BUTTON_NAME "snake_start_game_button" //Name of the "Start" button #define START_GAME_BUTTON_TEXT "Start" //Text of the "Start" button #define PAUSE_GAME_BUTTON_NAME "snake_pause_game_button" //Name of the "Pause" button #define PAUSE_GAME_BUTTON_TEXT "Pause" //Text of the "Pause" button #define STOP_GAME_BUTTON_NAME "snake_stop_game_button" //Name of the "Stop" button #define STOP_GAME_BUTTON_TEXT "Stop" //Text of the "Stop" button #define CONTROL_WIDTH (COUNT_COLUMNS*(SQUARE_WIDTH-1)+1)/3//Control Panel Width (1/3 of playing field width) #define CONTROL_HEIGHT 40 //Control Panel Height #define CONTROL_BACKGROUND C'240,240,240' //Color of Control Panel buttons #define CONTROL_COLOR Black //Text Color of Control Panel Buttons #define STATUS_WIDTH (COUNT_COLUMNS*(SQUARE_WIDTH-1)+1)/3//Status Panel Width (1/3 of playing field width) #define STATUS_HEIGHT 40 //Status Panel Height #define STATUS_BACKGROUND LemonChiffon //Status Panel Background Color #define STATUS_COLOR Black //Status Panel Text Color #define HEADER_BUTTON_NAME "snake_header_button" //Name of the "Header" button #define HEADER_BUTTON_TEXT "Snake" //Text of the "Header" button #define HEADER_WIDTH COUNT_COLUMNS*(SQUARE_WIDTH-1)+1 //Width of the "Header" button (playing field width) #define HEADER_HEIGHT 40 //Height of the "Header" button #define HEADER_BACKGROUND BurlyWood //Header Background Color #define HEADER_COLOR Black //Headet Text Color #define COUNT_FOOD 3 //Number of "Foods" at playing field #define LIVES_SNAKE 5 //Number of snake lives at each level #define SPEED_SNAKE 100 //Snake Speed (in milliseconds) #define MAX_LENGTH_SNAKE 15 //Maximal Snake Length #define MAX_LEVEL COUNT_LEVELS-1 //Maximal Level int game_level[][20][20]= { { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,3,2,1,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} } , { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,9,9,9,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} } , { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,9,1,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,9,9,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,9,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} } , { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0}, {0,0,0,0,0,9,9,0,0,0,0,0,3,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,9,9,9,9,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} } , { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,1,2,3,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,9,9,9,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,9,9,9,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,9,9,9,9,0}, {0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,9,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} } , { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0}, {0,1,2,3,0,0,0,0,0,0,0,9,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,9,9,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,9,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,9,9,9,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,9,9,0,0,0,0,0,0,9,9,9,9,0}, {0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,9,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,9,9,0,0,0,0,0} } }; //+------------------------------------------------------------------+
Sehen wir uns zum Beispiel die Festlegung der Konstanten #define SQUARE_BMP_LABEL_NAME "snake_square_% u_% U“ einmal genauer an. Dazu legen wir das Spielfeld an. Jede Zelle des Spielfelds ist ein grafisches Symbol, das eine eindeutige Bezeichnung haben muss. Mithilfe dieser Konstanten wird die Zellenbezeichnung festgelegt, das Format der Zellenbezeichnung ist %u, was bedeutet, dass es sich um eine vorzeichenlose ganze Zahl handelt.
Wenn ein grafisches Symbol angelegt wird, ist dessen Bezeichnung wie folgt anzugeben: StringFormat (SQUARE_BMP_LABEL_NAME, 1,0), sodass sie „snake_square_1_0“ lautet.
Die Klassen
Für die Programmierung des Spiels wurden in seiner Hauptdatei „Snake.mq5“ zwei benutzerdefinierte Klassen angelegt.
Die Klasse ChartFieldElement:
//+------------------------------------------------------------------+ //| CChartFieldElement class | //+------------------------------------------------------------------+ class CChartFieldElement:public CChartObjectBmpLabel { private: int pos_x,pos_y; public: int GetPosX(){return pos_x;} int GetPosY(){return pos_y;} //setting position (pos_x,pos_y) in internal coordinates void SetPos(int val_pos_x,int val_pos_y) { pos_x=(val_pos_x==-1)?COUNT_COLUMNS-1:((val_pos_x==COUNT_COLUMNS)?0:val_pos_x); pos_y=(val_pos_y==-1)?COUNT_ROWS-1:((val_pos_y==COUNT_ROWS)?0:val_pos_y); } //conversion of internal coordinates to absolute and object movement on the chart void Move(int start_pos_x,int start_pos_y) { X_Distance(start_pos_x+pos_x*SQUARE_WIDTH-pos_x+(SQUARE_WIDTH-X_Size())/2); Y_Distance(start_pos_y+pos_y*SQUARE_HEIGHT-pos_y+(SQUARE_HEIGHT-Y_Size())/2); } };
Die Klasse CChartFiledElement ist ein Ableger der Klasse CChartObjectBmpLabel, erweitert diese somit. Alle Objekte des Spielfeldes, z. B. die Zellen, Hindernisse sowie Kopf, Körper und Schwanz der Schlange und das Element „Futter“ sind Objekte dieser Klasse. Bei den Eigenschaften pos_x und pos_y handelt es sich um relative Koordinaten der Elemente auf dem Spielfeld, d. h. um die Nummern der Zeile und der Spalte, in denen das Element liegt. Die Methode SetPos legt diese Koordinaten fest. Die Methode Move wandelt die relativen Koordinaten in Abstände bezüglich der x- und y-Achsen in Bildpunkte (Pixel) um, und verlagert das Element. Dazu werden die Methoden X_Distance und YDistance der Klasse CChartObjectBmpLabel aufgerufen.
Die Klasse CSnakeGame:
//+------------------------------------------------------------------+ //| CSnakeGame class | //+------------------------------------------------------------------+ class CSnakeGame { private: CArrayObj *square_obj_arr; //Array of playing field cells CArrayObj *control_panel_obj_arr; //Array of control panel buttons CArrayObj *status_panel_obj_arr; //Array of control panel edits CArrayObj *obstacle_obj_arr; //Array of an obstacles CArrayObj *food_obj_arr; //Array of "Food" CArrayObj *snake_element_obj_arr; //Array of snake elements CChartObjectButton *header; //Header int direction; //Snake movement direction int current_lives; //Number of snake Lives int current_level; //Level int header_left; //Left position of a header (X) int header_top; //Top position of a header (Y) public: //class constructor void CSnakeGame() { current_lives=LIVES_SNAKE; current_level=0; header_left=START_POS_X; header_top=START_POS_Y; } //method for definition of header_left and header_top fields void SetHeaderPos(int val_header_left,int val_header_top) { header_left=val_header_left; header_top=val_header_top; }; //Get/Set direction methods void SetDirection(int d){direction=d;} int GetDirection(){return direction;} //Header creation and deletion methods void CreateHeader(); void DeleteHeader(); //Playing field creation, movement and deletion methods void CreateField(); void FieldMoveOnChart(); void DeleteField(); //Obstacle creation, movement and deletion methods void CreateObstacle(); void ObstacleMoveOnChart(); void DeleteObstacle(); //Snake creation, movement and deletion methods void CreateSnake(); void SnakeMoveOnChart(); void SnakeMoveOnField(); //snake movement on the playing field void SetTrueSnake(); //setting the images of the current snake's head and tail int Check(); //check for the collision with the playing field elements void DeleteSnake(); //Food creation, movement and deletion methods void CreateFood(); void FoodMoveOnChart(); void FoodMoveOnField(int food_num); void DeleteFood(); //Status panel creation, movement and deletion methods void CreateControlPanel(); void ControlPanelMoveOnChart(); void DeleteControlPanel(); //Control panel creation, movement and deletion methods void CreateStatusPanel(); void StatusPanelMoveOnChart(); void DeleteStatusPanel(); //Move all elements on the chart void AllMoveOnChart(); //Game initialization void Init(); //Game deinitialization void Deinit(); //Game control methods void StartGame(); void PauseGame(); void StopGame(); void ResetGame(); void NextLevel(); };
Bei der Klasse CSnakeGame handelt es sich um die Hauptklasse des Spiels, sie beinhaltet die Felder und die Methoden zur Umsetzung der Erstellung, Verlagerung und Löschung von Spielelementen. Wie wir sehen, werden am Anfang der Klasse Felder für den Aufbau dynamischer Datenfelder mit Adressenverweisen für Spielelemente deklariert. In dem Feld snake_element_obj_arr werden zum Beispiel die Adressenverweise der Schlangenelemente gespeichert. Die Kennziffer Null des Datenfeldes snake_element_obj_arr steht für den Kopf der Schlange, die letzte Kennziffer für ihren Schwanz. Wenn man das weiß, lässt sich die Schlange auf dem Spielfeld ganz einfach bewegen.
Im Weiteren werden alle Methoden der Klasse CSnakeGame festgelegt. Die Methoden werden auf der Grundlage der im „theoretischen Teil“ dieses Beitrags dargelegten Theorie umgesetzt.
Bearbeiten der Kopfzeile
//+------------------------------------------------------------------+ //| Header creation method | //+------------------------------------------------------------------+ void CSnakeGame::CreateHeader(void) { //creating a new object of CChartObjectButton class and specifying the properties of header of CSnakeGame class header=new CChartObjectButton; header.Create(0,HEADER_BUTTON_NAME,0,header_left,header_top,HEADER_WIDTH,HEADER_HEIGHT); header.BackColor(HEADER_BACKGROUND); header.Color(HEADER_COLOR); header.Description(HEADER_BUTTON_TEXT); //the header is selectable header.Selectable(true); } //+------------------------------------------------------------------+ //| Header deletion method | //+------------------------------------------------------------------+ void CSnakeGame::DeleteHeader(void) { delete header; }
Bearbeiten des Spielfeldes
//+------------------------------------------------------------------+ //| Playing Field creation method | //+------------------------------------------------------------------+ void CSnakeGame::CreateField() { int i,j; CChartFieldElement *square_obj; //creating an object of CArrayObj class and assign the square_obj_arr properties of CSnakeGame class square_obj_arr=new CArrayObj(); for(i=0;i<COUNT_ROWS;i++) for(j=0;j<COUNT_COLUMNS;j++) { square_obj=new CChartFieldElement(); square_obj.Create(0,StringFormat(SQUARE_BMP_LABEL_NAME,i,j),0,0,0); square_obj.BmpFileOn(IMG_FILE_NAME_SQUARE); //specifying the internal coordinates of the cell square_obj.SetPos(j,i); square_obj_arr.Add(square_obj); } //moving the playing field cells FieldMoveOnChart(); ChartRedraw(); } //+------------------------------------------------------------------+ //| The movement of playing field cells on the chart | //+------------------------------------------------------------------+ void CSnakeGame::FieldMoveOnChart() { CChartFieldElement *square_obj; int i; i=0; while((square_obj=square_obj_arr.At(i))!=NULL) { square_obj.Move(header_left,header_top+HEADER_HEIGHT); i++; } ChartRedraw(); } //+------------------------------------------------------------------+ //| Deletion of a playing field | //+------------------------------------------------------------------+ void CSnakeGame::DeleteField() { delete square_obj_arr; ChartRedraw(); }
Bearbeiten der Hindernisse
//+------------------------------------------------------------------+ //| Creation of the obstacles | //+------------------------------------------------------------------+ void CSnakeGame::CreateObstacle() { int i,j; CChartFieldElement *obstacle_obj; //creating an object of CArrayObj class and assign the obstacle_obj_arr properties of CSnakeGame class obstacle_obj_arr=new CArrayObj(); for(i=0;i<COUNT_ROWS;i++) for(j=0;j<COUNT_COLUMNS;j++) if(game_level[current_level][i][j]==9) { obstacle_obj=new CChartFieldElement(); obstacle_obj.Create(0,StringFormat(OBSTACLE_BMP_LABEL_NAME,i,j),0,0,0); obstacle_obj.BmpFileOn(IMG_FILE_NAME_OBSTACLE); //specifying the internal coordinates of the obstacle obstacle_obj.SetPos(j,i); obstacle_obj_arr.Add(obstacle_obj); } //moving the obstacle on the chart ObstacleMoveOnChart(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Obstacle movement method | //+------------------------------------------------------------------+ void CSnakeGame::ObstacleMoveOnChart() { CChartFieldElement *obstacle_obj; int i; i=0; while((obstacle_obj=obstacle_obj_arr.At(i))!=NULL) { obstacle_obj.Move(header_left,header_top+HEADER_HEIGHT); i++; } ChartRedraw(); } //+------------------------------------------------------------------+ //| Obstacle deletion method | //+------------------------------------------------------------------+ void CSnakeGame::DeleteObstacle() { delete obstacle_obj_arr; ChartRedraw(); }
Bearbeiten der Schlange
//+------------------------------------------------------------------+ //| Snake creation method | //+------------------------------------------------------------------+ void CSnakeGame::CreateSnake() { int i,j; CChartFieldElement *snake_element_obj,*snake_arr[]; ArrayResize(snake_arr,3); //creating an object of CArrayObj class and assign it to the snake_element_obj_arr properties of CSnakeGame class snake_element_obj_arr=new CArrayObj(); for(i=0;i<COUNT_COLUMNS;i++) for(j=0;j<COUNT_ROWS;j++) if(game_level[current_level][i][j]==1 || game_level[current_level][i][j]==2 || game_level[current_level][i][j]==3) { snake_element_obj=new CChartFieldElement(); snake_element_obj.Create(0,StringFormat(SNAKE_ELEMENT_BMP_LABEL_NAME,game_level[current_level][i][j]),0,0,0); snake_element_obj.BmpFileOn(IMG_FILE_NAME_SNAKE_BODY); //specifying the internal coordinates of the snake element snake_element_obj.SetPos(j,i); snake_arr[game_level[current_level][i][j]-1]=snake_element_obj; } snake_element_obj_arr.Add(snake_arr[0]); snake_element_obj_arr.Add(snake_arr[1]); snake_element_obj_arr.Add(snake_arr[2]); //moving the snake on the chart SnakeMoveOnChart(); //setting the correct images of the snake's head and tail SetTrueSnake(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Snake movement on the chart | //+------------------------------------------------------------------+ void CSnakeGame::SnakeMoveOnChart() { CChartFieldElement *snake_element_obj; int i; i=0; while((snake_element_obj=snake_element_obj_arr.At(i))!=NULL) { snake_element_obj.Move(header_left,header_top+HEADER_HEIGHT); i++; } ChartRedraw(); } //+------------------------------------------------------------------+ //| Snake movement on the playing field | //+------------------------------------------------------------------+ void CSnakeGame::SnakeMoveOnField() { int prev_x,prev_y,next_x,next_y,check; CChartFieldElement *snake_head_obj,*snake_body_obj,*snake_tail_obj; //getting the snake's head from the array snake_head_obj=snake_element_obj_arr.At(0); //saving the coordinates of a head prev_x=snake_head_obj.GetPosX(); prev_y=snake_head_obj.GetPosY(); //setting the new internal coordinates for the head depending on the movement direction switch(direction) { case DIRECTION_LEFT:snake_head_obj.SetPos(prev_x-1,prev_y);break; case DIRECTION_UP:snake_head_obj.SetPos(prev_x,prev_y-1);break; case DIRECTION_RIGHT:snake_head_obj.SetPos(prev_x+1,prev_y);break; case DIRECTION_DOWN:snake_head_obj.SetPos(prev_x,prev_y+1);break; } //moving the snake's head snake_head_obj.Move(header_left,header_top+HEADER_HEIGHT); //check for the snake's head collision with the other playing field elements (obstacle, snake body, food) check=Check(); //getting the last element of the snake's body snake_body_obj=snake_element_obj_arr.Detach(snake_element_obj_arr.Total()-2); //saving coordinates of the snake's body next_x=snake_body_obj.GetPosX(); next_y=snake_body_obj.GetPosY(); //moving the snake's body to the previous head's position snake_body_obj.SetPos(prev_x,prev_y); snake_body_obj.Move(header_left,header_top+HEADER_HEIGHT); //saving the previous position of the snake's body prev_x=next_x; prev_y=next_y; //inserting the snake's body to the first position of the snake_element_obj_arr array snake_element_obj_arr.Insert(snake_body_obj,1); //if the snake's head has collided with the "Food" if(check>=CRASH_FOOD) { //creating new element of the snake's body snake_body_obj=new CChartFieldElement(); snake_body_obj.Create(0,StringFormat(SNAKE_ELEMENT_BMP_LABEL_NAME,snake_element_obj_arr.Total()+1),0,0,0); snake_body_obj.BmpFileOn(IMG_FILE_NAME_SNAKE_BODY); //moving the body element to the end of the snake before the tail snake_body_obj.SetPos(prev_x,prev_y); snake_body_obj.Move(header_left,header_top+HEADER_HEIGHT); //adding the body to the penultimate position of the snake_element_obj_arr array snake_element_obj_arr.Insert(snake_body_obj,snake_element_obj_arr.Total()-1); //if snake's body isn't equal to the maximal snake length if(snake_element_obj_arr.Total()!=MAX_LENGTH_SNAKE) { //moving the eaten element on the new place on the playing field FoodMoveOnField(check-CRASH_FOOD); } //else we generate the custom event, that indicates that current snake length is the maximal possible else EventChartCustom(0,2,0,0,""); } //else if there isn't collision with the food, moving the tail to the position of the snake's body else { snake_tail_obj=snake_element_obj_arr.At(snake_element_obj_arr.Total()-1); snake_tail_obj.SetPos(prev_x,prev_y); snake_tail_obj.Move(header_left,header_top+HEADER_HEIGHT); } //setting the correct images for the head and tail SetTrueSnake(); ChartRedraw(); //generating the custom event for periodic call of this snake movement function EventChartCustom(0,0,0,0,""); Sleep(SPEED_SNAKE); } //+------------------------------------------------------------------+ //| Setting the correct images for the snake's head and tail | //+------------------------------------------------------------------+ void CSnakeGame::SetTrueSnake() { CChartFieldElement *snake_head,*snake_body,*snake_tail; int total,x1,x2,y1,y2; total=snake_element_obj_arr.Total(); //getting the snake's head snake_head=snake_element_obj_arr.At(0); //saving position of a head x1=snake_head.GetPosX(); y1=snake_head.GetPosY(); //getting the first element of the snake's body snake_body=snake_element_obj_arr.At(1); //saving coordinates of the body x2=snake_body.GetPosX(); y2=snake_body.GetPosY(); //choosing the file with an image depening on the position of the head and the first body element relative to each other //setting the snake's movement direction depending on the snake's head direction if(x1-x2==1 || x1-x2==-(COUNT_COLUMNS-1)) { snake_head.BmpFileOn(IMG_FILE_NAME_SNAKE_HEAD_RIGHT); direction=DIRECTION_RIGHT; } else if(y1-y2==1 || y1-y2==-(COUNT_ROWS-1)) { snake_head.BmpFileOn(IMG_FILE_NAME_SNAKE_HEAD_DOWN); direction=DIRECTION_DOWN; } else if(x1-x2==-1 || x1-x2==COUNT_COLUMNS-1) { snake_head.BmpFileOn(IMG_FILE_NAME_SNAKE_HEAD_LEFT); direction=DIRECTION_LEFT; } else { snake_head.BmpFileOn(IMG_FILE_NAME_SNAKE_HEAD_UP); direction=DIRECTION_UP; } //getting the last element of the snake's body snake_body=snake_element_obj_arr.At(total-2); //saving coordinates of the body x1=snake_body.GetPosX(); y1=snake_body.GetPosY(); //getting the tail of the snake snake_tail=snake_element_obj_arr.At(total-1); //saving coordinates of the tail x2=snake_tail.GetPosX(); y2=snake_tail.GetPosY(); //choosing the file with an image depening on the position of the tail and the last body element relative to each other if(x1-x2==1 || x1-x2==-(COUNT_COLUMNS-1)) snake_tail.BmpFileOn(IMG_FILE_NAME_SNAKE_TAIL_RIGHT); else if(y1-y2==1 || y1-y2==-(COUNT_ROWS-1)) snake_tail.BmpFileOn(IMG_FILE_NAME_SNAKE_TAIL_DOWN); else if(x1-x2==-1 || x1-x2==COUNT_COLUMNS-1) snake_tail.BmpFileOn(IMG_FILE_NAME_SNAKE_TAIL_LEFT); else snake_tail.BmpFileOn(IMG_FILE_NAME_SNAKE_TAIL_UP); } //+------------------------------------------------------------------+ //| Check for snake's head collision with the playing field elements | //+------------------------------------------------------------------+ int CSnakeGame::Check() { int i; CChartFieldElement *snake_head_obj,*snake_element_obj,*obstacle_obj,*food_obj; //getting the snake's head snake_head_obj=snake_element_obj_arr.At(0); i=0; //check for the head's collision with the obstacle while((obstacle_obj=obstacle_obj_arr.At(i))!=NULL) { if(snake_head_obj.GetPosX()==obstacle_obj.GetPosX() && snake_head_obj.GetPosY()==obstacle_obj.GetPosY()) { EventChartCustom(0,1,0,0,""); return CRASH_OBSTACLE_OR_SNAKE; } i++; } i=0; //check for the collision of head with the food while((food_obj=food_obj_arr.At(i))!=NULL) { if(snake_head_obj.GetPosX()==food_obj.GetPosX() && snake_head_obj.GetPosY()==food_obj.GetPosY()) { //hiding the food food_obj.Background(true); return(CRASH_FOOD+i); } i++; } i=3; //check for the collision of a head with the body and tail while((snake_element_obj=snake_element_obj_arr.At(i))!=NULL) { //we don't check for the collision with the last snake's element, because it hasn't been moved yet if(snake_element_obj_arr.At(i+1)==NULL) break; if(snake_head_obj.GetPosX()==snake_element_obj.GetPosX() && snake_head_obj.GetPosY()==snake_element_obj.GetPosY()) { EventChartCustom(0,1,0,0,""); //hiding the snake's element we have collided snake_element_obj.Background(true); return CRASH_OBSTACLE_OR_SNAKE; } i++; } return CRASH_NO; } //+------------------------------------------------------------------+ //| Snake deletion | //+------------------------------------------------------------------+ void CSnakeGame::DeleteSnake() { delete snake_element_obj_arr; ChartRedraw(); }
Nach der Verlagerung des Schlangenkopfes prüft die Funktion Check(), ob es zu einem Zusammenstoß gekommen ist, und gibt den entsprechenden Bezeichner für den Zusammenstoß aus.
Die Funktion SetTrueSnake() sorgt für die korrekte Abbildung des Kopfes und des Schwanzes der Schlange in Abhängigkeit von der Lage der ihnen am nächsten liegenden Elemente.
Bearbeiten der Elemente, die als Schlangenfutter dienen
//+------------------------------------------------------------------+ //| Food creation | //+------------------------------------------------------------------+ void CSnakeGame::CreateFood() { int i; CChartFieldElement *food_obj; MathSrand(uint(TimeLocal())); //creating an object of CArrayObj class and assign it to the food_obj_arr properties of CSnakeGame class food_obj_arr=new CArrayObj(); i=0; while(i<COUNT_FOOD) { //creating the food food_obj=new CChartFieldElement; food_obj.Create(0,StringFormat(FOOD_BMP_LABEL_NAME,i),0,0,0); food_obj.BmpFileOn(IMG_FILE_NAME_FOOD); food_obj_arr.Add(food_obj); //setting the field coordinates on the field and moving it on the playing field FoodMoveOnField(i); i++; } } //+------------------------------------------------------------------+ //| Food movement method | //+------------------------------------------------------------------+ void CSnakeGame::FoodMoveOnChart() { CChartFieldElement *food_obj; int i; i=0; while((food_obj=food_obj_arr.At(i))!=NULL) { food_obj.Move(header_left,header_top+HEADER_HEIGHT); i++; } ChartRedraw(); } //+---------------------------------------------------------------------------+ //| A method to set coordinates of a food and to move it on the playing field | //+---------------------------------------------------------------------------+ void CSnakeGame::FoodMoveOnField(int food_num) { int i,j,k,n,m; CChartFieldElement *snake_element_obj,*food_obj; CChartObjectEdit *edit_obj; //setting a new value for "Foods left" on the status panel edit_obj=status_panel_obj_arr.At(1); edit_obj.Description(StringFormat(spaces2+FOOD_LEFT_OVER_EDIT_TEXT,MAX_LENGTH_SNAKE-snake_element_obj_arr.Total())); bool b; b=false; k=0; //generating randomly the food coordinates until the we get the free cells while(true) { //generating a row number i=(int)(MathRand()/32767.0*(COUNT_ROWS-1)); //generating a column number j=(int)(MathRand()/32767.0*(COUNT_COLUMNS-1)); n=0; //check, if there are any elements of the snake while((snake_element_obj=snake_element_obj_arr.At(n))!=NULL) { if(j!=snake_element_obj.GetPosX() && i!=snake_element_obj.GetPosY()) b=true; else { b=false; break; } n++; } //checking for the other food presence if(b==true) { n=0; while((food_obj=food_obj_arr.At(n))!=NULL) { if(j!=food_obj.GetPosX() && i!=food_obj.GetPosY()) b=true; else { b=false; break; } n++; } } //checking for the presence of the obstacle if(b==true && game_level[current_level][i][j]!=9) break; k++; } food_obj=food_obj_arr.At(food_num); //show food food_obj.Background(false); //setting new coordinates food_obj.SetPos(j,i); //moving the food food_obj.Move(header_left,header_top+HEADER_HEIGHT); ChartRedraw(); } //+------------------------------------------------------------------+ //| Food deletion | //+------------------------------------------------------------------+ void CSnakeGame::DeleteFood() { delete food_obj_arr; ChartRedraw(); }
Die Anordnung des „Futters“ auf dem Spielfeld erfolgt zufällig unter der Bedingung, dass die Zellen des Feldes, in denen das Futter platziert werden soll, keine anderen Elemente enthalten.
Bearbeiten der Zustandsanzeige
//+------------------------------------------------------------------+ //| Status Panel Creation | //+------------------------------------------------------------------+ void CSnakeGame::CreateStatusPanel() { CChartObjectEdit *edit_obj; //creating an object of CArrayObj class and assign it to the status_panel_obj_arr properties of CSnakeGame class status_panel_obj_arr=new CArrayObj(); //creating the "Level" edit edit_obj=new CChartObjectEdit; edit_obj.Create(0,LEVEL_EDIT_NAME,0,0,0,CONTROL_WIDTH,CONTROL_HEIGHT); edit_obj.BackColor(STATUS_BACKGROUND); edit_obj.Color(STATUS_COLOR); edit_obj.Description(StringFormat(spaces6+LEVEL_EDIT_TEXT,current_level,MAX_LEVEL)); edit_obj.Selectable(false); edit_obj.ReadOnly(true); status_panel_obj_arr.Add(edit_obj); //creating the "Food left over" edit edit_obj=new CChartObjectEdit; edit_obj.Create(0,FOOD_LEFT_OVER_EDIT_NAME,0,0,0,CONTROL_WIDTH,CONTROL_HEIGHT); edit_obj.BackColor(STATUS_BACKGROUND); edit_obj.Color(STATUS_COLOR); edit_obj.Description(StringFormat(spaces2+FOOD_LEFT_OVER_EDIT_TEXT,MAX_LENGTH_SNAKE-3)); edit_obj.Selectable(false); edit_obj.ReadOnly(true); status_panel_obj_arr.Add(edit_obj); //creating the "Lives" edit edit_obj=new CChartObjectEdit; edit_obj.Create(0,LIVES_EDIT_NAME,0,0,0,CONTROL_WIDTH,CONTROL_HEIGHT); edit_obj.BackColor(STATUS_BACKGROUND); edit_obj.Color(STATUS_COLOR); edit_obj.Description(StringFormat(spaces8+LIVES_EDIT_TEXT,current_lives)); edit_obj.Selectable(false); edit_obj.ReadOnly(true); status_panel_obj_arr.Add(edit_obj); //moving the status panel StatusPanelMoveOnChart(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Status Panel movement method | //+------------------------------------------------------------------+ void CSnakeGame::StatusPanelMoveOnChart() { CChartObjectEdit *edit_obj; int x,y,i; x=header_left; y=header_top+HEADER_HEIGHT+COUNT_ROWS*(SQUARE_HEIGHT-1)+1; i=0; while((edit_obj=status_panel_obj_arr.At(i))!=NULL) { edit_obj.X_Distance(x+i*CONTROL_WIDTH); edit_obj.Y_Distance(y); i++; } ChartRedraw(); } //+------------------------------------------------------------------+ //| Status Panel deletion method | //+------------------------------------------------------------------+ void CSnakeGame::DeleteStatusPanel() { delete status_panel_obj_arr; ChartRedraw(); }
Bearbeiten des Bedienfeldes
//+------------------------------------------------------------------+ //| Control Panel creation method | //+------------------------------------------------------------------+ void CSnakeGame::CreateControlPanel() { CChartObjectButton *button_obj; //creating an object of CArrayObj class and assign it to the control_panel_obj_arr properties of CSnakeGame class control_panel_obj_arr=new CArrayObj(); //creating the "Start" button button_obj=new CChartObjectButton; button_obj.Create(0,START_GAME_BUTTON_NAME,0,0,0,CONTROL_WIDTH,CONTROL_HEIGHT); button_obj.BackColor(CONTROL_BACKGROUND); button_obj.Color(CONTROL_COLOR); button_obj.Description(START_GAME_BUTTON_TEXT); button_obj.Selectable(false); control_panel_obj_arr.Add(button_obj); //creating the "Pause" button button_obj=new CChartObjectButton; button_obj.Create(0,PAUSE_GAME_BUTTON_NAME,0,0,0,CONTROL_WIDTH,CONTROL_HEIGHT); button_obj.BackColor(CONTROL_BACKGROUND); button_obj.Color(CONTROL_COLOR); button_obj.Description(PAUSE_GAME_BUTTON_TEXT); button_obj.Selectable(false); control_panel_obj_arr.Add(button_obj); //creating the "Stop" button button_obj=new CChartObjectButton; button_obj.Create(0,STOP_GAME_BUTTON_NAME,0,0,0,CONTROL_WIDTH,CONTROL_HEIGHT); button_obj.BackColor(CONTROL_BACKGROUND); button_obj.Color(CONTROL_COLOR); button_obj.Description(STOP_GAME_BUTTON_TEXT); button_obj.Selectable(false); control_panel_obj_arr.Add(button_obj); //moving the control panel ControlPanelMoveOnChart(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Control Panel movement method | //+------------------------------------------------------------------+ void CSnakeGame::ControlPanelMoveOnChart() { CChartObjectButton *button_obj; int x,y,i; x=header_left; y=header_top+HEADER_HEIGHT+COUNT_ROWS*(SQUARE_HEIGHT-1)+1; i=0; while((button_obj=control_panel_obj_arr.At(i))!=NULL) { button_obj.X_Distance(x+i*CONTROL_WIDTH); button_obj.Y_Distance(y+CONTROL_HEIGHT); i++; } ChartRedraw(); } //+------------------------------------------------------------------+ //| Control Panel deletion method | //+------------------------------------------------------------------+ void CSnakeGame::DeleteControlPanel() { delete control_panel_obj_arr; ChartRedraw(); }
Verlagern aller Spielelemente sowie Bereitstellung und Bereinigung des Spiels
//+------------------------------------------------------------------+ //| Game elements movement method | //+------------------------------------------------------------------+ void CSnakeGame::AllMoveOnChart() { FieldMoveOnChart(); StatusPanelMoveOnChart(); ControlPanelMoveOnChart(); ObstacleMoveOnChart(); SnakeMoveOnChart(); FoodMoveOnChart(); } //+------------------------------------------------------------------+ //| Game initialization | //+------------------------------------------------------------------+ void CSnakeGame::Init() { CreateHeader(); CreateField(); CreateStatusPanel(); CreateControlPanel(); CreateObstacle(); CreateSnake(); CreateFood(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Game deinitialization | //+------------------------------------------------------------------+ void CSnakeGame::Deinit() { DeleteFood(); DeleteSnake(); DeleteObstacle(); DeleteControlPanel(); DeleteStatusPanel(); DeleteField(); DeleteHeader(); }
Steuerung des Spiels
//+------------------------------------------------------------------+ //| Dummy Start game method | //+------------------------------------------------------------------+ void CSnakeGame::StartGame() { return; } //+------------------------------------------------------------------+ //| Dummy Pause game method | //+------------------------------------------------------------------+ void CSnakeGame::PauseGame() { return; } //+------------------------------------------------------------------+ //| Stop game method | //+------------------------------------------------------------------+ void CSnakeGame::StopGame() { CChartObjectEdit *edit_obj; current_level=0; current_lives=LIVES_SNAKE; //setting new value for the "Level" field of the status panel edit_obj=status_panel_obj_arr.At(0); edit_obj.Description(StringFormat(spaces6+LEVEL_EDIT_TEXT,current_level,MAX_LEVEL)); //setting new value for the "Lives" field of the status panel edit_obj=status_panel_obj_arr.At(2); edit_obj.Description(StringFormat(spaces8+LIVES_EDIT_TEXT,current_lives)); DeleteFood(); DeleteSnake(); DeleteObstacle(); CreateObstacle(); CreateSnake(); CreateFood(); } //+------------------------------------------------------------------+ //| Level restart method | //+------------------------------------------------------------------+ void CSnakeGame::ResetGame() { CChartObjectEdit *edit_obj; if(current_lives-1==-1)StopGame(); else { //decreasing the number of lives current_lives--; //updating it at the status panel edit_obj=status_panel_obj_arr.At(2); edit_obj.Description(StringFormat(spaces8+LIVES_EDIT_TEXT,current_lives)); DeleteFood(); DeleteSnake(); CreateSnake(); CreateFood(); } } //+------------------------------------------------------------------+ //| Next level method | //+------------------------------------------------------------------+ void CSnakeGame::NextLevel() { CChartObjectEdit *edit_obj; current_lives=LIVES_SNAKE; //to the initial level if there isn't next level if(current_level+1>MAX_LEVEL)StopGame(); else { //else increasing the level and updating the startus panel contents current_level++; edit_obj=status_panel_obj_arr.At(0); edit_obj.Description(StringFormat(spaces6+LEVEL_EDIT_TEXT,current_level,MAX_LEVEL)); edit_obj=status_panel_obj_arr.At(2); edit_obj.Description(StringFormat(spaces8+LIVES_EDIT_TEXT,current_lives)); DeleteFood(); DeleteSnake(); DeleteObstacle(); CreateObstacle(); CreateSnake(); CreateFood(); } }
Verarbeiten von Ereignissen (endgültiger Programmcode)
// Declaring and creating an object of CSnakeGame type at global level CSnakeGame snake_field; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { snake_field.Init(); EventSetTimer(1); return(0); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { snake_field.Deinit(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnTimer() { //setting the buttons unpressed if(ObjectFind(0,START_GAME_BUTTON_NAME)>=0 && ObjectGetInteger(0,START_GAME_BUTTON_NAME,OBJPROP_STATE)==true) ObjectSetInteger(0,START_GAME_BUTTON_NAME,OBJPROP_STATE,false); if(ObjectFind(0,PAUSE_GAME_BUTTON_NAME)>=0 && ObjectGetInteger(0,PAUSE_GAME_BUTTON_NAME,OBJPROP_STATE)==true) ObjectSetInteger(0,PAUSE_GAME_BUTTON_NAME,OBJPROP_STATE,false); if(ObjectFind(0,STOP_GAME_BUTTON_NAME)>=0 && ObjectGetInteger(0,STOP_GAME_BUTTON_NAME,OBJPROP_STATE)==true) ObjectSetInteger(0,STOP_GAME_BUTTON_NAME,OBJPROP_STATE,false); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { long x,y; static bool press_key=true; static bool press_button=false; static bool move=false; //if key has been pressed and the snake has moved, let's specify the new movement direction if(id==CHARTEVENT_KEYDOWN && press_key==false) { if((lparam==VK_LEFT) && (snake_field.GetDirection()!=DIRECTION_LEFT && snake_field.GetDirection()!=DIRECTION_RIGHT)) snake_field.SetDirection(DIRECTION_LEFT); else if((lparam==VK_RIGHT) && (snake_field.GetDirection()!=DIRECTION_LEFT && snake_field.GetDirection()!=DIRECTION_RIGHT)) snake_field.SetDirection(DIRECTION_RIGHT); else if((lparam==VK_DOWN) && (snake_field.GetDirection()!=DIRECTION_UP && snake_field.GetDirection()!=DIRECTION_DOWN)) snake_field.SetDirection(DIRECTION_DOWN); else if((lparam==VK_UP) && (snake_field.GetDirection()!=DIRECTION_UP && snake_field.GetDirection()!=DIRECTION_DOWN)) snake_field.SetDirection(DIRECTION_UP); press_key=true; } //if "Start" button has been pressed and press_button=false if(id==CHARTEVENT_OBJECT_CLICK && sparam==START_GAME_BUTTON_NAME && press_button==false) { //waiting 1 second Sleep(1000); //generating new event for snake movement EventChartCustom(0,0,0,0,""); press_button=true; } //if "Pause" button has been pressed else if(id==CHARTEVENT_OBJECT_CLICK && sparam==PAUSE_GAME_BUTTON_NAME) { press_button=false; } //if "Stop" button has been pressed else if(id==CHARTEVENT_OBJECT_CLICK && sparam==STOP_GAME_BUTTON_NAME) { snake_field.StopGame(); press_key=true; press_button=false; } //processing of the snake movement event, if press_button=true else if(id==CHARTEVENT_CUSTOM && press_button==true) { snake_field.SnakeMoveOnField(); press_key=false; } //processing of the game restart event else if(id==CHARTEVENT_CUSTOM+1) { snake_field.ResetGame(); Sleep(1000); press_key=true; press_button=false; } //processing of the next level event else if(id==CHARTEVENT_CUSTOM+2) { snake_field.NextLevel(); Sleep(1000); press_key=true; press_button=false; } //processing of the header movement event else if(id==CHARTEVENT_OBJECT_DRAG && sparam==HEADER_BUTTON_NAME) { x=ObjectGetInteger(0,sparam,OBJPROP_XDISTANCE); y=ObjectGetInteger(0,sparam,OBJPROP_YDISTANCE); snake_field.SetHeaderPos(x,y); snake_field.AllMoveOnChart(); } } //+------------------------------------------------------------------+
Die beiden statischen Variablen press_key und press_button werden in der Ereignisverarbeitungsfunktion OnChartEvent eingerichtet.
Der Spielbeginn ist möglich, wenn der Wert der Variablen press_button = „false“ ist. Nach Betätigung der Schaltfläche „Start“ erhält die Variable press_button den Wert „true“ (wodurch die wiederholte Ausführung des Codes zum Starten des Spiels verhindert wird) und behält ihn bis zum Eintreten eines der folgenden Ereignisse:
- Neustart der aktuellen Stufe;
- Übergang zur nächsten Stufe;
- Spielunterbrechung (Betätigen der Schaltfläche „Pause“);
- Beendigung des Spiels (Betätigen der Schaltfläche „Stop“).
Eine Richtungsänderung bei der Verlagerung der Schlange ist nur dann möglich, wenn sie rechtwinklig zu der aktuellen Richtung erfolgt, sowie nachdem die Schlange mindestens eine Verlagerung auf dem Spielfeld vollzogen hat (dafür sorgt die Variable press_key) Diese Bedingungen spiegeln sich auch bei der Verarbeitung des Ereignisses CHARTEVENT_KEYDOWN (Betätigung einer Tastaturtaste) wider.
Bei der Verlagerung der Kopfzeile wird das Ereignis CHARTEVENT_OBJECT_DRAG erzeugt, anhand dessen die Felder header_left und header_top der Klasse CSnakeGame eingestellt werden. Entsprechend den Werten dieser Felder erfolgt die Verlagerung der übrigen Spielelemente.
Die Verlagerung des Spielfeldes erfolgt wie in dem Beispiel TradePad_Sample.
Fazit
In diesem Beitrag haben wir uns mit einem Beispiel für die Programmierung eines Schlangenspiels in MQL5 befasst.
Sie haben die Standardbibliothek, und zwar insbesondere die Klassen der Steuerelemente, dazu die Besonderheiten der Klasse CArrayObj sowie einige Verfahren für den regelmäßigen Aufruf der Funktion mit der Möglichkeit der Verarbeitung von Ereignissen kennengelernt.
Unten können Sie eine gepackte Datei mit einem Musterschlangenspiel herunterladen. Dieses Archiv muss in den Ordner terminal_data_folder entpackt werden.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/65
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.