English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Пример написания игры "Змейка" на MQL5

Пример написания игры "Змейка" на MQL5

MetaTrader 5Эксперты | 9 апреля 2010, 15:56
6 655 12
MRoVas
[Удален]

Введение

В данной статье рассматривается пример написания игры "Змейка" на MQL5.

Создание игр в 5-ой версии языка MQL стало возможным, в первую очередь, благодаря обработке событий, в том числе пользовательских. Поддержка же объектно-ориентированного программирования значительно упрощает проектирование такого рода программ, делает код более понятным, снижает в нем число ошибок.

Из статьи вы узнаете особенности обработки событий, возникающих при работе с графиком, примеры работы со стандартной библиотекой MQL5, а также способы циклического вызова функций через определенное время для проведения каких-либо расчетов.

Описание игры

Выбор "Змейки" в качестве примера игры на MQL5 связан, в первую очередь, с простотой ее реализации. Каждый, кто в той или иной мере увлекался программированием, способен написать данную игру.

Согласно Википедии:

Snake (Питон, Удав, Змейка) - компьютерная игра, возникшая в середине или конце 1970-х.

Игрок управляет длинным, тонким существом, напоминающим змею, которое ползает по плоскости (как правило, ограниченной стенками), собирая еду (или другие предметы), избегая столкновения с собственным хвостом и краями игрового поля. В некоторых вариантах на поле присутствуют дополнительные препятствия. Каждый раз, когда змея съедает кусок пищи, она становится длиннее, что постепенно усложняет игру. Игрок управляет направлением движения головы змеи (обычно 4 направления: вверх, вниз, влево, вправо), а хвост змеи движется следом. Игрок не может остановить движение змеи.

В данной реализации на MQL5 игра будет иметь ряд ограничений и особенностей.

Количество уровней равно 6 (от 0 до 5). В каждом уровне доступно 5 жизней. При использовании всех жизней, а также при прохождении всех уровней, происходит переход на начальный уровень. Возможно создание собственных уровней. Скорость змейки и ее максимальная длина одинаковы на каждом уровне.

Область игры состоит из четырех элементов:

  1. Заголовок игры. Используется для позиционирования области игры на графике. Перемещая заголовок, перемещаются все элементы игры.
  2. Игровое поле. Представляет собой массив (таблицу) клеток размеров 20x20. Каждая клетка имеет размер 20x20 пикселей. На игровом поле располагаются:
    • Змейка. Состоит как минимум из трех последовательно расположенных элементов - голова, тело, хвост. Голова может перемещаться влево, вправо, вверх и вниз. Все остальные элементы змейки перемещаются вслед за ее головой. 
    • Препятствие. Представляет собой черный прямоугольник, при столкновении с которым головы змейки, текущий уровень перезапускается, а количество жизней уменьшается на одну.
    • Предмет "еда". Ягодка, при столкновении с которой размер змейки увеличивается, а точнее увеличивается ее тело. Съев 12 предметов, происходит переход на следующий уровень.
  3. Информационная панель (панель состояния игры). Состоит из трех элементов:
    • Level. Показывает текущий уровень.
    • Food left over. Показывает, сколько осталось съесть ягод.
    • Lives. Показывает количество доступных жизней.
  4. Панель управления. Состоит из трех кнопок:
    • Кнопка "Start". Запускает текущий уровень.
    • Кнопка "Pause". Приостанавливает игру.
    • Кнопка "Stop". Останавливает игру, при этом происходит переход на начальный уровень.

Все перечисленные элементы можно увидеть на рисунке 1:


Рисунок 1. Элементы игры "Змейка"

Заголовок игры представляет собой объект типа "Кнопка". Все элементы игрового поля - объекты типа "Графическая метка". Информационная панель состоит их трех объектов типа "Поле ввода", панель управления из трех объектов "Кнопка". Все объекты позиционируются заданием расстояния по осям X и Y в пикселах относительно верхнего левого угла графика.

Стоит отметить, что края игрового поля не представляют препятствие для змейки. Заходя, например, за левую границу, змейка появляется справа. Это можно видеть на рисунке 2:


Рисунок 2. Прохождение змейки через края игрового поля

Голова змейки и ее хвост, в отличие от тела, могут быть повернуты. Направление головы задается направлением движения змейки или положением ее соседнего элемента. Направление хвоста - только положением соседнего элемента. 

Если, например, соседний элемент хвоста находится слева от него, то хвост повернут влево. Немного иначе можно сказать насчет головы. Голова повернута влево, если ее соседний элемент находится справа. Примеры направления головы и хвоста можно увидеть на рисунках ниже. Обратите внимание на поворот головы и хвоста относительно их соседних элементов.





Голова и хвост направлены влевоГолова и хвост направлены вправоГолова и хвост направлены внизГолова и хвост направлены вверх 


Перемещение змейки осуществляется в три этапа:

  1. Перемещение головы на одну клетку вправо, влево, вверх или вниз в зависимости от направления.
  2. Перемещение последнего элемента тела змейки на предыдущее место головы.
  3. Перемещение хвоста змейки на предыдущее место последнего элемента тела.

Если змейка съедает предмет, то перемещение хвоста не происходит. Вместо этого создается новый элемент тела, который перемещается на предыдущее место последнего элемента тела. 

На рисунках ниже можно увидеть пример перемещения змейки влево.





Исходное положениеПеремещение головы на одну клетку влевоПеремещение последнего элемента тела
на предыдущее место головы
Перемещение хвоста на предыдущее место
последнего элемента тела

Теоретическая часть 

Далее будут рассмотрены те средства и приемы, которые используются при написании игры.

Стандартная библиотека

Для манипулирования (создания, перемещения, удаления) однотипными объектами игры (например, клетками игрового поля, элементами змейки) удобно использовать массивы таких объектов. Организацию таких массивов и объектов можно осуществить, используя классы стандартной библиотеки MQL5.

Стандартная библиотека MQL5 позволяет облегчить написание программ. При написании игры будем пользоваться следующими классами данной библиотеки:

  1. CArrayObj - класс для организации данных (динамический массив указателей).
  2. CChartObjectEditCChartObjectButtonCChartObjectBmpLabel - классы элементов управления, представляющие, соответственно, "Поле ввода", "Кнопку" и "Графическую метку"

Для того чтобы воспользоваться классами из стандартной библиотеки необходимо подключить их директивой:

#include <путь_к_файлу_с_описанием_классов>

Например, для подключения CChartObjectButton необходимо написать:

#include <ChartObjects\ChartObjectsTxtControls.mqh>

Пути к файлам можно найти в справочнике.

При работе с классами из стандартной библиотеки важно понимать, что некоторые из них наследуют друг друга. Например, CChartObjectButton наследует CChartObjectEdit, в свою очередь CChartObjectEdit наследует CChatObjectLabel и т.д. Это означает, что классу-наследнику доступны свойства и методы класса-родителя.

Чтобы понять преимущества использования в программах классов стандартной библиотеки, рассмотрим пример, в котором создадим кнопку двумя способами (без использования и с использованием классов).

Пример без использования классов:

