在本文中,我们将考虑一个用 MQL5 编写“贪吃蛇”游戏的例子。
从 MQL 5 起,游戏编程变为可能,主要是因为事件处理功能,包括自定义事件。面向对象编程简化此类程序的设计,使代码更加清晰,并且减少错误的数量。
在阅读本文之后,您将了解 OnChart 事件处理、MQL5 标准库类的使用例子和在一定时间之后循环调用函数来进行任何计算的方法。
选择“贪吃蛇”游戏作为例子主要是因为其实施非常简单。每一个对编程有极大兴趣的人都能编写此游戏。
依据维基百科的解释:
“贪吃蛇”是一款产生于二十世纪七十年代中后期的视频游戏,从出现之后就一直广受欢迎,成为经典。
玩家控制一个类似于蛇的细长生物,该生物在一个有边界的平面中四处移动,一路拾起食物(或类似物品),尝试避免碰到自己的尾巴或围绕游戏区域的“墙”。在某些变异中还有额外的障碍物。贪吃蛇每吃一件食物,其尾巴就变长一些,让游戏的难度渐渐变大。用户控制蛇头的方向(上、下、左、右),蛇身跟随蛇头移动。游戏正在进行时,玩家不能停止贪吃蛇的移动,并且不能让贪吃蛇反向。
用 MQL5 实施“贪吃蛇”有某些限制和特点。
有 6 关(从 0 至 5)。每一关 5 条命。在用完所有命之后或者通关之后,游戏返回到第一关。您可以创建您自己的关。对于第一关,蛇的速度及其最大长度是相同的。
游戏区由 4 个元素构成:
图 1 显示了所有这些元素:
图 1. “贪吃蛇”游戏的元素
游戏标题是一个“按钮”类型的对象。所有移动区元素是 "BmpLabel"(位图标签) 类型的对象。信息面板由三个“编辑”类型的对象构成,控制面板由三个“按钮”类型的对象构成。所有对象都依据相对于图表左上角的沿 X 轴和 Y 轴的像素距离来定位。
应注意,移动区边缘并不是贪吃蛇移动的障碍物。例如,贪吃蛇穿过左边,从右边出现。如图 2 所示:
图 2. 贪吃蛇穿过移动区边缘
蛇头和蛇尾与蛇身不同,能够旋转。头的方向由移动方向或其相邻元素的位置决定。尾的方向仅由相邻元素的位置决定。
举例而言,如果相邻的蛇尾元素在左侧,则蛇尾左转。蛇头稍有不同。如果相邻元素在右侧,则蛇头左转。下面的示意图举例说明了蛇头和蛇尾的方向。注意,蛇头和蛇尾的转向与它们的相邻元素有关。
蛇头和蛇尾向左 | 蛇头和蛇尾向右 | 蛇头和蛇尾向下 | 蛇头和蛇尾向上 |
蛇的移动分为三个阶段:
如果贪吃蛇吃食物,则蛇尾不移动。然而,在最后一个蛇身元素的过去位置创建一个新的蛇身元素。
以下示意图说明了一个向左移动的贪吃蛇例子:
初始位置 | 向左移动一个单元格 |
最后一个蛇身元素移动 到蛇头的上一个位置 |
蛇尾移到最后一个蛇身 元素的过去位置 |
接下来,我们将讨论在编写游戏时使用的工具和技术。
使用相同类型的对象数组(例如移动区单元格、贪吃蛇元素)来处理它们(创建、移动、删除)非常方便。可以使用 MQL5 标准库类实施这些数组和对象。
使用 MQL5 标准库类能够简化编写程序的过程。对于游戏,我们将使用以下库类:
要使用 MQL5 标准库类,必须使用以下编译器指令包含它们:
#include <path_to_the_file_with_classes_description>
例如,要使用 CChartObjectButton 类型的对象,我们需要写:
#include <ChartObjects\ChartObjectsTxtControls.mqh>
不能在 MQL5 参考中找到文件路径。
在处理 MQL5 标准库类时,理解它们之间的相互继承关系非常重要。例如,CChartObjectButton 类继承 CChartObjectEdit 类,而 CChartObjectEdit 类又继承 CChatObjectLabel 类,等等。这意味着对于继承的类,可以使用父类的属性和方法。
为了理解使用 MQL5 标准库类的优点,让我们考虑一个用两种方式(使用类和不使用类)创建和实施按钮的例子。
以下是不使用类的例子:
//创建一个按钮, 名称为 "button" ObjectCreate(0,"button",OBJ_BUTTON,0,0,0); //指定按钮上的文字 ObjectSetString(0,"button",OBJPROP_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=new CChartObjectButton; //用属性创建按钮 (以像素为单位): (宽度=100, 高度=20, 位置: X=10,Y=10) button.Create(0,"button",0,10,10,100,20); //指定按钮上的文字 button.Description("按钮文字");
可以看到,使用类更加简单。此外,类对象可以存储在数组中,从而轻松处理。
标准库类的 MQL5 参考清晰明确地说明了对象控制类的方法和属性。
我们将使用标准库中的 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 object 指针加到 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 object 指针加到 array_obj 数组末尾 array_obj.Add(my_obj);
编写游戏时,将使用 CArrayObj 类的以下方法:
以下是使用 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); } } //+------------------------------------------------------------------+ //| EA交易初始化函数 | //+------------------------------------------------------------------+ 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("按钮文字"); while(true) { //应该被定时调用的代码 } } //+------------------------------------------------------------------+ //| EA交易初始化函数 | //+------------------------------------------------------------------+ 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("点击了按钮"); }
一个无穷 while 循环不允许从 MyFunction 函数返回。OnChartEvent 函数无法获取控制。因此,按下按钮并不调用 Alert 函数。
在游戏中,需要定期调用贪吃蛇的移动函数,并且能够在一段时间之后进行事件处理。但是如上所述,一个无穷循环导致并不会调用 OnChartEvent 函数,因而事件处理变为不可能。
因此必须发明另一种方式来定期执行代码。
MQL5 语言有一个特殊的 OnTimer 函数,该函数依据指定的秒数定期调用。为此,我们将使用 EventSetTimer 函数。
上一个例子可以重写为:
#include <ChartObjects\ChartObjectsTxtControls.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void MyFunction() { //应该被定期执行的代码 } //+------------------------------------------------------------------+ //| EA 交易初始化函数 | //+------------------------------------------------------------------+ int OnInit() { CChartObjectButton *button; button=new CChartObjectButton; button.Create(0,"button",0,10,10,100,20); button.Description("按钮文字"); 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("点击了按钮"); }
在 OnInit 函数中,创建一个按钮,并且为调用 OnTimer 函数定义一段等于一秒的时间。每秒钟调用一次 OnTimer 函数,OnTimer 函数调用应定期执行的代码 (MyFunction)。
注意,OnTimer 函数的调用需要几秒钟的时间。要在指定的毫秒数之后调用函数,需要使用另一方法。此方法就是使用自定义事件。
自定义事件由 EventChartCustom 函数生成,事件 ID 及其参数在 EventChartCustom 函数的输入参数中定义。自定义 ID 的数量最多为 65536,从 0 至 65535。MQL5 编译器将 CHARTEVENT_CUSTOM 常量标识符加到 ID 以将自定义事件从其他类型的事件区别开来。因此,自定义 ID 的实际范围是从 CHARTEVENT_CUSTOM 至 CHARTEVENT_CUSTOM+65535 ( CHARTEVENT_CUSTOM_LAST )。
以下介绍一个使用自定义事件定期调用 MyFunction 的例子:
#include <ChartObjects\ChartObjectsTxtControls.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void MyFunction() { //应该被定期执行的代码 Sleep(200); EventChartCustom(0,0,0,0,""); } //+------------------------------------------------------------------+ //| EA 交易初始化函数 | //+------------------------------------------------------------------+ int OnInit() { CChartObjectButton *button; button=new CChartObjectButton; button.Create(0,"button",0,10,10,100,20); button.Description("按钮文字"); MyFunction(); return(0); } //+------------------------------------------------------------------+ //| OnChartEvent 处理函数 | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id==CHARTEVENT_OBJECT_CLICK && sparam=="button") Alert("点击了按钮"); if(id==CHARTEVENT_CUSTOM) MyFunction(); }
在这个例子中,在 MyFunction 函数之前,有一个 200 ms 的延迟(定期调用此函数的时间),并且生成自定义事件。OnChartEvent 函数处理所有事件,对于自定义事件,它再次调用 MyFunction 函数。因此,通过这种方式实施 MyFunction 函数的定期调用,并且能够将调用周期设置为毫秒级。
让我们考虑编写“贪吃蛇”游戏的例子。
地图关卡是一个单独的包含(报头)文件 "Snake.mqh",并且也是一个三维数组关卡 [6] [20] [20]。关卡地图位于一个单独的报头文件 "Snake.mqh" 中,表示为一个 game_level[6][20][20] 三维数组。此数组的每一个元素是一个包含单独关卡说明的两维数组。如果元素的值等于 9,则它是一个障碍物。如果数组元素的值等于 1、2 或 3,则它分别是蛇头、蛇身或蛇尾,这定义了蛇在移动区中的初始位置。您可以添加新的关卡,或修改关卡数组中的现有关卡。
此外,"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 //游戏的起点横坐标 #define START_POS_Y 0 //游戏的起点纵坐标 #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 "关数: %u 共 %u" //"关数" 编辑框的文字 #define FOOD_LEFT_OVER_EDIT_NAME "snake_food_available_edit" //"剩余食物" 编辑框的名称 #define FOOD_LEFT_OVER_EDIT_TEXT "剩余食物: %u" //"剩余食物" 编辑框的文字 #define LIVES_EDIT_NAME "snake_lives_edit" //"生命数" 编辑框的名称 #define LIVES_EDIT_TEXT "生命: %u" //"生命数" 编辑框的文字 #define START_GAME_BUTTON_NAME "snake_start_game_button" //"开始" 按钮的名称 #define START_GAME_BUTTON_TEXT "开始" //"开始" 按钮的文字 #define PAUSE_GAME_BUTTON_NAME "snake_pause_game_button" //"暂停" 按钮的名字 #define PAUSE_GAME_BUTTON_TEXT "暂停" //"暂停" 按钮的文字 #define STOP_GAME_BUTTON_NAME "snake_stop_game_button" //"停止" 按钮的名字 #define STOP_GAME_BUTTON_TEXT "停止" //"停止"按钮的文字 #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 "贪吃蛇" //"抬头" 按钮文字 #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" 文件中。
ChartFieldElement 类:
//+------------------------------------------------------------------+ //| 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 轴的像素距离,然后移动元素。为此,它调用 CChartObjectBmpLabel 类的 X_Distance 和 YDistance 方法。
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; }; //取得/设置 方向的方法 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 数组的 0 数组索引是蛇头,最后一个是蛇尾。知道这一点后,您可以轻松在移动区中处理蛇。
让我们考虑 CSnakeGame 类的所有方法。方法依据在本文的“理论”一章中介绍的理论实施。
//+------------------------------------------------------------------+ //| 抬头创建方法 | //+------------------------------------------------------------------+ void CSnakeGame::CreateHeader(void) { //创建一个 CChartObjectButton 类的新对象并设定 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 类的对象并赋值给CSnakeGame 类的 square_obj_arr属性 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.Add(square_obj); } //移动游戏区域的单元 FieldMoveOnChart(); ChartRedraw(); } //+------------------------------------------------------------------+ //| 在图表上移动游戏区域的单元 | //+------------------------------------------------------------------+ 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(); } //+------------------------------------------------------------------+ //| 删除游戏区域 | //+------------------------------------------------------------------+ void CSnakeGame::DeleteField() { delete square_obj_arr; ChartRedraw(); }
//+------------------------------------------------------------------+ //| 创建障碍物 | //+------------------------------------------------------------------+ void CSnakeGame::CreateObstacle() { int i,j; CChartFieldElement *obstacle_obj; //创建一个 CArrayObj 类的对象并把它赋值给 CSnakeGame 类的 obstacle_obj_arr 属性 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); //指定障碍物的内部坐标 obstacle_obj.SetPos(j,i); 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) { 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 类的对象并且把它赋值给 CSnakeGame 类的 snake_element_obj_arr 属性 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); //指定蛇元素的内部坐标 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]); //在图表上移动贪吃蛇 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) { 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 类对象并把它赋值给 CSnakeGame 类的 food_obj_arr 属性 food_obj_arr=new CArrayObj(); i=0; 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.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) { 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(spaces2+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 类的对象并把它赋值给 CSnakeGame 类的 status_panel_obj_arr 属性 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(spaces6+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(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); //创建 "生命数" 编辑框 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); //移动状态面板 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 类的对象并且把它赋值给 CSnakeGame 类的 control_panel_obj_arr 属性 control_panel_obj_arr=new CArrayObj(); //创建 "开始" 按钮 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); //创建 "暂停" 按钮 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); //创建 "停止" 按钮 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(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(); } //+------------------------------------------------------------------+ //| 关卡重新开始方法 | //+------------------------------------------------------------------+ 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(spaces8+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(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(); } }
// 声明并创建一个 CSnakeGame 类型的全局对象 CSnakeGame snake_field; //+------------------------------------------------------------------+ //| EA交易初始化函数 | //+------------------------------------------------------------------+ int OnInit() { snake_field.Init(); EventSetTimer(1); return(0); } //+------------------------------------------------------------------+ //| EA 交易去初始化函数 | //+------------------------------------------------------------------+ 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; } //如果按下了 "开始" 按钮并且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; } //如果已经按下了 "暂停" 按钮 else if(id==CHARTEVENT_OBJECT_CLICK && sparam==PAUSE_GAME_BUTTON_NAME) { press_button=false; } //如果已经按下了 "停止" 按钮 else if(id==CHARTEVENT_OBJECT_CLICK && sparam==STOP_GAME_BUTTON_NAME) { snake_field.StopGame(); press_key=true; press_button=false; } //如果 press_button=true 处理贪吃蛇移动事件 else if(id==CHARTEVENT_CUSTOM && press_button==true) { snake_field.SnakeMoveOnField(); press_key=false; } //处理游戏重新开始事件 else if(id==CHARTEVENT_CUSTOM+1) { snake_field.ResetGame(); Sleep(1000); press_key=true; press_button=false; } //处理下一关事件 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(); } } //+------------------------------------------------------------------+
press_key 和 press_button 是两个静态变量,在 OnChartEvent 事件处理函数中定义。
如果 press_button 变量为 false,则允许开始游戏。在单击 "Start"(开始)按钮之后,press_button 变量被设置为 true(禁止重新执行用于开始游戏的代码),在发生以下事件之前,此状态都保持不变:
如果蛇的移动方向与当前方向垂直,以及在蛇已经在移动区中移动之后(press_key 变量的值表示这一状态),可以改变移动方向。 在事件处理函数 CHARTEVENT_KEYDOWN(按键事件)中考虑了这些条件。
之后移动蛇头,生成 CHARTEVENT_OBJECT_DRAG 事件,重新定义 CSnakeGame 类的 header_left 和 header_top 字段。其他游戏元素的移动由这些值来决定。
移动区的移动通过在 TradePad_Sample 中介绍的方式来实施。
在本文中,我们考虑了一个用 MQL5 编写游戏的例子。
我们介绍了标准库类(控制类)、CArrayObj 类,还学习了如何通过事件处理执行定期执行的函数。
可以从下面的参考中下载“贪吃蛇”游戏的源代码压缩档案。压缩档案应解压到文件夹 client_terminal_folder 中。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程