用 MQL5 创建贪吃蛇游戏(基础篇)
◍ 在MT5里跑一个贪吃蛇
很多人以为 MT5 只能做交易程序,其实它的图形对象和定时器足以撑起一个轻量游戏。2013 年 12 月发布的这篇 EA 示例,累计被查看 2577 次、收藏 15 次,说明用 MQL5 写非交易逻辑的需求一直存在。 核心思路是用 OBJ_RECTANGLE 画格子、用 CHARTEVENT_TIMER 驱动帧刷新,再把方向键消息映射到蛇头坐标数组。下面这段是初始化蛇身与食物的基础结构,开 MT5 新建 EA 后可直接粘进去验证。 贵金属与外汇交易本身高风险,本文仅展示平台编程能力边界,不涉及任何交易策略收益暗示。
「为什么 MT5 能跑贪吃蛇」
从 MQL5 开始,终端里写游戏不再是不可能的事,核心在于事件处理机制,尤其是自定义事件的引入。没有这套机制,图表上的交互和定时刷新都无从谈起。 面向对象编程在这类程序里不是噱头,它让状态管理和绘制逻辑拆得更干净,客观上减少了出错量。 读完这篇你能直接动手验证三件事:OnChart 事件怎么接管鼠标键盘、MQL5 标准库类怎么拿来就用、以及用自定义事件在固定间隔后循环触发函数做运算。 外汇和贵金属图表上跑这类实验属高风险环境,实盘终端请勿加载未经验证的脚本。
MT5 里跑贪吃蛇的边界与命数规则
拿贪吃蛇做 MQL5 练手,核心不是游戏好玩,而是它状态少、逻辑直白,任何想碰图表对象编程的人都能照着复现。在 MT5 上它有一套固定框架:总共 6 关(编号 0 到 5),每关给 5 条命,命用完或通关后回到第 0 关,想加自己的关也行,但第 0 关里蛇速和最大长度是锁死的。 移动区是 20×20 的单元格阵列,单格 20×20 像素,蛇头四个方向都能走,蛇身被动跟随。吃到 12 颗浆果直接进下一关;撞到灰色障碍就重开本关并扣一条命。注意一个反直觉点:移动区边缘不是墙,蛇从左边出去会从右边钻回来,图 2 那种穿屏逻辑必须先在脑子里跑通再写代码。 对象类型上,游戏标题是 Button,移动区所有格子是 BmpLabel 位图标签,状态栏三个读数用 Edit 控件,开始/暂停/停止是三个 Button,全部按相对图表左上角的像素坐标定位。蛇头蛇尾能转向,转向只取决于相邻节的位置——比如尾元素左边有节,尾就左转;头元素右边有节,头左转,这和身段处理不一样。 单步移动拆成三段:头按方向推一格;末节身段补到旧头位;尾补到末节身段旧位。若头吃到浆果,尾不动,而是在末节身段旧位新生一节,长度 +1。这套相位差写错一格,蛇就会自己绞死自己。
◍ 用标准库类接管图表对象
在 MT5 里批量管理移动区块、贪吃蛇元素这类同构对象,手写 ObjectCreate + 一堆 ObjectSetInteger 会很笨重。MQL5 标准库把常用图表控件封装成了类,直接拿来用能省掉大量重复代码。 游戏场景里最常用到三个控制类:CChartObjectEdit(编辑框)、CChartObjectButton(按钮)、CChartObjectBmpLabel(位图标签)。它们都继承自更基础的图表对象类,比如 CChartObjectButton 继承 CChartObjectEdit,再往上追是 CChartObjectLabel,所以父类的方法和属性子类全都能调。 引入方式靠编译器指令,例如按钮类写在 ChartObjectsTxtControls.mqh 里,用 #include <ChartObjects\ChartObjectsTxtControls.mqh> 挂进来即可,文件路径不会出现在参考文档的搜索结果中,得按库目录结构自己找。 下面这段对照代码很说明问题:不用类时要 7 行函数调用才能画出一个 100×20、偏移 (10,10) 的按钮;用 CChartObjectButton 只要 new 出来、Create 一次、Description 填字就完事。类对象还能塞进 CArrayObj 动态指针数组,增删元素不用手动重设数组大小。外汇与贵金属图表上做这类自定义控件,需注意过度绘制可能拖慢老机型终端,属于低风险但需自测的操作。
class="macro">#include <path_to_the_file_with_classes_description> class="macro">#include <ChartObjects\ChartObjectsTxtControls.mqh> class=class="str">"cmt">//创建一个按钮, 名称为 "button" ObjectCreate(class="num">0,"button",OBJ_BUTTON,class="num">0,class="num">0,class="num">0); class=class="str">"cmt">//指定按钮上的文字 ObjectSetString(class="num">0,"button",OBJPROP_TEXT,"按钮文字"); class=class="str">"cmt">//指定按钮大小 ObjectSetInteger(class="num">0,"button",OBJPROP_XSIZE,class="num">100); ObjectSetInteger(class="num">0,"button",OBJPROP_YSIZE,class="num">20); class=class="str">"cmt">//指定按钮位置 ObjectSetInteger(class="num">0,"button",OBJPROP_XDISTANCE,class="num">10); ObjectSetInteger(class="num">0,"button",OBJPROP_YDISTANCE,class="num">10); CChartObjectButton *button; class=class="str">"cmt">//创建 CChartObjectButton 类的一个对象并把指针赋予按钮变量 button=new CChartObjectButton; class=class="str">"cmt">//用属性创建按钮 (以像素为单位): (宽度=class="num">100, 高度=class="num">20, 位置: X=class="num">10,Y=class="num">10) button.Create(class="num">0,"button",class="num">0,class="num">10,class="num">10,class="num">100,class="num">20); class=class="str">"cmt">//指定按钮上的文字 button.Description("按钮文字");
「用 CArrayObj 管对象指针数组的坑与法」
CArrayObj 是 MT5 标准库里一个专门装对象指针的动态数组类,它要求元素必须继承自 CObject——标准库里所有类都从 CObject 派生,所以你能直接塞 CTrade、CIndicator 这类对象指针;自己写的类若想进数组,也得 public 继承 CObject,否则编译器直接报错。 常用方法就五个够用:Add 尾部追加、Insert 指定位置插、Detach 抽走某位置元素(不销毁对象)、Total 取长度、At 按索引取指针不删除。下面这段初始化里先循环灌了 a/b/c 三个对象,打印出来是 (a,b,c);Insert 在索引 1 插了 d,变成 (a,d,b,c);Detach(2) 把 b 抽走,剩 (a,d,c)。 [CODE] #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); } } int OnInit() { CArrayObj *array_obj=new CArrayObj(); CMyClass *my_obj; for(int i='a';i<='c';i++) { my_obj=new CMyClass(); my_obj.s=char(i); array_obj.Add(my_obj); } MyPrint(array_obj); 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); return(INIT_SUCCEEDED); } [/CODE] 逐行拆一下:#include 把数组类拉进来;CMyClass 继承 CObject 才有进数组的资格;MyPrint 用 Total 拿数量、At 挨个取指针打印 s 字段;OnInit 里 new 出数组和三个对象,Add 填完打一遍,Insert(位置1) 插 d 再打,Detach(2) 抽索引2的元素后打最后一遍。 析构是个暗雷:delete 这个数组对象时,默认 FreeMode 为 true,析构函数会把数组里指针指向的对象也全删了。若那些对象你还想在别处用,删数组前必须 array_obj.FreeMode(false) 关掉自动回收,否则指针变悬空。外汇贵金属 EA 里管理挂单对象或指标句柄时,这个标记设错就可能让程序在波动行情中崩在线程外。
class="macro">#include <Arrays\ArrayObj.mqh> class CMyClass:class="kw">public CObject { class="kw">public: class="type">char s; }; class="type">void MyPrint(CArrayObj *array_obj) { CMyClass *my_obj; for(class="type">int i=class="num">0;i<array_obj.Total();i++) { my_obj=array_obj.At(i); printf("%C",my_obj.s); } } class="type">int OnInit() { CArrayObj *array_obj=new CArrayObj(); CMyClass *my_obj; for(class="type">int i=&class="macro">#x27;a&class="macro">#x27;;i<=&class="macro">#x27;c&class="macro">#x27;;i++) { my_obj=new CMyClass(); my_obj.s=class="type">char(i); array_obj.Add(my_obj); } MyPrint(array_obj); my_obj=new CMyClass(); my_obj.s=&class="macro">#x27;d&class="macro">#x27;; array_obj.Insert(my_obj,class="num">1); MyPrint(array_obj); my_obj=array_obj.Detach(class="num">2); MyPrint(array_obj); class="kw">return(INIT_SUCCEEDED); }
动态数组与对象指针的收尾清理
在 MQL5 里用动态数组装对象指针时,若数组被设为自动管理(FreeMode 默认 true),出作用域会替你 delete 指针。但当你手动接管生命周期,就得显式调用 delete array_obj 释放整块数组及其持有的指针,否则 MT5 终端会留下肉眼难查的内存碎片。 下面这段收尾代码先把 FreeMode 关掉,意味着数组不再自动析构内部对象,紧接着一句 delete array_obj 把数组本身和其中对象指针一并清掉,最后 return(0) 干净退出。 外汇与贵金属 EA 跑在实时行情下,这类遗漏在回测不显、实盘连跑数日后可能拖慢终端,属于高风险环境下的隐性损耗,建议每次开 MT5 跑完自定义数组逻辑后查一眼内存占用曲线。
class=class="str">"cmt">//删除动态数组和数组中所有对象的指针 class="kw">delete array_obj; class="kw">return(class="num">0); } array_obj.FreeMode(false); class="kw">delete array_obj;