//создаем кнопку с именем "button"
ObjectCreate(0,"button",OBJ_BUTTON,0,0,0);
//задаем текст на кнопке
ObjectSetString(0,"button",OBJPROP_TEXT,"Button text");
//задаем размеры кнопки
ObjectSetInteger(0,"button",OBJPROP_XSIZE,100);
ObjectSetInteger(0,"button",OBJPROP_YSIZE,20);
//задаем положение кнопки
ObjectSetInteger(0,"button",OBJPROP_XDISTANCE,10);
ObjectSetInteger(0,"button",OBJPROP_YDISTANCE,10);

Пример с использованием классов:

CChartObjectButton *button;
//создаем объект класса CChartObjectButton и присваиваем указатель на него переменной button
button=new CChartObjectButton;
//создаем кнопку шириной 100, высотой 20 пикселей, с положением по X=10 и по Y=10 пикселей
button.Create(0,"button",0,10,10,100,20);
//задаем текст на кнопке
button.Description("Button text");

Как видно, работать с классами проще. Кроме того, объекты классов можно хранить в массивах и легко их обрабатывать.

Методы и свойства классов элементов управления хорошо и понятно описаны в справочнике по стандартной библиотеке.

Для организации массива объектов будем использовать класс CArrayObj из стандартной библиотеки, избавляющий пользователя от многих рутинных операций (например, изменение размера массива при добавлении нового элемента, уничтожение всех объектов в массиве и др.).

Особенности класса CArrayObj

Класс CArrayObj позволяет организовать динамический массив указателей на объекты класса CObject. CObject является родительским классом всех классов стандартной библиотеки. Это означает, что мы можем создать динамический массив указателей на объекты любого класса стандартной библиотеки. Если необходимо создать динамический массив объектов своего класса, то ему следует наследовать CObject.

В следующем примере компилятор не выдаст ошибки, т.к. пользовательский класс является наследником CObject:

#include <Arrays\ArrayObj.mqh>
class CMyClass:public CObject
  {
   //поля и методы
  };
//создаем объект класса CMyClass и присваиваем указатель на этот объект переменной my_obj
CMyClass *my_obj=new CMyClass;
//объявляем динамический массив указателей
CArrayObj array_obj;
//добавляем указатель на объект my_obj в конец динамического массива указателей array_obj
array_obj.Add(my_obj);

В следующем случае компилятор выдаст ошибку, т.к. my_obj не является указателем на объект класса CObject или класса, наследующего CObject:

#include <Arrays\ArrayObj.mqh>
class CMyClass
  {
   //поля и методы
  };
//создаем объект класса CMyClass и присваиваем указатель на этот объект переменной my_obj
CMyClass *my_obj=new CMyClass;
//объявляем динамический массив указателей
CArrayObj array_obj;
//добавляем указатель на объект my_obj в конец динамического массива указателей array_obj
array_obj.Add(my_obj);

При написании игры будут использоваться следующие методы класса CArrayObj:

  • Add. Добавляет элемент в конец массива.
  • Insert. Вставляет элемент в указанную позицию массива.
  • Detach. Изымает элемент из указанной позиции (элемент удаляется из массива).
  • Totat. Получает количество элементов в массиве.
  • At. Получает элемент из указанной позиции (элемент не удаляется из массива).
Далее приведен пример работы с классом 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()
  {
   //создаем указатель на объект класса CArrayObj
   CArrayObj *array_obj=new CArrayObj();
   //объявляем указатель на объект класса CMyClass
   CMyClass *my_obj;
   //заполняем динамический массив array_obj
   for(int i='a';i<='c';i++)
     {
      //создаем объект класса CMyClass
      my_obj=new CMyClass();
      my_obj.s=char(i);
      //добавляем объект класса CMyClass в конец динамического массива array_obj
      array_obj.Add(my_obj);
     }
   //выводим результат
   MyPrint(array_obj);
   //создаем новый объект класса CMyClass
   my_obj=new CMyClass();
   my_obj.s='d';
   //вставляем в первую позицию массива новый элемент
   array_obj.Insert(my_obj,1);
   //выводим результат
   MyPrint(array_obj);
   //извлекаем элемент из третьей позиции массива
   my_obj=array_obj.Detach(2);
   //выводим результат
   MyPrint(array_obj);
   //удаляем динамический массив и все объекты, чьи указатели хранятся в нем
   delete array_obj;
   return(0);
  }

В данном примере в функции OnInit создается динамический массив из трех элементов. Вывод содержимого массива осуществляется вызовом функции MyPrint.

После заполнения массива с помощью метода Add, его содержимое можно представить, как {a,b,c}. 

После применения метода Insert содержимое массива можно представить, как {a,d,b,c}.

Наконец, после применения метода Detach массив будет выглядеть, как {a,d,c}.

При применения оператора delete к переменной array_obj вызывается деструктор класса CArrayObj, который удаляет не только массив array_obj, но и объекты, чьи указатели в нем хранятся. Чтобы этого не произошло, перед применением команды delete необходимо установить флаг управления памятью класса CArrayObj в значение false. Данный флаг устанавливается методом FreeMode.

Таким образом, если нет необходимости при удалении динамического массива удалять объекты, указатели на которые в нем хранятся, необходимо написать так:

array_obj.FreeMode(false);
delete array_obj;

Обработка событий

При возникновении множества событий, они накапливаются в очереди, откуда последовательно поступают в функцию обработки событий.

Для обработки событий, возникающих при работе с графиком, а также пользовательских событий, в MQL5 имеется функция OnChartEvent. Каждое событие имеет идентификатор и параметры, которые передаются функции OnChartEvent

OnChartEvent вызывается только тогда, когда поток выполнения находится вне всех остальных функций программы. Таким образом, в следующем примере OnChartEvent никогда не получит управление.

#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)
     {
      //код, требующий периодического выполнения
     }
  }
//+------------------------------------------------------------------+
//| 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");
  }

Бесконечный цикл while не дает потоку выполнения программы выйти из функции MyFunction. Функция OnChartEvent не может получить управление. Таким образом, нажатие на созданную кнопку не приводит к вызову Alert.

Периодическое выполнение кода с возможностью обработки событий

В игре для перемещения змейки через определенный интервал времени необходим периодический вызов функции перемещения с возможностью обработки событий. Но как было показано выше, бесконечный цикл приводит к тому, что функция OnChartEvent не вызывается, и обработка событий становится невозможной.

В связи с этим необходимо придумать другой способ периодического выполнения кода.

Использование OnTimer

В языке MQL5 есть специальная функция OnTimer, которая периодически вызывается через заранее установленное количество секунд. Для это используется команда EventSetTimer.

Тогда предыдущий пример можно переписать так:

#include <ChartObjects\ChartObjectsTxtControls.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void MyFunction()
  {
   //код, требующий периодического выполнения
  }
//+------------------------------------------------------------------+
//| 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");
  }

В функции OnInit создается кнопка и задается период вызова функции OnTimer, равный одной секунде. Каждую секунду происходит вызов функции OnTimer, из которой вызывается код (функция MyFunction), требующий периодических расчетов.

