做威纶通触摸屏项目的时间一长,很多人会进入一个舒适区:拖几个按钮、做两个报警页面、连一下PLC变量,感觉EBpro就是个“画界面”的工具。直到某一天你接到一个需要在触摸屏本地做逻辑处理、画面还要有连续动态反馈的需求,才会发现这套组态软件底下藏着一整套宏指令引擎。我的建议是,与其等项目逼你啃手册,不如先用一个经典小游戏把整套机制跑通。贪吃蛇就是一个特别合适的练手项目,它逻辑闭环完整、画面反馈直观,而且几乎把宏指令、触发方式、触摸元件交互这些核心能力全部覆盖了。这篇文章就把拆解这个项目的过程完整写出来,从宏指令基础到触碰机制,再到可以照着写的代码思路,适合所有刚接触EBpro又想真正把触摸屏玩明白的工程师。
1. 为什么拿贪吃蛇当练手项目
1.1 宏指令在HMI项目里的真实定位
宏指令在EBpro里到底是什么?把它理解为触摸屏内置的一套脚本引擎比较准确。它不同于PLC里的梯形图,而是类似C语言的简化脚本,可以直接读写触摸屏内部的LW(字寄存器)、LB(位寄存器),也能通过通讯驱动读写PLC地址。配合条件判断、循环、数组和数学运算,宏指令可以完成相当多“页面元件做不到”的事情。
常见应用包括配方数据的组合与校验、触摸屏本地数据预处理、自定义报警优先级、通讯协议转换、数据记录导出前的格式整理等等。很多工程师对宏指令的使用往往停留在“数值转换”或者“设备地址复制”这一步,公式编辑器能用就绝不开宏。但如果项目里出现“需要根据一段时间内的数据趋势自动切换画面窗口”或者“要在触摸屏端完成一组数据的排序查找”,单纯靠页面元件就会变得很痛苦,这时候宏指令的价值才真正体现出来。
贪吃蛇游戏恰好把这些能力全部串联在一起:它需要连续的状态更新、数组存储蛇身坐标、碰撞检测、随机数生成、定时驱动、按键事件响应。完整写一遍,等于把宏指令的常用API和地址操作全部过了一遍,以后再做工程功能会顺手很多。
1.2 触碰机制:最容易被忽略的输入层
触摸屏项目里,工程师对“输出”的重视程度通常远高于“输入”。画面做得好不好看、报警显示得清不清楚,大家很在意,但一个按键按下之后到底在什么时刻改变变量、按住不松又会触发几次,很多人反而不太清楚。
解决这个问题最快的方式就是做游戏。贪吃蛇需要方向键长时间按住、快速连按、以及按键之间的互斥处理,每一种行为都会在“触碰机制”层面产生新的问题。比如方向键按住不放时蛇是否会连续转向、按下和弹起是否会被误判成两次触发、左右两个键同时按下时系统会以哪个为准。这些问题在普通工程里一般不会暴露,但在游戏里会被无限放大。
把触碰机制吃透,对实际项目的帮助非常直接。常见的安全互锁、误触过滤、长按加速、双击确认这些交互逻辑,本质上都是在跟“触摸按下和弹起”这两个事件打交道。之前做工程时遇到过一个场景:操作工误触画面导致设备意外启动,后来就是利用了位状态开关的弹起触发方式和延时确认逻辑来解决的。这类经验,从游戏项目中反而学得最快。
2. EBpro宏指令基础:地址、触发与执行周期
2.1 三类触发方式
EBpro中宏指令的触发方式主要有三种:元件触发、周期触发、上电/窗口触发。
元件触发是最常用的,在画面上放置一个与宏绑定的触发元件,操作员按下或弹起时执行对应的宏。这也正是“触碰机制解析”里最核心的部分,后文会展开。
周期触发适合做定时任务。EBpro中可以给宏设置固定执行周期,比如每隔200毫秒执行一次。贪吃蛇游戏的移动节拍就依赖这个机制。要注意的是,周期任务的执行时间不能无限缩短,触摸屏的CPU有限,宏执行本身也要耗CPU时间,如果同时存在多个执行时间较长的周期宏,画面刷新率会明显下降。
上电触发就是触摸屏启动时执行一次,适合做设备自检、初始变量装载、画面跳转等一次性操作。窗口触发则是进入某个窗口时执行一次,在做多页面工程时会用到。
宏本身不是一个常驻内存的进程,它是被这些事件驱动执行的一段代码。理解这一点很重要:宏执行完就结束了,里面的局部变量不会保留到下一次执行,需要跨周期保存的数据必须写到LW或LB地址上。
2.2 LW和LB:宏与画面之间的共享内存
威纶通触摸屏内部有一套本地寄存器体系,LW是16位的字寄存器,LB是位寄存器。EBpro页面上的元件可以直接绑定这些地址来显示或修改数值,宏指令同样可以读写它们。这就像一块共享内存,让“画面显示”“按键操作”“宏逻辑”三者之间有了公共的数据交换层。
宏指令里最常用的两个API是GetData和SetData:
short value GetData(value, "LW", 0) // 读取LW0的值到变量value SetData(value, "LW", 10) // 把value的值写到LW10这个用法很直观。GetData把地址上的值“取”进宏内部变量,SetData把宏里的值“写”到地址上。对LB位寄存器也是一样的操作,只是地址类型字符串要换成"LB"。
画面元件绑定地址后,一旦LW地址上的值发生变化,元件外观就会刷新。也就是说,宏只要把计算结果写到LW地址,画面不需要任何额外操作就会跟着变化。这也是为什么用LW地址数组来映射游戏网格是可行的方案。
2.3 宏指令的数据类型与数组
EBpro宏指令语法接近C语言,支持short、int、float等基本类型,不过实际项目中用short和int的频率最高。数组同样是支持的,比如声明一组short类型数组来存储蛇身坐标,这在贪吃蛇项目里是最核心的数据结构。
需要注意,宏内部的变量生命周期很短,只有本次执行内有效。如果你希望另一个宏也能读取这些坐标数据,就必须把它们写到LW地址上。这意味着贪吃蛇项目的存储方案有两种选择:一是把蛇身坐标全部映射到LW区域,宏之间通过LW地址组装数据;二是每次移动宏把坐标数组写到LW地址上,同时画面元件直接读取。我建议采用后者,逻辑清晰也方便调试。
关于数组和地址的对应关系,这里有一个常用技巧:把short数组连续写到LW区域时,数组元素下标和LW地址偏移成一一对应关系。比如snakeX[0]对应LW200,snakeX[1]对应LW201,这样不管是宏内访问还是用画面元件监控地址,都能快速定位。
3. 贪吃蛇核心逻辑拆解
3.1 游戏状态机
贪吃蛇游戏的逻辑虽然不复杂,但如果没有状态管理,代码很快就会乱。整个游戏流程可以拆成三个状态:等待开始、运行中、游戏结束。用LW0地址来存储当前状态值,0表示等待开始,1表示运行中,2表示游戏结束。
状态机的价值在于把不同阶段的处理逻辑分开。等待开始状态下,蛇静止在画面中央,方向键按下后进入运行状态;运行状态下,定时宏负责每间隔固定时间移动一步;游戏结束后,画面定格在最终局面,需要通过一个重置按钮回到等待开始状态并初始化蛇身。
在宏代码里,移动逻辑的最外层判断就是检查状态值。如果状态不是运行中,就跳过移动逻辑。这个设计在后续扩展时也非常方便,比如增加暂停功能,只要在状态机里多增加一个状态即可。
3.2 蛇身存储与移动算法
蛇身用两个数组存储,一个存X坐标,一个存Y坐标,数组的下标代表蛇身的节数顺序,0号元素是蛇头。
移动算法的核心是“新头进、旧尾出”。每次移动时,先根据当前方向计算新的蛇头坐标,然后从数组尾部开始往前遍历,把每一个节点的坐标赋给它的后一个节点,最后把新的蛇头坐标放到0号位置。这段逻辑用伪代码表达如下:
short newX, newY short i // 根据方向计算新蛇头坐标 if dir == 0 // 向右 newX = snakeX[0] + 1 newY = snakeY[0] else if dir == 1 // 向左 newX = snakeX[0] - 1 newY = snakeY[0] else if dir == 2 // 向上 newX = snakeX[0] newY = snakeY[0] - 1 else if dir == 3 // 向下 newX = snakeX[0] newY = snakeY[0] + 1 end if // 从尾部往前移动蛇身 for i = snakeLen - 1 downto 1 snakeX[i] = snakeX[i - 1] snakeY[i] = snakeY[i - 1] end for snakeX[0] = newX snakeY[0] = newY这里有一个非常容易踩的坑:遍历方向必须从尾部往前,不能从头部往后。如果从头部开始,就会先把蛇头坐标覆盖到第二节,然后下一轮循环又把第二节的“新值”覆盖到第三节,最终整条蛇变成同一份坐标数据。很多新手第一次写贪吃蛇都会在这个问题上卡住,需要特别注意。
3.3 碰撞检测与食物生成
碰撞检测分两类:一是边界检测,蛇头坐标超出地图范围;二是自碰检测,蛇头坐标与蛇身任意一节的坐标重合。
边界检测放在移动逻辑最前面,如果新蛇头超出地图边界,直接置状态为游戏结束。自碰检测则遍历整个蛇身数组,逐项比较坐标。有一个细节:检测时应该从第0节(蛇头本身)之后开始遍历,因为新蛇头坐标还没有写入数组,正常情况下不会跟旧蛇头坐标相同,但排除一下更安全。
食物生成的逻辑要处理的是随机性和有效性。先在地图范围内随机生成一组坐标,然后检查该坐标是否已经被蛇身占用,如果占用就重新生成。这个循环可能会在理论上出现反复的情况,通常会设置一个最大尝试次数,比如100次,超过这个次数就放弃本次生成,等到下一次移动再尝试。实际项目中,地图越大、蛇越短,遇到死循环的概率就越低,但无论如何都要做一个最大尝试次数的保护,否则极端情况下宏会卡死在这个循环里。
EBpro宏指令支持rand()函数,返回一个非负整数。生成坐标为rand() % 地图宽度、rand() % 地图高度。要注意不同版本的宏指令对rand的支持程度略有差异,以实际机型手册为准。
3.4 方向控制与移动节拍分离
这里有一个设计上的关键点:方向控制宏和移动宏要分开,不要混在一起。方向键按下时只修改方向变量,不执行移动逻辑;移动宏由周期触发定时执行。这样做的原因是,按键触发和移动节拍是两件频率完全不同的事。按键事件是离散的、随机的,移动节拍是固定周期的,如果把它们耦合在一起,会出现按住方向键时蛇加速移动的问题。
方向控制宏需要做反向判断。比如当前方向向右,玩家按下左键,这在逻辑上是180度掉头,在贪吃蛇游戏里应该被忽略,否则蛇头会直接撞上第二节身体,导致游戏结束。判断反向的逻辑很简单:如果当前方向是0且按下的是1方向的键,忽略;如果当前方向是3且按下的是2方向的键,忽略,以此类推。
移动节拍则通过周期宏实现。在EBpro里给宏设置一个固定执行周期,比如400毫秒,这样就得到一个稳定、可控的“游戏帧率”。想提高游戏难度,就缩短周期;想降低难度,就拉长周期。
4. 触摸屏触碰机制解析
4.1 触发元件类型与触发时机
在EBpro中,跟触摸输入相关的元件类型主要有位状态开关、窗口切换开关、宏指令触发元件等。位状态开关操作的是LB或LW地址,按下时可以把一个位地址置1或置0,或者在位地址间做循环切换。窗口切换开关则是直接触发页面切换,更多用于导航。
宏指令触发元件是游戏项目最需要关注的类型。它不像位状态开关那样绑定地址,而是直接绑定一个宏。触发方式一般有“按下时执行”“弹起时执行”“按下和弹起各执行一次”等选择。贪吃蛇方向键应该选择“按下时执行”,按下瞬间立即改变方向,弹起时不需要再做处理。
这个触发时机的理解对实际工程非常重要。位状态开关改变地址的时机和宏触发元件调用宏的时机是不同的。举个实际场景:一个启动按钮,如果绑定的是位状态开关,按下瞬间位地址就变了;如果绑定宏触发元件,宏内部可以决定是按下瞬间就改变输出,还是等到弹起时再改变。涉及到设备启停这类安全相关的操作,后者的可控性会好很多。
4.2 按下、弹起、持续按住怎么辨识
触摸屏本身没有太多复杂的触摸手势判定,但通过LB位地址配合宏,我们可以自己搭建一套“按下/弹起/按住”的识别逻辑。
一个标准做法是这样的:方向键元件关联一个宏,宏内部把对应的LB标志位置1,表示“这个方向键当前处于按下状态”。同时画面里放置一个透明的位状态开关元件,绑定同一个LB位,但触发方式设置为“弹起时置0”。这样按下时标志位变1,弹起时标志位恢复0,宏就能通过读取LB来感知持续按住的状态。
实际项目中还有一种更简单的方案:方向键元件直接用位状态开关绑定LB标志位,按下置1,弹起置0,然后方向控制宏周期检测这些LB位。这种方式逻辑更直观,也更容易调试。在EBpro模拟器里,可以直接在地址监控窗口中实时查看这些LB位的变化,排查问题非常方便。
4.3 方向键互斥与误触过滤
触摸屏物理上支持多点触摸,但EBpro中多个按键同时按下时,各个触发元件会根据自身配置分别执行,这就可能出现上下两个方向键同时触发的场景。解决方式是在方向控制宏中增加优先级判断和互斥逻辑。比较简单的实现是:同一时刻只响应一个方向,比如检测的顺序固定为上、下、左、右,先检测到谁就响应谁,其他方向的标志位置零。
另一种情况是误触过滤。触摸屏实际使用中,手指按下瞬间可能出现短暂的接触抖动,如果触发条件设置得太敏感,会导致方向瞬间变化两次。在EBpro中,可以通过在元件属性里设置“延时触发”或者“触发延迟时间”来过滤掉这种极短按下的情况。游戏项目里意义不大,但在工业设备的实体按键上非常实用。
互斥逻辑放在宏里实现还有一个好处:它不增加任何画面元件的复杂度,只需要在方向控制宏中读取四个LB标志位,做一次优先级判断后写方向变量即可。后面如果增加新的按键功能,只需要扩展宏逻辑,不需要改动画面元件。
5. 实操搭建:从零开始做一个可跑的贪吃蛇
5.1 地址规划
先做地址规划是一个非常好的习惯,可以避免写到后面地址冲突。以一个12×8的网格地图为例,规划如下:
| 地址范围 | 用途 |
|---|---|
| LW0 | 游戏状态,0等待、1运行、2结束 |
| LW1 | 当前方向,0右、1左、2上、3下 |
| LW10 ~ LW105 | 网格状态,按行优先排列,0空、1蛇身、2食物 |
| LW200 ~ LW249 | 蛇身X坐标数组 |
| LW250 ~ LW299 | 蛇身Y坐标数组 |
| LW300 | 蛇的当前长度 |
| LW301 | 得分 |
| LW310 | 食物X坐标 |
| LW311 | 食物Y坐标 |
把地址规划表先写出来,然后在宏里定义常量,后续代码维护要省心很多。比如地图宽度12、高度8、最大蛇长50,都定义成宏顶部的常量,不要散落在代码里。
5.2 画面布局与网格元件绑定
画面上需要三块区域:游戏网格区、方向键区、状态显示区。游戏网格区直接摆出12×8共96个格子,每个格子是一个“多状态显示”元件,绑定LW10开始的对应地址。
这里有一个实际操作的效率问题:96个多状态显示元件,如果一个个拖拽绑定地址会非常累。EBpro支持元件的批处理操作,可以把第一个元件绑定好LW10,然后复制元件,在新位置粘贴时根据坐标自动递增地址偏移。具体操作路径为:先放置一个网格元件,设置好三状态的外观(背景色、蛇身颜色、食物颜色),然后在编辑器中复制,手动粘贴并修改地址。熟练之后布置96个格子大约十几分钟。
网格元件的坐标需要计算好,每个格子之间留1到2个像素的间隙,看起来会更清晰。12列、8行的网格在常见的7英寸屏上大约占一半画面面积,剩余空间用来放方向键和文字指示。
5.3 关键宏代码实现
宏代码分为四个:初始化宏、方向控制宏、移动宏、食物生成宏。初始化宏在上电触发或重置按钮触发,方向控制宏由方向键触发,移动宏由周期触发。
初始化宏:
short i, j short k // 初始化蛇,长度3,位于地图中间 snakeLen = 3 snakeX[0] = 5 snakeY[0] = 4 snakeX[1] = 4 snakeY[1] = 4 snakeX[2] = 3 snakeY[2] = 4 dir = 0 score = 0 state = 1 // 清除网格 for i = 0 to 11 for j = 0 to 7 grid[i * 12 + j] = 0 end for end for // 把蛇身写入网格 grid[5 * 12 + 4] = 1 grid[4 * 12 + 4] = 1 grid[3 * 12 + 4] = 1 // 同步到LW地址 for k = 0 to 95 SetData(grid[k], "LW", 10 + k) end for SetData(snakeLen, "LW", 300) SetData(score, "LW", 301) SetData(state, "LW", 0)方向控制宏以上方向键为例:
short currentDir GetData(currentDir, "LW", 1) // 不允许反向 if currentDir != 3 SetData(2, "LW", 1) // 更新方向为向上 end if移动宏是核心,逻辑如下:
short state short headX, headY short newX, newY short i, k short len GetData(state, "LW", 0) if state != 1 return end if GetData(headX, "LW", 200) GetData(headY, "LW", 250) GetData(len, "LW", 300) GetData(dir, "LW", 1) if dir == 0 newX = headX + 1 newY = headY else if dir == 1 newX = headX - 1 newY = headY else if dir == 2 newX = headX newY = headY - 1 else if dir == 3 newX = headX newY = headY + 1 end if // 边界检测 if newX < 0 || newX >= 12 || newY < 0 || newY >= 8 SetData(2, "LW", 0) return end if // 自碰检测 for i = 0 to len - 1 if newX == snakeX[i] && newY == snakeY[i] SetData(2, "LW", 0) return end if end for // 移动蛇身,从尾部往前 for i = len - 1 downto 1 snakeX[i] = snakeX[i - 1] snakeY[i] = snakeY[i - 1] end for snakeX[0] = newX snakeY[0] = newY // 同步网格并刷新画面 for k = 0 to 95 grid[k] = 0 end for for i = 0 to len - 1 grid[snakeY[i] * 12 + snakeX[i]] = 1 end for // 食物处理省略,由食物生成宏管理 for k = 0 to 95 SetData(grid[k], "LW", 10 + k) end for食物生成宏使用rand函数生成新食物并写入网格,同时更新LW310和LW311:
short fx, fy short i, k short len short gridVal short occupied for k = 0 to 100 fx = rand() % 12 fy = rand() % 8 occupied = 0 GetData(len, "LW", 300) for i = 0 to len - 1 if snakeX[i] == fx && snakeY[i] == fy occupied = 1 end if end for if occupied == 0 break end if end for SetData(fx, "LW", 310) SetData(fy, "LW", 311) SetData(1, "LW", 310 + fy * 12 + fx) // 等待实际地址绑定以上代码是经过简化的可读版本,语法细节需要根据实际使用的EBpro微机型和宏指令版本来调整,但整体逻辑可以直接套用。
5.4 模拟器测试与实机下载
EBpro自带的模拟器功能非常实用,不需要连接真实触摸屏就可以跑起来。建议在模拟器阶段就把所有逻辑全部调通,再下载到实机。模拟器可以实时监控LW地址变化,调试游戏状态、蛇身坐标、网格显示之间的对应关系,效率很高。
下载到实机后,需要注意画面的刷新效果。模拟器上的时序和实机不完全一致,如果发现蛇身移动过程出现残影,多半是网格元件状态刷新不及时,可以尝试把移动宏执行周期加长一些,或者减少一次移动中重复写LW的次数。另外一个容易忽略的点是,触摸屏的声音反馈和背光亮度会影响操作手感,实机测试时要模拟真实操作场景。
6. 常见问题与调试记录
6.1 蛇身移动卡顿或跳跃
表现是蛇身移动不流畅,每隔几步会有明显的跳动。这种情况一般是移动宏执行周期和画面元件刷新周期不匹配导致的。EBpro中周期宏执行和画面刷新是并行的,如果移动宏里一次写入96个LW地址,耗时较长,可能会出现画面刷新赶不上宏执行节奏的情况。
处理方式有两种:一是增大移动周期,让每次移动间隔更久;二是优化宏代码,减少重复写LW的次数,比如只在状态变化时写对应的单个网格地址,而不是每次把96个地址全部重写一遍。
6.2 方向键按下没有反应
先检查方向键元件是否绑定对了宏,再检查宏内部是否对LW1地址做了正确的读写。比较隐蔽的问题是方向键元件选择了“弹起时执行”,按下瞬间已经错过了触发时机。另外还要确认方向键元件是否被游戏状态判断逻辑拦截,比如在等待开始状态,某些方向键宏里限制了必须状态为运行才能生效,这会导致按下没反应。
排查时可以在方向控制宏开头添加一个临时变量写入LW320,然后在地址监控中观察按键后LW320是否变化。如果没变化,说明元件触发链路有问题;如果有变化,问题在后续逻辑。
6.3 食物生成死循环
食物生成宏里的最大尝试次数必须保留。如果地图面积小、蛇的长度大,随机生成落在蛇身上的概率会非常高。比如12×8的地图只有96个格子,蛇长到60以上时,可用空格只有36个,生成一次命中空格的概率只有37%,不设尝试上限就可能在极端情况下卡死。
把最大尝试次数设置到100次,同时不断刷新LW310和LW311。如果超过次数依然没有找到合适位置,建议直接返回,不要阻塞宏线程。
6.4 变量地址冲突导致画面闪动
贪吃蛇项目的地址规划相对简单,但如果和已有的工程功能混在一起,很容易出现LW地址重叠。比如LW10到LW105被网格占用了,工程里原有的配方功能也用了LW100,那么每次配方写入都会导致画面上的网格状态被意外修改。
解决办法就是做好地址规划表,每个功能模块划分独立的地址区间,并预留扩展空间。这类问题在模拟器阶段很难发现,因为模拟器里没有其他正在运行的工程逻辑,只有下载到实机配合完整工程运行时才会暴露。
每次调试遇到奇怪现象,第一步永远是打开EBpro的在线监控,看相关LW地址的实际值变化。这比盲目修改代码要高效得多。我自己调试贪吃蛇项目时,最常用的一组监控窗口就是LW0状态值、LW1方向值、LW200蛇头X和LW250蛇头Y,基本能追踪90%的逻辑问题。
做完这个项目,最大的收获并不是“会写一个贪吃蛇”,而是对宏指令的执行节奏、LW地址作为数据中转层的运用、触摸元件触发时机之间的配合有了更实际的体感。后面再做配方管理、报警过滤、数据本地处理这些工程功能,心里明显更有底。建议第一次接触EBpro宏指令的朋友,不要急着去看手册里那些枯燥的函数列表,直接用一个小游戏把整个流程跑起来,比读十遍文档都管用。