Но стоит обратить внимание на то, что период вызова OnTimer кратен секундам. Поэтому для вызова функции через определенное количество миллисекунд необходим другой способ. Этот способ заключается в применении пользовательских событий.

Использование пользовательских событий

Пользовательское событие генерируется функцией EventChartCustom, в параметрах которой задается идентификатор события и его параметры. Возможно задание до 65536 идентификаторов пользовательских событий - от 0 до 65535. При вызове функции EventChartCustom компилятор MQL5 автоматически добавляет к идентификатору константу CHARTEVENT_CUSTOM для того, чтобы отличать пользовательские события от других видов событий. Таким образом, реальный диапазон идентификаторов пользовательских событий - от CHARTEVENT_CUSTOM до CHARTEVENT_CUSTOM+65535 (CHARTEVENT_CUSTOM_LAST).

Ниже приведен пример периодического вызова функции MyFunction с использованием пользовательских событий:

#include <ChartObjects\ChartObjectsTxtControls.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void MyFunction()
  {
   //код, требующий периодического расчета
   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);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
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();
  }

В данном примере перед выходом из функции MyFunction происходит задержка в 200 мс (время периодического вызова данной функции) и генерируется пользовательское событие. Функция OnChartEvent обрабатывает очередь событий, и когда встречается пользовательское, снова вызывает функцию MyFunction. Таким образом реализуется периодический вызов функции MyFunction, при этом возможный период вызова кратен миллисекундам.

Практическая часть

Далее будет рассмотрен непосредственно пример написания игры "Змейка".

Определение констант и карты уровней

Карта уровней располагается в отдельном включаемом (заголовочном) файле "Snake.mqh" и представляет собой трехмерный массив game_level[6][20][20]. Каждый элемент массива представляет собой двумерный массив - описание отдельного уровня. Если элемент данного массива равен 9, то это препятствие. Если элемент равен 1, 2 или 3, то это соответственно, голова, тело или хвост змеи, задающие ее начальное положение на игровом поле. В массив level можно добавить новые уровни или изменить существующие.

Также в файле "Snake.mqh" определены константы, используемые для написания игры. Переопределив, например, константы SPEED_SNAKE и MAX_LENGTH_SNAKE, можно увеличить/уменьшить скорость змейки и ее максимальную длину в каждом уровне. Все константы содержат после себя комментарии.

//+------------------------------------------------------------------+
//|                                                        Snake.mqh |
//|                                        Copyright Roman Martynyuk |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Roman Martynyuk"
#property link      "http://www.mql5.com"
#include <VirtualKeys.mqh>                                                    //файл номеров клавиш клавиатуры
#include <Arrays\ArrayObj.mqh>                                                //файл определения класса CArrayObj
#include <ChartObjects\ChartObjectsBmpControls.mqh>                           //файл определения класса CChartObjectBmpLabel
#include <ChartObjects\ChartObjectsTxtControls.mqh>                           //файл опред. классов CChartObjectButton и CChartObjectEdit
#define CRASH_NO                          0                                   //нет удара
#define CRASH_OBSTACLE_OR_SNAKE           1                                   //удар о препятствие или змейку
#define CRASH_FOOD                        2                                   //удар о предмет "еда"
#define DIRECTION_LEFT                    0                                   //движение влево
#define DIRECTION_UP                      1                                   //движение вверх
#define DIRECTION_RIGHT                   2                                   //движение вправо
#define DIRECTION_DOWN                    3                                   //движение вниз
#define COUNT_COLUMNS                     ArrayRange(game_level,2)            //число клеток игрового поля по вертикали
#define COUNT_ROWS                        ArrayRange(game_level,1)            //число клеток игрового поля по горизонтали
#define COUNT_LEVELS                      ArrayRange(game_level,0)            //число уровней игры
#define START_POS_X                       0                                   //стартовая позиция области игры по оси X (в пикселах)
#define START_POS_Y                       0                                   //стартовая позиция области игры по оси Y (в пикселах)
#define SQUARE_WIDTH                      20                                  //ширина клетки (в пикселах)
#define SQUARE_HEIGHT                     20                                  //высота клетки (в пикселах)
#define IMG_FILE_NAME_SQUARE              "\\Images\\Games\\Snake\\square.bmp"          //расположение рисунка клетки
#define IMG_FILE_NAME_OBSTACLE            "\\Images\\Games\\Snake\\obstacle.bmp"        //расположение рисунка препятствия
#define IMG_FILE_NAME_SNAKE_HEAD_LEFT     "\\Images\\Games\\Snake\\head_left.bmp"       //расположение рисунка головы змейки, повернутой влево
#define IMG_FILE_NAME_SNAKE_HEAD_UP       "\\Images\\Games\\Snake\\head_up.bmp"         //расположение рисунка головы змейки, повернутой вверх
#define IMG_FILE_NAME_SNAKE_HEAD_RIGHT    "\\Images\\Games\\Snake\\head_right.bmp"      //расположение рисунка головы змейки, повернутой вправо
#define IMG_FILE_NAME_SNAKE_HEAD_DOWN     "\\Images\\Games\\Snake\\head_down.bmp"       //расположение рисунка головы змейки, повернутой вниз
#define IMG_FILE_NAME_SNAKE_BODY          "\\Images\\Games\\Snake\\body.bmp"            //расположение рисунка тела змейки
#define IMG_FILE_NAME_SNAKE_TAIL_LEFT     "\\Images\\Games\\Snake\\tail_left.bmp"       //расположение рисунка хвоста змейки, повернутого влево
#define IMG_FILE_NAME_SNAKE_TAIL_UP       "\\Images\\Games\\Snake\\tail_up.bmp"         //расположение рисунка хвоста змейки, повернутого вверх
#define IMG_FILE_NAME_SNAKE_TAIL_RIGHT    "\\Images\\Games\\Snake\\tail_right.bmp"      //расположение рисунка хвоста змейки, повернутого вправо
#define IMG_FILE_NAME_SNAKE_TAIL_DOWN     "\\Images\\Games\\Snake\\tail_down.bmp"       //расположение рисунка хвоста змейки, повернутого вниз
#define IMG_FILE_NAME_FOOD                "\\Images\\Games\\Snake\food.bmp"             //расположение рисунка "еды"
#define SQUARE_BMP_LABEL_NAME             "snake_square_%u_%u"                //имя графической метки "Клетка"
#define OBSTACLE_BMP_LABEL_NAME           "snake_obstacle_%u_%u"              //имя графической метки "Препятствие"
#define SNAKE_ELEMENT_BMP_LABEL_NAME      "snake_element_%u"                  //имя графической метки "Элемент змейки"
#define FOOD_BMP_LABEL_NAME               "snake_food_%u"                     //имя графической метки "Еда"
#define LEVEL_EDIT_NAME                   "snake_level_edit"                  //имя поле ввода "Уровень"
#define LEVEL_EDIT_TEXT                   "Level: %u of %u"                   //текст в поле ввода "Уровень"
#define FOOD_LEFT_OVER_EDIT_NAME          "snake_food_available_edit"         //имя поле ввода "Число предметов"
#define FOOD_LEFT_OVER_EDIT_TEXT          "Food left over: %u"                //текст в поле ввода "Число предметов"
#define LIVES_EDIT_NAME                   "snake_lives_edit"                  //имя поле ввода "Количество жизней"
#define LIVES_EDIT_TEXT                   "Lives: %u"                         //текст в поле ввода "Количество жизней"
#define START_GAME_BUTTON_NAME            "snake_start_game_button"           //имя кнопки "Start"
#define START_GAME_BUTTON_TEXT            "Start"                             //текст на кнопке "Start"
#define PAUSE_GAME_BUTTON_NAME            "snake_pause_game_button"           //имя кнопки "Pause"
#define PAUSE_GAME_BUTTON_TEXT            "Pause"                             //текст на кнопке "Pause"
#define STOP_GAME_BUTTON_NAME             "snake_stop_game_button"            //имя кнопки "Stop"
#define STOP_GAME_BUTTON_TEXT             "Stop"                              //текст на кнопке "Stop"
#define CONTROL_WIDTH                     (COUNT_COLUMNS*(SQUARE_WIDTH-1)+1)/3//ширина кнопки панели управления (1/3 ширины игрового поля)
#define CONTROL_HEIGHT                    40                                  //высота кнопки панели управления
#define CONTROL_BACKGROUND                C'240,240,240'                      //цвет фона кнопок управления
#define CONTROL_COLOR                     Black                               //цвет текста кнопок управления
#define STATUS_WIDTH                      (COUNT_COLUMNS*(SQUARE_WIDTH-1)+1)/3//ширина поля ввода статусной панели (1/3 ширины игр. поля)
#define STATUS_HEIGHT                     40                                  //высота поля ввода статусной панели
#define STATUS_BACKGROUND                 LemonChiffon                        //цвет фона полей ввода статусной панели
#define STATUS_COLOR                      Black                               //цвет текста полей ввода статусной панели
#define HEADER_BUTTON_NAME                "snake_header_button"               //имя кнопки "Заголовок"
#define HEADER_BUTTON_TEXT                "Snake"                             //текст на кнопке "Заголовок"
#define HEADER_WIDTH                      COUNT_COLUMNS*(SQUARE_WIDTH-1)+1    //ширина кнопки "Заголовок" (ширина игрового поля)
#define HEADER_HEIGHT                     40                                  //высота кнопки "Заголовок"
#define HEADER_BACKGROUND                 BurlyWood                           //фон заголовка
#define HEADER_COLOR                      Black                               //цвет текста заголовка
#define COUNT_FOOD                        3                                   //число предметов "еда" на игровом поле
#define LIVES_SNAKE                       5                                   //количество жизней в каждом уровне
#define SPEED_SNAKE                       100                                 //скорость змейки (мс)
#define MAX_LENGTH_SNAKE                  15                                  //максимальная длина змейки
#define MAX_LEVEL                         COUNT_LEVELS-1                      //номер максимального уровня
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}
     }
  };
//+------------------------------------------------------------------+

Обратим внимание, например, на определение константы #define SQUARE_BMP_LABEL_NAME  "snake_square_%u_%u". Мы будем создавать игровое поле. Каждая клетка игрового поля представляет собой графическую метку, которая должна иметь уникальное имя. Этой константой определяется строка, задающая имя клетки, а точнее это строка задает формат имени клетки. %u - спецификация формата, говорящая что на ее месте должно быть беззнаковое целое число.

Если при создании графической метки, указать ее имя, как StringFormat(SQUARE_BMP_LABEL_NAME,1,0), то оно будет равно "snake_square_1_0".

Классы

Для написания игры определены два пользовательских класса, которые находятся в главном файле игры "Snake.mq5".

Класс CChartFieldElement:

//+------------------------------------------------------------------+
//| Класс CChartFieldElement                                         |
//+------------------------------------------------------------------+
class CChartFieldElement:public CChartObjectBmpLabel
  {
private:
   int               pos_x,pos_y;
public:
   int               GetPosX(){return pos_x;}
   int               GetPosY(){return pos_y;}
   //задаем условные координаты pos_x и pos_y
   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);
     }
   //переводим условные координаты в абсолютные и перемещаем объект на графике
   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);
     }
  };

Класс CChartFiledElement наследует класс CChartObjectBmpLabel, таким образом дополняя его. Все объекты игрового поля, такие как клетка, препятствие, голова, тело и хвост змейки, а также предмет "еда" являются объектами данного класса. Свойства pos_x и pos_y - условные координаты элементов на игровом поле, т.е. номер строки и столбца, где будет расположен элемент. Метод SetPos устанавливает данные координаты. Метод Move преобразует условные координаты в расстояние по оси X и Y в пикселах и перемещает элемент. Для этого вызываются методы X_Distance и YDistance класса CChartObjectBmpLabel.

Класс CSnakeGame:

//+------------------------------------------------------------------+
//| Класс CSnakeGame                                                 |
//+------------------------------------------------------------------+
class CSnakeGame
  {
private:
   CArrayObj        *square_obj_arr;                     //указатель на массив клеток игрового поля
   CArrayObj        *control_panel_obj_arr;              //указатель на массив кнопок панели управления
   CArrayObj        *status_panel_obj_arr;               //указатель на массив полей ввода статусной панели
   CArrayObj        *obstacle_obj_arr;                   //указатель на массив препятствий
   CArrayObj        *food_obj_arr;                       //указатель на массив предметов еды
   CArrayObj        *snake_element_obj_arr;              //указатель на массив элементов змейки
   CChartObjectButton *header;                           //указатель на заголовок
   int               direction;                          //направление перемещения змейки
   int               current_lives;                      //текущее количество жизней
   int               current_level;                      //текущий уровень
   int               header_left;                        //расстояние заголовка по оси X
   int               header_top;                         //расстояние заголовка по оси Y
public:
   //конструктор класса
   void              CSnakeGame()
     {
      current_lives=LIVES_SNAKE;
      current_level=0;
      header_left=START_POS_X;
      header_top=START_POS_Y;
     }
   //метод, задающий поля header_left и header_top
   void              SetHeaderPos(int val_header_left,int val_header_top)
     {
      header_left=val_header_left;
      header_top=val_header_top;
     };
   //методы для получения и задания значения поля direction
   void              SetDirection(int d){direction=d;}
   int               GetDirection(){return direction;}
   //методы для создания и удаления заголовка
   void              CreateHeader();
   void              DeleteHeader();
   //методы для создания, перемещения и удаления игрового поля   
   void              CreateField();
   void              FieldMoveOnChart();
   void              DeleteField();
   //методы для создания, перемещения и удаления препятствий
   void              CreateObstacle();
   void              ObstacleMoveOnChart();
   void              DeleteObstacle();
   //методы для создания, перемещения и удаления змейки
   void              CreateSnake();
   void              SnakeMoveOnChart();
   void              SnakeMoveOnField();                 //перемещение змейки по игровому полю
   void              SetTrueSnake();                     //установка правильного рисунка головы и хвоста
   int               Check();                            //проверка столкновения змейки с элементами игрового поля
   void              DeleteSnake();
   //методы для создания, перемещения и удаления предметов еды
   void              CreateFood();
   void              FoodMoveOnChart();
   void              FoodMoveOnField(int food_num);
   void              DeleteFood();
   //методы для создания, перемещения и удаления статусной (информационной) панели игры
   void              CreateControlPanel();
   void              ControlPanelMoveOnChart();
   void              DeleteControlPanel();
   //методы для создания, перемещения и удаления панели управления
   void              CreateStatusPanel();
   void              StatusPanelMoveOnChart();
   void              DeleteStatusPanel();
   //перемещение всех элементов на графике
   void              AllMoveOnChart();
   //инициализация игры
   void              Init();
   //деинициализация игры
   void              Deinit();
   //методы, реализующие управление игрой
   void              StartGame();
   void              PauseGame();
   void              StopGame();
   void              ResetGame();
   void              NextLevel();
  };

Класс CSnakeGame является главным классом игры и содержит поля и методы, осуществляющие создание, перемещение и удаление элементов игры. Как видно в начале класса объявлены поля для организации динамических массивов указателей на соответствующие элементы игры. Например, в поле snake_element_obj_arr будут храниться указатели на элементы змейки. Нулевой индекс массива snake_element_obj_arr будет представлять собой голову змейки, а последний - ее хвост. Зная это, можно легко манипулировать змейкой на игровом поле.

Далее будут определены все методы класса CSnakeGame. Методы реализованы на основе теории, данной в "Теоретической части" этой статьи.

Управление заголовком

//+------------------------------------------------------------------+
//| Создание заголовка                                               |
//+------------------------------------------------------------------+
void CSnakeGame::CreateHeader(void)
  {
   //создаем указатель на объект класса CChartObjectButton и присваиваем его полю header класса CSnakeGame
   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);
   //заголовок можно выбирать и перемещать на графике
   header.Selectable(true);
  }
//+------------------------------------------------------------------+
//| Удаление заголовка                                               |
//+------------------------------------------------------------------+
void CSnakeGame::DeleteHeader(void)
  {
   delete header;
  }

Управление игровым полем

//+------------------------------------------------------------------+
//| Создание игрового поля                                           |
//+------------------------------------------------------------------+
void CSnakeGame::CreateField()
  {
   int i,j;
   CChartFieldElement *square_obj;
   //создаем указатель на объект класса CArrayObj и присваиваем его полю square_obj_arr класса CSnakeGame
   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);
         //задаем условные координаты клетки поля
         square_obj.SetPos(j,i);
         //добавляем клетку в массив square_obj_arr
         square_obj_arr.Add(square_obj);
        }
   //перемещаем клетки игрового поля
   FieldMoveOnChart();
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Перемещение игрового поля (клеток игрового поля)                 |
//+------------------------------------------------------------------+
void CSnakeGame::FieldMoveOnChart()
  {
   CChartFieldElement *square_obj;
   int i;
   i=0;
   //перебираем все элементы массива клеток square_obj_arr
    while((square_obj=square_obj_arr.At(i))!=NULL)
     {
      //перемещаем i-ую клетку
      square_obj.Move(header_left,header_top+HEADER_HEIGHT);
      i++;
     }
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Удаление игрового поля                                           |
//+------------------------------------------------------------------+
void CSnakeGame::DeleteField()
  {
   delete square_obj_arr;
   ChartRedraw();
  }

Управление препятствиями

//+------------------------------------------------------------------+
//| Создание препятствий                                             |
//+------------------------------------------------------------------+
void CSnakeGame::CreateObstacle()
  {
   int i,j;
   CChartFieldElement *obstacle_obj;
   //создаем указатель на объект класса CArrayObj и присваиваем его полю obstacle_obj_arr класса CSnakeGame
   obstacle_obj_arr=new CArrayObj();
   //просматриваем массив level
   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);
            //задаем условные координаты препятствия
            obstacle_obj.SetPos(j,i);
            //добавляем препятствие в массив препятствий obstacle_obj_arr
            obstacle_obj_arr.Add(obstacle_obj);
           }
   //перемещаем препятствия
   ObstacleMoveOnChart();
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Перемещение препятствий                                          |
//+------------------------------------------------------------------+
void CSnakeGame::ObstacleMoveOnChart()
  {
   CChartFieldElement *obstacle_obj;
   int i;
   i=0;
   //перебираем массив препятствий
   while((obstacle_obj=obstacle_obj_arr.At(i))!=NULL)
     {
      //перемещаем i-ое препятствие
      obstacle_obj.Move(header_left,header_top+HEADER_HEIGHT);
      i++;
     }
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Удаление препятствий                                             |
//+------------------------------------------------------------------+
void CSnakeGame::DeleteObstacle()
  {
   delete obstacle_obj_arr;
   ChartRedraw();
  }

Управление змейкой

//+------------------------------------------------------------------+
//| Создание змейки                                                  |
//+------------------------------------------------------------------+
void CSnakeGame::CreateSnake()
  {
   int i,j;
   CChartFieldElement *snake_element_obj,*snake_arr[];
   ArrayResize(snake_arr,3);
   //создаем указатель на объект класса CArrayObj и присваиваем его полю snake_element_obj_arr класса CSnakeGame
   snake_element_obj_arr=new CArrayObj();
   //проходим по массиву level
   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,level[current_level][i][j]),0,0,0);
            snake_element_obj.BmpFileOn(IMG_FILE_NAME_SNAKE_BODY);
            //задаем условные координаты элемента змейки
            snake_element_obj.SetPos(j,i);
            //помещаем k-ый элемент змейки в k-ую позицию временного массива
            snake_arr[game_level[current_level][i][j]-1]=snake_element_obj;
           }
   //копируем элементы змейки из временного массива в массив snake_element_obj_arr
   snake_element_obj_arr.Add(snake_arr[0]);
   snake_element_obj_arr.Add(snake_arr[1]);
   snake_element_obj_arr.Add(snake_arr[2]);
   //перемещаем змейку на графике
   SnakeMoveOnChart();
   //устанавливаем правильные рисунки головы и хвоста
   SetTrueSnake();
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Перемещение змейки на графике                                    |
//+------------------------------------------------------------------+
void CSnakeGame::SnakeMoveOnChart()
  {
   CChartFieldElement *snake_element_obj;
   int i;
   i=0;
   //перебираем массив элементов змейки
   while((snake_element_obj=snake_element_obj_arr.At(i))!=NULL)
     {
      //перемещаем i-ый элемент змейки
      snake_element_obj.Move(header_left,header_top+HEADER_HEIGHT);
      i++;
     }
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Перемещение змейки по игровому полю                              |
//+------------------------------------------------------------------+
void CSnakeGame::SnakeMoveOnField()
  {
   int prev_x,prev_y,next_x,next_y,check;
   CChartFieldElement *snake_head_obj,*snake_body_obj,*snake_tail_obj;
   //получаем указатель на объект "голова" змейки
   snake_head_obj=snake_element_obj_arr.At(0);
   //запоминаем координаты головы
   prev_x=snake_head_obj.GetPosX();
   prev_y=snake_head_obj.GetPosY();
   //задаем новые условные координаты головы змейки в зависимости от направления перемещения
   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;
     }
   //перемещаем голову
   snake_head_obj.Move(header_left,header_top+HEADER_HEIGHT);
   //проверяем на столкновение головы с элементами игрового поля (препятствие, элементы змейки, еда)
   check=Check();
   //получаем последний элемент тела змейки
   snake_body_obj=snake_element_obj_arr.Detach(snake_element_obj_arr.Total()-2);
   //запоминаем координаты тела
   next_x=snake_body_obj.GetPosX();
   next_y=snake_body_obj.GetPosY();
   //перемещаем тело на предыдущее место головы
   snake_body_obj.SetPos(prev_x,prev_y);
   snake_body_obj.Move(header_left,header_top+HEADER_HEIGHT);
   //запоминаем предыдущее место тела змейки
   prev_x=next_x;
   prev_y=next_y;
   //вставляем тело в первую позицию массива snake_element_obj_arr
   snake_element_obj_arr.Insert(snake_body_obj,1);
   //если голова змейки столкнулась с едой
   if(check>=CRASH_FOOD)
     {
      //создаем новый элемент змейки (тело)
      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);
      //перемещаем тело в конец змейки перед хвостом
      snake_body_obj.SetPos(prev_x,prev_y);
      snake_body_obj.Move(header_left,header_top+HEADER_HEIGHT);
      //добавляем тело в предпоследнюю позицию массива snake_element_obj_arr
      snake_element_obj_arr.Insert(snake_body_obj,snake_element_obj_arr.Total()-1);
      //если длина змейки не равна максимальной ее длине
      if(snake_element_obj_arr.Total()!=MAX_LENGTH_SNAKE)
        {
         //перемещаем съеденный элемент еды на новое место игрового поля
         FoodMoveOnField(check-CRASH_FOOD);
        }
      //иначе генерируем пользовательское событие на переход на следующий уровень (длина змейки максимально возможная)
      else EventChartCustom(0,2,0,0,"");
     }
   //иначе, если столкновения с едой не было, перемещаем хвост на предыдущее положение тела
   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);
     }
   //устанавливаем правильные рисунки головы и хвоста
   SetTrueSnake();
   ChartRedraw();
   //генерируем пользовательское событие для периодического вызова данной функции
   EventChartCustom(0,0,0,0,"");
   //задаем задержку, определяющую скорость перемещения змейки
   Sleep(SPEED_SNAKE);
  }
//+------------------------------------------------------------------+
//| Задание правильных рисунков головы и хвоста змейки               |
//+------------------------------------------------------------------+
void CSnakeGame::SetTrueSnake()
  {
   CChartFieldElement *snake_head,*snake_body,*snake_tail;
   int total,x1,x2,y1,y2;
   total=snake_element_obj_arr.Total();
   //получаем объект "голова" змейки
   snake_head=snake_element_obj_arr.At(0);
   //запоминаем координаты головы
   x1=snake_head.GetPosX();
   y1=snake_head.GetPosY();
   //получаем первый элемент тела змейки
   snake_body=snake_element_obj_arr.At(1);
   //запоминаем координаты тела
   x2=snake_body.GetPosX();
   y2=snake_body.GetPosY();
   //выбираем файл рисунка головы в зависимости от положения головы и первого элемента тела змейки относительно друг друга
   //задаем направление перемещения по направлению головы змейки
   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;
     }
   //получаем последний элемент тела змейки
   snake_body=snake_element_obj_arr.At(total-2);
   //запоминаем координаты тела
   x1=snake_body.GetPosX();
   y1=snake_body.GetPosY();
   //получаем объект "хвост" змейки
   snake_tail=snake_element_obj_arr.At(total-1);
   //запоминаем координаты хвоста
   x2=snake_tail.GetPosX();
   y2=snake_tail.GetPosY();
   //выбираем файл рисунка хвоста в зависимости от положения хвоста и последнего элемента тела змейки относительно друг друга
   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);
  }
//+------------------------------------------------------------------+
//| Проверка столкновения головы змейки с элементами игрового поля   |
//+------------------------------------------------------------------+
int CSnakeGame::Check()
  {
   int i;
   CChartFieldElement *snake_head_obj,*snake_element_obj,*obstacle_obj,*food_obj;
   //получаем указатель на голову змейки
   snake_head_obj=snake_element_obj_arr.At(0);
   i=0;
   //проверяем столкновение головы с препятствием
   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;
   //проверяем столкновение головы с едой
   while((food_obj=food_obj_arr.At(i))!=NULL)
     {
      //если змейка столкнулась с едой
       if(snake_head_obj.GetPosX()==food_obj.GetPosX() && snake_head_obj.GetPosY()==food_obj.GetPosY())
        {
         //прячем еду
         food_obj.Background(true);
         return(CRASH_FOOD+i);
        }
      i++;
     }
   i=3;
   //проверяем столкновение головы с телом и хвостом змейки
   while((snake_element_obj=snake_element_obj_arr.At(i))!=NULL)
     {
      //столкновение с последним элементом не проверяем, т.к. он еще не был перемещен
      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,"");
         //прячем элемент змейки, с которым столкнулись
         snake_element_obj.Background(true);
         return CRASH_OBSTACLE_OR_SNAKE;
        }
      i++;
     }
   return CRASH_NO;
  }
//+------------------------------------------------------------------+
//| Удаление змейки                                                  |
//+------------------------------------------------------------------+
void CSnakeGame::DeleteSnake()
  {
   delete snake_element_obj_arr;
   ChartRedraw();
  }

После перемещения головы змейки, вызывается функция проверки столкновения Check(), которая возвращает идентификатор столкновения (указывает, с каким элементом произошло столкновение).

Функция SetTrueSnake() предназначена для задания правильного рисунка головы и хвоста змейки в зависимости от расположения их соседних элементов.

Управление предметами, представляющими еду змейки 

//+------------------------------------------------------------------+
//| Создание еды                                                     |
//+------------------------------------------------------------------+
void CSnakeGame::CreateFood()
  {
   int i;
   CChartFieldElement *food_obj;
   //задаем начальное состояние для генерации ряда псевдослучайных чисел
    MathSrand(uint(TimeLocal()));
   //создаем указатель на объект класса CArrayObj и присваиваем его полю food_obj_arr класса CSnakeGame
   food_obj_arr=new CArrayObj();
   i=0;
   //количество еды на игровом поле равно COUNT_FOOD
   while(i<COUNT_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
      food_obj_arr.Add(food_obj);
      //устанавливаем координаты еды на поле и перемещаем ее
      FoodMoveOnField(i);
      i++;
     }
  }
//+------------------------------------------------------------------+
//| Перемещение еды на графике                                       |
//+------------------------------------------------------------------+
void CSnakeGame::FoodMoveOnChart()
  {
   CChartFieldElement *food_obj;
   int i;
   i=0;
   //перебираем массив еды
   while((food_obj=food_obj_arr.At(i))!=NULL)
     {
      //перемещаем i-ую еду
      food_obj.Move(header_left,header_top+HEADER_HEIGHT);
      i++;
     }
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Задание координат еды и перемещение ее на игровом поле           |
//+------------------------------------------------------------------+
void CSnakeGame::FoodMoveOnField(int food_num)
  {
   int i,j,k,n,m;
   CChartFieldElement *snake_element_obj,*food_obj;
   CChartObjectEdit *edit_obj;
   //задаем новое значение поля "Количество еды" статусной панели
   edit_obj=status_panel_obj_arr.At(1);
   edit_obj.Description(StringFormat(FOOD_LEFT_OVER_EDIT_TEXT,MAX_LENGTH_SNAKE-snake_element_obj_arr.Total()));
   bool b;
   b=false;
   k=0;
   //случайным образом генерируем координаты еды на поле до тех пор, пока они не будут указывать на свободное пространство поля
   while(true)
     {
      //генерируем номер строки игрового поля
      i=(int)(MathRand()/32767.0*(COUNT_ROWS-1));
      //номер столбца игрового поля
      j=(int)(MathRand()/32767.0*(COUNT_COLUMNS-1));
      n=0;
      //проверяем, не приходятся ли координаты еды на элементы змейки
      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++;
        }
      //проверяем, не приходятся ли координаты еды на другую еду
      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++;
           }
        }
      //проверяем, не приходятся ли координаты еды на препятствие
      if(b==true && game_level[current_level][i][j]!=9) break;
      k++;
     }
   food_obj=food_obj_arr.At(food_num);
   //показываем еду
   food_obj.Background(false);
   //задаем новые координаты
   food_obj.SetPos(j,i);
   //перемещаем еду
   food_obj.Move(header_left,header_top+HEADER_HEIGHT);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Удаление еды                                                     |
//+------------------------------------------------------------------+
void CSnakeGame::DeleteFood()
  {
   delete food_obj_arr;
   ChartRedraw();
  }

Расположение еды на игровом поле задается случайным образом при условии, что клетка поля, на которой будет расположена еда, не содержит других элементов.

Управление панелью состояния

//+------------------------------------------------------------------+
//| Создание статусной панели игры                                   |
//+------------------------------------------------------------------+
void CSnakeGame::CreateStatusPanel()
  {
   CChartObjectEdit *edit_obj;
   //создаем указатель на объект класса CArrayObj и присваиваем его полю status_panel_obj_arr класса CSnakeGame
   status_panel_obj_arr=new CArrayObj();
   //создание информационного поля "Уровень"
   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(LEVEL_EDIT_TEXT,current_level,MAX_LEVEL));
   edit_obj.Selectable(false);
   edit_obj.ReadOnly(true);
   status_panel_obj_arr.Add(edit_obj);
   //создание поля "Количество еды"
   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(FOOD_LEFT_OVER_EDIT_TEXT,MAX_LENGTH_SNAKE-3));
   edit_obj.Selectable(false);
   edit_obj.ReadOnly(true);
   status_panel_obj_arr.Add(edit_obj);
   //создание поля "Количество жизней"
   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(LIVES_EDIT_TEXT,current_lives));
   edit_obj.Selectable(false);
   edit_obj.ReadOnly(true);
   status_panel_obj_arr.Add(edit_obj);
   //перемещение статусной панели
   StatusPanelMoveOnChart();
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Перемещение статусной панели                                     |
//+------------------------------------------------------------------+
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();
  }
//+------------------------------------------------------------------+
//| Удаление статусной панели                                        |
//+------------------------------------------------------------------+
void CSnakeGame::DeleteStatusPanel()
  {
   delete status_panel_obj_arr;
   ChartRedraw();
  }

Управление панелью управления

//+------------------------------------------------------------------+
//| Создание панели управления                                       |
//+------------------------------------------------------------------+
void CSnakeGame::CreateControlPanel()
  {
   CChartObjectButton *button_obj;
   //создаем указатель на объект класса CArrayObj и присваиваем его полю control_panel_obj_arr класса CSnakeGame
   control_panel_obj_arr=new CArrayObj();
   //создание кнопки "Start"
   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);
   //создание кнопки "Pause"
   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);
   //создание кнопки "Stop"
   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);
   //перемещение панели управления
   ControlPanelMoveOnChart();
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Перемещение панели управления                                    |
//+------------------------------------------------------------------+
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();
  }
//+------------------------------------------------------------------+
//| Удаление панели управления                                       |
//+------------------------------------------------------------------+
void CSnakeGame::DeleteControlPanel()
  {
   delete control_panel_obj_arr;
   ChartRedraw();
  }

Перемещение всех элементов игры, инициализация и деинициализация игры

//+------------------------------------------------------------------+
//| Перемещение всех элементов игры                                  |
//+------------------------------------------------------------------+
void  CSnakeGame::AllMoveOnChart()
  {
   FieldMoveOnChart();
   StatusPanelMoveOnChart();
   ControlPanelMoveOnChart();
   ObstacleMoveOnChart();
   SnakeMoveOnChart();
   FoodMoveOnChart();
  }
//+------------------------------------------------------------------+
//| Инициализация игры                                               |
//+------------------------------------------------------------------+
void CSnakeGame::Init()
  {
   CreateHeader();
   CreateField();
   CreateStatusPanel();
   CreateControlPanel();
   CreateObstacle();
   CreateSnake();
   CreateFood();
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Деинициализация игры                                             |
//+------------------------------------------------------------------+
void  CSnakeGame::Deinit()
  {
   DeleteFood();
   DeleteSnake();
   DeleteObstacle();
   DeleteControlPanel();
   DeleteStatusPanel();
   DeleteField();
   DeleteHeader();
  }

Управление игрой

//+------------------------------------------------------------------+
//| Заглушка функции начала игры                                     |
//+------------------------------------------------------------------+
void CSnakeGame::StartGame()
  {
   return;
  }
//+------------------------------------------------------------------+
//| Заглушка функции приостановки игры                               |
//+------------------------------------------------------------------+
void CSnakeGame::PauseGame()
  {
   return;
  }
//+------------------------------------------------------------------+
//| Остановка игры                                                   |
//+------------------------------------------------------------------+
void CSnakeGame::StopGame()
  {
   CChartObjectEdit *edit_obj;
   current_level=0;
   current_lives=LIVES_SNAKE;
   //задаем новое значение поля "Уровень" статусной панели
   edit_obj=status_panel_obj_arr.At(0);
   edit_obj.Description(StringFormat(LEVEL_EDIT_TEXT,current_level,MAX_LEVEL));
   //задаем новое значение поля "Количество жизней" статусной панели
   edit_obj=status_panel_obj_arr.At(2);
   edit_obj.Description(StringFormat(LIVES_EDIT_TEXT,current_lives));
   DeleteFood();
   DeleteSnake();
   DeleteObstacle();
   CreateObstacle();
   CreateSnake();
   CreateFood();
  }
//+------------------------------------------------------------------+
//| Перезапуск уровня                                                |
//+------------------------------------------------------------------+
void CSnakeGame::ResetGame()
  {
   CChartObjectEdit *edit_obj;
   if(current_lives-1==-1)StopGame();
   else
     {
      //уменьшаем количество жизней
      current_lives--;
      //задаем новое значение поля "Количество жизней" статусной панели
      edit_obj=status_panel_obj_arr.At(2);
      edit_obj.Description(StringFormat(LIVES_EDIT_TEXT,current_lives));
      DeleteFood();
      DeleteSnake();
      CreateSnake();
      CreateFood();
     }
  }
//+------------------------------------------------------------------+
//| Переход на следующий уровень                                     |
//+------------------------------------------------------------------+
void CSnakeGame::NextLevel()
  {
   CChartObjectEdit *edit_obj;
   current_lives=LIVES_SNAKE;
   //если нет следующего уровня, переходим на начальный уровень
   if(current_level+1>MAX_LEVEL)StopGame();
   else
     {
      //иначе увеличиваем уровень и обновляем содержимое статусной панели
      current_level++;
      edit_obj=status_panel_obj_arr.At(0);
      edit_obj.Description(StringFormat(LEVEL_EDIT_TEXT,current_level,MAX_LEVEL));
      edit_obj=status_panel_obj_arr.At(2);
      edit_obj.Description(StringFormat(LIVES_EDIT_TEXT,current_lives));
      DeleteFood();
      DeleteSnake();
      DeleteObstacle();
      CreateObstacle();
      CreateSnake();
      CreateFood();
     }
  }

Обработка событий (заключительный код)

CSnakeGame snake_field;	//объявляем и создаем глобальный объект класса CSnakeGame
//+------------------------------------------------------------------+
//| 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()
  {
   //возвращаем кнопки в отжатое состояние 
   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(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;
     }
   //если нажата кнопка "Start" и press_button=false
   if(id==CHARTEVENT_OBJECT_CLICK && sparam==START_GAME_BUTTON_NAME && press_button==false)
     {
         //ждем 1 секунду
         Sleep(1000);
         //генерируем событие на перемещение змейки
         EventChartCustom(0,0,0,0,"");
         press_button=true;
     }
   //если нажата кнопка "Pause"
   else if(id==CHARTEVENT_OBJECT_CLICK && sparam==PAUSE_GAME_BUTTON_NAME)
     {
      press_button=false;
     }
   //если нажата кнопка "Stop"
   else if(id==CHARTEVENT_OBJECT_CLICK && sparam==STOP_GAME_BUTTON_NAME)
     {
      snake_field.StopGame();
      press_key=true;
      press_button=false;
     }
   //обработка пользовательского события с идентификатором 0 на перемещение змейки, если press_button=true
   else if(id==CHARTEVENT_CUSTOM && press_button==true)
     {
      snake_field.SnakeMoveOnField();
      press_key=false;
     }
   //обработка пользовательского события 1 перезапуска игры
   else if(id==CHARTEVENT_CUSTOM+1)
     {
      snake_field.ResetGame();
      Sleep(1000);
      press_key=true;
      press_button=false;
     }
   //обработка пользовательского события 2 перехода на следующий уровень
   else if(id==CHARTEVENT_CUSTOM+2)
     {
      snake_field.NextLevel();
      Sleep(1000);
      press_key=true;
      press_button=false;
     }
   //обработка события перемещения заголовка
   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();
     }
  }

В функции обработки событий OnChartEvent определены две статические переменные press_key и press_button.

Старт игры разрешается при press_button=false. После нажатия на кнопку "Start" press_button устанавливается в значение true (тем самым запрещая повторное выполнение кода старта игры) и сохраняется до наступления одного из следующих событий:

  • перезапуск текущего уровня;
  • переход на следующий уровень;
  • приостановка игры (нажатие кнопки "Pause");
  • остановка игры (нажатие кнопки "Stop").

Изменение направления перемещения змейки возможно только в том случае, если оно перпендикулярно текущему направлению, а также после того, как змейка произвела хоть одно перемещение по игровому полю (за это отвечает переменная press_key). Эти условия отражены при обработке события CHARTEVENT_KEYDOWN (событие нажатия клавиши).

При перемещении заголовка генерируется событие CHARTEVENT_OBJECT_DRAG, по которому задаются поля header_left и header_top класса CSnakeGame. По значению этих полей осуществляется перемещение остальных элементов игры.

Перемещение игровой области реализовано по примеру TradePad_Sample.

Заключение

В статье был рассмотрен пример написания игры на MQL5. Вы познакомились со Стандартной библиотекой, а именно с классами элементов управления, особенностями класса CArrayObj, а также узнали способы периодического вызова функции с возможностью обработки событий.

Ниже вы можете скачать архив с примером игры "Змейка". Архив следует распаковать в папку каталог_терминала.

Прикрепленные файлы |
snake-doc.zip (394.55 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (12)
John
John | 10 апр. 2010 в 00:36
getch:

Возможно, некоторые удивятся, но самая богатая контора на FOREX стала такой в

Самая богатая. :)))) Ага. Щас.
[Удален] | 10 апр. 2010 в 01:18
SProgrammer писал(а)  :
Самая богатая. :)))) Ага. Щас.
Retail.
Andrey Vasiliev
Andrey Vasiliev | 10 апр. 2010 в 14:27
Чем ярче и красивее - тем ближе к казино.
Евгений
Евгений | 12 апр. 2010 в 13:59
MoneyJinn писал(а)  :
Чем ярче и красивее - тем ближе к казино.
согласен, эта вся красотень способствует только повышению азарта, что непременно ведёт к сливу
Vasily
Vasily | 12 апр. 2010 в 15:28
интересно! а счастливый фермер будет реализован на мкл?
MQL5 для "чайников": Как проектировать и конструировать классы объектов MQL5 для "чайников": Как проектировать и конструировать классы объектов
На примере создания программы визуального программирования показано, как проектировать и конструировать классы на MQL5. Статья предназначена для начинающих разработчиков приложений МТ5. Предлагается простая и понятная технология создания собственных классов без глубокого погружения в теорию объектно-ориентированного программирования.
Создание тиковых индикаторов Создание тиковых индикаторов
В этой статье описывается создание двух индикаторов: строящего тиковый график цены и рисующего "тиковые свечи" - свечи, содержащие заданное число тиков. Каждый из рассмотренных индикаторов записывает поступающие значения цен в файл для построения индикаторов при повторном запуске терминала (эти данные также могут использоваться другими приложениями).
Создание активных панелей управления  на MQL5 для торговли Создание активных панелей управления на MQL5 для торговли
Статья посвящена разработке активных панелей управления на MQL5. Управление элементами интерфейса осуществляется при помощи механизма обработки событий, есть возможность гибкой настройки свойств элементов управления. Реализована работа с позициями а также возможность выставления, модификации и удаления рыночных и отложенных ордеров.
Стили рисования в MQL5 Стили рисования в MQL5
В MQL4 есть 6 типов графического отображения индикаторов, а MQL5 доступно уже 18 стилей рисования. Поэтому, возможно, стоит написать статью о стилях рисования в MQL5. В данной статье мы рассмотрим подробности работы со стилями графического отображения индикаторов. Кроме того, мы создадим индикатор для иллюстрации всех этих стилей и уточним особенности графических построений.